Repository: streamlink/streamlink

Issue:
YouTube live stream not detected from channel page
## Issue

<!-- Replace the space character between the square brackets with an x in order to check the boxes -->
- [x] This is not a bug report, feature request, or plugin issue/request.
- [x] I have read the contribution guidelines.
- [ ] I am using the latest development version from the master branch.


### Description

Streamlink doesn't detect a YouTube live stream when you put in channel home page as parameter. Tested with several YT channels. If I input direct video link works, but detecting that video from the channel page shows a no plugin error


### Expected / Actual behavior

Streamlink should find an ID of the live stream and start downloading/streaming it. Instead, no plugin error is shown.


### Reproduction steps / Explicit stream URLs to test

Find a YouTube channel that has a live stream
Input: streamlink https://www.youtube.com/c/xxxxxx
Output: error: No plugin can handle URL


### Log output

```
streamlink --loglevel debug https://www.youtube.com/c/xxxxxxx
[cli][debug] OS:         Linux-5.4.0-72-generic-x86_64-with-glibc2.29
[cli][debug] Python:     3.8.5
[cli][debug] Streamlink: 2.2.0
[cli][debug] Requests(2.22.0), Socks(1.6.7), Websocket(0.58.0)
[cli][debug] Arguments:
[cli][debug]  url=https://www.youtube.com/c/xxxxxxx
[cli][debug]  --loglevel=debug
error: No plugin can handle URL: https://www.youtube.com/c/xxxxxxx
```


Discussion:
See https://github.com/streamlink/streamlink/pull/3797#issuecomment-864443385

Finding a live stream from a channel page is not possible anymore with the removal of the old private API in #3797, even with the fallback API quests added in #3809.

You have to add the `/live` suffix to the URL, otherwise the URL won't match the Youtube plugin, which was an intentional change due to that.

However, as I mentioned in #3797, a URL translation could be added which redirects to `/live` automatically. That is all that can be done. Let me prepare a PR...

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
@@ -9,7 +9,7 @@
 from streamlink.plugin.api.utils import itertags
 from streamlink.stream import HLSStream, HTTPStream
 from streamlink.stream.ffmpegmux import MuxedStream
-from streamlink.utils import parse_json
+from streamlink.utils import parse_json, search_dict
 
 log = logging.getLogger(__name__)
 
@@ -29,12 +29,13 @@
         |
         embed/live_stream\?channel=(?P<embed_live>[^/?&]+)
         |
-        (?:c(?:hannel)?/|user/)?[^/?]+/live/?$
+        (?:c(?:hannel)?/|user/)?(?P<channel>[^/?]+)(?P<channel_live>/live)?/?$
     )
     |
     https?://youtu\.be/(?P<video_id_short>[\w-]{11})
 """, re.VERBOSE))
 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+meta\s*=""", re.DOTALL)
     _re_mime_type = re.compile(r"""^(?P<type>\w+)/(?P<container>\w+); codecs="(?P<codecs>.+)"$""")
 
@@ -214,10 +215,11 @@ def _get_res(self, url):
             res = self.session.http.post("https://consent.youtube.com/s", data=c_data)
         return res
 
-    def _get_data(self, res):
-        match = re.search(self._re_ytInitialPlayerResponse, res.text)
+    @staticmethod
+    def _get_data_from_regex(res, regex, descr):
+        match = re.search(regex, res.text)
         if not match:
-            log.debug("Missing initial player response data")
+            log.debug(f"Missing {descr}")
             return
         return parse_json(match.group(1))
 
@@ -265,6 +267,14 @@ def _get_data_from_api(self, res):
         )
         return parse_json(res.text)
 
+    @staticmethod
+    def _data_video_id(data):
+        if data:
+            for videoRenderer in search_dict(data, "videoRenderer"):
+                videoId = videoRenderer.get("videoId")
+                if videoId is not None:
+                    return videoId
+
     def _data_status(self, data):
         if not data:
             return False
@@ -276,7 +286,17 @@ def _data_status(self, data):
 
     def _get_streams(self):
         res = self._get_res(self.url)
-        data = self._get_data(res)
+
+        if self.match.group("channel") and not self.match.group("channel_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:
+                log.error("Could not find videoId on channel page")
+                return
+            self.url = self._url_canonical.format(video_id=video_id)
+            res = self._get_res(self.url)
+
+        data = self._get_data_from_regex(res, self._re_ytInitialPlayerResponse, "initial player response")
         if not self._data_status(data):
             data = self._get_data_from_api(res)
             if not self._data_status(data):


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
@@ -11,14 +11,22 @@ class TestPluginCanHandleUrlYouTube(PluginCanHandleUrl):
     __plugin__ = YouTube
 
     should_match = [
-        "https://www.youtube.com/EXAMPLE/live",
-        "https://www.youtube.com/EXAMPLE/live/",
-        "https://www.youtube.com/c/EXAMPLE/live",
-        "https://www.youtube.com/c/EXAMPLE/live/",
-        "https://www.youtube.com/channel/EXAMPLE/live",
-        "https://www.youtube.com/channel/EXAMPLE/live/",
-        "https://www.youtube.com/user/EXAMPLE/live",
-        "https://www.youtube.com/user/EXAMPLE/live/",
+        "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",
@@ -30,16 +38,13 @@ class TestPluginCanHandleUrlYouTube(PluginCanHandleUrl):
     should_not_match = [
         "https://accounts.google.com/",
         "https://www.youtube.com",
-        "https://www.youtube.com/account",
+        "https://www.youtube.com/",
         "https://www.youtube.com/feed/guide_builder",
         "https://www.youtube.com/t/terms",
-        "https://www.youtube.com/c/EXAMPLE",
-        "https://www.youtube.com/channel/EXAMPLE",
-        "https://www.youtube.com/user/EXAMPLE",
         "https://youtu.be",
         "https://youtu.be/",
-        "https://youtu.be/c/CHANNEL",
-        "https://youtu.be/c/CHANNEL/live",
+        "https://youtu.be/c/CHANNELNAME",
+        "https://youtu.be/c/CHANNELNAME/live",
     ]