diff options
| author | Astounds <kirito@disroot.org> | 2026-04-05 14:56:51 -0500 |
|---|---|---|
| committer | Astounds <kirito@disroot.org> | 2026-04-05 14:56:51 -0500 |
| commit | f0649be5dec84ce06a3164a2d9ee90f5385ac92f (patch) | |
| tree | 6dcae30ff3e0d66c895033aab9e92a4c9e4ed513 /youtube/watch.py | |
| parent | 62a028968e6d9b4e821b6014d6658b8317328fcf (diff) | |
| download | yt-local-f0649be5dec84ce06a3164a2d9ee90f5385ac92f.tar.lz yt-local-f0649be5dec84ce06a3164a2d9ee90f5385ac92f.tar.xz yt-local-f0649be5dec84ce06a3164a2d9ee90f5385ac92f.zip | |
Add HLS support to multi-audio
Diffstat (limited to 'youtube/watch.py')
| -rw-r--r-- | youtube/watch.py | 637 |
1 files changed, 547 insertions, 90 deletions
diff --git a/youtube/watch.py b/youtube/watch.py index 332a9d5..4cc6223 100644 --- a/youtube/watch.py +++ b/youtube/watch.py @@ -42,73 +42,68 @@ def codec_name(vcodec): def get_video_sources(info, target_resolution): - '''return dict with organized sources: { - 'uni_sources': [{}, ...], # video and audio in one file - 'uni_idx': int, # default unified source index - 'pair_sources': [{video: {}, audio: {}, quality: ..., ...}, ...], - 'pair_idx': int, # default pair source index - } - ''' - audio_sources = [] + '''return dict with organized sources''' + audio_by_track = {} video_only_sources = {} uni_sources = [] pair_sources = [] - for fmt in info['formats']: if not all(fmt[attr] for attr in ('ext', 'url', 'itag')): continue - - # unified source if fmt['acodec'] and fmt['vcodec']: - source = { - 'type': 'video/' + fmt['ext'], - 'quality_string': short_video_quality_string(fmt), - } + if fmt.get('audio_track_is_default', True) is False: + continue + source = {'type': 'video/' + fmt['ext'], + 'quality_string': short_video_quality_string(fmt)} source['quality_string'] += ' (integrated)' source.update(fmt) uni_sources.append(source) continue - if not (fmt['init_range'] and fmt['index_range']): - continue - - # audio source - if fmt['acodec'] and not fmt['vcodec'] and ( - fmt['audio_bitrate'] or fmt['bitrate']): - if fmt['bitrate']: # prefer this one, more accurate right now + # Allow HLS-backed audio tracks (served locally, no init/index needed) + if not fmt.get('url', '').startswith('http://127.') and not '/ytl-api/' in fmt.get('url', ''): + continue + # Mark as HLS for frontend + fmt['is_hls'] = True + if fmt['acodec'] and not fmt['vcodec'] and (fmt['audio_bitrate'] or fmt['bitrate']): + if fmt['bitrate']: fmt['audio_bitrate'] = int(fmt['bitrate']/1000) - source = { - 'type': 'audio/' + fmt['ext'], - 'quality_string': audio_quality_string(fmt), - } + source = {'type': 'audio/' + fmt['ext'], + 'quality_string': audio_quality_string(fmt)} source.update(fmt) - source['mime_codec'] = (source['type'] + '; codecs="' - + source['acodec'] + '"') - audio_sources.append(source) - # video-only source - elif all(fmt[attr] for attr in ('vcodec', 'quality', 'width', 'fps', - 'file_size')): + source['mime_codec'] = source['type'] + '; codecs="' + source['acodec'] + '"' + tid = fmt.get('audio_track_id') or 'default' + if tid not in audio_by_track: + audio_by_track[tid] = { + 'name': fmt.get('audio_track_name') or 'Default', + 'is_default': fmt.get('audio_track_is_default', True), + 'sources': [], + } + audio_by_track[tid]['sources'].append(source) + elif all(fmt[attr] for attr in ('vcodec', 'quality', 'width', 'fps', 'file_size')): if codec_name(fmt['vcodec']) == 'unknown': continue - source = { - 'type': 'video/' + fmt['ext'], - 'quality_string': short_video_quality_string(fmt), - } + source = {'type': 'video/' + fmt['ext'], + 'quality_string': short_video_quality_string(fmt)} source.update(fmt) - source['mime_codec'] = (source['type'] + '; codecs="' - + source['vcodec'] + '"') + source['mime_codec'] = source['type'] + '; codecs="' + source['vcodec'] + '"' quality = str(fmt['quality']) + 'p' + str(fmt['fps']) - if quality in video_only_sources: - video_only_sources[quality].append(source) - else: - video_only_sources[quality] = [source] - - audio_sources.sort(key=lambda source: source['audio_bitrate']) + video_only_sources.setdefault(quality, []).append(source) + + audio_tracks = [] + default_track_id = 'default' + for tid, ti in audio_by_track.items(): + audio_tracks.append({'id': tid, 'name': ti['name'], 'is_default': ti['is_default']}) + if ti['is_default']: + default_track_id = tid + audio_tracks.sort(key=lambda t: (not t['is_default'], t['name'])) + + default_audio = audio_by_track.get(default_track_id, {}).get('sources', []) + default_audio.sort(key=lambda s: s['audio_bitrate']) uni_sources.sort(key=lambda src: src['quality']) - - webm_audios = [a for a in audio_sources if a['ext'] == 'webm'] - mp4_audios = [a for a in audio_sources if a['ext'] == 'mp4'] + webm_audios = [a for a in default_audio if a['ext'] == 'webm'] + mp4_audios = [a for a in default_audio if a['ext'] == 'mp4'] for quality_string, sources in video_only_sources.items(): # choose an audio source to go with it @@ -166,11 +161,19 @@ def get_video_sources(info, target_resolution): break pair_idx = i + audio_track_sources = {} + for tid, ti in audio_by_track.items(): + srcs = ti['sources'] + srcs.sort(key=lambda s: s.get('audio_bitrate', 0)) + audio_track_sources[tid] = srcs + return { 'uni_sources': uni_sources, 'uni_idx': uni_idx, 'pair_sources': pair_sources, 'pair_idx': pair_idx, + 'audio_tracks': audio_tracks, + 'audio_track_sources': audio_track_sources, } @@ -423,8 +426,115 @@ def extract_info(video_id, use_invidious, playlist_id=None, index=None): 'captionTracks', default=[]) info['_android_caption_tracks'] = android_caption_tracks + # Save streamingData for multi-audio extraction + pr_streaming_data = pr_data.get('streamingData', {}) + info['_streamingData'] = pr_streaming_data + yt_data_extract.update_with_new_urls(info, player_response) + # HLS manifest - try multiple clients in case one is blocked + info['hls_manifest_url'] = None + info['hls_audio_tracks'] = {} + hls_data = None + hls_client_used = None + for hls_client in ('ios', 'ios_vr', 'android'): + try: + resp = fetch_player_response(hls_client, video_id) or {} + hls_data = json.loads(resp) if isinstance(resp, str) else resp + hls_manifest_url = (hls_data.get('streamingData') or {}).get('hlsManifestUrl', '') + if hls_manifest_url: + hls_client_used = hls_client + break + except Exception as e: + print(f'HLS fetch with {hls_client} failed: {e}') + + if hls_manifest_url: + info['hls_manifest_url'] = hls_manifest_url + import re as _re + from urllib.parse import urljoin + hls_manifest = util.fetch_url(hls_manifest_url, + headers=(('User-Agent', 'Mozilla/5.0'),), + debug_name='hls_manifest').decode('utf-8') + + # Parse EXT-X-MEDIA audio tracks from HLS manifest + for line in hls_manifest.split('\n'): + if '#EXT-X-MEDIA' not in line or 'TYPE=AUDIO' not in line: + continue + name_m = _re.search(r'NAME="([^"]+)"', line) + lang_m = _re.search(r'LANGUAGE="([^"]+)"', line) + default_m = _re.search(r'DEFAULT=(YES|NO)', line) + group_m = _re.search(r'GROUP-ID="([^"]+)"', line) + uri_m = _re.search(r'URI="([^"]+)"', line) + if not uri_m or not lang_m: + continue + lang = lang_m.group(1) + is_default = default_m and default_m.group(1) == 'YES' + group = group_m.group(1) if group_m else '0' + key = lang + absolute_hls_url = urljoin(hls_manifest_url, uri_m.group(1)) + if key not in info['hls_audio_tracks'] or group > info['hls_audio_tracks'][key].get('group', '0'): + info['hls_audio_tracks'][key] = { + 'name': name_m.group(1) if name_m else lang, + 'lang': lang, + 'hls_url': absolute_hls_url, + 'group': group, + 'is_default': is_default, + } + + # Register HLS audio tracks for proxy access + added = 0 + for lang, track in info['hls_audio_tracks'].items(): + ck = video_id + '_' + lang + from youtube.hls_cache import register_track + register_track(ck, track['hls_url'], + video_id=video_id, track_id=lang) + + fmt = { + 'audio_track_id': lang, + 'audio_track_name': track['name'], + 'audio_track_is_default': track['is_default'], + 'itag': 'hls_' + lang, + 'ext': 'mp4', + 'audio_bitrate': 128, + 'bitrate': 128000, + 'acodec': 'mp4a.40.2', + 'vcodec': None, + 'width': None, + 'height': None, + 'file_size': None, + 'audio_sample_rate': 44100, + 'duration_ms': None, + 'fps': None, + 'init_range': {'start': 0, 'end': 0}, + 'index_range': {'start': 0, 'end': 0}, + 'url': '/ytl-api/audio-track?id=' + urllib.parse.quote(ck), + 's': None, + 'sp': None, + 'quality': None, + 'type': 'audio/mp4', + 'quality_string': track['name'], + 'mime_codec': 'audio/mp4; codecs="mp4a.40.2"', + 'is_hls': True, + } + info['formats'].append(fmt) + added += 1 + + if added: + print(f"Added {added} HLS audio tracks (via {hls_client_used})") + else: + print("No HLS manifest available from any client") + info['hls_manifest_url'] = None + info['hls_audio_tracks'] = {} + info['hls_unavailable'] = True + + # Register HLS manifest for proxying + if info['hls_manifest_url']: + ck = video_id + '_video' + from youtube.hls_cache import register_track + register_track(ck, info['hls_manifest_url'], video_id=video_id, track_id='video') + # Use proxy URL instead of direct Google Video URL + info['hls_manifest_url'] = '/ytl-api/hls-manifest?id=' + urllib.parse.quote(ck) + # Fallback to 'ios' if no valid URLs are found if not info.get('formats') or info.get('player_urls_missing'): print(f"No URLs found in '{primary_client}', attempting with '{fallback_client}'.") @@ -556,6 +666,339 @@ def format_bytes(bytes): return '%.2f%s' % (converted, suffix) +@yt_app.route('/ytl-api/audio-track-proxy') +def audio_track_proxy(): + """Proxy for DASH audio tracks to avoid throttling.""" + cache_key = request.args.get('id', '') + audio_url = request.args.get('url', '') + + if not audio_url: + flask.abort(400, 'Missing URL') + + try: + headers = ( + ('User-Agent', 'Mozilla/5.0'), + ('Accept', '*/*'), + ) + content = util.fetch_url(audio_url, headers=headers, + debug_name='audio_dash', report_text=None) + return flask.Response(content, mimetype='audio/mp4', + headers={'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'max-age=3600'}) + except Exception as e: + flask.abort(502, f'Audio fetch failed: {e}') + + +@yt_app.route('/ytl-api/audio-track') +def get_audio_track(): + """Proxy HLS audio/video: playlist or individual segment.""" + from youtube.hls_cache import get_hls_url, _tracks + + cache_key = request.args.get('id', '') + seg_url = request.args.get('seg', '') + playlist_url = request.args.get('url', '') + + # Handle playlist/manifest URL (used for audio track playlists) + if playlist_url: + # Unwrap if double-proxied + if '/ytl-api/audio-track' in playlist_url: + import urllib.parse as _up + parsed = _up.parse_qs(_up.urlparse(playlist_url).query) + if 'url' in parsed: + playlist_url = parsed['url'][0] + + try: + playlist = util.fetch_url(playlist_url, + headers=(('User-Agent', 'Mozilla/5.0'),), + debug_name='audio_playlist').decode('utf-8') + + # Rewrite segment URLs + import re as _re + from urllib.parse import urljoin + base_url = request.url_root.rstrip('/') + playlist_base = playlist_url.rsplit('/', 1)[0] + '/' + + playlist_lines = [] + for line in playlist.split('\n'): + line = line.strip() + if not line or line.startswith('#'): + playlist_lines.append(line) + continue + + # Resolve and proxy segment URL + seg = line if line.startswith('http') else urljoin(playlist_base, line) + # Always use &seg= parameter, never &url= for segments + playlist_lines.append( + base_url + '/ytl-api/audio-track?id=' + + urllib.parse.quote(cache_key) + + '&seg=' + urllib.parse.quote(seg, safe='') + ) + + playlist = '\n'.join(playlist_lines) + + return flask.Response(playlist, mimetype='application/vnd.apple.mpegurl', + headers={'Access-Control-Allow-Origin': '*'}) + except Exception as e: + import traceback + traceback.print_exc() + flask.abort(502, f'Playlist fetch failed: {e}') + + # Handle individual segment or nested playlist + if seg_url: + # Check if seg_url is already a proxied URL + if '/ytl-api/audio-track' in seg_url: + import urllib.parse as _up + parsed = _up.parse_qs(_up.urlparse(seg_url).query) + if 'seg' in parsed: + seg_url = parsed['seg'][0] + elif 'url' in parsed: + seg_url = parsed['url'][0] + + # Check if this is a nested playlist (m3u8) that needs rewriting + # Playlists END with .m3u8 (optionally followed by query params) + # Segments may contain /index.m3u8/ in their path but end with .ts or similar + url_path = urllib.parse.urlparse(seg_url).path + + # Only treat as playlist if path ends with .m3u8 + # Don't use 'in' check because segments can have /index.m3u8/ in their path + is_playlist = url_path.endswith('.m3u8') + + if is_playlist: + # This is a variant playlist - fetch and rewrite it + try: + raw_content = util.fetch_url(seg_url, + headers=(('User-Agent', 'Mozilla/5.0'),), + debug_name='nested_playlist') + + # Check if this is actually binary data (segment) misidentified as playlist + try: + playlist = raw_content.decode('utf-8') + except UnicodeDecodeError: + is_playlist = False # Fall through to segment handler + + if is_playlist: + # Rewrite segment URLs in this playlist + from urllib.parse import urljoin + import re as _re + base_url = request.url_root.rstrip('/') + playlist_base = seg_url.rsplit('/', 1)[0] + '/' + + def proxy_url(url): + """Rewrite a single URL to go through the proxy""" + if not url or url.startswith('/ytl-api/'): + return url + if not url.startswith('http://') and not url.startswith('https://'): + url = urljoin(playlist_base, url) + return (base_url + '/ytl-api/audio-track?id=' + + urllib.parse.quote(cache_key) + + '&seg=' + urllib.parse.quote(url, safe='')) + + playlist_lines = [] + for line in playlist.split('\n'): + line = line.strip() + if not line: + playlist_lines.append(line) + continue + + # Handle tags with URI attributes (EXT-X-MAP, EXT-X-KEY, etc.) + if line.startswith('#') and 'URI=' in line: + def rewrite_uri_attr(match): + uri = match.group(1) + return 'URI="' + proxy_url(uri) + '"' + line = _re.sub(r'URI="([^"]+)"', rewrite_uri_attr, line) + playlist_lines.append(line) + elif line.startswith('#'): + # Other tags pass through unchanged + playlist_lines.append(line) + else: + # This is a segment URL line + seg = line if line.startswith('http') else urljoin(playlist_base, line) + playlist_lines.append(proxy_url(seg)) + + playlist = '\n'.join(playlist_lines) + + return flask.Response(playlist, mimetype='application/vnd.apple.mpegurl', + headers={'Access-Control-Allow-Origin': '*'}) + except Exception as e: + import traceback + traceback.print_exc() + flask.abort(502, f'Nested playlist fetch failed: {e}') + + # This is an actual segment - fetch and serve it + try: + headers = ( + ('User-Agent', 'Mozilla/5.0'), + ('Accept', '*/*'), + ) + content = util.fetch_url(seg_url, headers=headers, + debug_name='hls_seg', report_text=None) + + # Determine content type based on URL or content + # HLS segments are usually MPEG-TS (.ts) but can be MP4 (.mp4, .m4s) + if '.mp4' in seg_url or '.m4s' in seg_url or seg_url.lower().endswith('.mp4'): + content_type = 'video/mp4' + elif '.webm' in seg_url or seg_url.lower().endswith('.webm'): + content_type = 'video/webm' + else: + # Default to MPEG-TS for HLS + content_type = 'video/mp2t' + + return flask.Response(content, mimetype=content_type, + headers={ + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Range, Content-Type', + 'Cache-Control': 'max-age=3600', + 'Content-Type': content_type, + }) + except Exception as e: + import traceback + traceback.print_exc() + flask.abort(502, f'Segment fetch failed: {e}') + + # Legacy: Proxy the HLS playlist for audio tracks (using get_hls_url) + hls_url = get_hls_url(cache_key) + if not hls_url: + flask.abort(404, 'Audio track not found') + + try: + playlist = util.fetch_url(hls_url, + headers=(('User-Agent', 'Mozilla/5.0'),), + debug_name='audio_hls_playlist').decode('utf-8') + + # Rewrite segment URLs to go through our proxy endpoint + import re as _re + from urllib.parse import urljoin + hls_base_url = hls_url.rsplit('/', 1)[0] + '/' + + def make_proxy_url(segment_url): + if segment_url.startswith('/ytl-api/audio-track'): + return segment_url + base_url = request.url_root.rstrip('/') + return (base_url + '/ytl-api/audio-track?id=' + + urllib.parse.quote(cache_key) + + '&seg=' + urllib.parse.quote(segment_url)) + + playlist_lines = [] + for line in playlist.split('\n'): + line = line.strip() + if not line or line.startswith('#'): + playlist_lines.append(line) + continue + + if line.startswith('http://') or line.startswith('https://'): + segment_url = line + else: + segment_url = urljoin(hls_base_url, line) + + playlist_lines.append(make_proxy_url(segment_url)) + + playlist = '\n'.join(playlist_lines) + + return flask.Response(playlist, mimetype='application/vnd.apple.mpegurl', + headers={'Access-Control-Allow-Origin': '*'}) + except Exception as e: + flask.abort(502, f'Playlist fetch failed: {e}') + + +@yt_app.route('/ytl-api/hls-manifest') +def get_hls_manifest(): + """Proxy HLS video manifest, rewriting ALL URLs including audio tracks.""" + from youtube.hls_cache import get_hls_url + + cache_key = request.args.get('id', '') + is_audio = '_audio_' in cache_key or cache_key.endswith('_audio') + print(f'[hls-manifest] Request: id={cache_key[:40] if cache_key else ""}... (audio={is_audio})') + + hls_url = get_hls_url(cache_key) + print(f'[hls-manifest] HLS URL: {hls_url[:80] if hls_url else None}...') + if not hls_url: + flask.abort(404, 'HLS manifest not found') + + try: + print(f'[hls-manifest] Fetching HLS manifest...') + manifest = util.fetch_url(hls_url, + headers=(('User-Agent', 'Mozilla/5.0'),), + debug_name='hls_manifest').decode('utf-8') + print(f'[hls-manifest] Successfully fetched manifest ({len(manifest)} bytes)') + + # Rewrite all URLs in the manifest to go through our proxy + import re as _re + from urllib.parse import urljoin + + # Get the base URL for resolving relative URLs + hls_base_url = hls_url.rsplit('/', 1)[0] + '/' + base_url = request.url_root.rstrip('/') + + # Rewrite URLs - handle both segment URLs and audio track URIs + def rewrite_url(url, is_audio_track=False): + if not url or url.startswith('/ytl-api/'): + return url + + # Resolve relative URLs + if not url.startswith('http://') and not url.startswith('https://'): + url = urljoin(hls_base_url, url) + + if is_audio_track: + # Audio track playlist - proxy through audio-track endpoint + return (base_url + '/ytl-api/audio-track?id=' + + urllib.parse.quote(cache_key) + + '&url=' + urllib.parse.quote(url, safe='')) + else: + # Video segment or variant playlist - proxy through audio-track endpoint + return (base_url + '/ytl-api/audio-track?id=' + + urllib.parse.quote(cache_key) + + '&seg=' + urllib.parse.quote(url, safe='')) + + # Parse and rewrite the manifest + manifest_lines = [] + rewritten_count = 0 + for line in manifest.split('\n'): + line = line.strip() + if not line: + manifest_lines.append(line) + continue + + # Handle EXT-X-MEDIA tags with URI (audio tracks) + if line.startswith('#EXT-X-MEDIA:') and 'URI=' in line: + # Extract and rewrite the URI attribute + def rewrite_media_uri(match): + nonlocal rewritten_count + uri = match.group(1) + rewritten_count += 1 + return 'URI="' + rewrite_url(uri, is_audio_track=True) + '"' + line = _re.sub(r'URI="([^"]+)"', rewrite_media_uri, line) + manifest_lines.append(line) + elif line.startswith('#'): + # Other tags pass through + manifest_lines.append(line) + else: + # This is a URL (segment or variant playlist) + if line.startswith('http://') or line.startswith('https://'): + url = line + else: + url = urljoin(hls_base_url, line) + rewritten_count += 1 + manifest_lines.append(rewrite_url(url)) + + manifest = '\n'.join(manifest_lines) + print(f'[hls-manifest] Rewrote manifest with {len(manifest_lines)} lines, {rewritten_count} URLs rewritten') + + return flask.Response(manifest, mimetype='application/vnd.apple.mpegurl', + headers={ + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Range, Content-Type', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/vnd.apple.mpegurl', + }) + except Exception as e: + print(f'[hls-manifest] Error: {e}') + import traceback + traceback.print_exc() + flask.abort(502, f'Manifest fetch failed: {e}') + + @yt_app.route('/ytl-api/storyboard.vtt') def get_storyboard_vtt(): """ @@ -731,47 +1174,50 @@ def get_watch_page(video_id=None): if (settings.route_tor == 2) or info['tor_bypass_used']: target_resolution = 240 else: - target_resolution = settings.default_resolution - - source_info = get_video_sources(info, target_resolution) - uni_sources = source_info['uni_sources'] - pair_sources = source_info['pair_sources'] - uni_idx, pair_idx = source_info['uni_idx'], source_info['pair_idx'] - - pair_quality = yt_data_extract.deep_get(pair_sources, pair_idx, 'quality') - uni_quality = yt_data_extract.deep_get(uni_sources, uni_idx, 'quality') - - pair_error = abs((pair_quality or 360) - target_resolution) - uni_error = abs((uni_quality or 360) - target_resolution) - if uni_error == pair_error: - # use settings.prefer_uni_sources as a tiebreaker - closer_to_target = 'uni' if settings.prefer_uni_sources else 'pair' - elif uni_error < pair_error: - closer_to_target = 'uni' + res = settings.default_resolution + target_resolution = 1080 if res == 'auto' else int(res) + + # Get video sources for no-JS fallback and DASH (av-merge) fallback + video_sources = get_video_sources(info, target_resolution) + uni_sources = video_sources['uni_sources'] + pair_sources = video_sources['pair_sources'] + pair_idx = video_sources['pair_idx'] + audio_track_sources = video_sources['audio_track_sources'] + + # Build audio tracks list from HLS + audio_tracks = [] + hls_audio_tracks = info.get('hls_audio_tracks', {}) + hls_manifest_url = info.get('hls_manifest_url') + if hls_audio_tracks: + # Prefer "original" audio track + original_lang = None + for lang, track in hls_audio_tracks.items(): + if 'original' in (track.get('name') or '').lower(): + original_lang = lang + break + + # Add tracks, preferring original as default + for lang, track in hls_audio_tracks.items(): + is_default = (lang == original_lang) if original_lang else track['is_default'] + if is_default: + audio_tracks.insert(0, { + 'id': lang, + 'name': track['name'], + 'is_default': True, + }) + else: + audio_tracks.append({ + 'id': lang, + 'name': track['name'], + 'is_default': False, + }) else: - closer_to_target = 'pair' + # Fallback: single default audio track + audio_tracks = [{'id': 'default', 'name': 'Default', 'is_default': True}] - if settings.prefer_uni_sources == 2: - # Use uni sources unless there's no choice. - using_pair_sources = ( - bool(pair_sources) and (not uni_sources) - ) - else: - # Use the pair sources if they're closer to the desired resolution - using_pair_sources = ( - bool(pair_sources) - and (not uni_sources or closer_to_target == 'pair') - ) - if using_pair_sources: - video_height = pair_sources[pair_idx]['height'] - video_width = pair_sources[pair_idx]['width'] - else: - video_height = yt_data_extract.deep_get( - uni_sources, uni_idx, 'height', default=360 - ) - video_width = yt_data_extract.deep_get( - uni_sources, uni_idx, 'width', default=640 - ) + # Get video dimensions + video_height = info.get('height') or 360 + video_width = info.get('width') or 640 @@ -818,7 +1264,14 @@ def get_watch_page(video_id=None): other_downloads = other_downloads, video_info = json.dumps(video_info), hls_formats = info['hls_formats'], + hls_manifest_url = hls_manifest_url, + audio_tracks = audio_tracks, subtitle_sources = subtitle_sources, + uni_sources = uni_sources, + pair_sources = pair_sources, + pair_idx = pair_idx, + hls_unavailable = info.get('hls_unavailable', False), + playback_mode = settings.playback_mode, related = info['related_videos'], playlist = info['playlist'], music_list = info['music_list'], @@ -855,16 +1308,20 @@ def get_watch_page(video_id=None): 'video_duration': info['duration'], 'settings': settings.current_settings_dict, 'has_manual_captions': any(s.get('on') for s in subtitle_sources), - **source_info, - 'using_pair_sources': using_pair_sources, + 'audio_tracks': audio_tracks, + 'hls_manifest_url': hls_manifest_url, 'time_start': time_start, 'playlist': info['playlist'], 'related': info['related_videos'], 'playability_error': info['playability_error'], + 'hls_unavailable': info.get('hls_unavailable', False), + 'pair_sources': pair_sources, + 'pair_idx': pair_idx, + 'uni_sources': uni_sources, + 'uni_idx': video_sources['uni_idx'], + 'using_pair_sources': bool(pair_sources), }, font_family = youtube.font_choices[settings.font], # for embed page - **source_info, - using_pair_sources = using_pair_sources, ) |
