mirror of
				https://github.com/yt-dlp/yt-dlp.git
				synced 2025-10-31 06:35:12 +00:00 
			
		
		
		
	[ie/twitch] Support --live-from-start (#13202)
				
					
				
			Closes #10520 Authored by: bashonly
This commit is contained in:
		| @@ -187,7 +187,7 @@ class TwitchBaseIE(InfoExtractor): | ||||
|             'url': thumbnail, | ||||
|         }] if thumbnail else None | ||||
| 
 | ||||
|     def _extract_twitch_m3u8_formats(self, path, video_id, token, signature): | ||||
|     def _extract_twitch_m3u8_formats(self, path, video_id, token, signature, live_from_start=False): | ||||
|         formats = self._extract_m3u8_formats( | ||||
|             f'{self._USHER_BASE}/{path}/{video_id}.m3u8', video_id, 'mp4', query={ | ||||
|                 'allow_source': 'true', | ||||
| @@ -204,7 +204,10 @@ class TwitchBaseIE(InfoExtractor): | ||||
|         for fmt in formats: | ||||
|             if fmt.get('vcodec') and fmt['vcodec'].startswith('av01'): | ||||
|                 # mpegts does not yet have proper support for av1 | ||||
|                 fmt['downloader_options'] = {'ffmpeg_args_out': ['-f', 'mp4']} | ||||
|                 fmt.setdefault('downloader_options', {}).update({'ffmpeg_args_out': ['-f', 'mp4']}) | ||||
|             if live_from_start: | ||||
|                 fmt.setdefault('downloader_options', {}).update({'ffmpeg_args': ['-live_start_index', '0']}) | ||||
|                 fmt['is_from_start'] = True | ||||
| 
 | ||||
|         return formats | ||||
| 
 | ||||
| @@ -550,7 +553,8 @@ class TwitchVodIE(TwitchBaseIE): | ||||
|         access_token = self._download_access_token(vod_id, 'video', 'id') | ||||
| 
 | ||||
|         formats = self._extract_twitch_m3u8_formats( | ||||
|             'vod', vod_id, access_token['value'], access_token['signature']) | ||||
|             'vod', vod_id, access_token['value'], access_token['signature'], | ||||
|             live_from_start=self.get_param('live_from_start')) | ||||
|         formats.extend(self._extract_storyboard(vod_id, video.get('storyboard'), info.get('duration'))) | ||||
| 
 | ||||
|         self._prefer_source(formats) | ||||
| @@ -633,6 +637,10 @@ class TwitchPlaylistBaseIE(TwitchBaseIE): | ||||
|     _PAGE_LIMIT = 100 | ||||
| 
 | ||||
|     def _entries(self, channel_name, *args): | ||||
|         """ | ||||
|         Subclasses must define _make_variables() and _extract_entry(), | ||||
|         as well as set _OPERATION_NAME, _ENTRY_KIND, _EDGE_KIND, and _NODE_KIND | ||||
|         """ | ||||
|         cursor = None | ||||
|         variables_common = self._make_variables(channel_name, *args) | ||||
|         entries_key = f'{self._ENTRY_KIND}s' | ||||
| @@ -672,7 +680,22 @@ class TwitchPlaylistBaseIE(TwitchBaseIE): | ||||
|                 break | ||||
| 
 | ||||
| 
 | ||||
| class TwitchVideosIE(TwitchPlaylistBaseIE): | ||||
| class TwitchVideosBaseIE(TwitchPlaylistBaseIE): | ||||
|     _OPERATION_NAME = 'FilterableVideoTower_Videos' | ||||
|     _ENTRY_KIND = 'video' | ||||
|     _EDGE_KIND = 'VideoEdge' | ||||
|     _NODE_KIND = 'Video' | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def _make_variables(channel_name, broadcast_type, sort): | ||||
|         return { | ||||
|             'channelOwnerLogin': channel_name, | ||||
|             'broadcastType': broadcast_type, | ||||
|             'videoSort': sort.upper(), | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
| class TwitchVideosIE(TwitchVideosBaseIE): | ||||
|     _VALID_URL = r'https?://(?:(?:www|go|m)\.)?twitch\.tv/(?P<id>[^/]+)/(?:videos|profile)' | ||||
| 
 | ||||
|     _TESTS = [{ | ||||
| @@ -751,11 +774,6 @@ class TwitchVideosIE(TwitchPlaylistBaseIE): | ||||
|         'views': 'Popular', | ||||
|     } | ||||
| 
 | ||||
|     _OPERATION_NAME = 'FilterableVideoTower_Videos' | ||||
|     _ENTRY_KIND = 'video' | ||||
|     _EDGE_KIND = 'VideoEdge' | ||||
|     _NODE_KIND = 'Video' | ||||
| 
 | ||||
|     @classmethod | ||||
|     def suitable(cls, url): | ||||
|         return (False | ||||
| @@ -764,14 +782,6 @@ class TwitchVideosIE(TwitchPlaylistBaseIE): | ||||
|                     TwitchVideosCollectionsIE)) | ||||
|                 else super().suitable(url)) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def _make_variables(channel_name, broadcast_type, sort): | ||||
|         return { | ||||
|             'channelOwnerLogin': channel_name, | ||||
|             'broadcastType': broadcast_type, | ||||
|             'videoSort': sort.upper(), | ||||
|         } | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def _extract_entry(node): | ||||
|         return _make_video_result(node) | ||||
| @@ -919,7 +929,7 @@ class TwitchVideosCollectionsIE(TwitchPlaylistBaseIE): | ||||
|             playlist_title=f'{channel_name} - Collections') | ||||
| 
 | ||||
