Repository: streamlink/streamlink

Issue:
Aloula
### Checklist

- [X] This is a plugin request and not a different kind of issue
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed plugin requests](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+request%22)

### Description

https://www.aloula.sa/
https://www.aloula.sa/en
Aloula, free video-on-demand and TV catch up service in the Kingdom of Saudi Arabia and the Middle East.

### Input URLs

https://www.aloula.sa/live/saudiatv
https://www.aloula.sa/live/sbc-channel
https://www.aloula.sa/live/thikrayat-tv
https://www.aloula.sa/live/alekhbariya
https://www.aloula.sa/live/ksa-sports1
https://www.aloula.sa/live/ksa-sports2
https://www.aloula.sa/live/qurantvsa
https://www.aloula.sa/live/sunnatvsa
https://www.aloula.sa/live/riyadhradio
https://www.aloula.sa/live/jeddahradio
https://www.aloula.sa/live/jeddahradio
https://www.aloula.sa/live/nidaalislam
https://www.aloula.sa/live/saudiaradio

https://www.aloula.sa/en/live/saudiatv
https://www.aloula.sa/en/live/sbc-channel
https://www.aloula.sa/en/live/thikrayat-tv
https://www.aloula.sa/en/live/alekhbariya
https://www.aloula.sa/en/live/ksa-sports1
https://www.aloula.sa/en/live/ksa-sports2
https://www.aloula.sa/en/live/qurantvsa
https://www.aloula.sa/en/live/sunnatvsa
https://www.aloula.sa/en/live/riyadhradio
https://www.aloula.sa/en/live/jeddahradio
https://www.aloula.sa/en/live/jeddahradio
https://www.aloula.sa/en/live/nidaalislam
https://www.aloula.sa/en/live/saudiaradio


It gets the channels by changing the number here

https://aloula.faulio.com/api/v1/channels/1
https://aloula.faulio.com/api/v1/channels/2
e.t.c.


Discussion:

Resolving patch:
diff --git a/src/streamlink/plugins/aloula.py b/src/streamlink/plugins/aloula.py
new file mode 100644
--- /dev/null
+++ b/src/streamlink/plugins/aloula.py
@@ -0,0 +1,113 @@
+"""
+$description Live TV channels and video on-demand service from the SBA, a Saudi, state-owned broadcaster.
+$url aloula.sa
+$type live, vod
+"""
+
+import logging
+import re
+
+from streamlink.plugin import Plugin, pluginmatcher
+from streamlink.plugin.api import validate
+from streamlink.stream.hls import HLSStream
+
+log = logging.getLogger(__name__)
+
+
+@pluginmatcher(re.compile(r"""
+    https?://(?:www\.)?aloula\.sa/(?:\w{2}/)?
+    (?:
+        live/(?P<live_slug>[^/?&]+)
+    |
+        episode/(?P<vod_id>\d+)
+    )
+""", re.VERBOSE))
+class Aloula(Plugin):
+    def get_live(self, live_slug):
+        live_data = self.session.http.get(
+            "https://aloula.faulio.com/api/v1/channels",
+            schema=validate.Schema(
+                validate.parse_json(),
+                [{
+                    "id": int,
+                    "url": str,
+                    "title": str,
+                    "has_live": bool,
+                    "has_vod": bool,
+                    "streams": {
+                        "hls": validate.url(),
+                    },
+                }],
+                validate.filter(lambda k: k["url"] == live_slug),
+            ),
+        )
+        if not live_data:
+            return
+        live_data = live_data[0]
+        log.trace(f"{live_data!r}")
+
+        if not live_data["has_live"]:
+            log.error("Stream is not live")
+            return
+
+        self.id = live_data["id"]
+        self.author = "SBA"
+        self.title = live_data["title"]
+        self.category = "Live"
+        return HLSStream.parse_variant_playlist(self.session, live_data["streams"]["hls"])
+
+    def get_vod(self, vod_id):
+        vod_data = self.session.http.get(
+            f"https://aloula.faulio.com/api/v1/video/{vod_id}",
+            acceptable_status=(200, 401),
+            schema=validate.Schema(
+                validate.parse_json(),
+                validate.any(
+                    validate.all(
+                        {"blocks": [{
+                            "id": str,
+                            "program_title": str,
+                            "title": str,
+                            "season_number": int,
+                            "episode": int,
+                        }]},
+                        validate.get(("blocks", 0)),
+                    ),
+                    {"cms_error": str, "message": str},
+                ),
+            ),
+        )
+
+        log.trace(f"{vod_data!r}")
+        if "cms_error" in vod_data and vod_data["cms_error"] == "auth":
+            log.error("This stream requires a logged-in session cookie to be supplied")
+            return
+        if "cms_error" in vod_data:
+            log.error(f"API error: {vod_data['cms_error']} ({vod_data['message']})")
+            return
+        self.id = vod_data["id"]
+        self.author = vod_data["program_title"]
+        self.title = vod_data["title"]
+        self.category = f"S{vod_data['season_number']}E{vod_data['episode']}"
+
+        hls_url = self.session.http.get(
+            f"https://aloula.faulio.com/api/v1/video/{vod_id}/player",
+            schema=validate.Schema(
+                validate.parse_json(),
+                {"settings": {"protocols": {"hls": validate.url()}}},
+                validate.get(("settings", "protocols", "hls")),
+            ),
+        )
+        return HLSStream.parse_variant_playlist(self.session, hls_url)
+
+    def _get_streams(self):
+        live_slug = self.match.group("live_slug")
+        vod_id = self.match.group("vod_id")
+
+        if live_slug:
+            return self.get_live(live_slug)
+        elif vod_id:
+            return self.get_vod(vod_id)
+
+
+__plugin__ = Aloula


Test patch:
diff --git a/tests/plugins/test_aloula.py b/tests/plugins/test_aloula.py
new file mode 100644
--- /dev/null
+++ b/tests/plugins/test_aloula.py
@@ -0,0 +1,31 @@
+from streamlink.plugins.aloula import Aloula
+from tests.plugins import PluginCanHandleUrl
+
+
+class TestPluginCanHandleUrlAloula(PluginCanHandleUrl):
+    __plugin__ = Aloula
+
+    should_match_groups = [
+        ("https://www.aloula.sa/live/slug", {"live_slug": "slug"}),
+        ("https://www.aloula.sa/en/live/slug", {"live_slug": "slug"}),
+        ("https://www.aloula.sa/de/live/slug/abc", {"live_slug": "slug"}),
+        ("https://www.aloula.sa/episode/123", {"vod_id": "123"}),
+        ("https://www.aloula.sa/en/episode/123", {"vod_id": "123"}),
+        ("https://www.aloula.sa/episode/123abc/456", {"vod_id": "123"}),
+        ("https://www.aloula.sa/de/episode/123abc/456", {"vod_id": "123"}),
+        ("https://www.aloula.sa/episode/123?continue=8", {"vod_id": "123"}),
+        ("https://www.aloula.sa/xx/episode/123?continue=8", {"vod_id": "123"}),
+    ]
+
+    should_not_match = [
+        "https://www.aloula.sa/en/any",
+        "https://www.aloula.sa/de/any/path",
+        "https://www.aloula.sa/live/",
+        "https://www.aloula.sa/abc/live/slug",
+        "https://www.aloula.sa/en/live/",
+        "https://www.aloula.sa/episode/",
+        "https://www.aloula.sa/abc/episode/123",
+        "https://www.aloula.sa/en/episode/",
+        "https://www.aloula.sa/episode/abc",
+        "https://www.aloula.sa/de/episode/abc",
+    ]