aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--test/test_youtube_lists.py13
-rw-r--r--yt_dlp/extractor/youtube.py328
3 files changed, 253 insertions, 92 deletions
diff --git a/README.md b/README.md
index e094ccba7..e9ea99ebf 100644
--- a/README.md
+++ b/README.md
@@ -88,7 +88,7 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
* Supports some (but not all) age-gated content without cookies
* Download livestreams from the start using `--live-from-start` (*experimental*)
* `255kbps` audio is extracted (if available) from YouTube Music when premium cookies are given
- * Redirect channel's home URL automatically to `/video` to preserve the old behaviour
+ * Channel URLs download all uploads of the channel, including shorts and live
* **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser BROWSER[+KEYRING][:PROFILE][::CONTAINER]`
@@ -142,7 +142,7 @@ Some of yt-dlp's default options are different from that of youtube-dl and youtu
* `playlist_index` behaves differently when used with options like `--playlist-reverse` and `--playlist-items`. See [#302](https://github.com/yt-dlp/yt-dlp/issues/302) for details. You can use `--compat-options playlist-index` if you want to keep the earlier behavior
* The output of `-F` is listed in a new format. Use `--compat-options list-formats` to revert this
* Live chats (if available) are considered as subtitles. Use `--sub-langs all,-live_chat` to download all subtitles except live chat. You can also use `--compat-options no-live-chat` to prevent any live chat/danmaku from downloading
-* YouTube channel URLs are automatically redirected to `/video`. Append a `/featured` to the URL to download only the videos in the home page. If the channel does not have a videos tab, we try to download the equivalent `UU` playlist instead. For all other tabs, if the channel does not show the requested tab, an error will be raised. Also, `/live` URLs raise an error if there are no live videos instead of silently downloading the entire channel. You may use `--compat-options no-youtube-channel-redirect` to revert all these redirections
+* YouTube channel URLs download all uploads of the channel. To download only the videos in a specific tab, pass the tab's URL. If the channel does not show the requested tab, an error will be raised. Also, `/live` URLs raise an error if there are no live videos instead of silently downloading the entire channel. You may use `--compat-options no-youtube-channel-redirect` to revert all these redirections
* Unavailable videos are also listed for YouTube playlists. Use `--compat-options no-youtube-unavailable-videos` to remove this
* The upload dates extracted from YouTube are in UTC [when available](https://github.com/yt-dlp/yt-dlp/blob/89e4d86171c7b7c997c77d4714542e0383bf0db0/yt_dlp/extractor/youtube.py#L3898-L3900). Use `--compat-options no-youtube-prefer-utc-upload-date` to prefer the non-UTC upload date.
* If `ffmpeg` is used as the downloader, the downloading and merging of formats happen in a single step when possible. Use `--compat-options no-direct-merge` to revert this
diff --git a/test/test_youtube_lists.py b/test/test_youtube_lists.py
index c2dd0ac30..b3f323e21 100644
--- a/test/test_youtube_lists.py
+++ b/test/test_youtube_lists.py
@@ -10,6 +10,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from test.helper import FakeYDL, is_download_test
from yt_dlp.extractor import YoutubeIE, YoutubeTabIE
+from yt_dlp.utils import ExtractorError
@is_download_test
@@ -53,6 +54,18 @@ class TestYoutubeLists(unittest.TestCase):
self.assertEqual(video['duration'], 10)
self.assertEqual(video['uploader'], 'Philipp Hagemeister')
+ def test_youtube_channel_no_uploads(self):
+ dl = FakeYDL()
+ dl.params['extract_flat'] = True
+ ie = YoutubeTabIE(dl)
+ # no uploads
+ with self.assertRaisesRegex(ExtractorError, r'no uploads'):
+ ie.extract('https://www.youtube.com/channel/UC2yXPzFejc422buOIzn_0CA')
+
+ # no uploads and no UCID given
+ with self.assertRaisesRegex(ExtractorError, r'no uploads'):
+ ie.extract('https://www.youtube.com/news')
+
if __name__ == '__main__':
unittest.main()
diff --git a/yt_dlp/extractor/youtube.py b/yt_dlp/extractor/youtube.py
index 804d0ea34..33419e74a 100644
--- a/yt_dlp/extractor/youtube.py
+++ b/yt_dlp/extractor/youtube.py
@@ -4589,13 +4589,16 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor):
@staticmethod
def _extract_selected_tab(tabs, fatal=True):
- for tab in tabs:
- renderer = dict_get(tab, ('tabRenderer', 'expandableTabRenderer')) or {}
- if renderer.get('selected') is True:
- return renderer
- else:
- if fatal:
- raise ExtractorError('Unable to find selected tab')
+ for tab_renderer in tabs:
+ if tab_renderer.get('selected'):
+ return tab_renderer
+ if fatal:
+ raise ExtractorError('Unable to find selected tab')
+
+ @staticmethod
+ def _extract_tab_renderers(response):
+ return traverse_obj(
+ response, ('contents', 'twoColumnBrowseResultsRenderer', 'tabs', ..., ('tabRenderer', 'expandableTabRenderer')), expected_type=dict)
def _extract_from_tabs(self, item_id, ytcfg, data, tabs):
playlist_id = title = description = channel_url = channel_name = channel_id = None
@@ -4897,8 +4900,7 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor):
webpage, data = self._extract_webpage(url, item_id, fatal=webpage_fatal)
ytcfg = ytcfg or self.extract_ytcfg(item_id, webpage)
# Reject webpage data if redirected to home page without explicitly requesting
- selected_tab = self._extract_selected_tab(traverse_obj(
- data, ('contents', 'twoColumnBrowseResultsRenderer', 'tabs'), expected_type=list, default=[]), fatal=False) or {}
+ selected_tab = self._extract_selected_tab(self._extract_tab_renderers(data), fatal=False) or {}
if (url != 'https://www.youtube.com/feed/recommended'
and selected_tab.get('tabIdentifier') == 'FEwhat_to_watch' # Home page
and 'no-youtube-channel-redirect' not in self.get_param('compat_opts', [])):
@@ -5392,18 +5394,19 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
'categories': ['News & Politics'],
'tags': list,
'like_count': int,
- 'release_timestamp': 1642502819,
+ 'release_timestamp': int,
'channel': 'Sky News',
'channel_id': 'UCoMdktPbSTixAyNGwb-UYkQ',
'age_limit': 0,
'view_count': int,
- 'thumbnail': 'https://i.ytimg.com/vi/GgL890LIznQ/maxresdefault_live.jpg',
+ 'thumbnail': r're:https?://i\.ytimg\.com/vi/[^/]+/maxresdefault(?:_live)?\.jpg',
'playable_in_embed': True,
- 'release_date': '20220118',
+ 'release_date': r're:\d+',
'availability': 'public',
'live_status': 'is_live',
'channel_url': 'https://www.youtube.com/channel/UCoMdktPbSTixAyNGwb-UYkQ',
- 'channel_follower_count': int
+ 'channel_follower_count': int,
+ 'concurrent_view_count': int,
},
'params': {
'skip_download': True,
@@ -5538,16 +5541,14 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
],
'playlist_mincount': 101,
}, {
- 'note': 'Topic without a UU playlist',
+ # Destination channel with only a hidden self tab (tab id is UCtFRv9O2AHqOZjjynzrv-xg)
+ # Treat as a general feed
'url': 'https://www.youtube.com/channel/UCtFRv9O2AHqOZjjynzrv-xg',
'info_dict': {
'id': 'UCtFRv9O2AHqOZjjynzrv-xg',
'title': 'UCtFRv9O2AHqOZjjynzrv-xg',
'tags': [],
},
- 'expected_warnings': [
- 'the playlist redirect gave error',
- ],
'playlist_mincount': 9,
}, {
'note': 'Youtube music Album',
@@ -5615,6 +5616,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
'skip_download': True,
'extractor_args': {'youtubetab': {'skip': ['webpage']}}
},
+ 'skip': 'Query for sorting no longer works',
}, {
'note': 'API Fallback: Topic, should redirect to playlist?list=UU...',
'url': 'https://music.youtube.com/browse/UC9ALqqC4aIeG5iDs7i90Bfw',
@@ -5633,10 +5635,6 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
'uploader_url': 'https://www.youtube.com/channel/UC9ALqqC4aIeG5iDs7i90Bfw',
'availability': 'public',
},
- 'expected_warnings': [
- 'does not have a videos tab',
- r'[Uu]navailable videos (are|will be) hidden',
- ],
'playlist_mincount': 101,
'params': {
'skip_download': True,
@@ -5715,13 +5713,155 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
},
'playlist_mincount': 50,
+ }, {
+ # Channel with a real live tab (not to be mistaken with streams tab)
+ # Do not treat like it should redirect to live stream
+ 'url': 'https://www.youtube.com/channel/UCEH7P7kyJIkS_gJf93VYbmg/live',
+ 'info_dict': {
+ 'id': 'UCEH7P7kyJIkS_gJf93VYbmg',
+ 'title': 'UCEH7P7kyJIkS_gJf93VYbmg - Live',
+ 'tags': [],
+ },
+ 'playlist_mincount': 20,
+ }, {
+ # Tab name is not the same as tab id
+ 'url': 'https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg/letsplay',
+ 'info_dict': {
+ 'id': 'UCQvWX73GQygcwXOTSf_VDVg',
+ 'title': 'UCQvWX73GQygcwXOTSf_VDVg - Let\'s play',
+ 'tags': [],
+ },
+ 'playlist_mincount': 8,
+ }, {
+ # Home tab id is literally home. Not to get mistaken with featured
+ 'url': 'https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg/home',
+ 'info_dict': {
+ 'id': 'UCQvWX73GQygcwXOTSf_VDVg',
+ 'title': 'UCQvWX73GQygcwXOTSf_VDVg - Home',
+ 'tags': [],
+ },
+ 'playlist_mincount': 8,
+ }, {
+ # Should get three playlists for videos, shorts and streams tabs
+ 'url': 'https://www.youtube.com/channel/UCK9V2B22uJYu3N7eR_BT9QA',
+ 'info_dict': {
+ 'id': 'UCK9V2B22uJYu3N7eR_BT9QA',
+ 'title': 'Uploads for UCK9V2B22uJYu3N7eR_BT9QA'
+ },
+ 'playlist_count': 3,
+ }, {
+ # Shorts tab with channel with handle
+ 'url': 'https://www.youtube.com/@NotJustBikes/shorts',
+ 'info_dict': {
+ 'id': 'UC0intLFzLaudFG-xAvUEO-A',
+ 'title': 'Not Just Bikes - Shorts',
+ 'tags': 'count:12',
+ 'uploader': 'Not Just Bikes',
+ 'channel_url': 'https://www.youtube.com/channel/UC0intLFzLaudFG-xAvUEO-A',
+ 'description': 'md5:7513148b1f02b924783157d84c4ea555',
+ 'channel_follower_count': int,
+ 'uploader_id': 'UC0intLFzLaudFG-xAvUEO-A',
+ 'channel_id': 'UC0intLFzLaudFG-xAvUEO-A',
+ 'uploader_url': 'https://www.youtube.com/channel/UC0intLFzLaudFG-xAvUEO-A',
+ 'channel': 'Not Just Bikes',
+ },
+ 'playlist_mincount': 10,
+ }, {
+ # Streams tab
+ 'url': 'https://www.youtube.com/channel/UC3eYAvjCVwNHgkaGbXX3sig/streams',
+ 'info_dict': {
+ 'id': 'UC3eYAvjCVwNHgkaGbXX3sig',
+ 'title': '中村悠一 - Live',
+ 'tags': 'count:7',
+ 'channel_id': 'UC3eYAvjCVwNHgkaGbXX3sig',
+ 'channel_url': 'https://www.youtube.com/channel/UC3eYAvjCVwNHgkaGbXX3sig',
+ 'uploader_id': 'UC3eYAvjCVwNHgkaGbXX3sig',
+ 'channel': '中村悠一',
+ 'uploader_url': 'https://www.youtube.com/channel/UC3eYAvjCVwNHgkaGbXX3sig',
+ 'channel_follower_count': int,
+ 'uploader': '中村悠一',
+ 'description': 'md5:e744f6c93dafa7a03c0c6deecb157300',
+ },
+ 'playlist_mincount': 60,
+ }, {
+ # Channel with no uploads and hence no videos, streams, shorts tabs or uploads playlist. This should fail.
+ # See test_youtube_lists
+ 'url': 'https://www.youtube.com/channel/UC2yXPzFejc422buOIzn_0CA',
+ 'only_matching': True,
+ }, {
+ # No uploads and no UCID given. Should fail with no uploads error
+ # See test_youtube_lists
+ 'url': 'https://www.youtube.com/news',
+ 'only_matching': True
+ }, {
+ # No videos tab but has a shorts tab
+ 'url': 'https://www.youtube.com/c/TKFShorts',
+ 'info_dict': {
+ 'id': 'UCgJ5_1F6yJhYLnyMszUdmUg',
+ 'title': 'Shorts Break - Shorts',
+ 'tags': 'count:32',
+ 'channel_id': 'UCgJ5_1F6yJhYLnyMszUdmUg',
+ 'channel': 'Shorts Break',
+ 'description': 'md5:a6c234cf3d50d878ef8721e34457cd11',
+ 'uploader': 'Shorts Break',
+ 'channel_follower_count': int,
+ 'uploader_id': 'UCgJ5_1F6yJhYLnyMszUdmUg',
+ 'uploader_url': 'https://www.youtube.com/channel/UCgJ5_1F6yJhYLnyMszUdmUg',
+ 'channel_url': 'https://www.youtube.com/channel/UCgJ5_1F6yJhYLnyMszUdmUg',
+ },
+ 'playlist_mincount': 30,
+ }, {
+ # Trending Now Tab. tab id is empty
+ 'url': 'https://www.youtube.com/feed/trending',
+ 'info_dict': {
+ 'id': 'trending',
+ 'title': 'trending - Now',
+ 'tags': [],
+ },
+ 'playlist_mincount': 30,
+ }, {
+ # Trending Gaming Tab. tab id is empty
+ 'url': 'https://www.youtube.com/feed/trending?bp=4gIcGhpnYW1pbmdfY29ycHVzX21vc3RfcG9wdWxhcg%3D%3D',
+ 'info_dict': {
+ 'id': 'trending',
+ 'title': 'trending - Gaming',
+ 'tags': [],
+ },
+ 'playlist_mincount': 30,
}]
@classmethod
def suitable(cls, url):
return False if YoutubeIE.suitable(url) else super().suitable(url)
- _URL_RE = re.compile(rf'(?P<pre>{_VALID_URL})(?(not_channel)|(?P<tab>/\w+))?(?P<post>.*)$')
+ _URL_RE = re.compile(rf'(?P<pre>{_VALID_URL})(?(not_channel)|(?P<tab>/[^?#/]+))?(?P<post>.*)$')
+
+ def _get_url_mobj(self, url):
+ mobj = self._URL_RE.match(url).groupdict()
+ mobj.update((k, '') for k, v in mobj.items() if v is None)
+ return mobj
+
+ def _extract_tab_id_and_name(self, tab, base_url='https://www.youtube.com'):
+ tab_name = (tab.get('title') or '').lower()
+ tab_url = urljoin(base_url, traverse_obj(
+ tab, ('endpoint', 'commandMetadata', 'webCommandMetadata', 'url')))
+
+ tab_id = (traverse_obj(tab, 'tabIdentifier', expected_type=str)
+ or tab_url and self._get_url_mobj(tab_url)['tab'][1:])
+ if tab_id:
+ return tab_id, tab_name
+
+ # Fallback to tab name if we cannot get the tab id.
+ # XXX: should we strip non-ascii letters? e.g. in case of 'let's play' tab example on special gaming channel
+ # Note that in the case of translated tab name this may result in an empty string, which we don't want.
+ self.write_debug(f'Falling back to selected tab name: {tab_name}')
+ return {
+ 'home': 'featured',
+ 'live': 'streams',
+ }.get(tab_name, tab_name), tab_name
+
+ def _has_tab(self, tabs, tab_id):
+ return any(self._extract_tab_id_and_name(tab)[0] == tab_id for tab in tabs)
@YoutubeTabBaseInfoExtractor.passthrough_smuggled_data
def _real_extract(self, url, smuggled_data):
@@ -5730,14 +5870,8 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
urllib.parse.urlparse(url)._replace(netloc='www.youtube.com'))
compat_opts = self.get_param('compat_opts', [])
- def get_mobj(url):
- mobj = self._URL_RE.match(url).groupdict()
- mobj.update((k, '') for k, v in mobj.items() if v is None)
- return mobj
-
- mobj, redirect_warning = get_mobj(url), None
- # Youtube returns incomplete data if tabname is not lower case
- pre, tab, post, is_channel = mobj['pre'], mobj['tab'].lower(), mobj['post'], not mobj['not_channel']
+ mobj = self._get_url_mobj(url)
+ pre, tab, post, is_channel = mobj['pre'], mobj['tab'], mobj['post'], not mobj['not_channel']
if is_channel:
if smuggled_data.get('is_music_url'):
if item_id[:2] == 'VL': # Youtube music VL channels have an equivalent playlist
@@ -5750,19 +5884,16 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
get_all=False, expected_type=str)
if not murl:
raise ExtractorError('Failed to resolve album to playlist')
- return self.url_result(murl, ie=YoutubeTabIE.ie_key())
+ return self.url_result(murl, YoutubeTabIE)
elif mobj['channel_type'] == 'browse': # Youtube music /browse/ should be changed to /channel/
pre = f'https://www.youtube.com/channel/{item_id}'
- original_tab_name = tab
+ original_tab_id = tab[1:]
if is_channel and not tab and 'no-youtube-channel-redirect' not in compat_opts:
- # Home URLs should redirect to /videos/
- redirect_warning = ('A channel/user page was given. All the channel\'s videos will be downloaded. '
- 'To download only the videos in the home page, add a "/featured" to the URL')
tab = '/videos'
url = ''.join((pre, tab, post))
- mobj = get_mobj(url)
+ mobj = self._get_url_mobj(url)
# Handle both video/playlist URLs
qs = parse_qs(url)
@@ -5775,77 +5906,94 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
# Common mistake: https://www.youtube.com/watch?list=playlist_id
self.report_warning(f'A video URL was given without video ID. Trying to download playlist {playlist_id}')
url = f'https://www.youtube.com/playlist?list={playlist_id}'
- mobj = get_mobj(url)
+ mobj = self._get_url_mobj(url)
- if video_id and playlist_id:
- if self.get_param('noplaylist'):
- self.to_screen(f'Downloading just video {video_id} because of --no-playlist')
- return self.url_result(f'https://www.youtube.com/watch?v={video_id}',
- ie=YoutubeIE.ie_key(), video_id=video_id)
- self.to_screen(f'Downloading playlist {playlist_id}; add --no-playlist to just download video {video_id}')
+ if not self._yes_playlist(playlist_id, video_id):
+ return self.url_result(
+ f'https://www.youtube.com/watch?v={video_id}', YoutubeIE, video_id)
data, ytcfg = self._extract_data(url, item_id)
# YouTube may provide a non-standard redirect to the regional channel
# See: https://github.com/yt-dlp/yt-dlp/issues/2694
+ # https://support.google.com/youtube/answer/2976814#zippy=,conditional-redirects
redirect_url = traverse_obj(
data, ('onResponseReceivedActions', ..., 'navigateAction', 'endpoint', 'commandMetadata', 'webCommandMetadata', 'url'), get_all=False)
if redirect_url and 'no-youtube-channel-redirect' not in compat_opts:
redirect_url = ''.join((
urljoin('https://www.youtube.com', redirect_url), mobj['tab'], mobj['post']))
- self.to_screen(f'This playlist is likely not available in your region. Following redirect to regional playlist {redirect_url}')
- return self.url_result(redirect_url, ie=YoutubeTabIE.ie_key())
+ self.to_screen(f'This playlist is likely not available in your region. Following conditional redirect to {redirect_url}')
+ return self.url_result(redirect_url, YoutubeTabIE)
- tabs = traverse_obj(data, ('contents', 'twoColumnBrowseResultsRenderer', 'tabs'), expected_type=list)
- if tabs:
+ tab_results = []
+ tabs = self._extract_tab_renderers(data)
+ if is_channel and tabs and 'no-youtube-channel-redirect' not in compat_opts:
selected_tab = self._extract_selected_tab(tabs)
- selected_tab_url = urljoin(
- url, traverse_obj(selected_tab, ('endpoint', 'commandMetadata', 'webCommandMetadata', 'url')))
- translated_tab_name = selected_tab.get('title', '').lower()
-
- # Prefer tab name from tab url as it is always in en,
- # but only when preferred lang is set as it may not extract reliably in all cases.
- selected_tab_name = (self._preferred_lang in (None, 'en') and translated_tab_name
- or selected_tab_url and get_mobj(selected_tab_url)['tab'][1:] # primary
- or translated_tab_name)
-
- if selected_tab_name == 'home':
- selected_tab_name = 'featured'
- requested_tab_name = mobj['tab'][1:]
-
- if 'no-youtube-channel-redirect' not in compat_opts:
- if requested_tab_name == 'live': # Live tab should have redirected to the video
- raise UserNotLive(video_id=mobj['id'])
- if requested_tab_name not in ('', selected_tab_name):
- redirect_warning = f'The channel does not have a {requested_tab_name} tab'
- if not original_tab_name:
- if item_id[:2] == 'UC':
- # Topic channels don't have /videos. Use the equivalent playlist instead
- pl_id = f'UU{item_id[2:]}'
- pl_url = f'https://www.youtube.com/playlist?list={pl_id}'
- try:
- data, ytcfg = self._extract_data(pl_url, pl_id, ytcfg=ytcfg, fatal=True, webpage_fatal=True)
- except ExtractorError:
- redirect_warning += ' and the playlist redirect gave error'
- else:
- item_id, url, selected_tab_name = pl_id, pl_url, requested_tab_name
- redirect_warning += f'. Redirecting to playlist {pl_id} instead'
- if selected_tab_name and selected_tab_name != requested_tab_name:
- redirect_warning += f'. {selected_tab_name} tab is being downloaded instead'
+ selected_tab_id, selected_tab_name = self._extract_tab_id_and_name(selected_tab, url) # NB: Name may be translated
+ self.write_debug(f'Selected tab: {selected_tab_id!r} ({selected_tab_name}), Requested tab: {original_tab_id!r}')
+
+ if not original_tab_id and selected_tab_name:
+ self.to_screen('Channel URLs download all uploads of the channel. '
+ 'To download only the videos in a specific tab, pass the tab\'s URL')
+ if self._has_tab(tabs, 'streams'):
+ tab_results.append(self.url_result(''.join((pre, '/streams', post))))
+ if self._has_tab(tabs, 'shorts'):
+ tab_results.append(self.url_result(''.join((pre, '/shorts', post))))
+ # XXX: Members-only tab should also be extracted
+
+ if not tab_results and selected_tab_id != 'videos':
+ # Channel does not have streams, shorts or videos tabs
+ if item_id[:2] != 'UC':
+ raise ExtractorError('This channel has no uploads', expected=True)
+
+ # Topic channels don't have /videos. Use the equivalent playlist instead
+ pl_id = f'UU{item_id[2:]}'
+ pl_url = f'https://www.youtube.com/playlist?list={pl_id}'
+ try:
+ data, ytcfg = self._extract_data(pl_url, pl_id, ytcfg=ytcfg, fatal=True, webpage_fatal=True)
+ except ExtractorError:
+ raise ExtractorError('This channel has no uploads', expected=True)
else:
- raise ExtractorError(redirect_warning, expected=True)
+ item_id, url = pl_id, pl_url
+ self.to_screen(
+ f'The channel does not have a videos, shorts, or live tab. Redirecting to playlist {pl_id} instead')
+
+ elif tab_results and selected_tab_id != 'videos':
+ # When there are shorts/live tabs but not videos tab
+ url, data = ''.join((pre, post)), None
+
+ elif (original_tab_id or 'videos') != selected_tab_id:
+ if original_tab_id == 'live':
+ # Live tab should have redirected to the video
+ # Except in the case the channel has an actual live tab
+ # Example: https://www.youtube.com/channel/UCEH7P7kyJIkS_gJf93VYbmg/live
+ raise UserNotLive(video_id=mobj['id'])
+ elif selected_tab_name:
+ raise ExtractorError(f'This channel does not have a {original_tab_id} tab', expected=True)
+
+ # For channels such as https://www.youtube.com/channel/UCtFRv9O2AHqOZjjynzrv-xg
+ url = f'{pre}{post}'
- if redirect_warning:
- self.to_screen(redirect_warning)
self.write_debug(f'Final URL: {url}')
# YouTube sometimes provides a button to reload playlist with unavailable videos.
if 'no-youtube-unavailable-videos' not in compat_opts:
data = self._reload_with_unavailable_videos(item_id, data, ytcfg) or data
self._extract_and_report_alerts(data, only_once=True)
- tabs = traverse_obj(data, ('contents', 'twoColumnBrowseResultsRenderer', 'tabs'), expected_type=list)
+
+ tabs = self._extract_tab_renderers(data)
if tabs:
- return self._extract_from_tabs(item_id, ytcfg, data, tabs)
+ tab_results[:0] = [self._extract_from_tabs(item_id, ytcfg, data, tabs)]
+ tab_results[0].update({
+ 'extractor_key': YoutubeTabIE.ie_key(),
+ 'extractor': YoutubeTabIE.IE_NAME,
+ 'webpage_url': url,
+ })
+
+ if len(tab_results) == 1:
+ return tab_results[0]
+ elif len(tab_results) > 1:
+ return self.playlist_result(tab_results, item_id, title=f'Uploads for {item_id}')
playlist = traverse_obj(
data, ('contents', 'twoColumnWatchNextResults', 'playlist', 'playlist'), expected_type=dict)
@@ -5857,8 +6005,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
if video_id:
if mobj['tab'] != '/live': # live tab is expected to redirect to video
self.report_warning(f'Unable to recognize playlist. Downloading just video {video_id}')
- return self.url_result(f'https://www.youtube.com/watch?v={video_id}',
- ie=YoutubeIE.ie_key(), video_id=video_id)
+ return self.url_result(f'https://www.youtube.com/watch?v={video_id}', YoutubeIE, video_id)
raise ExtractorError('Unable to recognize tab page')
@@ -5891,12 +6038,13 @@ class YoutubePlaylistIE(InfoExtractor):
'uploader_id': 'UCKSpbfbl5kRQpTdL7kMc-1Q',
'description': 'md5:8fa6f52abb47a9552002fa3ddfc57fc2',
'view_count': int,
- 'uploader_url': 'https://www.youtube.com/user/Wickydoo',
+ 'uploader_url': 'https://www.youtube.com/c/WickmanVT',
'modified_date': r're:\d{8}',
'channel_id': 'UCKSpbfbl5kRQpTdL7kMc-1Q',
'channel': 'Wickman',
'tags': [],
- 'channel_url': 'https://www.youtube.com/user/Wickydoo',
+ 'channel_url': 'https://www.youtube.com/c/WickmanVT',
+ 'availability': 'public',
},
'playlist_mincount': 29,
}, {
@@ -5926,7 +6074,7 @@ class YoutubePlaylistIE(InfoExtractor):
'uploader_url': 'https://www.youtube.com/channel/UCEI1-PVPcYXjB73Hfelbmaw',
'availability': 'public',
},
- 'expected_warnings': [r'[Uu]navailable videos (are|will be) hidden'],
+ 'expected_warnings': [r'[Uu]navailable videos? (is|are|will be) hidden'],
}, {
'url': 'http://www.youtube.com/embed/_xDOZElKyNU?list=PLsyOSbh5bs16vubvKePAQ1x3PhKavfBIl',
'playlist_mincount': 455,