| 
 | ||||
| class TwitchStreamIE(TwitchBaseIE): | ||||
| class TwitchStreamIE(TwitchVideosBaseIE): | ||||
|     IE_NAME = 'twitch:stream' | ||||
|     _VALID_URL = r'''(?x) | ||||
|                     https?:// | ||||
| @@ -982,6 +992,7 @@ class TwitchStreamIE(TwitchBaseIE): | ||||
|             'skip_download': 'Livestream', | ||||
|         }, | ||||
|     }] | ||||
|     _PAGE_LIMIT = 1 | ||||
| 
 | ||||
|     @classmethod | ||||
|     def suitable(cls, url): | ||||
| @@ -995,6 +1006,20 @@ class TwitchStreamIE(TwitchBaseIE): | ||||
|                     TwitchClipsIE)) | ||||
|                 else super().suitable(url)) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def _extract_entry(node): | ||||
|         if not isinstance(node, dict) or not node.get('id'): | ||||
|             return None | ||||
|         video_id = node['id'] | ||||
|         return { | ||||
|             '_type': 'url', | ||||
|             'ie_key': TwitchVodIE.ie_key(), | ||||
|             'id': 'v' + video_id, | ||||
|             'url': f'https://www.twitch.tv/videos/{video_id}', | ||||
|             'title': node.get('title'), | ||||
|             'timestamp': unified_timestamp(node.get('publishedAt')) or 0, | ||||
|         } | ||||
| 
 | ||||
|     def _real_extract(self, url): | ||||
|         channel_name = self._match_id(url).lower() | ||||
| 
 | ||||
| @@ -1029,6 +1054,16 @@ class TwitchStreamIE(TwitchBaseIE): | ||||
|         if not stream: | ||||
|             raise UserNotLive(video_id=channel_name) | ||||
| 
 | ||||
|         timestamp = unified_timestamp(stream.get('createdAt')) | ||||
| 
 | ||||
|         if self.get_param('live_from_start'): | ||||
|             self.to_screen(f'{channel_name}: Extracting VOD to download live from start') | ||||
|             entry = next(self._entries(channel_name, None, 'time'), None) | ||||
|             if entry and entry.pop('timestamp') >= (timestamp or float('inf')): | ||||
|                 return entry | ||||
|             self.report_warning( | ||||
|                 'Unable to extract the VOD associated with this livestream', video_id=channel_name) | ||||
| 
 | ||||
|         access_token = self._download_access_token( | ||||
|             channel_name, 'stream', 'channelName') | ||||
| 
 | ||||
| @@ -1038,7 +1073,6 @@ class TwitchStreamIE(TwitchBaseIE): | ||||
|         self._prefer_source(formats) | ||||
| 
 | ||||
|         view_count = stream.get('viewers') | ||||
|         timestamp = unified_timestamp(stream.get('createdAt')) | ||||
| 
 | ||||
|         sq_user = try_get(gql, lambda x: x[1]['data']['user'], dict) or {} | ||||
|         uploader = sq_user.get('displayName') | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 bashonly
					bashonly