Repository: streamlink/streamlink
Pull request #3810 (2021-06-20 16:07:55)

plugins.nos: Fixed Livestream and VOD
```
$ streamlink https://nos.nl/collectie/13781/livestream/2385461-ek-voetbal-voorbeschouwing-italie-wales-18-00-uur
[cli][info] Found matching plugin nos for URL https://nos.nl/collectie/13781/livestream/2385461-ek-voetbal-voorbeschouwing-italie-wales-18-00-uur
[cli][info] Available streams: 150k (worst), 216p, 270p, 432p, 576p, 1080p (best)
[cli][info] Opening stream: 1080p (hls)
```

closes https://github.com/streamlink/streamlink/issues/3801


Patch:
diff --git a/src/streamlink/plugins/nos.py b/src/streamlink/plugins/nos.py
index 5e1fda18..e1d766a9 100644
--- a/src/streamlink/plugins/nos.py
+++ b/src/streamlink/plugins/nos.py
@@ -1,53 +1,91 @@
-"""Plugin for NOS: Nederlandse Omroep Stichting
-
-Supports:
-   MP$: http://nos.nl/uitzending/nieuwsuur.html
-   Live: http://www.nos.nl/livestream/*
-   Tour: http://nos.nl/tour/live
-"""
 import logging
 import re
-from urllib.parse import urljoin
 
 from streamlink.plugin import Plugin
+from streamlink.plugin.api import validate
 from streamlink.plugin.api.utils import itertags
 from streamlink.stream import HLSStream
+from streamlink.utils import parse_json
 
 log = logging.getLogger(__name__)
 
 
 class NOS(Plugin):
-    _url_re = re.compile(r"https?://(?:\w+\.)?nos.nl/")
+    _re_url = re.compile(r"""https?://(?:\w+\.)?nos\.nl/(?:
+        livestream
+        |
+        collectie
+        |
+        video
+        |
+        uitzendingen
+    )""", re.VERBOSE)
+    _msg_live_offline = "This livestream is offline."
+    title = None
+    vod_keys = {
+        "pages/Collection/Video/Video": "item",
+        "pages/Video/Video": "video",
+    }
 
     @classmethod
     def can_handle_url(cls, url):
-        return cls._url_re.match(url)
+        return cls._re_url.match(url) is not None
 
-    def _resolve_stream(self):
-        res = self.session.http.get(self.url)
-        for video in itertags(res.text, 'video'):
-            stream_url = video.attributes.get("data-stream")
-            log.debug("Stream data: {0}".format(stream_url))
-            return HLSStream.parse_variant_playlist(self.session, stream_url)
+    def get_title(self):
+        return self.title
 
-    def _get_source_streams(self):
+    def _get_streams(self):
         res = self.session.http.get(self.url)
+        for script in itertags(res.text, "script"):
+            _type = script.attributes.get("type")
+            if not (_type and _type == "application/json"):
+                continue
 
-        for atag in itertags(res.text, 'a'):
-            if "video-play__link" in atag.attributes.get("class", ""):
-                href = urljoin(self.url, atag.attributes.get("href"))
-                log.debug("Loading embedded video page")
-                vpage = self.session.http.get(href, params=dict(ajax="true", npo_cc_skip_wall="true"))
-                for source in itertags(vpage.text, 'source'):
-                    return HLSStream.parse_variant_playlist(self.session, source.attributes.get("src"))
+            video_url = None
+            _data_ssr_name = script.attributes.get("data-ssr-name")
+            if not _data_ssr_name:
+                continue
 
-    def _get_streams(self):
-        if "/livestream/" in self.url or "/tour/" in self.url:
-            log.debug("Finding live streams")
-            return self._resolve_stream()
-        else:
-            log.debug("Finding VOD streams")
-            return self._get_source_streams()
+            log.trace(f"Found _data_ssr_name={_data_ssr_name}")
+            if _data_ssr_name == "pages/Broadcasts/Broadcasts":
+                self.title, video_url, is_live = parse_json(script.text, schema=validate.Schema({
+                    "currentLivestream": {
+                        "is_live": bool,
+                        "title": str,
+                        "stream": validate.url(),
+                    }},
+                    validate.get("currentLivestream"),
+                    validate.union_get("title", "stream", "is_live")))
+                if not is_live:
+                    log.error(self._msg_live_offline)
+                    continue
+            elif _data_ssr_name == "pages/Livestream/Livestream":
+                self.title, video_url, is_live = parse_json(script.text, schema=validate.Schema({
+                    "streamIsLive": bool,
+                    "title": str,
+                    "stream": validate.url(),
+                }, validate.union_get("title", "stream", "streamIsLive")))
+                if not is_live:
+                    log.error(self._msg_live_offline)
+                    continue
+            elif _data_ssr_name in self.vod_keys.keys():
+                _key = self.vod_keys[_data_ssr_name]
+                self.title, video_url = parse_json(script.text, schema=validate.Schema({
+                    _key: {
+                        "title": str,
+                        "aspect_ratios": {
+                            "profiles": validate.all([{
+                                "name": str,
+                                "url": validate.url(),
+                            }], validate.filter(lambda n: n["name"] == "hls_unencrypted"))
+                        }
+                    }},
+                    validate.get(_key),
+                    validate.union_get("title", ("aspect_ratios", "profiles", 0, "url"))))
+
+            if video_url is not None:
+                yield from HLSStream.parse_variant_playlist(self.session, video_url).items()
+                break
 
 
 __plugin__ = NOS
