diff --git a/yt_dlp/downloader/hls.py b/yt_dlp/downloader/hls.py index 1f36a07f5f..2256305785 100644 --- a/yt_dlp/downloader/hls.py +++ b/yt_dlp/downloader/hls.py @@ -94,12 +94,19 @@ def real_download(self, filename, info_dict): can_download, message = self.can_download(s, info_dict, self.params.get('allow_unplayable_formats')), None if can_download: has_ffmpeg = FFmpegFD.available() - no_crypto = not Cryptodome.AES and '#EXT-X-KEY:METHOD=AES-128' in s - if no_crypto and has_ffmpeg: - can_download, message = False, 'The stream has AES-128 encryption and pycryptodomex is not available' - elif no_crypto: - message = ('The stream has AES-128 encryption and neither ffmpeg nor pycryptodomex are available; ' - 'Decryption will be performed natively, but will be extremely slow') + if not Cryptodome.AES and '#EXT-X-KEY:METHOD=AES-128' in s: + # Even if pycryptodomex isn't available, force HlsFD for m3u8s that won't work with ffmpeg + ffmpeg_can_dl = not traverse_obj(info_dict, (( + 'extra_param_to_segment_url', 'extra_param_to_key_url', + 'hls_media_playlist_data', ('hls_aes', ('uri', 'key', 'iv')), + ), any)) + message = 'The stream has AES-128 encryption and {} available'.format( + 'neither ffmpeg nor pycryptodomex are' if ffmpeg_can_dl and not has_ffmpeg else + 'pycryptodomex is not') + if has_ffmpeg and ffmpeg_can_dl: + can_download = False + else: + message += '; decryption will be performed natively, but will be extremely slow' elif info_dict.get('extractor_key') == 'Generic' and re.search(r'(?m)#EXT-X-MEDIA-SEQUENCE:(?!0$)', s): install_ffmpeg = '' if has_ffmpeg else 'install ffmpeg and ' message = ('Live HLS streams are not supported by the native downloader. If this is a livestream, ' diff --git a/yt_dlp/extractor/twitch.py b/yt_dlp/extractor/twitch.py index e4f2aec465..1b60202045 100644 --- a/yt_dlp/extractor/twitch.py +++ b/yt_dlp/extractor/twitch.py @@ -6,6 +6,7 @@ import urllib.parse from .common import InfoExtractor +from ..networking.exceptions import HTTPError from ..utils import ( ExtractorError, UserNotLive, @@ -188,19 +189,39 @@ def _get_thumbnails(self, thumbnail): }] if thumbnail else None 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', - 'allow_audio_only': 'true', - 'allow_spectre': 'true', - 'p': random.randint(1000000, 10000000), - 'platform': 'web', - 'player': 'twitchweb', - 'supported_codecs': 'av1,h265,h264', - 'playlist_include_framerate': 'true', - 'sig': signature, - 'token': token, - }) + try: + formats = self._extract_m3u8_formats( + f'{self._USHER_BASE}/{path}/{video_id}.m3u8', video_id, 'mp4', query={ + 'allow_source': 'true', + 'allow_audio_only': 'true', + 'allow_spectre': 'true', + 'p': random.randint(1000000, 10000000), + 'platform': 'web', + 'player': 'twitchweb', + 'supported_codecs': 'av1,h265,h264', + 'playlist_include_framerate': 'true', + 'sig': signature, + 'token': token, + }) + except ExtractorError as e: + if ( + not isinstance(e.cause, HTTPError) + or e.cause.status != 403 + or e.cause.response.get_header('content-type') != 'application/json' + ): + raise + + error_info = traverse_obj(e.cause.response.read(), ({json.loads}, 0, {dict})) or {} + if error_info.get('error_code') in ('vod_manifest_restricted', 'unauthorized_entitlements'): + common_msg = 'access to this subscriber-only content' + if self._get_cookies('https://gql.twitch.tv').get('auth-token'): + raise ExtractorError(f'Your account does not have {common_msg}', expected=True) + self.raise_login_required(f'You must be logged into an account that has {common_msg}') + + if error_msg := join_nonempty('error_code', 'error', from_dict=error_info, delim=': '): + raise ExtractorError(error_msg, expected=True) + raise + for fmt in formats: if fmt.get('vcodec') and fmt['vcodec'].startswith('av01'): # mpegts does not yet have proper support for av1 diff --git a/yt_dlp/extractor/youtube/_video.py b/yt_dlp/extractor/youtube/_video.py index f13dbb3161..8fa3b0a347 100644 --- a/yt_dlp/extractor/youtube/_video.py +++ b/yt_dlp/extractor/youtube/_video.py @@ -3978,7 +3978,9 @@ def get_lang_code(track): def process_language(container, base_url, lang_code, sub_name, client_name, query): lang_subs = container.setdefault(lang_code, []) for fmt in self._SUBTITLE_FORMATS: - query = {**query, 'fmt': fmt} + # xosf=1 results in undesirable text position data for vtt, json3 & srv* subtitles + # See: https://github.com/yt-dlp/yt-dlp/issues/13654 + query = {**query, 'fmt': fmt, 'xosf': []} lang_subs.append({ 'ext': fmt, 'url': urljoin('https://www.youtube.com', update_url_query(base_url, query)),