Repository: yt-dlp/yt-dlp

Issue:
[PlaySuisse] Failed to download m3u8 information: HTTP Error 400: Bad Request
### DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE

- [X] I understand that I will be **blocked** if I *intentionally* remove or skip any mandatory\* field

### Checklist

- [X] I'm reporting that yt-dlp is broken on a **supported** site
- [X] I've verified that I'm running yt-dlp version **2023.07.06** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
- [X] I've checked that all provided URLs are playable in a browser with the same IP and same login details
- [X] I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
- [X] I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
- [X] I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
- [X] I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required

### Region

Switzerland

### Provide a description that is worded well enough to be understood

Link to Stream/Movie: https://www.playsuisse.ch/watch/2254297?locale=de aborts with Failed to download m3u8 information: HTTP Error 400: Bad Request

Log:

C:\yt-dlp>yt-dlp.exe -F https://www.playsuisse.ch/watch/2254297?locale=de
[PlaySuisse] Extracting URL: https://www.playsuisse.ch/watch/2254297?locale=de
[PlaySuisse] 2254297: Downloading JSON metadata
[PlaySuisse] 2254297: Downloading m3u8 information
WARNING: [PlaySuisse] Failed to download m3u8 information: HTTP Error 400: Bad Request
ERROR: [PlaySuisse] 2254297: No video formats found!; please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U

### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [X] If using API, add `'verbose': True` to `YoutubeDL` params instead
- [X] Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below

### Complete Verbose Output

```shell
[debug] Command-line config: ['-vU', '--embed-metadata', '--embed-subs', '--output', '\\\\192.168.1.150\\downloads\\%(title)s.%(ext)s', '--sub-langs', 'all,-_AD', '--audio-multistreams', '--merge-output-format', 'mkv', '-f', 'bv*', 'https://www.playsuisse.ch/watch/2254297?locale=de']
[debug] Encodings: locale cp1252, fs utf-8, pref cp1252, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version stable@2023.07.06 [b532a3481] (win_exe)
[debug] Python 3.8.10 (CPython AMD64 64bit) - Windows-10-10.0.22621-SP0 (OpenSSL 1.1.1k  25 Mar 2021)
[debug] exe versions: ffmpeg 2022-08-31-git-319e8a49b5-full_build-www.gyan.dev (setts), ffprobe 2022-08-31-git-319e8a49b5-full_build-www.gyan.dev
[debug] Optional libraries: Cryptodome-3.18.0, brotli-1.0.9, certifi-2023.05.07, mutagen-1.46.0, sqlite3-2.6.0, websockets-11.0.3
[debug] Proxy map: {}
[debug] Loaded 1855 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Available version: stable@2023.07.06, Current version: stable@2023.07.06
Current Build Hash: 5ff3e702171a50175c34397494e2d18ce35d771c2110b1e59bd173ec2fb352aa
yt-dlp is up to date (stable@2023.07.06)
[PlaySuisse] Extracting URL: https://www.playsuisse.ch/watch/2254297?locale=de
[PlaySuisse] 2254297: Downloading JSON metadata
[PlaySuisse] 2254297: Downloading m3u8 information
WARNING: [PlaySuisse] Failed to download m3u8 information: HTTP Error 400: Bad Request
ERROR: [PlaySuisse] 2254297: No video formats found!; please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
Traceback (most recent call last):
  File "yt_dlp\YoutubeDL.py", line 1560, in wrapper
  File "yt_dlp\YoutubeDL.py", line 1709, in __extract_info
  File "yt_dlp\YoutubeDL.py", line 1768, in process_ie_result
  File "yt_dlp\YoutubeDL.py", line 2732, in process_video_result
  File "yt_dlp\YoutubeDL.py", line 1073, in raise_no_formats
yt_dlp.utils.ExtractorError: [PlaySuisse] 2254297: No video formats found!; please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
```

[PlaySuisse] Failed to download m3u8 information: HTTP Error 400: Bad Request
### DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE

- [X] I understand that I will be **blocked** if I *intentionally* remove or skip any mandatory\* field

### Checklist

- [X] I'm reporting that yt-dlp is broken on a **supported** site
- [X] I've verified that I'm running yt-dlp version **2023.07.06** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
- [X] I've checked that all provided URLs are playable in a browser with the same IP and same login details
- [X] I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
- [X] I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
- [X] I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
- [X] I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required

### Region

Switzerland

### Provide a description that is worded well enough to be understood

Link to Stream/Movie: https://www.playsuisse.ch/watch/2254297?locale=de aborts with Failed to download m3u8 information: HTTP Error 400: Bad Request

Log:

C:\yt-dlp>yt-dlp.exe -F https://www.playsuisse.ch/watch/2254297?locale=de
[PlaySuisse] Extracting URL: https://www.playsuisse.ch/watch/2254297?locale=de
[PlaySuisse] 2254297: Downloading JSON metadata
[PlaySuisse] 2254297: Downloading m3u8 information
WARNING: [PlaySuisse] Failed to download m3u8 information: HTTP Error 400: Bad Request
ERROR: [PlaySuisse] 2254297: No video formats found!; please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U

### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [X] If using API, add `'verbose': True` to `YoutubeDL` params instead
- [X] Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below

### Complete Verbose Output

```shell
[debug] Command-line config: ['-vU', '--embed-metadata', '--embed-subs', '--output', '\\\\192.168.1.150\\downloads\\%(title)s.%(ext)s', '--sub-langs', 'all,-_AD', '--audio-multistreams', '--merge-output-format', 'mkv', '-f', 'bv*', 'https://www.playsuisse.ch/watch/2254297?locale=de']
[debug] Encodings: locale cp1252, fs utf-8, pref cp1252, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version stable@2023.07.06 [b532a3481] (win_exe)
[debug] Python 3.8.10 (CPython AMD64 64bit) - Windows-10-10.0.22621-SP0 (OpenSSL 1.1.1k  25 Mar 2021)
[debug] exe versions: ffmpeg 2022-08-31-git-319e8a49b5-full_build-www.gyan.dev (setts), ffprobe 2022-08-31-git-319e8a49b5-full_build-www.gyan.dev
[debug] Optional libraries: Cryptodome-3.18.0, brotli-1.0.9, certifi-2023.05.07, mutagen-1.46.0, sqlite3-2.6.0, websockets-11.0.3
[debug] Proxy map: {}
[debug] Loaded 1855 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Available version: stable@2023.07.06, Current version: stable@2023.07.06
Current Build Hash: 5ff3e702171a50175c34397494e2d18ce35d771c2110b1e59bd173ec2fb352aa
yt-dlp is up to date (stable@2023.07.06)
[PlaySuisse] Extracting URL: https://www.playsuisse.ch/watch/2254297?locale=de
[PlaySuisse] 2254297: Downloading JSON metadata
[PlaySuisse] 2254297: Downloading m3u8 information
WARNING: [PlaySuisse] Failed to download m3u8 information: HTTP Error 400: Bad Request
ERROR: [PlaySuisse] 2254297: No video formats found!; please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
Traceback (most recent call last):
  File "yt_dlp\YoutubeDL.py", line 1560, in wrapper
  File "yt_dlp\YoutubeDL.py", line 1709, in __extract_info
  File "yt_dlp\YoutubeDL.py", line 1768, in process_ie_result
  File "yt_dlp\YoutubeDL.py", line 2732, in process_video_result
  File "yt_dlp\YoutubeDL.py", line 1073, in raise_no_formats
yt_dlp.utils.ExtractorError: [PlaySuisse] 2254297: No video formats found!; please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
```



Discussion:
the m3u8 URL giving the error 400 response is:
`https://playsuisse.ch/api/playback?asset_id=2254297&stipo_env=production&language=de`
if opened in browser, you can see its error message is `id_token not found`
which suggests they have added some sort of security token that the extractor is missing
From what I've been able to see with the developer tools, the URL for the m3u8 has now two tokens `id_token` and `userinfo_token`. In a private navigation window from a browser where I never logged in the PlaySuisse website, I've been able to download the m3u8 file using only the `id_token`. Alas, I haven't been able to pinpoint the source for this value while browsing the website.

Is there anything I could do to help you fix this issue?
In the devtools you could investigate various things:
* maybe first, check what is returned when logging in: is there an `id_token` in the response headers or body?
* otherwise, does the site JS still fetch the media data using a POST to https://4bbepzm4ef.execute-api.eu-central-1.amazonaws.com/prod/graphql with post data `{
    'operationName': 'AssetWatch',
    'query': complex_GQL_query_here,
    'variables': {'assetId': media_id}
    }` and headers `{'Content-Type': 'application/json', 'locale': 'de'}`?
* if so, does the `complex_GQL_query_here` match `PlaySuisseIE._GRAPHQL_QUERY` at l.99 of `extractor/playsuisse.py`?
* by all means report any differences that you see or how the data is fetched now.
* in any case, is there any `id_token` in the response?

From your posts I gather that the URL you showed is only missing the `id_token`. Probably the `id_token` is being used to sign the media URLs and https://playsuisse.ch/api/playback is verifying the value.

When logging in, there is this request that returns a 302 that also includes the `id_token`:

Request:
```
scheme
	https
