Repository: streamlink/streamlink

Issue:
plugins.euronews: 404 Client Error
### 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

Hello I m facing an issue with euronews



### Debug log

```text
/usr/local/bin/streamlink --loglevel debug  "https://fr.euronews.com/live"
[cli][info] streamlink is running as root! Be careful!
[cli][debug] OS:         Linux-5.15.0-56-generic-x86_64-with-glibc2.29
[cli][debug] Python:     3.8.10
[cli][debug] Streamlink: 5.1.2
[cli][debug] Dependencies:
[cli][debug]  certifi: 2019.11.28
[cli][debug]  isodate: 0.6.0
[cli][debug]  lxml: 4.8.0
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.9.9
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.27.1
[cli][debug]  urllib3: 1.26.13
[cli][debug]  websocket-client: 1.3.2
[cli][debug] Arguments:
[cli][debug]  url=https://fr.euronews.com/live
[cli][debug]  --loglevel=debug
[cli][info] Found matching plugin euronews for URL https://fr.euronews.com/live
error: Unable to open URL: https://fr.euronews.com/api/watchlive.json (404 Client Error: Not Found for url: https://fr.euronews.com/api/watchlive.json)
```



Discussion:
Seems like it's broken on their website.  It's not loading via a web browser either.  Neither is `https://www.euronews.com/live` for that matter.
yes,  https://www.euronews.com/live is loading for me actually? 

https://i.gyazo.com/e7cc397999ce1d5e4776cb28f025bd4c.mp4

Ah, one of my add-ons seemed to be stopping it.
Why does this plugin even exist? It's just a wrapper for YouTube live streams and HTTPStream VODs. The API stuff for non-embedded live streams doesn't seem to exist anymore. At least I can't find anything. This means that the plugin should be removed, even though a fix would be trivial for the current issue where the youtube stream ID has been moved to a different data- tag with a small JSON string.

Live streams can be found here:
https://www.youtube.com/@euronews/streams
Apparently, some languages are not available on YT and they are using a third party streaming service with an HLS stream that's retrieved from a different API endpoint.

Resolving patch:
diff --git a/src/streamlink/plugins/euronews.py b/src/streamlink/plugins/euronews.py
--- a/src/streamlink/plugins/euronews.py
+++ b/src/streamlink/plugins/euronews.py
@@ -4,73 +4,110 @@
 $type live
 """
 
+import logging
 import re
-from urllib.parse import urlparse
 
-from streamlink.plugin import Plugin, pluginmatcher
+from streamlink.plugin import Plugin, PluginError, pluginmatcher
 from streamlink.plugin.api import validate
 from streamlink.stream.hls import HLSStream
 from streamlink.stream.http import HTTPStream
-from streamlink.utils.url import update_scheme
+
+
+log = logging.getLogger(__name__)
 
 
 @pluginmatcher(re.compile(
-    r'https?://(?:(?P<subdomain>\w+)\.)?euronews\.com/'
+    r"https?://(?:(?P<subdomain>\w+)\.)?euronews\.com/(?P<live>live$)?"
 ))
 class Euronews(Plugin):
-    API_URL = "https://{subdomain}.euronews.com/api/watchlive.json"
-
-    def _get_vod_stream(self):
-        root = self.session.http.get(self.url, schema=validate.Schema(
-            validate.parse_html()
-        ))
-
-        video_url = root.xpath("string(.//meta[@property='og:video'][1]/@content)")
-        if video_url:
-            return dict(vod=HTTPStream(self.session, video_url))
+    API_URL = "https://{subdomain}.euronews.com/api/live/data"
+
+    def _get_live(self):
+        if not self.match["live"] or not self.match["subdomain"]:
+            return
+
+        try:
+            log.debug("Querying live API")
+            stream_url = self.session.http.get(
+                self.API_URL.format(subdomain=self.match["subdomain"]),
+                params={"locale": self.match["subdomain"]},
+                schema=validate.Schema(
+                    validate.parse_json(),
+                    {"videoPrimaryUrl": validate.url(path=validate.endswith(".m3u8"))},
+                    validate.get("videoPrimaryUrl"),
+                ),
+            )
+        except PluginError:
+            pass
+        else:
+            return HLSStream.parse_variant_playlist(self.session, stream_url)
 
-        video_id = root.xpath("string(.//div[@data-google-src]/@data-video-id)")
-        if video_id:
+    def _get_embed(self, root):
+        schema_video_id = validate.Schema(
+            validate.xml_xpath_string(".//div[@data-video][1]/@data-video"),
+            str,
+            validate.parse_json(),
+            {
+                "player": "pfp",
+                "videoId": str,
+            },
+            validate.get("videoId"),
+        )
+        try:
+            log.debug("Looking for YouTube video ID")
+            video_id = schema_video_id.validate(root)
+        except PluginError:
+            pass
+        else:
             return self.session.streams(f"https://www.youtube.com/watch?v={video_id}")
 
-        video_url = root.xpath("string(.//iframe[@id='pfpPlayer'][starts-with(@src,'https://www.youtube.com/')][1]/@src)")
-        if video_url:
+        schema_video_url = validate.Schema(
+            validate.xml_xpath_string(".//iframe[@id='pfpPlayer'][starts-with(@src,'https://www.youtube.com/')][1]/@src"),
+            str,
+        )
+        try:
+            log.debug("Looking for embedded YouTube iframe")
+            video_url = schema_video_url.validate(root)
+        except PluginError:
+            pass
+        else:
             return self.session.streams(video_url)
 
-    def _get_live_streams(self):
-        video_id = self.session.http.get(self.url, schema=validate.Schema(
-            validate.parse_html(),
-            validate.xml_xpath_string(".//div[@data-google-src]/@data-video-id")
-        ))
+    def _get_vod(self, root):
+        schema_vod = validate.Schema(
+            validate.any(
+                validate.all(
+                    validate.xml_xpath_string(".//meta[@property='og:video'][1]/@content"),
+                    str,
+                ),
+                validate.all(
+                    validate.xml_xpath_string(".//div[@data-video][1]/@data-video"),
+                    str,
+                    validate.parse_json(),
+                    {"url": str},
+                    validate.get("url"),
+                ),
+            ),
+            validate.url(),
+        )
+        try:
+            log.debug("Looking for VOD URL")
+            video_url = schema_vod.validate(root)
+        except PluginError:
+            pass
+        else:
+            return dict(vod=HTTPStream(self.session, video_url))
 
-        if video_id:
-            return self.session.streams(f"https://www.youtube.com/watch?v={video_id}")
+    def _get_streams(self):
+        live = self._get_live()
+        if live:
+            return live
 
-        info_url = self.session.http.get(self.API_URL.format(subdomain=self.match.group("subdomain")), schema=validate.Schema(
-            validate.parse_json(),
-            {"url": validate.url()},
-            validate.get("url"),
-            validate.transform(lambda url: update_scheme("https://", url))
-        ))
-        hls_url = self.session.http.get(info_url, schema=validate.Schema(
-            validate.parse_json(),
-            {
-                "status": "ok",
-                "protocol": "hls",
-                "primary": validate.url()
-            },
-            validate.get("primary")
+        root = self.session.http.get(self.url, schema=validate.Schema(
+            validate.parse_html(),
         ))
 
-        return HLSStream.parse_variant_playlist(self.session, hls_url)
-
-    def _get_streams(self):
-        parsed = urlparse(self.url)
-
-        if parsed.path == "/live":
-            return self._get_live_streams()
-        else:
-            return self._get_vod_stream()
+        return self._get_embed(root) or self._get_vod(root)
 
 
 __plugin__ = Euronews


Test patch:
diff --git a/tests/plugins/test_euronews.py b/tests/plugins/test_euronews.py
--- a/tests/plugins/test_euronews.py
+++ b/tests/plugins/test_euronews.py
@@ -6,19 +6,19 @@ class TestPluginCanHandleUrlEuronews(PluginCanHandleUrl):
     __plugin__ = Euronews
 
     should_match = [
-        "http://www.euronews.com/live",
-        "http://fr.euronews.com/live",
-        "http://de.euronews.com/live",
-        "http://it.euronews.com/live",
-        "http://es.euronews.com/live",
-        "http://pt.euronews.com/live",
-        "http://ru.euronews.com/live",
-        "http://ua.euronews.com/live",
-        "http://tr.euronews.com/live",
-        "http://gr.euronews.com/live",
-        "http://hu.euronews.com/live",
-        "http://fa.euronews.com/live",
-        "http://arabic.euronews.com/live",
-        "http://www.euronews.com/2017/05/10/peugeot-expects-more-opel-losses-this-year",
-        "http://fr.euronews.com/2017/05/10/l-ag-de-psa-approuve-le-rachat-d-opel"
+        "https://www.euronews.com/live",
+        "https://fr.euronews.com/live",
+        "https://de.euronews.com/live",
+        "https://it.euronews.com/live",
+        "https://es.euronews.com/live",
+        "https://pt.euronews.com/live",
+        "https://ru.euronews.com/live",
+        "https://ua.euronews.com/live",
+        "https://tr.euronews.com/live",
+        "https://gr.euronews.com/live",
+        "https://hu.euronews.com/live",
+        "https://fa.euronews.com/live",
+        "https://arabic.euronews.com/live",
+        "https://www.euronews.com/video",
+        "https://www.euronews.com/2023/01/02/giving-europe-a-voice-television-news-network-euronews-turns-30",
     ]