Repository: streamlink/streamlink

Issue:
plugins.artetv: error: Unable to validate response text: ValidationError(dict):
### 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.5.0

### Description

I  fix this issue 
```
by adding '**API_HLS_NG**'     in line 51  of file 
`/usr/lib/python3.11/site-packages/streamlink/plugins/artetv.py`
like  this :
```
![image](https://github.com/streamlink/streamlink/assets/19744191/b78f47ba-67b2-439b-b336-85bef7e4615a)
link/streamlink/assets/19744191/b78f47ba-67b2-439b-b336-85bef7e4615a)

### Debug log

```text
error: Unable to validate response text: ValidationError(dict):
  Unable to validate value of key 'data'
  Context(dict):
    Unable to validate value of key 'attributes'
    Context(dict):
      Unable to validate value of key 'streams'
      Context(AnySchema):
        ValidationError(AnySchema):
        ValidationError(AnySchema):
          ValidationError(dict):
            Unable to validate value of key 'protocol'
            Context(AnySchema):
              ValidationError(equality):
                'API_HLS_NG' does not equal 'HLS'
              ValidationError(equality):
                'API_HLS_NG' does not equal 'HLS_NG'
```


Discussion:
Firmeware: @bastimeyer 
It's work with yours changes 
```
 [arch@archLinux ]$ sudo cp  ~/Téléchargements/artetv.py  /usr/lib/python3.11/site-packages/streamlink/plugins/artetv.py 
[arch@archLinux ]$ savestream https://www.arte.tv/fr/videos/111697-000-A/y-a-t-il-un-francais-dans-la-salle/  titi 
[cli][info] Found matching plugin artetv for URL https://www.arte.tv/fr/videos/111697-000-A/y-a-t-il-un-francais-dans-la-salle/
[cli][info] Available streams: 216p (worst), 360p, 432p, 720p, 1080p (best)
[cli][info] Opening stream: 1080p (hls-multi)
[cli][info] Writing output to
/partilha/dwhelper/titi.ts
[utils.named_pipe][info] Creating pipe streamlinkpipe-476825-1-190
[utils.named_pipe][info] Creating pipe streamlinkpipe-476825-2-2289
[download] Written 1.65 GiB to titi.ts (52s @ 33.17 MiB/s)                                                                                                                                      
[cli][info] Stream ended
[cli][info] Closing currently open stream...
```
Thanks

Patch:
diff --git a/src/streamlink/plugins/artetv.py b/src/streamlink/plugins/artetv.py
index eaff55a8..55ea7d9c 100644
--- a/src/streamlink/plugins/artetv.py
+++ b/src/streamlink/plugins/artetv.py
@@ -2,6 +2,7 @@
 $description European public service channel promoting culture, including magazine shows, concerts and documentaries.
 $url arte.tv
 $type live, vod
+$metadata id
 $metadata title
 """
 
@@ -17,38 +18,41 @@ from streamlink.stream.hls import HLSStream
 log = logging.getLogger(__name__)
 
 
-@pluginmatcher(re.compile(r"""
-    https?://(?:\w+\.)?arte\.tv/(?:guide/)?
-    (?P<language>[a-z]{2})/
-    (?:
-        (?:videos/)?(?P<video_id>(?!RC-|videos)[^/]+?)/.+
-        |
-        (?:direct|live)
-    )
-""", re.VERBOSE))
+@pluginmatcher(
+    name="live",
+    pattern=re.compile(
+        r"https?://(?:\w+\.)?arte\.tv/(?P<language>[a-z]{2})/(?:direct|live)/?",
+    ),
+)
+@pluginmatcher(
+    name="vod",
+    pattern=re.compile(
+        r"https?://(?:\w+\.)?arte\.tv/(?:guide/)?(?P<language>[a-z]{2})/(?:videos/)?(?P<video_id>(?!RC-|videos)[^/]+?)/.+",
+    ),
+)
 class ArteTV(Plugin):
-    API_URL = "https://api.arte.tv/api/player/v2/config/{0}/{1}"
-    API_TOKEN = "MzYyZDYyYmM1Y2Q3ZWRlZWFjMmIyZjZjNTRiMGY4MzY4NzBhOWQ5YjE4MGQ1NGFiODJmOTFlZDQwN2FkOTZjMQ"
+    API_URL = "https://api.arte.tv/api/player/v2/config/{language}/{id}"
 
     def _get_streams(self):
-        language = self.match.group("language")
-        video_id = self.match.group("video_id")
+        self.id = self.match["video_id"] if self.matches["vod"] else "LIVE"
 
-        json_url = self.API_URL.format(language, video_id or "LIVE")
-        headers = {
-            "Authorization": f"Bearer {self.API_TOKEN}",
-        }
-        streams, metadata = self.session.http.get(json_url, headers=headers, schema=validate.Schema(
+        json_url = self.API_URL.format(
+            language=self.match["language"],
+            id=self.id,
+        )
+        streams, metadata = self.session.http.get(json_url, schema=validate.Schema(
             validate.parse_json(),
-            {"data": {"attributes": {
+            {"data": {"attributes": dict}},
+            validate.get(("data", "attributes")),
+            {
                 "streams": validate.any(
                     [],
                     [
                         validate.all(
                             {
-                                "url": validate.url(),
                                 "slot": int,
-                                "protocol": validate.any("HLS", "HLS_NG"),
+                                "protocol": str,
+                                "url": validate.url(),
                             },
                             validate.union_get("slot", "protocol", "url"),
                         ),
@@ -58,17 +62,15 @@ class ArteTV(Plugin):
                     "title": str,
                     "subtitle": validate.any(None, str),
                 },
-            }}},
-            validate.get(("data", "attributes")),
+            },
             validate.union_get("streams", "metadata"),
         ))
 
-        if not streams:
-            return
-
         self.title = f"{metadata['title']} - {metadata['subtitle']}" if metadata["subtitle"] else metadata["title"]
 
-        for _slot, _protocol, url in sorted(streams, key=itemgetter(0)):
+        for _slot, protocol, url in sorted(streams, key=itemgetter(0)):
+            if "HLS" not in protocol:
+                continue
             return HLSStream.parse_variant_playlist(self.session, url)
 
 


Test patch:
diff --git a/tests/plugins/test_artetv.py b/tests/plugins/test_artetv.py
index 62f244c3..40492bb8 100644
--- a/tests/plugins/test_artetv.py
+++ b/tests/plugins/test_artetv.py
@@ -5,30 +5,51 @@ from tests.plugins import PluginCanHandleUrl
 class TestPluginCanHandleUrlArteTV(PluginCanHandleUrl):
     __plugin__ = ArteTV
 
-    should_match = [
-        # new url
-        "http://www.arte.tv/fr/direct/",
-        "http://www.arte.tv/de/live/",
-        "http://www.arte.tv/de/videos/074633-001-A/gesprach-mit-raoul-peck",
-        "http://www.arte.tv/en/videos/071437-010-A/sunday-soldiers",
-        "http://www.arte.tv/fr/videos/074633-001-A/entretien-avec-raoul-peck",
-        "http://www.arte.tv/pl/videos/069873-000-A/supermama-i-businesswoman",
+    should_match_groups = [
+        # live
+        (
+            ("live", "https://www.arte.tv/fr/direct"),
+            {"language": "fr"},
+        ),
+        (
+            ("live", "https://www.arte.tv/fr/direct/"),
+            {"language": "fr"},
+        ),
+        (
+            ("live", "https://www.arte.tv/de/live"),
+            {"language": "de"},
+        ),
+        (
+            ("live", "https://www.arte.tv/de/live/"),
+            {"language": "de"},
+        ),
 
-        # old url - some of them get redirected and some are 404
-        "http://www.arte.tv/guide/fr/direct",
-        "http://www.arte.tv/guide/de/live",
-        "http://www.arte.tv/guide/fr/024031-000-A/le-testament-du-docteur-mabuse",
-        "http://www.arte.tv/guide/de/024031-000-A/das-testament-des-dr-mabuse",
-        "http://www.arte.tv/guide/en/072544-002-A/christmas-carols-from-cork",
-        "http://www.arte.tv/guide/es/068380-000-A/una-noche-en-florencia",
-        "http://www.arte.tv/guide/pl/068916-006-A/belle-and-sebastian-route-du-rock",
+        # vod
+        (
+            ("vod", "https://www.arte.tv/de/videos/097372-001-A/mysterium-satoshi-bitcoin-wie-alles-begann-1-6/"),
+            {"language": "de", "video_id": "097372-001-A"},
+        ),
+        (
+            ("vod", "https://www.arte.tv/en/videos/097372-001-A/the-satoshi-mystery-the-story-of-bitcoin/"),
+            {"language": "en", "video_id": "097372-001-A"},
+        ),
+
+        # old vod URLs with redirects
+        (
+            ("vod", "https://www.arte.tv/guide/de/097372-001-A/mysterium-satoshi-bitcoin-wie-alles-begann-1-6/"),
+            {"language": "de", "video_id": "097372-001-A"},
+        ),
+        (
+            ("vod", "https://www.arte.tv/guide/en/097372-001-A/the-satoshi-mystery-the-story-of-bitcoin/"),
+            {"language": "en", "video_id": "097372-001-A"},
+        ),
     ]
 
     should_not_match = [
-        # shouldn't match
-        "http://www.arte.tv/guide/fr/plus7/",
-        "http://www.arte.tv/guide/de/plus7/",
+        "https://www.arte.tv/guide/de/live/",
+        "https://www.arte.tv/guide/fr/plus7/",
+        "https://www.arte.tv/guide/de/plus7/",
         # shouldn't match - playlists without video ids in url
-        "http://www.arte.tv/en/videos/RC-014457/the-power-of-forests/",
-        "http://www.arte.tv/en/videos/RC-013118/street-art/",
+        "https://www.arte.tv/en/videos/RC-014457/the-power-of-forests/",
+        "https://www.arte.tv/en/videos/RC-013118/street-art/",
     ]