Repository: streamlink/streamlink

Issue:
plugins.tv5monde: broken
### 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

The TV5Monde is completely outdated and useless with the actuel website and urls. I have to remove it completely after every streamlink update, otherwise i'm not able to use any of the TV5 hls links from the website. So I think it's better to remove it or update it if possible. Thanks.

### Debug log

```text
N/A
```



Discussion:
> i'm not able to use any of the TV5 hls links from the website

https://streamlink.github.io/cli/protocols.html#playing-built-in-streaming-protocols-directly
```
streamlink 'hls://https://...' best
```
Post a full debug log please, as requested by the issue form
> > i'm not able to use any of the TV5 hls links from the website
> 
> https://streamlink.github.io/cli/protocols.html#playing-built-in-streaming-protocols-directly
> 
> ```
> streamlink 'hls://https://...' best
> ```

"**otherwise** i'm not able to use any of the TV5 hls links from the website" = which means that with the plugin installed in Streamlink, it will start instantly when trying to watch any TV5 channels and stop any attempt to watch the direct HLS links from the TV5 website. This is why it must be removed OR updated in the next Streamlink update in order to avoid this problem...
No, it does not if you use the `hls://` prefix for any HLS playlist input URLs. The **HLS plugin** [does have two matchers](https://github.com/streamlink/streamlink/blob/5.3.1/src/streamlink/plugins/hls.py#L13-L18), one with the `hls://` prefix and normal priority (which is obviously not used by any other plugin), and one with `.m3u8` URL path suffixes and the low priority. If the **tv5monde plugin** matches first because you're not using the special `hls://` prefix, then this won't work of course.

If the **tv5monde plugin** is broken and can't resolve the right HLS URLs, then it needs fixing. It should only get removed if it can't be fixed or if it violates the plugin requirement rules. As said, post the full debug log when trying to use the tv5monde plugin.
Here is the log, only TV5Monde Europe is not working and need to be updated in the plugin (https://europe.tv5monde.com/fr/direct - hls direct link: https://ott.tv5monde.com/Content/HLS/Live/channel(europe)/index.m3u8). The rest is working fine.

TV5Monde France Belgique Suisse Monaco
[cli][info] Found matching plugin tv5monde for URL https://live.tv5monde.com/fbs.html?
[cli][info] Available streams: 216p (worst), 270p, 360p, 540p, 720p, 1080p (best)
[cli][info] Starting server, access with one of:
[cli][info]  http://10.10.20.15:129/
[cli][info]  http://127.0.0.1:129/
[cli][info] Got HTTP request from VLC/3.0.18 LibVLC/3.0.18
[cli][info] Opening stream: 1080p (hls-multi)
[utils.named_pipe][info] Creating pipe streamlinkpipe-14200-1-6511
[utils.named_pipe][info] Creating pipe streamlinkpipe-14200-2-2045

TV5Monde Europe
[cli][info] Found matching plugin tv5monde for URL https://europe.tv5monde.com/fr/direct
error: No playable streams found on this URL: https://europe.tv5monde.com/fr/direct

TV5Monde Maghreb-Orient
[cli][info] Found matching plugin tv5monde for URL https://maghreb-orient.tv5monde.com/fr/direct
[cli][info] Available streams: 216p (worst), 270p, 360p, 540p, 720p, 1080p (best)
[cli][info] Starting server, access with one of:
[cli][info]  http://10.10.20.15:129/
[cli][info]  http://10.34.93.116:129/
[cli][info]  http://127.0.0.1:129/
[cli][info] Got HTTP request from VLC/3.0.18 LibVLC/3.0.18
[cli][info] Opening stream: 1080p (hls-multi)
[utils.named_pipe][info] Creating pipe streamlinkpipe-14384-1-8345
[utils.named_pipe][info] Creating pipe streamlinkpipe-14384-2-4766

TV5Monde Info
[cli][info] Found matching plugin tv5monde for URL https://maghreb-orient.tv5monde.com/fr/direct-tv5monde-info
[cli][info] Available streams: 216p (worst), 270p, 360p, 540p, 720p, 1080p (best)
[cli][info] Starting server, access with one of:
[cli][info]  http://10.10.20.15:129/
[cli][info]  http://10.34.93.116:129/
[cli][info]  http://127.0.0.1:129/
[cli][info] Got HTTP request from VLC/3.0.18 LibVLC/3.0.18
[cli][info] Opening stream: 1080p (hls-multi)
[utils.named_pipe][info] Creating pipe streamlinkpipe-3372-1-6004
[utils.named_pipe][info] Creating pipe streamlinkpipe-3372-2-7378

Tivi5
[cli][info] Found matching plugin tv5monde for URL https://maghreb-orient.tv5monde.com/fr/direct-tv5-jeunesse
[cli][info] Available streams: 216p (worst), 270p, 360p, 540p, 720p, 1080p (best)
[cli][info] Starting server, access with one of:
[cli][info]  http://10.10.20.15:129/
[cli][info]  http://10.34.93.116:129/
[cli][info]  http://127.0.0.1:129/
[cli][info] Got HTTP request from VLC/3.0.18 LibVLC/3.0.18
[cli][info] Opening stream: 1080p (hls-multi)
[utils.named_pipe][info] Creating pipe streamlinkpipe-8452-1-2605
[utils.named_pipe][info] Creating pipe streamlinkpipe-8452-2-4004

TV5Monde Style
[cli][info] Found matching plugin tv5monde for URL https://maghreb-orient.tv5monde.com/fr/direct-style-hd
[cli][info] Available streams: 216p (worst), 270p, 360p, 540p, 720p, 1080p (best)
[cli][info] Starting server, access with one of:
[cli][info]  http://10.10.20.15:129/
[cli][info]  http://10.34.93.116:129/
[cli][info]  http://127.0.0.1:129/
[cli][info] Got HTTP request from VLC/3.0.18 LibVLC/3.0.18
[cli][info] Opening stream: 1080p (hls-multi)
[utils.named_pipe][info] Creating pipe streamlinkpipe-9144-1-4944
[utils.named_pipe][info] Creating pipe streamlinkpipe-9144-2-4297

Resolving patch:
diff --git a/src/streamlink/plugins/tv5monde.py b/src/streamlink/plugins/tv5monde.py
--- a/src/streamlink/plugins/tv5monde.py
+++ b/src/streamlink/plugins/tv5monde.py
@@ -3,80 +3,93 @@
 $url tv5monde.com
 $url tivi5mondeplus.com
 $type live, vod
-$region France, Belgium, Switzerland
+$region France, Belgium, Switzerland, Africa
 """
 
 import re
-from urllib.parse import urlparse
+from urllib.parse import quote
 
-from streamlink.plugin import Plugin, PluginError, pluginmatcher
+from streamlink.plugin import Plugin, pluginmatcher
 from streamlink.plugin.api import validate
+from streamlink.stream.dash import DASHStream
 from streamlink.stream.hls import HLSStream
 from streamlink.stream.http import HTTPStream
 from streamlink.utils.url import update_scheme
 
 
-@pluginmatcher(re.compile(r"""
-    https?://(?:[\w-]+\.)*(?:tv5monde|tivi5mondeplus)\.com/
-""", re.VERBOSE))
+@pluginmatcher(re.compile(
+    r"https?://(?:[\w-]+\.)*(?:tv5monde|tivi5mondeplus)\.com/",
+))
 class TV5Monde(Plugin):
-    def _get_hls(self, root):
-        schema_live = validate.Schema(
-            validate.xml_xpath_string(".//*[contains(@data-broadcast,'m3u8')]/@data-broadcast"),
-            str,
-            validate.parse_json(),
-            validate.any(
-                validate.all({"files": list}, validate.get("files")),
-                list,
+    _URL_API = "https://api.tv5monde.com/player/asset/{asset}/resolve"
+
+    _MAP_STREAMTYPES = {
+        "application/x-mpegURL": HLSStream.parse_variant_playlist,
+        "application/dash+xml": DASHStream.parse_manifest,
+        "video/mp4": lambda *args: {"vod": HTTPStream(*args)},
+    }
+
+    def _find_streams(self, data):
+        schema_streams = validate.Schema([
+            validate.all(
+                {
+                    "type": str,
+                    "url": validate.url(),
+                },
+                validate.union_get("type", "url"),
             ),
-            [{
-                "url": validate.url(path=validate.endswith(".m3u8")),
-            }],
-            validate.get((0, "url")),
-            validate.transform(lambda content_url: update_scheme("https://", content_url)),
+        ])
+        streams = schema_streams.validate(data)
+
+        for mimetype, streamfactory in self._MAP_STREAMTYPES.items():
+            for streamtype, streamurl in streams:
+                if streamtype == mimetype:
+                    url = update_scheme("https://", streamurl)
+                    return streamfactory(self.session, url)
+
+    def _get_playlist(self, data):
+        schema_playlist = validate.Schema(
+            validate.parse_json(),
+            [{"sources": list}],
+            validate.get((0, "sources")),
         )
-        try:
-            live = schema_live.validate(root)
-        except PluginError:
-            return
-
-        return HLSStream.parse_variant_playlist(self.session, live)
-
-    def _get_vod(self, root):
-        schema_vod = validate.Schema(
-            validate.xml_xpath_string(".//script[@type='application/ld+json'][contains(text(),'VideoObject')][1]/text()"),
-            str,
-            validate.transform(lambda jsonlike: re.sub(r"[\r\n]+", "", jsonlike)),
+        playlist = schema_playlist.validate(data)
+
+        return self._find_streams(playlist)
+
+    def _get_broadcast(self, data):
+        schema_broadcast = validate.Schema(
             validate.parse_json(),
-            validate.any(
-                validate.all(
-                    {"@graph": [dict]},
-                    validate.get("@graph"),
-                    validate.filter(lambda obj: obj["@type"] == "VideoObject"),
-                    validate.get(0),
-                ),
-                dict,
-            ),
-            {"contentUrl": validate.url()},
-            validate.get("contentUrl"),
-            validate.transform(lambda content_url: update_scheme("https://", content_url)),
+            [{
+                "type": str,
+                "url": str,
+            }],
         )
-        try:
-            vod = schema_vod.validate(root)
-        except PluginError:
-            return
+        broadcast = schema_broadcast.validate(data)
 
-        if urlparse(vod).path.endswith(".m3u8"):
-            return HLSStream.parse_variant_playlist(self.session, vod)
+        deferred = next((item["url"] for item in broadcast if item["type"] == "application/deferred"), None)
+        if deferred:
+            url = self._URL_API.format(asset=quote(deferred))
+            broadcast = self.session.http.get(url, schema=validate.Schema(
+                validate.parse_json(),
+            ))
 
-        return {"vod": HTTPStream(self.session, vod)}
+        return self._find_streams(broadcast)
 
     def _get_streams(self):
-        root = self.session.http.get(self.url, schema=validate.Schema(
+        playlist, broadcast = self.session.http.get(self.url, schema=validate.Schema(
             validate.parse_html(),
+            validate.union((
+                validate.xml_xpath_string(".//*[@data-playlist][1]/@data-playlist"),
+                validate.xml_xpath_string(".//*[@data-broadcast][1]/@data-broadcast"),
+            )),
         ))
 
-        return self._get_hls(root) or self._get_vod(root)
+        # data-playlist needs higher priority (data-broadcast attribute exists on the same DOM node)
+        if playlist:
+            return self._get_playlist(playlist)
+        if broadcast:
+            return self._get_broadcast(broadcast)
 
 
 __plugin__ = TV5Monde


Test patch:
diff --git a/tests/plugins/test_tv5monde.py b/tests/plugins/test_tv5monde.py
--- a/tests/plugins/test_tv5monde.py
+++ b/tests/plugins/test_tv5monde.py
@@ -6,11 +6,11 @@ class TestPluginCanHandleUrlTV5Monde(PluginCanHandleUrl):
     __plugin__ = TV5Monde
 
     should_match = [
-        "http://live.tv5monde.com/fbs.html",
-        "https://www.tv5monde.com/emissions/episode/version-francaise-vf-83",
-        "https://revoir.tv5monde.com/toutes-les-videos/cinema/je-ne-reve-que-de-vous",
-        "https://revoir.tv5monde.com/toutes-les-videos/documentaires/des-russes-blancs-des-russes-blancs",
+        "https://live.tv5monde.com/fbs.html",
+        "https://europe.tv5monde.com/fr/direct",
+        "https://maghreb-orient.tv5monde.com/fr/direct",
+        "https://revoir.tv5monde.com/toutes-les-videos/documentaires/sauver-notre-dame-sauver-notre-dame-14-04-2020",
         "https://information.tv5monde.com/video/la-diplomatie-francaise-est-elle-en-crise",
-        "https://afrique.tv5monde.com/videos/exclusivites-web/les-tutos-de-magloire/season-1/episode-1",
+        "https://afrique.tv5monde.com/videos/afrykas-et-la-boite-magique",
         "https://www.tivi5mondeplus.com/conte-nous/episode-25",
     ]