Repository: streamlink/streamlink

Issue:
Blaze TV, a channel from the UK
### 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

Blaze TV is a TV channel from the UK, which mainly broadcasts entertainment shows, but sometimes airs documentaries too. Also has VOD content of the shows that are currently on the air or that have been, so that the listener can watch them at their own leisure. No signin is required to watch the channel but you must be in the UK or have a UK VPN or proxy. The streaming URL is protected by a token, a signature, a policy and a pair id of some sort, but I'm not sure whether or not that would be classed as DRM. 

### Input URLs

1. URL to watch the stream: http://watch.blaze.tv/live
2. Streaming API: https://live.blaze.tv/
3. Blaze TV's VODs are on the homepage directly at https://blaze.tv, just click 'Watch Now' under the episode names. Or there is a link in the top menu. https://watch.blaze.tv/replay/553


Discussion:

Resolving patch:
diff --git a/src/streamlink/plugins/blazetv.py b/src/streamlink/plugins/blazetv.py
new file mode 100644
--- /dev/null
+++ b/src/streamlink/plugins/blazetv.py
@@ -0,0 +1,108 @@
+"""
+$description British live TV channel and video on-demand service from Blaze, owned by A&E Networks UK.
+$url blaze.tv
+$type live, vod
+$region United Kingdom
+"""
+
+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?://(?:watch\.)?blaze\.tv/(?:(?P<is_live>live)|watch/replay/\d+)"
+))
+class BlazeTV(Plugin):
+    def _get_live_uvid(self, parsed_html):
+        return validate.validate(validate.Schema(
+            validate.xml_xpath_string(".//div[@id='live-player-root']/@data-player-uvid"),
+            str,
+        ), parsed_html)
+
+    def _get_vod_uvid(self, parsed_html):
+        json_re = re.compile(r"window\.nowPlaying\.setData\(({.*?})\);")
+        return validate.validate(validate.Schema(
+            validate.xml_xpath_string(".//script[contains(text(), 'window.nowPlaying.setData')]"),
+            str,
+            validate.transform(json_re.search),
+            validate.any(
+                None,
+                validate.all(
+                    validate.get(1),
+                    validate.parse_json(),
+                    {
+                        "id": str,
+                        "series_title": str,
+                        "title": str,
+                        "season": str,
+                        "episode": str,
+                    },
+                ),
+            ),
+        ), parsed_html)
+
+    def _get_tokenizer(self, type, uvid):
+        return self.session.http.get(
+            f"https://watch.blaze.tv/stream/{type}/widevine/{uvid}",
+            schema=validate.Schema(
+                validate.parse_json(),
+                {
+                    "tokenizer": {
+                        "url": validate.url(),
+                        "uvid": str,
+                        "expiry": int,
+                        "token": str,
+                    },
+                },
+                validate.get("tokenizer"),
+            ),
+        )
+
+    def _get_streams(self):
+        is_live = self.match.group("is_live")
+        parsed_html = self.session.http.get(self.url, schema=validate.Schema(validate.parse_html()))
+
+        if is_live:
+            uvid = self._get_live_uvid(parsed_html)
+            if not uvid or not uvid.isdecimal():
+                return
+            token_data = self._get_tokenizer("live", uvid)
+            self.id = uvid
+            self.author = "Blaze"
+            self.title = "Live TV"
+            self.category = "Live"
+        else:
+            data = self._get_vod_uvid(parsed_html)
+            if not data["id"] or not data["id"].isdecimal():
+                return
+            token_data = self._get_tokenizer("replay", data["id"])
+            self.id = data["id"]
+            self.author = data["series_title"]
+            self.title = data["title"]
+            self.category = f"S{data['season']}E{data['episode']}"
+
+        log.trace(f"token_data={token_data!r}")
+
+        hls_url = self.session.http.get(
+            token_data["url"],
+            headers={
+                "Token": token_data["token"],
+                "Token-Expiry": str(token_data["expiry"]),
+                "Uvid": token_data["uvid"],
+            },
+            schema=validate.Schema(
+                validate.parse_json(),
+                {"Streams": {"Adaptive": validate.url()}},
+                validate.get(("Streams", "Adaptive")),
+            ),
+        )
+        return HLSStream.parse_variant_playlist(self.session, hls_url)
+
+
+__plugin__ = BlazeTV


Test patch:
diff --git a/tests/plugins/test_blazetv.py b/tests/plugins/test_blazetv.py
new file mode 100644
--- /dev/null
+++ b/tests/plugins/test_blazetv.py
@@ -0,0 +1,18 @@
+from streamlink.plugins.blazetv import BlazeTV
+from tests.plugins import PluginCanHandleUrl
+
+
+class TestPluginCanHandleUrlBlazeTV(PluginCanHandleUrl):
+    __plugin__ = BlazeTV
+
+    should_match_groups = [
+        ("https://blaze.tv/live", {"is_live": "live"}),
+        ("https://watch.blaze.tv/live/", {"is_live": "live"}),
+        ("https://watch.blaze.tv/watch/replay/123456", {}),
+    ]
+
+    should_not_match = [
+        "https://blaze.tv/abc",
+        "https://watch.blaze.tv/watch/replay/",
+        "https://watch.blaze.tv/watch/replay/abc123",
+    ]