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:
18928172992817182: When you get around to this PR I made two commits here because I missed an import issue in my first commit. Is there a way you can squash the commits into one during the merge? It feels unnecessary with two separate commits for this PR. I know it can be done with Bitbucket when merging PR and hopefully Github has the same functionality.

Regards
bastimeyer: > Is there a way you can squash the commits into one during the merge?

Sure, but you can also do it yourself by rebasing via `git rebase --interactive master` and force-pushing your branch via `git push --force`.

Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 42538223..8ddd63ba 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -17,7 +17,6 @@ albavision              - tvc.com.ec         Yes   No    Some streams are geo-re
                         - rts.com.ec
                         - elnueve.com.ar
                         - atv.pe
-animelab                animelab.com         --    Yes   Requires a login. Streams may be geo-restricted to Australia and New Zealand.
 app17                   17app.co             Yes   --
 ard_live                daserste.de          Yes   Yes   Streams may be geo-restricted to Germany.
 ard_mediathek           - ardmediathek.de    Yes   Yes   Streams may be geo-restricted to Germany.
diff --git a/src/streamlink/plugins/.removed b/src/streamlink/plugins/.removed
index a541a831..4ac8249e 100644
--- a/src/streamlink/plugins/.removed
+++ b/src/streamlink/plugins/.removed
@@ -6,6 +6,7 @@ aftonbladet
 aliez
 alieztv
 aljazeeraen
+animelab
 antenna
 apac
 arconai
diff --git a/src/streamlink/plugins/animelab.py b/src/streamlink/plugins/animelab.py
deleted file mode 100644
index 888da43d..00000000
--- a/src/streamlink/plugins/animelab.py
+++ /dev/null
@@ -1,100 +0,0 @@
-import logging
-import re
-
-from streamlink.plugin import Plugin, PluginArgument, PluginArguments, pluginmatcher
-from streamlink.plugin.api import validate
-from streamlink.stream import HTTPStream
-from streamlink.utils import parse_json
-
-log = logging.getLogger(__name__)
-
-
-@pluginmatcher(re.compile(
-    r"https?://(?:www\.)?animelab\.com/player/"
-))
-class AnimeLab(Plugin):
-    login_url = "https://www.animelab.com/login"
-    video_collection_re = re.compile(r"VideoCollection\((\[.*?\])\);")
-    playlist_position_re = re.compile(r"playlistPosition\s*=\s*(\d+);")
-    video_collection_schema = validate.Schema(
-        validate.union({
-            "position": validate.all(
-                validate.transform(playlist_position_re.search),
-                validate.any(
-                    None,
-                    validate.all(validate.get(1), validate.transform(int))
-                )
-            ),
-            "playlist": validate.all(
-                validate.transform(video_collection_re.search),
-                validate.any(
-                    None,
-                    validate.all(
-                        validate.get(1),
-                        validate.transform(parse_json)
-                    )
-                )
-            )
-        })
-    )
-    arguments = PluginArguments(
-        PluginArgument(
-            "email",
-            requires=["password"],
-            metavar="EMAIL",
-            help="The email address used to register with animelab.com."
-        ),
-        PluginArgument(
-            "password",
-            sensitive=True,
-            metavar="PASSWORD",
-            help="A animelab.com account password to use with --animelab-email."
-        )
-    )
-
-    def login(self, email, password):
-        log.debug("Attempting to log in as {0}".format(email))
-        res = self.session.http.post(
-            self.login_url,
-            data=dict(email=email, password=password),
-            allow_redirects=False,
-            raise_for_status=False
-        )
-        loc = res.headers.get("Location", "")
-        if "geoblocked" in loc.lower():
-            log.error("AnimeLab is not available in your territory")
-        elif res.status_code >= 400:
-            log.error("Failed to login to AnimeLab, check your email/password combination")
-        else:
-            return True
-
-        return False
-
-    def _get_streams(self):
-        email, password = self.get_option("email"), self.get_option("password")
-        if not email or not password:
-            log.error("AnimeLab requires authentication, use --animelab-email "
-                      "and --animelab-password to set your email/password combination")
-            return
-
-        if self.login(email, password):
-            log.info(f"Successfully logged in as {email}")
-            video_collection = self.session.http.get(self.url, schema=self.video_collection_schema)
-            if video_collection["playlist"] is None or video_collection["position"] is None:
-                return
-
-            data = video_collection["playlist"][video_collection["position"]]
-
-            log.debug("Found {0} version {1} hard-subs".format(
-                data["language"]["name"],
-                "with" if data["hardSubbed"] else "without"
-            ))
-
-            for video in data["videoInstances"]:
-                if video["httpUrl"]:
-                    q = video["videoQuality"]["description"]
-                    s = HTTPStream(self.session, video["httpUrl"])
-                    yield q, s
-
-
-__plugin__ = AnimeLab
diff --git a/src/streamlink/plugins/svtplay.py b/src/streamlink/plugins/svtplay.py
index 367f1e3e..1eea9325 100644
--- 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 @@ class SVTPlay(Plugin):
                 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 @@ class SVTPlay(Plugin):
                     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:
diff --git a/tests/plugins/test_animelab.py b/tests/plugins/test_animelab.py
deleted file mode 100644
index fd1dd1ed..00000000
--- a/tests/plugins/test_animelab.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from streamlink.plugins.animelab import AnimeLab
-from tests.plugins import PluginCanHandleUrl
-
-
-class TestPluginCanHandleUrlAnimeLab(PluginCanHandleUrl):
-    __plugin__ = AnimeLab
-
-    should_match = [
-        'https://www.animelab.com/player/123',
-        'https://animelab.com/player/123',
-    ]