Repository: streamlink/streamlink

Issue:
plugins.twitch: TypeError: exception causes must derive from BaseException
### Checklist

- [X] This is a [plugin issue](https://streamlink.github.io/plugins.html) and not [a different kind of issue](https://github.com/streamlink/streamlink/issues/new/choose)
- [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

streamlink 6.2.0

### Description

I'm attempting to get a link for the free and publicly available stream of Thursday Night Football on Amazon's Twitch channel (twitch.tv/primevideo) to add to my Channels DVR so I can watch as if it was a traditional broadcast. It doesn't appear to matter if the channel is live or not as attempting to query offline channels simply returns a `No playable streams found on this URL` message whereas this causes a crash. 

### Debug log

```text
PS C:\Users\*user*> streamlink twitch.tv/primevideo best
[cli][info] Found matching plugin twitch for URL twitch.tv/primevideo
Traceback (most recent call last):
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 894, in _get_hls_streams
    streams = TwitchHLSStream.parse_variant_playlist(
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\stream\hls.py", line 710, in parse_variant_playlist
    res = cls._fetch_variant_playlist(session, url, **request_args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\stream\hls.py", line 666, in _fetch_variant_playlist
    res = session.http.get(url, exception=OSError, **request_args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\requests\sessions.py", line 602, in get
    return self.request("GET", url, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugin\api\http_session.py", line 190, in request
    raise err from None  # TODO: fix this
    ^^^^^^^^^^^^^^^^^^^
OSError: Unable to open URL: https://usher.ttvnw.net/api/channel/hls/primevideo.m3u8?player=twitchweb&p=358891&type=any&allow_source=true&allow_audio_only=true&allow_spectre=false&sig=f73c97aef3f78f0a09540d793663adcf94cdacfe&token=%7B%22adblock%22%3Afalse%2C%22authorization%22%3A%7B%22forbidden%22%3Afalse%2C%22reason%22%3A%22%22%7D%2C%22blackout_enabled%22%3Afalse%2C%22channel%22%3A%22primevideo%22%2C%22channel_id%22%3A168843586%2C%22chansub%22%3A%7B%22restricted_bitrates%22%3A%5B%5D%2C%22view_until%22%3A1924905600%7D%2C%22ci_gb%22%3Afalse%2C%22geoblock_reason%22%3A%22%22%2C%22device_id%22%3Anull%2C%22expires%22%3A1696547649%2C%22extended_history_allowed%22%3Afalse%2C%22game%22%3A%22%22%2C%22hide_ads%22%3Afalse%2C%22https_required%22%3Atrue%2C%22mature%22%3Afalse%2C%22partner%22%3Afalse%2C%22platform%22%3A%22web%22%2C%22player_type%22%3A%22embed%22%2C%22private%22%3A%7B%22allowed_to_view%22%3Atrue%7D%2C%22privileged%22%3Afalse%2C%22role%22%3A%22%22%2C%22server_ads%22%3Atrue%2C%22show_ads%22%3Atrue%2C%22subscriber%22%3Afalse%2C%22turbo%22%3Afalse%2C%22user_id%22%3Anull%2C%22user_ip%22%3A%2273.197.231.235%22%2C%22version%22%3A2%7D&fast_bread=True (403 Client Error: Forbidden for url: https://usher.ttvnw.net/api/channel/hls/primevideo.m3u8?player=twitchweb&p=358891&type=any&allow_source=true&allow_audio_only=true&allow_spectre=false&sig=f73c97aef3f78f0a09540d793663adcf94cdacfe&token=%7B%22adblock%22%3Afalse%2C%22authorization%22%3A%7B%22forbidden%22%3Afalse%2C%22reason%22%3A%22%22%7D%2C%22blackout_enabled%22%3Afalse%2C%22channel%22%3A%22primevideo%22%2C%22channel_id%22%3A168843586%2C%22chansub%22%3A%7B%22restricted_bitrates%22%3A%5B%5D%2C%22view_until%22%3A1924905600%7D%2C%22ci_gb%22%3Afalse%2C%22geoblock_reason%22%3A%22%22%2C%22device_id%22%3Anull%2C%22expires%22%3A1696547649%2C%22extended_history_allowed%22%3Afalse%2C%22game%22%3A%22%22%2C%22hide_ads%22%3Afalse%2C%22https_required%22%3Atrue%2C%22mature%22%3Afalse%2C%22partner%22%3Afalse%2C%22platform%22%3A%22web%22%2C%22player_type%22%3A%22embed%22%2C%22private%22%3A%7B%22allowed_to_view%22%3Atrue%7D%2C%22privileged%22%3Afalse%2C%22role%22%3A%22%22%2C%22server_ads%22%3Atrue%2C%22show_ads%22%3Atrue%2C%22subscriber%22%3Afalse%2C%22turbo%22%3Afalse%2C%22user_id%22%3Anull%2C%22user_ip%22%3A%2273.197.231.235%22%2C%22version%22%3A2%7D&fast_bread=True)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\bin\streamlink.exe\__main__.py", line 18, in <module>
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink_cli\main.py", line 929, in main
    handle_url()
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink_cli\main.py", line 551, in handle_url
    streams = fetch_streams(plugin)
              ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink_cli\main.py", line 449, in fetch_streams
    return plugin.streams(stream_types=args.stream_types,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugin\plugin.py", line 366, in streams
    ostreams = self._get_streams()
               ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 930, in _get_streams
    return self._get_hls_streams_live()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 875, in _get_hls_streams_live
    return self._get_hls_streams(url, restricted_bitrates)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 907, in _get_hls_streams
    raise PluginError(err) from err
TypeError: exception causes must derive from BaseException
PS C:\Users\*user*> streamlink twitch.tv/primevideo --disable-ads
usage: streamlink [OPTIONS] <URL> [STREAM]
streamlink: error: unrecognized arguments: --disable-ads
PS C:\Users\*user*> streamlink twitch.tv/primevideo --twitch-disable-ads
[cli][info] Found matching plugin twitch for URL twitch.tv/primevideo
Traceback (most recent call last):
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 894, in _get_hls_streams
    streams = TwitchHLSStream.parse_variant_playlist(
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\stream\hls.py", line 710, in parse_variant_playlist
    res = cls._fetch_variant_playlist(session, url, **request_args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\stream\hls.py", line 666, in _fetch_variant_playlist
    res = session.http.get(url, exception=OSError, **request_args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\requests\sessions.py", line 602, in get
    return self.request("GET", url, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugin\api\http_session.py", line 190, in request
    raise err from None  # TODO: fix this
    ^^^^^^^^^^^^^^^^^^^
OSError: Unable to open URL: https://usher.ttvnw.net/api/channel/hls/primevideo.m3u8?player=twitchweb&p=545759&type=any&allow_source=true&allow_audio_only=true&allow_spectre=false&sig=3fafdeab499760891161dd22b67516af6b183667&token=%7B%22adblock%22%3Afalse%2C%22authorization%22%3A%7B%22forbidden%22%3Afalse%2C%22reason%22%3A%22%22%7D%2C%22blackout_enabled%22%3Afalse%2C%22channel%22%3A%22primevideo%22%2C%22channel_id%22%3A168843586%2C%22chansub%22%3A%7B%22restricted_bitrates%22%3A%5B%5D%2C%22view_until%22%3A1924905600%7D%2C%22ci_gb%22%3Afalse%2C%22geoblock_reason%22%3A%22%22%2C%22device_id%22%3Anull%2C%22expires%22%3A1696547741%2C%22extended_history_allowed%22%3Afalse%2C%22game%22%3A%22%22%2C%22hide_ads%22%3Afalse%2C%22https_required%22%3Atrue%2C%22mature%22%3Afalse%2C%22partner%22%3Afalse%2C%22platform%22%3A%22web%22%2C%22player_type%22%3A%22embed%22%2C%22private%22%3A%7B%22allowed_to_view%22%3Atrue%7D%2C%22privileged%22%3Afalse%2C%22role%22%3A%22%22%2C%22server_ads%22%3Atrue%2C%22show_ads%22%3Atrue%2C%22subscriber%22%3Afalse%2C%22turbo%22%3Afalse%2C%22user_id%22%3Anull%2C%22user_ip%22%3A%2273.197.231.235%22%2C%22version%22%3A2%7D&fast_bread=True (403 Client Error: Forbidden for url: https://usher.ttvnw.net/api/channel/hls/primevideo.m3u8?player=twitchweb&p=545759&type=any&allow_source=true&allow_audio_only=true&allow_spectre=false&sig=3fafdeab499760891161dd22b67516af6b183667&token=%7B%22adblock%22%3Afalse%2C%22authorization%22%3A%7B%22forbidden%22%3Afalse%2C%22reason%22%3A%22%22%7D%2C%22blackout_enabled%22%3Afalse%2C%22channel%22%3A%22primevideo%22%2C%22channel_id%22%3A168843586%2C%22chansub%22%3A%7B%22restricted_bitrates%22%3A%5B%5D%2C%22view_until%22%3A1924905600%7D%2C%22ci_gb%22%3Afalse%2C%22geoblock_reason%22%3A%22%22%2C%22device_id%22%3Anull%2C%22expires%22%3A1696547741%2C%22extended_history_allowed%22%3Afalse%2C%22game%22%3A%22%22%2C%22hide_ads%22%3Afalse%2C%22https_required%22%3Atrue%2C%22mature%22%3Afalse%2C%22partner%22%3Afalse%2C%22platform%22%3A%22web%22%2C%22player_type%22%3A%22embed%22%2C%22private%22%3A%7B%22allowed_to_view%22%3Atrue%7D%2C%22privileged%22%3Afalse%2C%22role%22%3A%22%22%2C%22server_ads%22%3Atrue%2C%22show_ads%22%3Atrue%2C%22subscriber%22%3Afalse%2C%22turbo%22%3Afalse%2C%22user_id%22%3Anull%2C%22user_ip%22%3A%2273.197.231.235%22%2C%22version%22%3A2%7D&fast_bread=True)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
PS C:\Users\*user*> streamlink twitch.tv/primevideo best
[cli][info] Found matching plugin twitch for URL twitch.tv/primevideo
Traceback (most recent call last):
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 894, in _get_hls_streams
    streams = TwitchHLSStream.parse_variant_playlist(
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\stream\hls.py", line 710, in parse_variant_playlist
    res = cls._fetch_variant_playlist(session, url, **request_args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\stream\hls.py", line 666, in _fetch_variant_playlist
    res = session.http.get(url, exception=OSError, **request_args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\requests\sessions.py", line 602, in get
    return self.request("GET", url, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugin\api\http_session.py", line 190, in request
    raise err from None  # TODO: fix this
    ^^^^^^^^^^^^^^^^^^^
OSError: Unable to open URL: https://usher.ttvnw.net/api/channel/hls/primevideo.m3u8?player=twitchweb&p=686255&type=any&allow_source=true&allow_audio_only=true&allow_spectre=false&sig=d4cbfd5096874231459d870a473420927e28f7c3&token=%7B%22adblock%22%3Afalse%2C%22authorization%22%3A%7B%22forbidden%22%3Afalse%2C%22reason%22%3A%22%22%7D%2C%22blackout_enabled%22%3Afalse%2C%22channel%22%3A%22primevideo%22%2C%22channel_id%22%3A168843586%2C%22chansub%22%3A%7B%22restricted_bitrates%22%3A%5B%5D%2C%22view_until%22%3A1924905600%7D%2C%22ci_gb%22%3Afalse%2C%22geoblock_reason%22%3A%22%22%2C%22device_id%22%3Anull%2C%22expires%22%3A1696547814%2C%22extended_history_allowed%22%3Afalse%2C%22game%22%3A%22%22%2C%22hide_ads%22%3Afalse%2C%22https_required%22%3Atrue%2C%22mature%22%3Afalse%2C%22partner%22%3Afalse%2C%22platform%22%3A%22web%22%2C%22player_type%22%3A%22embed%22%2C%22private%22%3A%7B%22allowed_to_view%22%3Atrue%7D%2C%22privileged%22%3Afalse%2C%22role%22%3A%22%22%2C%22server_ads%22%3Atrue%2C%22show_ads%22%3Atrue%2C%22subscriber%22%3Afalse%2C%22turbo%22%3Afalse%2C%22user_id%22%3Anull%2C%22user_ip%22%3A%2273.197.231.235%22%2C%22version%22%3A2%7D&fast_bread=True (403 Client Error: Forbidden for url: https://usher.ttvnw.net/api/channel/hls/primevideo.m3u8?player=twitchweb&p=686255&type=any&allow_source=true&allow_audio_only=true&allow_spectre=false&sig=d4cbfd5096874231459d870a473420927e28f7c3&token=%7B%22adblock%22%3Afalse%2C%22authorization%22%3A%7B%22forbidden%22%3Afalse%2C%22reason%22%3A%22%22%7D%2C%22blackout_enabled%22%3Afalse%2C%22channel%22%3A%22primevideo%22%2C%22channel_id%22%3A168843586%2C%22chansub%22%3A%7B%22restricted_bitrates%22%3A%5B%5D%2C%22view_until%22%3A1924905600%7D%2C%22ci_gb%22%3Afalse%2C%22geoblock_reason%22%3A%22%22%2C%22device_id%22%3Anull%2C%22expires%22%3A1696547814%2C%22extended_history_allowed%22%3Afalse%2C%22game%22%3A%22%22%2C%22hide_ads%22%3Afalse%2C%22https_required%22%3Atrue%2C%22mature%22%3Afalse%2C%22partner%22%3Afalse%2C%22platform%22%3A%22web%22%2C%22player_type%22%3A%22embed%22%2C%22private%22%3A%7B%22allowed_to_view%22%3Atrue%7D%2C%22privileged%22%3Afalse%2C%22role%22%3A%22%22%2C%22server_ads%22%3Atrue%2C%22show_ads%22%3Atrue%2C%22subscriber%22%3Afalse%2C%22turbo%22%3Afalse%2C%22user_id%22%3Anull%2C%22user_ip%22%3A%2273.197.231.235%22%2C%22version%22%3A2%7D&fast_bread=True)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\bin\streamlink.exe\__main__.py", line 18, in <module>
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink_cli\main.py", line 929, in main
    handle_url()
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink_cli\main.py", line 551, in handle_url
    streams = fetch_streams(plugin)
              ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink_cli\main.py", line 449, in fetch_streams
    return plugin.streams(stream_types=args.stream_types,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugin\plugin.py", line 366, in streams
    ostreams = self._get_streams()
               ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 930, in _get_streams
    return self._get_hls_streams_live()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 875, in _get_hls_streams_live
    return self._get_hls_streams(url, restricted_bitrates)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 907, in _get_hls_streams
    raise PluginError(err) from err
TypeError: exception causes must derive from BaseException
```


Discussion:
adamef93: I can test it out but need a little guidance if possible. I have streamlink installed via winget on my PC and via homebrew on my Macbook. What would be the simplest way to replace the necessary files with what you wrote so I can try it out?

Ignore this if someone actually involved with streamlink development needs to test it
bastimeyer: Take a look here:
https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#pull-request-feedback
adamef93: High probability I did this wrong but here's what I got. At least the giant original error message is gone

First attempt only with new `twitch.py` in `Appdata\Roaming\streamlink\plugins`
```
PS C:\Users\*user*> streamlink twitch.tv/primevideo best
[cli][info] Found matching plugin twitch for URL twitch.tv/primevideo
[plugins.twitch][error] no_cdm_specified
error: No playable streams found on this URL: twitch.tv/primevideo
```
Second attempt with both `twitch.py` and `test_twitch.py` in `Appdata\Roaming\streamlink\plugins`
```
PS C:\Users\*user*> streamlink twitch.tv/primevideo best
[session][error] Failed to load plugin test_twitch from C:\Users\*user*\AppData\Roaming\streamlink\plugins
Traceback (most recent call last):
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\session.py", line 649, in load_plugins   
    mod = load_module(module_name, path)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\utils\module.py", line 14, in load_module
    spec.loader.exec_module(mod)
  File "<frozen importlib._bootstrap_external>", line 940, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "C:\Users\*user*\AppData\Roaming\streamlink\plugins\test_twitch.py", line 6, in <module>
    import pytest
ModuleNotFoundError: No module named 'pytest'
[cli][info] Found matching plugin twitch for URL twitch.tv/primevideo
[plugins.twitch][error] no_cdm_specified
error: No playable streams found on this URL: twitch.tv/primevideo

```
bastimeyer: Just to make this clear, this fix is about error handling, not about making "primevideo" work when you don't have access. If you can access the channel on Twitch's site but not via Streamlink, then this requires different changes, but as said, I can't access it, so I can't fix this.

Log outputs without the `debug` level are always meaningless, hence the requirement in plugin issues for `--loglevel=debug` (you've ignored that).
https://streamlink.github.io/cli.html#cmdoption-loglevel

The test file doesn't belong there, as you can clearly see from the error message after you've copied the file there.

> [plugins.twitch][error] no_cdm_specified

It's possible that in order to access these kind of streams that another access token parameter is required.
adamef93: My mistake, here's the output with the debug flag and the test file removed.

```
PS C:\Users\*user*> streamlink twitch.tv/primevideo best --loglevel=debug 
[session][debug] Plugin twitch is being overridden by C:\Users\*user*\AppData\Roaming\streamlink\plugins\twitch.py
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.11.5
[cli][debug] OpenSSL:    OpenSSL 3.0.9 30 May 2023
[cli][debug] Streamlink: 6.2.0
[cli][debug] Dependencies:
[cli][debug]  certifi: 2023.7.22
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.3
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.18.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  trio: 0.22.2
[cli][debug]  trio-websocket: 0.10.4
[cli][debug]  typing-extensions: 4.7.1
[cli][debug]  urllib3: 2.0.4
[cli][debug]  websocket-client: 1.6.3
[cli][debug] Arguments:
[cli][debug]  url=twitch.tv/primevideo
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --ffmpeg-ffmpeg=C:\Users\*user*\AppData\Local\Programs\Streamlink\ffmpeg\ffmpeg.exe
[cli][info] Found matching plugin twitch for URL twitch.tv/primevideo
[plugins.twitch][debug] Getting live HLS streams for primevideo
[plugins.twitch][debug] {'adblock': False, 'geoblock_reason': '', 'hide_ads': False, 'server_ads': True, 'show_ads': True}
[utils.l10n][debug] Language code: en_US
[plugins.twitch][error] no_cdm_specified
error: No playable streams found on this URL: twitch.tv/primevideo
```
bastimeyer: I'm going to merge this PR now.

Please open a new plugin issue with the `no_cdm_specified` error, so this can be tracked separately. Thanks.

Patch:
diff --git a/src/streamlink/plugins/twitch.py b/src/streamlink/plugins/twitch.py
index e2c65d3c..67509f56 100644
--- a/src/streamlink/plugins/twitch.py
+++ b/src/streamlink/plugins/twitch.py
@@ -17,12 +17,15 @@ import base64
 import logging
 import re
 import sys
+from contextlib import suppress
 from datetime import datetime, timedelta
 from json import dumps as json_dumps
 from random import random
 from typing import List, Mapping, NamedTuple, Optional, Tuple
 from urllib.parse import urlparse
 
+from requests.exceptions import HTTPError
+
 from streamlink.exceptions import NoStreamsError, PluginError
 from streamlink.plugin import Plugin, pluginargument, pluginmatcher
 from streamlink.plugin.api import validate
@@ -900,11 +903,22 @@ class Twitch(Plugin):
                 **extra_params,
             )
         except OSError as err:
-            err = str(err)
-            if "404 Client Error" in err or "Failed to parse playlist" in err:
+            # TODO: fix the "err" attribute set by HTTPSession.request()
+            orig = getattr(err, "err", None)
+            if isinstance(orig, HTTPError) and orig.response.status_code >= 400:
+                error = None
+                with suppress(PluginError):
+                    error = validate.Schema(
+                        validate.parse_json(),
+                        [{
+                            "type": "error",
+                            "error": str,
+                        }],
+                        validate.get((0, "error")),
+                    ).validate(orig.response.text)
+                log.error(error or "Could not access HLS playlist")
                 return
-            else:
-                raise PluginError(err) from err
+            raise PluginError(err) from err
 
         for name in restricted_bitrates:
             if name not in streams:


Test patch:
diff --git a/tests/plugins/test_twitch.py b/tests/plugins/test_twitch.py
index 059cfa04..a0c9d8f0 100644
--- a/tests/plugins/test_twitch.py
+++ b/tests/plugins/test_twitch.py
@@ -1,4 +1,5 @@
 import unittest
+from contextlib import nullcontext
 from datetime import datetime, timedelta, timezone
 from unittest.mock import MagicMock, Mock, call, patch
 
@@ -585,6 +586,60 @@ class TestTwitchAPIAccessToken:
         assert headers["Client-Integrity"] == "client-integrity-token"
 
 
+class TestTwitchHLSMultivariantResponse:
+    @pytest.fixture()
+    def plugin(self, request: pytest.FixtureRequest, requests_mock: rm.Mocker, session: Streamlink):
+        requests_mock.get("mock://multivariant", **getattr(request, "param", {}))
+        return Twitch(session, "https://twitch.tv/channelname")
+
+    @pytest.mark.parametrize(("plugin", "raises", "streams", "log"), [
+        pytest.param(
+            {"text": "#EXTM3U\n"},
+            nullcontext(),
+            {},
+            [],
+            id="success",
+        ),
+        pytest.param(
+            {"text": "Not an HLS playlist"},
+            pytest.raises(PluginError),
+            {},
+            [],
+            id="invalid HLS playlist",
+        ),
+        pytest.param(
+            {
+                "status_code": 403,
+                "json": [{
+                    "url": "mock://multivariant",
+                    "error": "Content Restricted In Region",
+                    "error_code": "content_geoblocked",
+                    "type": "error",
+                }],
+            },
+            nullcontext(),
+            None,
+            [("streamlink.plugins.twitch", "error", "Content Restricted In Region")],
+            id="geo restriction",
+        ),
+        pytest.param(
+            {
+                "status_code": 404,
+                "text": "Not found",
+            },
+            nullcontext(),
+            None,
+            [("streamlink.plugins.twitch", "error", "Could not access HLS playlist")],
+            id="non-json error response",
+        ),
+    ], indirect=["plugin"])
+    def test_multivariant_response(self, caplog: pytest.LogCaptureFixture, plugin: Twitch, raises, streams, log):
+        caplog.set_level("error", "streamlink.plugins.twitch")
+        with raises:
+            assert plugin._get_hls_streams("mock://multivariant", []) == streams
+        assert [(record.name, record.levelname, record.message) for record in caplog.records] == log
+
+
 class TestTwitchMetadata:
     @pytest.fixture()
     def metadata(self, request: pytest.FixtureRequest, session: Streamlink):