host
	playsuisse.ch
filename
	/api/sso-redirect/login
state
	{"locale":"de","redirectUrl":"https://www.playsuisse.ch/","policy":"b2c_1a__signinv2"}
code
       ...
```

Response:

```
https://www.playsuisse.ch/?refresh_token=...&id_token=...&userinfo_token=...&locale=de
```
And it is then stored in Local Storage under the `persist:root` key:

```json
{
  "userAccount": {
    "refreshToken": "...",
    "idToken": "...",
    "userInfoToken": "...",
    "profileId": "...",
    "sub": "...",
    "residency": false
  }
}

```

> In the devtools you could investigate various things:
> 
> * maybe first, check what is returned when logging in: is there an `id_token` in the response headers or body?

Right after the login, I see a call to `https://login.srgssr.ch/srgssrlogin.onmicrosoft.com/B2C_1A__SignInV2/api/CombinedSigninAndSignup/confirmed` with a payload consisting of (mainly) `csrf_token`, `tx`, and `p` values. The response has a code 302, with a location header to `playsuisse.ch/api/sso-redirect/login` with `state` and `code` parameters. (The `state` parameter is just a JSON with `locale`, `redirectUrl` to the root of playsuisse.ch, and a `policy` of "b2c_1a__signinv2". The `code` parameter is a long string of meaningless characters...) The response to this request has also a code 302 with location set to the root of playsuisse.ch, with three parameters: `refresh_token`, `id_token`, `userinfo_token`, and `locale`. This is the first mention of `id_token` I see in the call list.

> * otherwise, does the site JS still fetch the media data using a POST to https://4bbepzm4ef.execute-api.eu-central-1.amazonaws.com/prod/graphql with post data `{ 'operationName': 'AssetWatch', 'query': complex_GQL_query_here, 'variables': {'assetId': media_id} }` and headers `{'Content-Type': 'application/json', 'locale': 'de'}`?

The only calls to an amazonaws servers are to `
https://ly1wipmmpa.execute-api.eu-central-1.amazonaws.com/prod/remoteconfig`, the first one with the OPTIONS method, the second one with the POST method right after the login. 

The calls to graphql when watching a video are all to `https://www.playsuisse.ch/api/graphql?complex_subs?true&stipo_env=production2`, but none of them have the `AssetWatch` operation name. Here are the ones I see:

- `AppConfig`
- `InPlayerRecos`
- `UserProfileWithPreferences`
- `UpdateUserProfilePReferences`
- `ContinueWatching`
- `Categories`
- `AssetDetail`
- `AssetPlayer`
- `SwimlaneAsset`

None of them have a `query` value, some of them (`AssetDetail` and `SwimlaneAsset` most notably) have a `extensions\persistedQuery` value. And the `variables` map when present is now simply: `variables: {id: "1695597"}`.

> * if so, does the `complex_GQL_query_here` match `PlaySuisseIE._GRAPHQL_QUERY` at l.99 of `extractor/playsuisse.py`?

As I don't see any graphql with operation AssetWatch, I don't think it'll match.

> * by all means report any differences that you see or how the data is fetched now.
> * in any case, is there any `id_token` in the response?
> 
> From your posts I gather that the URL you showed is only missing the `id_token`. Probably the `id_token` is being used to sign the media URLs and https://playsuisse.ch/api/playback is verifying the value.

I've noted that the `id_token` value stays the same from the user connection until logout, i.e. request to multiple videos will use the same token. But it does change after each connection.

Well, this is some great work.

From the UK I found that the old API URL returned programme metadata and media links. Possibly if we have `id_token`, it can be added to those links, or else one or both of the GQL queries for `AssetDetail` and `AssetPlayer` to the site's own API will provided the required extraction data. `user_info` might possibly affect the media links returned, eg whether the audio track is FR, DE, IT.

So then we have to work out how the username and password are munged into the login request.

Also, whether this site has any technical aspects in common with the other Swiss sites supported in `extractor/srgssr.py`?
the m3u8 URL giving the error 400 response is:
`https://playsuisse.ch/api/playback?asset_id=2254297&stipo_env=production&language=de`
if opened in browser, you can see its error message is `id_token not found`
which suggests they have added some sort of security token that the extractor is missing
From what I've been able to see with the developer tools, the URL for the m3u8 has now two tokens `id_token` and `userinfo_token`. In a private navigation window from a browser where I never logged in the PlaySuisse website, I've been able to download the m3u8 file using only the `id_token`. Alas, I haven't been able to pinpoint the source for this value while browsing the website.

Is there anything I could do to help you fix this issue?
In the devtools you could investigate various things:
* maybe first, check what is returned when logging in: is there an `id_token` in the response headers or body?
* otherwise, does the site JS still fetch the media data using a POST to https://4bbepzm4ef.execute-api.eu-central-1.amazonaws.com/prod/graphql with post data `{
    'operationName': 'AssetWatch',
    'query': complex_GQL_query_here,
    'variables': {'assetId': media_id}
    }` and headers `{'Content-Type': 'application/json', 'locale': 'de'}`?
* if so, does the `complex_GQL_query_here` match `PlaySuisseIE._GRAPHQL_QUERY` at l.99 of `extractor/playsuisse.py`?
* by all means report any differences that you see or how the data is fetched now.
* in any case, is there any `id_token` in the response?

From your posts I gather that the URL you showed is only missing the `id_token`. Probably the `id_token` is being used to sign the media URLs and https://playsuisse.ch/api/playback is verifying the value.

When logging in, there is this request that returns a 302 that also includes the `id_token`:

Request:
```
scheme
	https
host
	playsuisse.ch
filename
	/api/sso-redirect/login
state
	{"locale":"de","redirectUrl":"https://www.playsuisse.ch/","policy":"b2c_1a__signinv2"}
code
       ...
```

Response:

```
https://www.playsuisse.ch/?refresh_token=...&id_token=...&userinfo_token=...&locale=de
```
And it is then stored in Local Storage under the `persist:root` key:

```json
{
  "userAccount": {
    "refreshToken": "...",
    "idToken": "...",
    "userInfoToken": "...",
    "profileId": "...",
    "sub": "...",
    "residency": false
  }
}

```

> In the devtools you could investigate various things:
> 
> * maybe first, check what is returned when logging in: is there an `id_token` in the response headers or body?

Right after the login, I see a call to `https://login.srgssr.ch/srgssrlogin.onmicrosoft.com/B2C_1A__SignInV2/api/CombinedSigninAndSignup/confirmed` with a payload consisting of (mainly) `csrf_token`, `tx`, and `p` values. The response has a code 302, with a location header to `playsuisse.ch/api/sso-redirect/login` with `state` and `code` parameters. (The `state` parameter is just a JSON with `locale`, `redirectUrl` to the root of playsuisse.ch, and a `policy` of "b2c_1a__signinv2". The `code` parameter is a long string of meaningless characters...) The response to this request has also a code 302 with location set to the root of playsuisse.ch, with three parameters: `refresh_token`, `id_token`, `userinfo_token`, and `locale`. This is the first mention of `id_token` I see in the call list.

> * otherwise, does the site JS still fetch the media data using a POST to https://4bbepzm4ef.execute-api.eu-central-1.amazonaws.com/prod/graphql with post data `{ 'operationName': 'AssetWatch', 'query': complex_GQL_query_here, 'variables': {'assetId': media_id} }` and headers `{'Content-Type': 'application/json', 'locale': 'de'}`?

The only calls to an amazonaws servers are to `
https://ly1wipmmpa.execute-api.eu-central-1.amazonaws.com/prod/remoteconfig`, the first one with the OPTIONS method, the second one with the POST method right after the login. 

The calls to graphql when watching a video are all to `https://www.playsuisse.ch/api/graphql?complex_subs?true&stipo_env=production2`, but none of them have the `AssetWatch` operation name. Here are the ones I see:

- `AppConfig`
- `InPlayerRecos`
- `UserProfileWithPreferences`
- `UpdateUserProfilePReferences`
- `ContinueWatching`
- `Categories`
- `AssetDetail`
- `AssetPlayer`
- `SwimlaneAsset`

None of them have a `query` value, some of them (`AssetDetail` and `SwimlaneAsset` most notably) have a `extensions\persistedQuery` value. And the `variables` map when present is now simply: `variables: {id: "1695597"}`.

> * if so, does the `complex_GQL_query_here` match `PlaySuisseIE._GRAPHQL_QUERY` at l.99 of `extractor/playsuisse.py`?

As I don't see any graphql with operation AssetWatch, I don't think it'll match.

> * by all means report any differences that you see or how the data is fetched now.
> * in any case, is there any `id_token` in the response?
> 
> From your posts I gather that the URL you showed is only missing the `id_token`. Probably the `id_token` is being used to sign the media URLs and https://playsuisse.ch/api/playback is verifying the value.

I've noted that the `id_token` value stays the same from the user connection until logout, i.e. request to multiple videos will use the same token. But it does change after each connection.

Well, this is some great work.

