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:
back-to: won't work with `/user/` into `/c/`

https://www.youtube.com/user/deutschewelleenglish

```
error: Unable to open URL: https://consent.youtube.com/s (404 Client Error: Not Found for url: https://www.youtube.com/c/deutschewelleenglish/live)
```

---

> Due to the removal of private API requests, this seems to be
> the only way to find live streams.

`ytInitialData` was used to get the video id from the userpage

https://github.com/streamlink/streamlink/blob/2.1.2/src/streamlink/plugins/youtube.py#L61

a bit ugly, but it worked.

`videoRenderer` -> `viewCountText` -> `runs`

https://github.com/streamlink/streamlink/blob/2.1.2/src/streamlink/plugins/youtube.py#L243-L273
bastimeyer: > won't work with /user/ into /c/

weird, it's working perfectly for a different channel where I tested it

> a bit ugly, but it worked.
> `videoRenderer` -> `viewCountText` -> `runs`

It's even uglier if you don't use the `search_dict` method:

```
ytInitialData
  .contents
  .twoColumnBrowseResultsRenderer
  .tabs[]
  .tabRenderer
  .content
  .sectionListRenderer
  .contents[]
  .itemSectionRenderer
  .contents[]
  .channelFeaturedContentRenderer
  .items[]
  .videoRenderer
  .videoId
```

Patch:
diff --git a/src/streamlink/plugins/tv8.py b/src/streamlink/plugins/tv8.py
index 86317f13..69b7dabb 100644
--- a/src/streamlink/plugins/tv8.py
+++ b/src/streamlink/plugins/tv8.py
@@ -12,23 +12,18 @@ log = logging.getLogger(__name__)
     r'https?://www\.tv8\.com\.tr/canli-yayin'
 ))
 class TV8(Plugin):
-    _player_schema = validate.Schema(validate.all({
-        'servers': {
-            validate.optional('manifest'): validate.url(),
-            'hlsmanifest': validate.url(),
-        }},
-        validate.get('servers')))
-
-    API_URL = 'https://static.personamedia.tv/player/config/tv8.json'
+    _re_hls = re.compile(r"""file\s*:\s*(["'])(?P<hls_url>https?://.*?\.m3u8.*?)\1""")
 
     def get_title(self):
         return 'TV8'
 
     def _get_streams(self):
-        res = self.session.http.get(self.API_URL)
-        data = self.session.http.json(res, schema=self._player_schema)
-        log.debug('{0!r}'.format(data))
-        return HLSStream.parse_variant_playlist(self.session, data['hlsmanifest'])
+        hls_url = self.session.http.get(self.url, schema=validate.Schema(
+            validate.transform(self._re_hls.search),
+            validate.any(None, validate.get("hls_url"))
+        ))
+        if hls_url is not None:
+            return HLSStream.parse_variant_playlist(self.session, hls_url)
 
 
 __plugin__ = TV8
diff --git a/src/streamlink/plugins/youtube.py b/src/streamlink/plugins/youtube.py
index 0878ea99..9a472ad9 100644
--- a/src/streamlink/plugins/youtube.py
+++ b/src/streamlink/plugins/youtube.py
@@ -9,7 +9,7 @@ from streamlink.plugin.api import useragents, validate
 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 @@ log = logging.getLogger(__name__)
         |
         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 @@ class YouTube(Plugin):
             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 @@ class YouTube(Plugin):
         )
         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 @@ class YouTube(Plugin):
 
     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
index af5cafe3..f05e070d 100644
--- 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",
     ]