Repository: streamlink/streamlink

Issue:
plugins.mitele: 403 Client Error: Forbidden token format
### 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

New changes on API

### Debug log

```text
[cli][debug] OS:         Linux-5.4.0-122-generic-x86_64-with-glibc2.31
[cli][debug] Python:     3.10.5
[cli][debug] Streamlink: 4.2.0
[cli][debug] Dependencies:
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.1
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.15.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.28.1
[cli][debug]  websocket-client: 1.3.3
[cli][debug] Arguments:
[cli][debug]  url=https://mitele.es/directo/telecinco
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --player=vlc
[cli][info] Found matching plugin mitele for URL https://mitele.es/directo/telecinco
[plugins.mitele][debug] Stream: https://directos.mitele.es/orilinear01/live/linear01/main/main.isml/web.m3u8 (hls)
[plugins.mitele][debug] Stream: https://directos.mitele.es/orilinear17/live/linear17/playlist/playlist.isml/ctv.m3u8 (hls)
[plugins.mitele][debug] Stream: https://livek.mediaset.es/orilinear01/live/linear01/main/main.isml/web.m3u8 (hls)
[plugins.mitele][debug] Stream: https://livek.mediaset.es/orilinear17/live/linear17/playlist/playlist.isml/ctv.m3u8 (hls)
[utils.l10n][debug] Language code: es_ES
error: Unable to open URL: https://livek.mediaset.es/orilinear17/live/linear17/playlist/playlist.isml/ctv.m3u8?hdnts=st%3D1660114575~exp%3D1660200974~acl%3D%2F%2A~hmac%3D2e0d916d43994680b72c50d5dfecadb812c920ee14a4cb918789c3aa8d803f54 (403 Client Error: Forbidden token format for url: https://livek.mediaset.es/orilinear17/live/linear17/playlist/playlist.isml/ctv.m3u8?hdnts=st%3D1660114575~exp%3D1660200974~acl%3D%2F%2A~hmac%3D2e0d916d43994680b72c50d5dfecadb812c920ee14a4cb918789c3aa8d803f54)
```



Discussion:
> 403 Client Error: Forbidden token format

The site requires a monthly subscription to access that channel. Not sure if that's because of me trying to access it with a German IP, but other channels are showing a regular geo-protection message.

Are other channels working fine?
> > 403 Client Error: Forbidden token format
> 
> The site requires a monthly subscription to access that channel. Not sure if that's because of me trying to access it with a German IP, but other channels are showing a regular geo-protection message.
> 
> Are other channels working fine?

Yes, and can access this channel free too.

It has geoblocked for Spain.

The issue still continue. I am accessing from Spain, an still it gives the "Client Error: Forbidden token" reported. On the website directly "https://www.mitele.es/directo/telecinco/" , without any subscription works perfect on Chrome.

Resolving patch:
diff --git a/src/streamlink/plugins/mitele.py b/src/streamlink/plugins/mitele.py
--- a/src/streamlink/plugins/mitele.py
+++ b/src/streamlink/plugins/mitele.py
@@ -21,83 +21,91 @@
     r"https?://(?:www\.)?mitele\.es/directo/(?P<channel>[\w-]+)"
 ))
 class Mitele(Plugin):
-    caronte_url = "https://caronte.mediaset.es/delivery/channel/mmc/{channel}/mtweb"
-    gbx_url = "https://mab.mediaset.es/1.0.0/get?oid=mtmw&eid=%2Fapi%2Fmtmw%2Fv2%2Fgbx%2Fmtweb%2Flive%2Fmmc%2F{channel}"
+    URL_CARONTE = "https://caronte.mediaset.es/delivery/channel/mmc/{channel}/mtweb"
+    URL_GBX = "https://mab.mediaset.es/1.0.0/get"
 