From the UK I found that the old API URL returned programme metadata and media links. Possibly if we have `id_token`, it can be added to those links, or else one or both of the GQL queries for `AssetDetail` and `AssetPlayer` to the site's own API will provided the required extraction data. `user_info` might possibly affect the media links returned, eg whether the audio track is FR, DE, IT.

So then we have to work out how the username and password are munged into the login request.

Also, whether this site has any technical aspects in common with the other Swiss sites supported in `extractor/srgssr.py`?

Resolving patch:
diff --git a/yt_dlp/extractor/playsuisse.py b/yt_dlp/extractor/playsuisse.py
--- a/yt_dlp/extractor/playsuisse.py
+++ b/yt_dlp/extractor/playsuisse.py
@@ -1,10 +1,18 @@
 import json
 
 from .common import InfoExtractor
-from ..utils import int_or_none, traverse_obj
+from ..utils import (
+    ExtractorError,
+    int_or_none,
+    parse_qs,
+    traverse_obj,
+    update_url_query,
+    urlencode_postdata,
+)
 
 
 class PlaySuisseIE(InfoExtractor):
+    _NETRC_MACHINE = 'playsuisse'
     _VALID_URL = r'https?://(?:www\.)?playsuisse\.ch/(?:watch|detail)/(?:[^#]*[?&]episodeId=)?(?P<id>[0-9]+)'
     _TESTS = [
         {
@@ -134,12 +142,47 @@ class PlaySuisseIE(InfoExtractor):
             id
             url
         }'''
+    _LOGIN_BASE_URL = 'https://login.srgssr.ch/srgssrlogin.onmicrosoft.com'
+    _LOGIN_PATH = 'B2C_1A__SignInV2'
+    _ID_TOKEN = None
+
+    def _perform_login(self, username, password):
+        login_page = self._download_webpage(
+            'https://www.playsuisse.ch/api/sso/login', None, note='Downloading login page',
+            query={'x': 'x', 'locale': 'de', 'redirectUrl': 'https://www.playsuisse.ch/'})
+        settings = self._search_json(r'var\s+SETTINGS\s*=', login_page, 'settings', None)
+
+        csrf_token = settings['csrf']
+        query = {'tx': settings['transId'], 'p': self._LOGIN_PATH}
+
+        status = traverse_obj(self._download_json(
+            f'{self._LOGIN_BASE_URL}/{self._LOGIN_PATH}/SelfAsserted', None, 'Logging in',
+            query=query, headers={'X-CSRF-TOKEN': csrf_token}, data=urlencode_postdata({
+                'request_type': 'RESPONSE',
+                'signInName': username,
+                'password': password
+            }), expected_status=400), ('status', {int_or_none}))
+        if status == 400:
+            raise ExtractorError('Invalid username or password', expected=True)
+
+        urlh = self._request_webpage(
+            f'{self._LOGIN_BASE_URL}/{self._LOGIN_PATH}/api/CombinedSigninAndSignup/confirmed',
+            None, 'Downloading ID token', query={
+                'rememberMe': 'false',
+                'csrf_token': csrf_token,
+                **query,
+                'diags': '',
+            })
+
+        self._ID_TOKEN = traverse_obj(parse_qs(urlh.url), ('id_token', 0))
+        if not self._ID_TOKEN:
+            raise ExtractorError('Login failed')
 
     def _get_media_data(self, media_id):
         # NOTE In the web app, the "locale" header is used to switch between languages,
         # However this doesn't seem to take effect when passing the header here.
         response = self._download_json(
-            'https://4bbepzm4ef.execute-api.eu-central-1.amazonaws.com/prod/graphql',
+            'https://www.playsuisse.ch/api/graphql',
             media_id, data=json.dumps({
                 'operationName': 'AssetWatch',
                 'query': self._GRAPHQL_QUERY,
@@ -150,6 +193,9 @@ def _get_media_data(self, media_id):
         return response['data']['assetV2']
 
     def _real_extract(self, url):
+        if not self._ID_TOKEN:
+            self.raise_login_required(method='password')
+
         media_id = self._match_id(url)
         media_data = self._get_media_data(media_id)
         info = self._extract_single(media_data)
@@ -168,7 +214,8 @@ def _extract_single(self, media_data):
             if not media.get('url') or media.get('type') != 'HLS':
                 continue
             f, subs = self._extract_m3u8_formats_and_subtitles(
-                media['url'], media_data['id'], 'mp4', m3u8_id='HLS', fatal=False)
+                update_url_query(media['url'], {'id_token': self._ID_TOKEN}),
+                media_data['id'], 'mp4', m3u8_id='HLS', fatal=False)
             formats.extend(f)
             self._merge_subtitles(subs, target=subtitles)
 


Test patch: