Partially fix age restricted videos

Does not work for videos that require decryption because
decryption is not working (giving 403) for some reason.

Related invidious issue for decryption not working:
https://github.com/iv-org/invidious/issues/3245

Partial fix for #146
This commit is contained in:
Jesus E 2023-05-28 21:30:51 -04:00
parent c6e1b366b5
commit e54596f3e9
No known key found for this signature in database
GPG Key ID: 159C8F8BC9AED8B6
3 changed files with 118 additions and 68 deletions

View File

@ -19,6 +19,46 @@ from urllib.parse import parse_qs, urlencode
from types import SimpleNamespace from types import SimpleNamespace
from math import ceil from math import ceil
# https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/extractor/youtube.py#L72
INNERTUBE_CLIENTS = {
'android': {
'INNERTUBE_API_KEY': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w',
'INNERTUBE_CONTEXT': {
'client': {
'clientName': 'ANDROID',
'clientVersion': '17.31.35',
'androidSdkVersion': 31,
'userAgent': 'com.google.android.youtube/17.31.35 (Linux; U; Android 11) gzip'
},
# https://github.com/yt-dlp/yt-dlp/pull/575#issuecomment-887739287
'thirdParty': {
'embedUrl': 'https://google.com', # Can be any valid URL
}
},
'INNERTUBE_CONTEXT_CLIENT_NAME': 3,
'REQUIRE_JS_PLAYER': False,
},
# This client can access age restricted videos (unless the uploader has disabled the 'allow embedding' option)
# See: https://github.com/zerodytrash/YouTube-Internal-Clients
'tv_embedded': {
'INNERTUBE_API_KEY': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
'INNERTUBE_CONTEXT': {
'client': {
'clientName': 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',
'clientVersion': '2.0',
},
# https://github.com/yt-dlp/yt-dlp/pull/575#issuecomment-887739287
'thirdParty': {
'embedUrl': 'https://google.com', # Can be any valid URL
}
},
'INNERTUBE_CONTEXT_CLIENT_NAME': 85,
'REQUIRE_JS_PLAYER': True,
},
}
try: try:
with open(os.path.join(settings.data_dir, 'decrypt_function_cache.json'), 'r') as f: with open(os.path.join(settings.data_dir, 'decrypt_function_cache.json'), 'r') as f:
decrypt_cache = json.loads(f.read())['decrypt_cache'] decrypt_cache = json.loads(f.read())['decrypt_cache']
@ -49,6 +89,8 @@ def get_video_sources(info, target_resolution):
video_only_sources = {} video_only_sources = {}
uni_sources = [] uni_sources = []
pair_sources = [] pair_sources = []
for fmt in info['formats']: for fmt in info['formats']:
if not all(fmt[attr] for attr in ('ext', 'url', 'itag')): if not all(fmt[attr] for attr in ('ext', 'url', 'itag')):
continue continue
@ -74,7 +116,6 @@ def get_video_sources(info, target_resolution):
fmt['audio_bitrate'] = int(fmt['bitrate']/1000) fmt['audio_bitrate'] = int(fmt['bitrate']/1000)
source = { source = {
'type': 'audio/' + fmt['ext'], 'type': 'audio/' + fmt['ext'],
'bitrate': fmt['audio_bitrate'],
'quality_string': audio_quality_string(fmt), 'quality_string': audio_quality_string(fmt),
} }
source.update(fmt) source.update(fmt)
@ -308,14 +349,6 @@ def save_decrypt_cache():
f.close() f.close()
watch_headers = (
('Accept', '*/*'),
('Accept-Language', 'en-US,en;q=0.5'),
('X-YouTube-Client-Name', '2'),
('X-YouTube-Client-Version', '2.20180830'),
) + util.mobile_ua
def decrypt_signatures(info, video_id): def decrypt_signatures(info, video_id):
'''return error string, or False if no errors''' '''return error string, or False if no errors'''
if not yt_data_extract.requires_decryption(info): if not yt_data_extract.requires_decryption(info):
@ -345,8 +378,28 @@ def _add_to_error(info, key, additional_message):
else: else:
info[key] = additional_message info[key] = additional_message
def fetch_player_response(client, video_id):
client_params = INNERTUBE_CLIENTS[client]
context = client_params['INNERTUBE_CONTEXT']
key = client_params['INNERTUBE_API_KEY']
host = client_params.get('INNERTUBE_HOST') or 'youtubei.googleapis.com'
user_agent = context['client'].get('userAgent') or util.mobile_user_agent
def extract_info(video_id, use_invidious, playlist_id=None, index=None): url = 'https://' + host + '/youtubei/v1/player?key=' + key
data = {
'videoId': video_id,
'context': context,
}
data = json.dumps(data)
headers = (('Content-Type', 'application/json'),('User-Agent', user_agent))
player_response = util.fetch_url(
url, data=data, headers=headers,
debug_name='youtubei_player_' + client,
report_text='Fetched ' + client + ' youtubei player'
).decode('utf-8')
return player_response
def fetch_watch_page_info(video_id, playlist_id, index):
# bpctr=9999999999 will bypass are-you-sure dialogs for controversial # bpctr=9999999999 will bypass are-you-sure dialogs for controversial
# videos # videos
url = 'https://m.youtube.com/embed/' + video_id + '?bpctr=9999999999' url = 'https://m.youtube.com/embed/' + video_id + '?bpctr=9999999999'
@ -354,52 +407,46 @@ def extract_info(video_id, use_invidious, playlist_id=None, index=None):
url += '&list=' + playlist_id url += '&list=' + playlist_id
if index: if index:
url += '&index=' + index url += '&index=' + index
watch_page = util.fetch_url(url, headers=watch_headers,
headers = (
('Accept', '*/*'),
('Accept-Language', 'en-US,en;q=0.5'),
('X-YouTube-Client-Name', '2'),
('X-YouTube-Client-Version', '2.20180830'),
) + util.mobile_ua
watch_page = util.fetch_url(url, headers=headers,
debug_name='watch') debug_name='watch')
watch_page = watch_page.decode('utf-8') watch_page = watch_page.decode('utf-8')
info = yt_data_extract.extract_watch_info_from_html(watch_page) return yt_data_extract.extract_watch_info_from_html(watch_page)
context = { def extract_info(video_id, use_invidious, playlist_id=None, index=None):
'client': { tasks = (
'clientName': 'ANDROID', # Get video metadata from here
'clientVersion': '17.29.35', gevent.spawn(fetch_watch_page_info, video_id, playlist_id, index),
'androidSdkVersion': '31',
'gl': 'US',
'hl': 'en',
},
# https://github.com/yt-dlp/yt-dlp/pull/575#issuecomment-887739287
'thirdParty': {
'embedUrl': 'https://google.com', # Can be any valid URL
}
}
if info['age_restricted'] or info['player_urls_missing']:
if info['age_restricted']:
print('Age restricted video. Fetching /youtubei/v1/player page')
else:
print('Missing player. Fetching /youtubei/v1/player page')
context['client']['clientScreen'] = 'EMBED'
else:
print('Fetching /youtubei/v1/player page')
# https://github.com/yt-dlp/yt-dlp/issues/574#issuecomment-887171136 # Get video URLs by spoofing as android client because its urls don't
# ANDROID is used instead because its urls don't require decryption # require decryption
# The URLs returned with WEB for videos requiring decryption # The URLs returned with WEB for videos requiring decryption
# couldn't be decrypted with the base.js from the web page for some # couldn't be decrypted with the base.js from the web page for some
# reason # reason
url ='https://youtubei.googleapis.com/youtubei/v1/player' # https://github.com/yt-dlp/yt-dlp/issues/574#issuecomment-887171136
url += '?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8' gevent.spawn(fetch_player_response, 'android', video_id)
data = { )
'videoId': video_id, gevent.joinall(tasks)
'context': context, util.check_gevent_exceptions(*tasks)
} info, player_response = tasks[0].value, tasks[1].value
data = json.dumps(data)
content_header = (('Content-Type', 'application/json'),)
player_response = util.fetch_url(
url, data=data, headers=util.mobile_ua + content_header,
debug_name='youtubei_player',
report_text='Fetched youtubei player page').decode('utf-8')
yt_data_extract.update_with_age_restricted_info(info, player_response) yt_data_extract.update_with_new_urls(info, player_response)
# Age restricted video, retry
if info['age_restricted'] or info['player_urls_missing']:
if info['age_restricted']:
print('Age restricted video, retrying')
else:
print('Player urls missing, retrying')
player_response = fetch_player_response('tv_embedded', video_id)
yt_data_extract.update_with_new_urls(info, player_response)
# signature decryption # signature decryption
decryption_error = decrypt_signatures(info, video_id) decryption_error = decrypt_signatures(info, video_id)
@ -422,8 +469,7 @@ def extract_info(video_id, use_invidious, playlist_id=None, index=None):
if (info['hls_manifest_url'] if (info['hls_manifest_url']
and (info['live'] or not info['formats'] or not info['urls_ready']) and (info['live'] or not info['formats'] or not info['urls_ready'])
): ):
manifest = util.fetch_url( manifest = util.fetch_url(info['hls_manifest_url'],
info['hls_manifest_url'],
debug_name='hls_manifest.m3u8', debug_name='hls_manifest.m3u8',
report_text='Fetched hls manifest' report_text='Fetched hls manifest'
).decode('utf-8') ).decode('utf-8')
@ -439,6 +485,7 @@ def extract_info(video_id, use_invidious, playlist_id=None, index=None):
# check for 403. Unnecessary for tor video routing b/c ip address is same # check for 403. Unnecessary for tor video routing b/c ip address is same
info['invidious_used'] = False info['invidious_used'] = False
info['invidious_reload_button'] = False info['invidious_reload_button'] = False
info['tor_bypass_used'] = False
if (settings.route_tor == 1 if (settings.route_tor == 1
and info['formats'] and info['formats'][0]['url']): and info['formats'] and info['formats'][0]['url']):
try: try:
@ -452,6 +499,7 @@ def extract_info(video_id, use_invidious, playlist_id=None, index=None):
if response.status == 403: if response.status == 403:
print('Access denied (403) for video urls.') print('Access denied (403) for video urls.')
print('Routing video through Tor') print('Routing video through Tor')
info['tor_bypass_used'] = True
for fmt in info['formats']: for fmt in info['formats']:
fmt['url'] += '&use_tor=1' fmt['url'] += '&use_tor=1'
elif 300 <= response.status < 400: elif 300 <= response.status < 400:
@ -682,20 +730,20 @@ def get_watch_page(video_id=None):
'codecs': codecs_string, 'codecs': codecs_string,
}) })
if (settings.route_tor == 2) or info['tor_bypass_used']:
target_resolution = 240
else:
target_resolution = settings.default_resolution target_resolution = settings.default_resolution
source_info = get_video_sources(info, target_resolution) source_info = get_video_sources(info, target_resolution)
uni_sources = source_info['uni_sources'] uni_sources = source_info['uni_sources']
pair_sources = source_info['pair_sources'] pair_sources = source_info['pair_sources']
uni_idx, pair_idx = source_info['uni_idx'], source_info['pair_idx'] uni_idx, pair_idx = source_info['uni_idx'], source_info['pair_idx']
video_height = yt_data_extract.deep_get(source_info, 'uni_sources',
uni_idx, 'height',
default=360)
video_width = yt_data_extract.deep_get(source_info, 'uni_sources',
uni_idx, 'width',
default=640)
pair_quality = yt_data_extract.deep_get(pair_sources, pair_idx, 'quality') pair_quality = yt_data_extract.deep_get(pair_sources, pair_idx, 'quality')
uni_quality = yt_data_extract.deep_get(uni_sources, uni_idx, 'quality') uni_quality = yt_data_extract.deep_get(uni_sources, uni_idx, 'quality')
pair_error = abs((pair_quality or 360) - target_resolution) pair_error = abs((pair_quality or 360) - target_resolution)
uni_error = abs((uni_quality or 360) - target_resolution) uni_error = abs((uni_quality or 360) - target_resolution)
if uni_error == pair_error: if uni_error == pair_error:
@ -705,6 +753,7 @@ def get_watch_page(video_id=None):
closer_to_target = 'uni' closer_to_target = 'uni'
else: else:
closer_to_target = 'pair' closer_to_target = 'pair'
using_pair_sources = ( using_pair_sources = (
bool(pair_sources) and (not uni_sources or closer_to_target == 'pair') bool(pair_sources) and (not uni_sources or closer_to_target == 'pair')
) )
@ -719,6 +768,8 @@ def get_watch_page(video_id=None):
uni_sources, uni_idx, 'width', default=640 uni_sources, uni_idx, 'width', default=640
) )
# 1 second per pixel, or the actual video width # 1 second per pixel, or the actual video width
theater_video_target_width = max(640, info['duration'] or 0, video_width) theater_video_target_width = max(640, info['duration'] or 0, video_width)
@ -751,14 +802,13 @@ def get_watch_page(video_id=None):
template_name = 'embed.html' template_name = 'embed.html'
else: else:
template_name = 'watch.html' template_name = 'watch.html'
return flask.render_template( return flask.render_template(template_name,
template_name,
header_playlist_names = local_playlist.get_playlist_names(), header_playlist_names = local_playlist.get_playlist_names(),
uploader_channel_url = ('/' + info['author_url']) if info['author_url'] else '', uploader_channel_url = ('/' + info['author_url']) if info['author_url'] else '',
time_published = info['time_published'], time_published = info['time_published'],
time_published_utc=time_utc_isoformat(info['time_published']),
view_count = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("view_count", None)), view_count = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("view_count", None)),
like_count = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("like_count", None)), like_count = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("like_count", None)),
dislike_count = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("dislike_count", None)),
download_formats = download_formats, download_formats = download_formats,
other_downloads = other_downloads, other_downloads = other_downloads,
video_info = json.dumps(video_info), video_info = json.dumps(video_info),
@ -807,7 +857,7 @@ def get_watch_page(video_id=None):
'related': info['related_videos'], 'related': info['related_videos'],
'playability_error': info['playability_error'], 'playability_error': info['playability_error'],
}, },
font_family=youtube.font_choices[settings.font], font_family=youtube.font_choices[settings.font], # for embed page
**source_info, **source_info,
using_pair_sources = using_pair_sources, using_pair_sources = using_pair_sources,
) )

