Repository: streamlink/streamlink

Issue:
plugins.svtplay: Cannot resolve playable stream
### Checklist

- [X] This is a plugin issue 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 issues](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

Latest stable release

### Description

When trying to play for example https://www.svtplay.se/video/32279075/draknastet/draknastet-sasong-1-avsnitt-2?id=KA2BmZD

It does not seem to parse the information correctly anymore to resolve a playable stream. This plugin compared to the one for tv4play is parsing the HTML page in order to resolve where to go if you are on the program page instead of the episode page.

This lookup resolves to nothing anymore:

```
latest_episode_url_re = re.compile(r'''
        class="play_titlepage__latest-video"\s+href="(?P<url>[^"]+)"
    ''', re.VERBOSE)
```

When debugging I find that this https://api.svt.se/video/KA2BmZD resolves the JSON.

```
{
  "svtId": "KA2BmZD",
  "programVersionId": "1400537-002A",
  "contentDuration": 3506,
  "blockedForChildren": false,
  "live": false,
  "programTitle": "Draknästet",
  "episodeTitle": "Avsnitt 2",
   ...
  }
```

With the following changes it resolves the video_id correctly from the HTML:
```
latest_episode_vod_id = re.compile(r'''
        data-rt="top-area-play-button"\s+href=".*(?:id=)(?P<video_id>[^"]+)"
    ''', re.VERBOSE)
```

If you are directly on the play page of the site you get the vod_id in the URL as a parameter. So I have refactored to support both.

Now it finds the vod_id and initiates the dash stream worker but it still doesn't run.

I get Exception in thread Thread-DASHStreamWorker and this one seems a little more tricky to figure out.

```
[cli][info] Opening stream: 1080p (dash)
[stream.dash][debug] Opening DASH reader for: 0 (video/mp4)
[stream.dash][debug] Opening DASH reader for: 5 (audio/mp4)
[stream.dash_manifest][debug] Generating segment timeline for static playlist (id=0))
[stream.dash_manifest][debug] Generating segment timeline for static playlist (id=5))
[cli][error] Try 1/1: Could not open stream <Stream()> (Could not open stream: cannot use FFMPEG)
[stream.ffmpegmux][debug] Closing ffmpeg thread
error: Could not open stream <Stream()>, tried 1 times, exiting
[stream.dash][debug] Download of segment: https://svt-vod-8r.akamaized.net/d0/se/20210723/841d135d-fb92-4acb-9dff-898c1db4af30/cmaf-video-avc-1920x1080p25-3089/cmaf-video-avc-1920x1080p25-3089-init.mp4 complete
Exception in thread Thread-DASHStreamWorker:
Traceback (most recent call last):
  File "/usr/lib64/python3.9/threading.py", line 973, in _bootstrap_inner
    self.run()
  File "/usr/local/lib/python3.9/site-packages/streamlink-2.1.2+79.g2b9ca5d.dirty-py3.9.egg/streamlink/stream/segmented.py", line 87, in run
    self.writer.put(segment)
  File "/usr/local/lib/python3.9/site-packages/streamlink-2.1.2+79.g2b9ca5d.dirty-py3.9.egg/streamlink/stream/segmented.py", line 140, in put
    future = self.executor.submit(self.fetch, segment, retries=self.retries)
  File "/usr/lib64/python3.9/concurrent/futures/thread.py", line 163, in submit
    raise RuntimeError('cannot schedule new futures after '
RuntimeError: cannot schedule new futures after interpreter shutdown
```


### Debug log

```text
[cli][debug] OS:         Linux-5.13.12-200.fc34.x86_64-x86_64-with-glibc2.33
[cli][debug] Python:     3.9.6
[cli][debug] Streamlink: 2.3.0
[cli][debug] Requests(2.25.1), Socks(1.7.1), Websocket(0.57.0)
[cli][debug] Arguments:
[cli][debug]  url=https://www.svtplay.se/video/32279075/draknastet/draknastet-sasong-1-avsnitt-2?id=KA2BmZD
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --player=mpv
[cli][debug]  --verbose-player=True
[cli][debug]  --player-passthrough=['hls']
[cli][debug]  --hls-segment-threads=2
[cli][info] Found matching plugin svtplay for URL https://www.svtplay.se/video/32279075/draknastet/draknastet-sasong-1-avsnitt-2?id=KA2BmZD
[plugins.svtplay][debug] Path=/
error: No playable streams found on this URL: https://www.svtplay.se/video/32279075/draknastet/draknastet-sasong-1-avsnitt-2?id=KA2BmZD
```



Discussion:
Oh, LUL. I didn't have ffmpeg installed and you seem to have to have that in order for this plugin to work. Hm, the others work without it so why is this plugin requiring it?

Well, with ffmpeg installed I can confirm that my refactoring worked and it can resolve streams and play correctly again. I guess I will push that part at least and then think about why this plugin requires a dependency the others I use don't.

Resolving patch:
diff --git a/src/streamlink/plugins/svtplay.py b/src/streamlink/plugins/svtplay.py
--- a/src/streamlink/plugins/svtplay.py
+++ b/src/streamlink/plugins/svtplay.py
@@ -1,6 +1,6 @@
 import logging
 import re
-from urllib.parse import urljoin
+from urllib.parse import parse_qsl, urlparse
 
 from streamlink.plugin import Plugin, PluginArgument, PluginArguments, pluginmatcher
 from streamlink.plugin.api import validate
@@ -21,16 +21,11 @@ class SVTPlay(Plugin):
     title = None
 
     latest_episode_url_re = re.compile(r'''
-        class="play_titlepage__latest-video"\s+href="(?P<url>[^"]+)"
+        data-rt="top-area-play-button"\s+href="(?P<url>[^"]+)"
     ''', re.VERBOSE)
 
     live_id_re = re.compile(r'.*/(?P<live_id>[^?]+)')
 
-    vod_id_re = re.compile(r'''
-        (?:DATA_LAKE\s+=\s+{"content":{"id":|"svtId":|data-video-id=)
-        "(?P<vod_id>[^"]+)"
-    ''', re.VERBOSE)
-
     _video_schema = validate.Schema({
         validate.optional('programTitle'): validate.text,
         validate.optional('episodeTitle'): validate.text,
@@ -87,18 +82,18 @@ def _get_live(self, path):
                 yield from DASHStream.parse_manifest(self.session, playlist['url']).items()
 
     def _get_vod(self):
-        res = self.session.http.get(self.url)
-        match = self.latest_episode_url_re.search(res.text)
-        if match:
-            res = self.session.http.get(
-                urljoin(self.url, match.group('url')),
-            )
-
-        match = self.vod_id_re.search(res.text)
-        if match is None:
+        vod_id = self._get_vod_id(self.url)
+
+        if vod_id is None:
+            res = self.session.http.get(self.url)
+            match = self.latest_episode_url_re.search(res.text)
+            if match is None:
+                return
+            vod_id = self._get_vod_id(match.group("url"))
+
+        if vod_id is None:
             return
 
-        vod_id = match.group('vod_id')
         log.debug("VOD ID={0}".format(vod_id))
 
         res = self.session.http.get(self.api_url.format(vod_id))
@@ -124,6 +119,10 @@ def _get_vod(self):
                     else:
                         yield q, s
 
+    def _get_vod_id(self, url):
+        qs = dict(parse_qsl(urlparse(url).query))
+        return qs.get("id")
+
     def _get_streams(self):
         path, live = self.match.groups()
         log.debug("Path={0}".format(path))


Test patch: