Repository: streamlink/streamlink

Issue:
plugins.youtube: New URL formats are unsupported
### 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 build from the master branch

### Description

Trying to open these URL formats lead to same `error: No plugin can handle URL` error.

Affected URL formats are at least:
```
https://www.youtube.com/live/<id>'
https://www.youtube.com/@<handle>/streams
```

### Debug log

```text
$ streamlink -l debug https://www.youtube.com/live/21X5lGlDOfg
[cli][debug] OS:         Linux-5.15.0-58-generic-x86_64-with-glibc2.35
[cli][debug] Python:     3.10.6
[cli][debug] Streamlink: 5.2.1+7.gfbb9ed64
[cli][debug] Dependencies:
[cli][debug]  certifi: 2022.6.15
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.8.0
[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]  urllib3: 1.26.5
[cli][debug]  websocket-client: 1.3.3
[cli][debug]  importlib-metadata: 4.6.4
[cli][debug] Arguments:
[cli][debug]  url=https://www.youtube.com/live/21X5lGlDOfg
[cli][debug]  --loglevel=debug
[cli][debug]  --player=mpv --cache
[cli][debug]  --default-stream=['best']
error: No plugin can handle URL: https://www.youtube.com/live/21X5lGlDOfg
```



Discussion:

Resolving patch:
diff --git a/src/streamlink/plugins/youtube.py b/src/streamlink/plugins/youtube.py
--- a/src/streamlink/plugins/youtube.py
+++ b/src/streamlink/plugins/youtube.py
@@ -22,26 +22,18 @@
 log = logging.getLogger(__name__)
 
 
-@pluginmatcher(re.compile(r"""
-    https?://(?:\w+\.)?youtube\.com/
-    (?:
-        (?:
-            (?:
-                watch\?(?:.*&)*v=
-                |
-                (?P<embed>embed)/(?!live_stream)
-                |
-                v/
-            )(?P<video_id>[\w-]{11})
-        )
-        |
-        embed/live_stream\?channel=(?P<embed_live>[^/?&]+)
-        |
-        (?:c(?:hannel)?/|user/)?(?P<channel>[^/?]+)(?P<channel_live>/live)?/?$
-    )
-    |
-    https?://youtu\.be/(?P<video_id_short>[\w-]{11})
-""", re.VERBOSE))
+@pluginmatcher(name="default", pattern=re.compile(
+    r"https?://(?:\w+\.)?youtube\.com/(?:v/|live/|watch\?(?:.*&)?v=)(?P<video_id>[\w-]{11})",
+))
+@pluginmatcher(name="channel", pattern=re.compile(
+    r"https?://(?:\w+\.)?youtube\.com/(?:@|c(?:hannel)?/)?(?P<channel>[^/?]+)(?P<live>/live)?/?$",
+))
+@pluginmatcher(name="embed", pattern=re.compile(
+    r"https?://(?:\w+\.)?youtube\.com/embed/(?:live_stream\?channel=(?P<live>[^/?&]+)|(?P<video_id>[\w-]{11}))",
+))
+@pluginmatcher(name="shorthand", pattern=re.compile(
+    r"https?://youtu\.be/(?P<video_id>[\w-]{11})",
+))
 class YouTube(Plugin):
     _re_ytInitialData = re.compile(r"""var\s+ytInitialData\s*=\s*({.*?})\s*;\s*</script>""", re.DOTALL)
     _re_ytInitialPlayerResponse = re.compile(r"""var\s+ytInitialPlayerResponse\s*=\s*({.*?});\s*var\s+\w+\s*=""", re.DOTALL)
@@ -81,12 +73,12 @@ def __init__(self, *args, **kwargs):
         # translate input URLs to be able to find embedded data and to avoid unnecessary HTTP redirects
         if parsed.netloc == "gaming.youtube.com":
             self.url = urlunparse(parsed._replace(scheme="https", netloc="www.youtube.com"))
-        elif self.match.group("video_id_short") is not None:
-            self.url = self._url_canonical.format(video_id=self.match.group("video_id_short"))
-        elif self.match.group("embed") is not None:
-            self.url = self._url_canonical.format(video_id=self.match.group("video_id"))
-        elif self.match.group("embed_live") is not None:
-            self.url = self._url_channelid_live.format(channel_id=self.match.group("embed_live"))
+        elif self.matches["shorthand"]:
+            self.url = self._url_canonical.format(video_id=self.match["video_id"])
+        elif self.matches["embed"] and self.match["video_id"]:
+            self.url = self._url_canonical.format(video_id=self.match["video_id"])
+        elif self.matches["embed"] and self.match["live"]:
+            self.url = self._url_channelid_live.format(channel_id=self.match["live"])
         elif parsed.scheme != "https":
             self.url = urlunparse(parsed._replace(scheme="https"))
 
@@ -132,7 +124,7 @@ def _schema_canonical(self, data):
         schema_canonical = validate.Schema(
             validate.parse_html(),
             validate.xml_xpath_string(".//link[@rel='canonical'][1]/@href"),
-            validate.transform(self.matcher.match),
+            validate.regex(self.matchers["default"].pattern),
             validate.get("video_id")
         )
         return schema_canonical.validate(data)