View File

@ -7,7 +7,7 @@ from .everything_else import (extract_channel_info, extract_search_info,
extract_playlist_metadata, extract_playlist_info, extract_comments_info) extract_playlist_metadata, extract_playlist_info, extract_comments_info)
from .watch_extraction import (extract_watch_info, get_caption_url, from .watch_extraction import (extract_watch_info, get_caption_url,
update_with_age_restricted_info, requires_decryption, update_with_new_urls, requires_decryption,
extract_decryption_function, decrypt_signatures, _formats, extract_decryption_function, decrypt_signatures, _formats,
update_format_with_type_info, extract_hls_formats, update_format_with_type_info, extract_hls_formats,
extract_watch_info_from_html, captions_available) extract_watch_info_from_html, captions_available)

View File

@ -791,7 +791,7 @@ def get_caption_url(info, language, format, automatic=False, translation_languag
url += '&tlang=' + translation_language url += '&tlang=' + translation_language
return url return url
def update_with_age_restricted_info(info, player_response): def update_with_new_urls(info, player_response):
'''Inserts urls from player_response json''' '''Inserts urls from player_response json'''
ERROR_PREFIX = 'Error getting missing player or bypassing age-restriction: ' ERROR_PREFIX = 'Error getting missing player or bypassing age-restriction: '