diff --git a/src/streamlink/plugins/rtvs.py b/src/streamlink/plugins/rtvs.py
index 40e344bc..b1ce725b 100644
--- a/src/streamlink/plugins/rtvs.py
+++ b/src/streamlink/plugins/rtvs.py
@@ -2,65 +2,46 @@ import re
 
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import validate
-from streamlink.stream import HLSStream, RTMPStream
-
-RUURL = "b=chrome&p=win&v=56&f=0&d=1"
-
-_url_re = re.compile(r"https?://www.rtvs.sk/televizia/live-[\w-]+")
-_playlist_url_re = re.compile(r'"playlist": "([^"]+)"')
-
-_playlist_schema = validate.Schema(
-    [
-        {
-            "sources": [
-                validate.any(
-                    {
-                        "type": "dash",
-                        "file": validate.url(scheme="http")
-                    }, {
-                        "type": "hls",
-                        "file": validate.url(scheme="http")
-                    }, {
-                        "type": "rtmp",
-                        "file": validate.text,
-                        "streamer": validate.url(scheme="rtmp")
-                    }
-                )
-            ]
-        }
-    ],
-    validate.get(0),
-    validate.get("sources")
-)
+from streamlink.stream import HLSStream
+from streamlink.utils import parse_json
 
 
 class Rtvs(Plugin):
+    _re_url = re.compile(r"https?://www\.rtvs\.sk/televizia/live-[\w-]+")
+    _re_channel_id = re.compile(r"'stream':\s*'live-(\d+)'")
+
     @classmethod
     def can_handle_url(cls, url):
-        return _url_re.match(url)
+        return cls._re_url.match(url) is not None
 
     def _get_streams(self):
         res = self.session.http.get(self.url)
-        match = _playlist_url_re.search(res.text)
-        if match is None:
+        m = self._re_channel_id.search(res.text)
+        if not m:
             return
 
-        res = self.session.http.get(match.group(1) + RUURL)
-        sources = self.session.http.json(res, schema=_playlist_schema)
-
-        streams = {}
-
-        for source in sources:
-            if source["type"] == "rtmp":
-                streams["rtmp_live"] = RTMPStream(self.session, {
-                    "rtmp": source["streamer"],
-                    "pageUrl": self.url,
-                    "live": True
-                })
-            elif source["type"] == "hls":
-                streams.update(HLSStream.parse_variant_playlist(self.session, source["file"]))
-
-        return streams
+        res = self.session.http.get(
+            "https://www.rtvs.sk/json/live5f.json",
+            params={
+                "c": m.group(1),
+                "b": "mozilla",
+                "p": "win",
+                "f": "0",
+                "d": "1",
+            }
+        )
+        videos = parse_json(res.text, schema=validate.Schema({
+            "clip": {
+                "sources": [{
+                    "src": validate.url(),
+                    "type": str,
+                }],
+            }},
+            validate.get(("clip", "sources")),
+            validate.filter(lambda n: n["type"] == "application/x-mpegurl"),
+        ))
+        for video in videos:
+            yield from HLSStream.parse_variant_playlist(self.session, video["src"]).items()
 
 
 __plugin__ = Rtvs


Test patch:
diff --git a/tests/plugins/test_nos.py b/tests/plugins/test_nos.py
index b5f039a3..9b78a6bf 100644
--- a/tests/plugins/test_nos.py
+++ b/tests/plugins/test_nos.py
@@ -7,4 +7,14 @@ class TestPluginCanHandleUrlNOS(PluginCanHandleUrl):
 
     should_match = [
         'https://nos.nl/livestream/2220100-wk-sprint-schaatsen-1-000-meter-mannen.html',
+        'https://nos.nl/collectie/13781/livestream/2385081-ek-voetbal-engeland-schotland',
+        'https://nos.nl/collectie/13781/livestream/2385461-ek-voetbal-voorbeschouwing-italie-wales-18-00-uur',
+        'https://nos.nl/collectie/13781/video/2385846-ek-in-2-21-gosens-show-tegen-portugal-en-weer-volle-bak-in-boedapest',
+        'https://nos.nl/video/2385779-dronebeelden-tonen-spoor-van-vernieling-bij-leersum',
+        'https://nos.nl/uitzendingen',
+        'https://nos.nl/uitzendingen/livestream/2385462',
+    ]
+
+    should_not_match = [
+        'https://nos.nl/artikel/2385784-explosieve-situatie-leidde-tot-verwoeste-huizen-en-omgewaaide-bomen-leersum',
     ]
diff --git a/tests/plugins/test_rtvs.py b/tests/plugins/test_rtvs.py
index 7bebb4fe..6cbee761 100644
--- a/tests/plugins/test_rtvs.py
+++ b/tests/plugins/test_rtvs.py
@@ -9,6 +9,8 @@ class TestPluginCanHandleUrlRtvs(PluginCanHandleUrl):
         'http://www.rtvs.sk/televizia/live-1',
         'http://www.rtvs.sk/televizia/live-2',
         'http://www.rtvs.sk/televizia/live-o',
+        'https://www.rtvs.sk/televizia/live-3',
+        'https://www.rtvs.sk/televizia/live-rtvs',
     ]
 
     should_not_match = [