@@ -281,7 +273,11 @@ def _get_data_from_regex(res, regex, descr):
         return parse_json(match.group(1))
 
     def _get_data_from_api(self, res):
-        _i_video_id = self.match.group("video_id")
+        try:
+            _i_video_id = self.match["video_id"]
+        except IndexError:
+            _i_video_id = None
+
         if _i_video_id is None:
             try:
                 _i_video_id = self._schema_canonical(res.text)
@@ -343,7 +339,7 @@ def _data_status(self, data, errorlog=False):
     def _get_streams(self):
         res = self._get_res(self.url)
 
-        if self.match.group("channel") and not self.match.group("channel_live"):
+        if self.matches["channel"] and not self.match["live"]:
             initial = self._get_data_from_regex(res, self._re_ytInitialData, "initial data")
             video_id = self._data_video_id(initial)
             if video_id is None:


Test patch:
diff --git a/tests/plugins/test_youtube.py b/tests/plugins/test_youtube.py
--- a/tests/plugins/test_youtube.py
+++ b/tests/plugins/test_youtube.py
@@ -9,40 +9,54 @@
 class TestPluginCanHandleUrlYouTube(PluginCanHandleUrl):
     __plugin__ = YouTube
 
-    should_match = [
-        "https://www.youtube.com/CHANNELNAME",
-        "https://www.youtube.com/CHANNELNAME/",
-        "https://www.youtube.com/CHANNELNAME/live",
-        "https://www.youtube.com/CHANNELNAME/live/",
-        "https://www.youtube.com/c/CHANNELNAME",
-        "https://www.youtube.com/c/CHANNELNAME/",
-        "https://www.youtube.com/c/CHANNELNAME/live",
-        "https://www.youtube.com/c/CHANNELNAME/live/",
-        "https://www.youtube.com/user/CHANNELNAME",
-        "https://www.youtube.com/user/CHANNELNAME/",
-        "https://www.youtube.com/user/CHANNELNAME/live",
-        "https://www.youtube.com/user/CHANNELNAME/live/",
-        "https://www.youtube.com/channel/CHANNELID",
-        "https://www.youtube.com/channel/CHANNELID/",
-        "https://www.youtube.com/channel/CHANNELID/live",
-        "https://www.youtube.com/channel/CHANNELID/live/",
-        "https://www.youtube.com/embed/aqz-KE-bpKQ",
-        "https://www.youtube.com/embed/live_stream?channel=UCNye-wNBqNL5ZzHSJj3l8Bg",
-        "https://www.youtube.com/v/aqz-KE-bpKQ",
-        "https://www.youtube.com/watch?v=aqz-KE-bpKQ",
-        "https://www.youtube.com/watch?foo=bar&baz=qux&v=aqz-KE-bpKQ",
-        "https://youtu.be/0123456789A",
-    ]
-
     should_match_groups = [
-        ("https://www.youtube.com/v/aqz-KE-bpKQ", {
+        (("default", "https://www.youtube.com/v/aqz-KE-bpKQ"), {
             "video_id": "aqz-KE-bpKQ",
         }),
-        ("https://www.youtube.com/embed/aqz-KE-bpKQ", {
-            "embed": "embed",
+        (("default", "https://www.youtube.com/live/aqz-KE-bpKQ"), {
+            "video_id": "aqz-KE-bpKQ",
+        }),
+        (("default", "https://www.youtube.com/watch?foo=bar&baz=qux&v=aqz-KE-bpKQ&asdf=1234"), {
+            "video_id": "aqz-KE-bpKQ",
+        }),
+
+        (("channel", "https://www.youtube.com/CHANNELNAME"), {
+            "channel": "CHANNELNAME",
+        }),
+        (("channel", "https://www.youtube.com/CHANNELNAME/live"), {
+            "channel": "CHANNELNAME",
+            "live": "/live",
+        }),
+        (("channel", "https://www.youtube.com/@CHANNELNAME"), {
+            "channel": "CHANNELNAME",
+        }),
+        (("channel", "https://www.youtube.com/@CHANNELNAME/live"), {
+            "channel": "CHANNELNAME",
+            "live": "/live",
+        }),
+        (("channel", "https://www.youtube.com/c/CHANNELNAME"), {
+            "channel": "CHANNELNAME",
+        }),
+        (("channel", "https://www.youtube.com/c/CHANNELNAME/live"), {
+            "channel": "CHANNELNAME",
+            "live": "/live",
+        }),
+        (("channel", "https://www.youtube.com/channel/CHANNELID"), {
+            "channel": "CHANNELID",
+        }),
+        (("channel", "https://www.youtube.com/channel/CHANNELID/live"), {
+            "channel": "CHANNELID",
+            "live": "/live",
+        }),
+
+        (("embed", "https://www.youtube.com/embed/aqz-KE-bpKQ"), {
             "video_id": "aqz-KE-bpKQ",
         }),
-        ("https://www.youtube.com/watch?v=aqz-KE-bpKQ", {
+        (("embed", "https://www.youtube.com/embed/live_stream?channel=CHANNELNAME"), {
+            "live": "CHANNELNAME",
+        }),
+
+        (("shorthand", "https://youtu.be/aqz-KE-bpKQ"), {
             "video_id": "aqz-KE-bpKQ",
         }),
     ]