mirror of
				https://github.com/yt-dlp/yt-dlp.git
				synced 2025-10-31 06:35:12 +00:00 
			
		
		
		
	[youtube] Add YoutubeStoriesIE (#3362)
Get channel stories with `ytstories:<channel UCID>` Authored-by: coletdjnz
This commit is contained in:
		| @@ -1,3 +1,4 @@ | ||||
| import base64 | ||||
| import calendar | ||||
| import copy | ||||
| import datetime | ||||
| @@ -2199,7 +2200,33 @@ class YoutubeIE(YoutubeBaseInfoExtractor): | ||||
|                 'description': 'md5:2ef1d002cad520f65825346e2084e49d', | ||||
|             }, | ||||
|             'params': {'skip_download': True} | ||||
|         }, | ||||
|         }, { | ||||
|             # Story. Requires specific player params to work. | ||||
|             # Note: stories get removed after some period of time | ||||
|             'url': 'https://www.youtube.com/watch?v=yN3x1t3sieA', | ||||
|             'info_dict': { | ||||
|                 'id': 'yN3x1t3sieA', | ||||
|                 'ext': 'mp4', | ||||
|                 'uploader': 'Linus Tech Tips', | ||||
|                 'duration': 13, | ||||
|                 'channel': 'Linus Tech Tips', | ||||
|                 'playable_in_embed': True, | ||||
|                 'tags': [], | ||||
|                 'age_limit': 0, | ||||
|                 'uploader_url': 'http://www.youtube.com/user/LinusTechTips', | ||||
|                 'upload_date': '20220402', | ||||
|                 'thumbnail': 'https://i.ytimg.com/vi_webp/yN3x1t3sieA/maxresdefault.webp', | ||||
|                 'title': 'Story', | ||||
|                 'live_status': 'not_live', | ||||
|                 'uploader_id': 'LinusTechTips', | ||||
|                 'view_count': int, | ||||
|                 'description': '', | ||||
|                 'channel_id': 'UCXuqSBlHAE6Xw-yeJA0Tunw', | ||||
|                 'categories': ['Science & Technology'], | ||||
|                 'channel_url': 'https://www.youtube.com/channel/UCXuqSBlHAE6Xw-yeJA0Tunw', | ||||
|                 'availability': 'unlisted', | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
| 
 | ||||
|     @classmethod | ||||
| @@ -2831,12 +2858,17 @@ class YoutubeIE(YoutubeBaseInfoExtractor): | ||||
|             lambda p: int_or_none(p, default=sys.maxsize), self._configuration_arg('max_comments', ) + [''] * 4) | ||||
| 
 | ||||
|         continuation = self._extract_continuation(root_continuation_data) | ||||
|         message = self._get_text(root_continuation_data, ('contents', ..., 'messageRenderer', 'text'), max_runs=1) | ||||
|         if message and not parent: | ||||
|             self.report_warning(message, video_id=video_id) | ||||
| 
 | ||||
|         response = None | ||||
|         is_forced_continuation = False | ||||
|         is_first_continuation = parent is None | ||||
|         if is_first_continuation and not continuation: | ||||
|             # Sometimes you can get comments by generating the continuation yourself, | ||||
|             # even if YouTube initially reports them being disabled - e.g. stories comments. | ||||
|             # Note: if the comment section is actually disabled, YouTube may return a response with | ||||
|             # required check_get_keys missing. So we will disable that check initially in this case. | ||||
|             continuation = self._build_api_continuation_query(self._generate_comment_continuation(video_id)) | ||||
|             is_forced_continuation = True | ||||
| 
 | ||||
|         for page_num in itertools.count(0): | ||||
|             if not continuation: | ||||
| @@ -2857,8 +2889,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor): | ||||
|             response = self._extract_response( | ||||
|                 item_id=None, query=continuation, | ||||
|                 ep='next', ytcfg=ytcfg, headers=headers, note=note_prefix, | ||||
|                 check_get_keys='onResponseReceivedEndpoints') | ||||
| 
 | ||||
|                 check_get_keys='onResponseReceivedEndpoints' if not is_forced_continuation else None) | ||||
|             is_forced_continuation = False | ||||
|             continuation_contents = traverse_obj( | ||||
|                 response, 'onResponseReceivedEndpoints', expected_type=list, default=[]) | ||||
| 
 | ||||
| @@ -2883,6 +2915,18 @@ class YoutubeIE(YoutubeBaseInfoExtractor): | ||||
|                 if continuation: | ||||
|                     break | ||||
| 
 | ||||
|         message = self._get_text(root_continuation_data, ('contents', ..., 'messageRenderer', 'text'), max_runs=1) | ||||
|         if message and not parent and tracker['running_total'] == 0: | ||||
|             self.report_warning(f'Youtube said: {message}', video_id=video_id, only_once=True) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def _generate_comment_continuation(video_id): | ||||
|         """ | ||||
|         Generates initial comment section continuation token from given video id | ||||
|         """ | ||||
|         token = f'\x12\r\x12\x0b{video_id}\x18\x062\'"\x11"\x0b{video_id}0\x00x\x020\x00B\x10comments-section' | ||||
|         return base64.b64encode(token.encode()).decode() | ||||
| 
 | ||||
|     def _get_comments(self, ytcfg, video_id, contents, webpage): | ||||
|         """Entry for comment extraction""" | ||||
|         def _real_comment_extract(contents): | ||||
| @@ -2936,7 +2980,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor): | ||||
|         headers = self.generate_api_headers( | ||||
|             ytcfg=player_ytcfg, account_syncid=syncid, session_index=session_index, default_client=client) | ||||
| 
 | ||||
|         yt_query = {'videoId': video_id} | ||||
|         yt_query = { | ||||
|             'videoId': video_id, | ||||
|             'params': '8AEB'  # enable stories | ||||
|         } | ||||
|         yt_query.update(self._generate_player_context(sts)) | ||||
|         return self._extract_response( | ||||
|             item_id=video_id, ep='player', query=yt_query, | ||||
| @@ -3251,7 +3298,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): | ||||
|         webpage = None | ||||
|         if 'webpage' not in self._configuration_arg('player_skip'): | ||||
|             webpage = self._download_webpage( | ||||
|                 webpage_url + '&bpctr=9999999999&has_verified=1', video_id, fatal=False) | ||||
|                 webpage_url + '&bpctr=9999999999&has_verified=1&pp=8AEB', video_id, fatal=False) | ||||
| 
 | ||||
|         master_ytcfg = self.extract_ytcfg(video_id, webpage) or self._get_default_ytcfg() | ||||
| 
 | ||||
| @@ -3696,7 +3743,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): | ||||
|             unified_strdate(get_first(microformats, 'uploadDate')) | ||||
|             or unified_strdate(search_meta('uploadDate'))) | ||||
|         if not upload_date or (not info.get('is_live') and not info.get('was_live') and info.get('live_status') != 'is_upcoming'): | ||||
|             upload_date = strftime_or_none(self._extract_time_text(vpir, 'dateText')[0], '%Y%m%d') | ||||
|             upload_date = strftime_or_none(self._extract_time_text(vpir, 'dateText')[0], '%Y%m%d') or upload_date | ||||
|         info['upload_date'] = upload_date | ||||
| 
 | ||||
|         for to, frm in fallbacks.items(): | ||||
| @@ -4211,7 +4258,7 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor): | ||||
|                 self._extract_visitor_data(data, ytcfg)), | ||||
|             **metadata) | ||||
| 
 | ||||
|     def _extract_mix_playlist(self, playlist, playlist_id, data, ytcfg): | ||||
|     def _extract_inline_playlist(self, playlist, playlist_id, data, ytcfg): | ||||
|         first_id = last_id = response = None | ||||
|         for page_num in itertools.count(1): | ||||
|             videos = list(self._playlist_entries(playlist)) | ||||
| @@ -4221,9 +4268,6 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor): | ||||
|             if start >= len(videos): | ||||
|                 return | ||||
|             for video in videos[start:]: | ||||
|                 if video['id'] == first_id: | ||||
|                     self.to_screen('First video %s found again; Assuming end of Mix' % first_id) | ||||
|                     return | ||||
|                 yield video | ||||
|             first_id = first_id or videos[0]['id'] | ||||
|             last_id = videos[-1]['id'] | ||||
| @@ -4255,13 +4299,18 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor): | ||||
|         playlist_url = urljoin(url, try_get( | ||||
|             playlist, lambda x: x['endpoint']['commandMetadata']['webCommandMetadata']['url'], | ||||
|             compat_str)) | ||||
|         if playlist_url and playlist_url != url: | ||||
| 
 | ||||
|         # Some playlists are unviewable but YouTube still provides a link to the (broken) playlist page [1] | ||||
|         # [1] MLCT, RLTDwFCb4jeqaKWnciAYM-ZVHg | ||||
|         is_known_unviewable = re.fullmatch(r'MLCT|RLTD[\w-]{22}', playlist_id) | ||||
| 
 | ||||
|         if playlist_url and playlist_url != url and not is_known_unviewable: | ||||
|             return self.url_result( | ||||
|                 playlist_url, ie=YoutubeTabIE.ie_key(), video_id=playlist_id, | ||||
|                 video_title=title) | ||||
| 
 | ||||
|         return self.playlist_result( | ||||
|             self._extract_mix_playlist(playlist, playlist_id, data, ytcfg), | ||||
|             self._extract_inline_playlist(playlist, playlist_id, data, ytcfg), | ||||
|             playlist_id=playlist_id, playlist_title=title) | ||||
| 
 | ||||
|     def _extract_availability(self, data): | ||||
| @@ -5798,6 +5847,22 @@ class YoutubeHistoryIE(YoutubeFeedsInfoExtractor): | ||||
|     }] | ||||
| 
 | ||||
| 
 | ||||
| class YoutubeStoriesIE(InfoExtractor): | ||||
|     IE_DESC = 'YouTube channel stories; "ytstories:" prefix' | ||||
|     IE_NAME = 'youtube:stories' | ||||
|     _VALID_URL = r'ytstories:UC(?P<id>[A-Za-z0-9_-]{21}[AQgw])$' | ||||
|     _TESTS = [{ | ||||
|         'url': 'ytstories:UCwFCb4jeqaKWnciAYM-ZVHg', | ||||
|         'only_matching': True, | ||||
|     }] | ||||
| 
 | ||||
|     def _real_extract(self, url): | ||||
|         playlist_id = f'RLTD{self._match_id(url)}' | ||||
|         return self.url_result( | ||||
|             f'https://www.youtube.com/playlist?list={playlist_id}&playnext=1', | ||||
|             ie=YoutubeTabIE, video_id=playlist_id) | ||||
| 
 | ||||
| 
 | ||||
| class YoutubeTruncatedURLIE(InfoExtractor): | ||||
|     IE_NAME = 'youtube:truncated_url' | ||||
|     IE_DESC = False  # Do not list | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 coletdev
					coletdev