-    error_schema = validate.Schema({"code": int})
-    caronte_schema = validate.Schema(validate.parse_json(), validate.any(
-        {
-            "cerbero": validate.url(),
-            "bbx": str,
-            "dls": [{
-                "lid": validate.all(int, validate.transform(str)),
-                "format": validate.any("hls", "dash", "smooth"),
-                "stream": validate.url(),
-                validate.optional("assetKey"): str,
-                "drm": bool,
-            }],
-        },
-        error_schema,
-    ))
-    gbx_schema = validate.Schema(
-        validate.parse_json(),
-        {"gbx": str},
-        validate.get("gbx")
-    )
-    cerbero_schema = validate.Schema(
-        validate.parse_json(),
-        validate.any(
-            validate.all(
-                {"tokens": {str: {"cdn": str}}},
-                validate.get("tokens")
-            ),
-            error_schema,
-        )
-    )
-    token_errors = {
-        4038: "User has no privileges"
+    TOKEN_ERRORS = {
+        4038: "User has no privileges",
     }
 
     def _get_streams(self):
         channel = self.match.group("channel")
 
-        pdata = self.session.http.get(self.caronte_url.format(channel=channel),
-                                      acceptable_status=(200, 403, 404),
-                                      schema=self.caronte_schema)
-        gbx = self.session.http.get(self.gbx_url.format(channel=channel),
-                                    schema=self.gbx_schema)
-
+        pdata = self.session.http.get(
+            self.URL_CARONTE.format(channel=channel),
+            acceptable_status=(200, 403, 404),
+            schema=validate.Schema(
+                validate.parse_json(),
+                validate.any(
+                    {"code": int},
+                    {
+                        "cerbero": validate.url(),
+                        "bbx": str,
+                        "dls": validate.all(
+                            [{
+                                "drm": bool,
+                                "format": str,
+                                "stream": validate.url(),
+                                "lid": validate.all(int, validate.transform(str)),
+                                validate.optional("assetKey"): str,
+                            }],
+                            validate.filter(lambda obj: obj["format"] == "hls")
+                        ),
+                    },
+                ),
+            ),
+        )
         if "code" in pdata:
-            log.error("error getting pdata: {}".format(pdata["code"]))
+            log.error(f"Error getting pdata: {pdata['code']}")
             return
 
-        tokens = self.session.http.post(pdata["cerbero"],
-                                        acceptable_status=(200, 403, 404),
-                                        json={"bbx": pdata["bbx"], "gbx": gbx},
-                                        headers={"origin": "https://www.mitele.es"},
-                                        schema=self.cerbero_schema)
+        gbx = self.session.http.get(
+            self.URL_GBX,
+            params={
+                "oid": "mtmw",
+                "eid": f"/api/mtmw/v2/gbx/mtweb/live/mmc/{channel}",
+            },
+            schema=validate.Schema(
+                validate.parse_json(),
+                {"gbx": str},
+                validate.get("gbx"),
+            ),
+        )
 
+        tokens = self.session.http.post(
+            pdata["cerbero"],
+            acceptable_status=(200, 403, 404),
+            json={
+                "bbx": pdata["bbx"],
+                "gbx": gbx,
+            },
+            headers={"origin": "https://www.mitele.es"},
+            schema=validate.Schema(
+                validate.parse_json(),
+                validate.any(
+                    {"code": int},
+                    validate.all(
+                        {"tokens": {str: {"cdn": str}}},
+                        validate.get("tokens")
+                    ),
+                ),
+            ),
+        )
         if "code" in tokens:
-            log.error("Could not get stream tokens: {} ({})".format(tokens["code"],
-                                                                    self.token_errors.get(tokens["code"], "unknown error")))
+            tokenerrors = self.TOKEN_ERRORS.get(tokens["code"], "unknown error")
+            log.error(f"Could not get stream tokens: {tokens['code']} ({tokenerrors})")
             return
 
-        list_urls = []
+        urls = set()
         for stream in pdata["dls"]:
             if stream["drm"]:
                 log.warning("Stream may be protected by DRM")
-            else:
-                sformat = stream.get("format")
-                log.debug("Stream: {} ({})".format(stream["stream"], sformat or "n/a"))
-                cdn_token = tokens.get(stream["lid"], {}).get("cdn", "")
-                qsd = parse_qsd(cdn_token)
-                if sformat == "hls":
-                    list_urls.append(update_qsd(stream["stream"], qsd))
-
-        if not list_urls:
-            return
+                continue
+            cdn_token = tokens.get(stream["lid"], {}).get("cdn", "")
+            qsd = parse_qsd(cdn_token)
+            urls.add(update_qsd(stream["stream"], qsd, quote_via=lambda string, *_, **__: string))
 
-        for url in list(set(list_urls)):
+        for url in urls:
             yield from HLSStream.parse_variant_playlist(self.session, url, name_fmt="{pixels}_{bitrate}").items()
 
 


Test patch: