diff options
-rw-r--r-- | server.py | 59 | ||||
-rw-r--r-- | settings.py | 157 | ||||
-rw-r--r-- | youtube/__init__.py | 17 | ||||
-rw-r--r-- | youtube/channel.py | 42 | ||||
-rw-r--r-- | youtube/templates/comments.html | 10 | ||||
-rw-r--r-- | youtube/templates/settings.html | 2 | ||||
-rw-r--r-- | youtube/templates/watch.html | 2 | ||||
-rw-r--r-- | youtube/util.py | 55 | ||||
-rw-r--r-- | youtube/watch.py | 14 |
9 files changed, 231 insertions, 127 deletions
@@ -32,24 +32,52 @@ def youtu_be(env, start_response): env['QUERY_STRING'] += '&v=' + id yield from yt_app(env, start_response) -def proxy_site(env, start_response): +def proxy_site(env, start_response, video=False): headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)', 'Accept': '*/*', } + if 'HTTP_RANGE' in env: + headers['Range'] = env['HTTP_RANGE'] + url = "https://" + env['SERVER_NAME'] + env['PATH_INFO'] if env['QUERY_STRING']: url += '?' + env['QUERY_STRING'] - - content, response = util.fetch_url(url, headers, return_response=True) + if video and settings.route_tor == 1: + response, cleanup_func = util.fetch_url_response(url, headers, + use_tor=False, + max_redirects=10) + elif video: + response, cleanup_func = util.fetch_url_response(url, headers, + max_redirects=10) + else: + response, cleanup_func = util.fetch_url_response(url, headers) headers = response.getheaders() if isinstance(headers, urllib3._collections.HTTPHeaderDict): headers = headers.items() - start_response('200 OK', headers ) - yield content + start_response(str(response.status) + ' ' + response.reason, headers) + while True: + # a bit over 3 seconds of 360p video + # we want each TCP packet to transmit in large multiples, + # such as 65,536, so we shouldn't read in small chunks + # such as 8192 lest that causes the socket library to limit the + # TCP window size + # Might need fine-tuning, since this gives us 4*65536 + # The tradeoff is that larger values (such as 6 seconds) only + # allows video to buffer in those increments, meaning user must wait + # until the entire chunk is downloaded before video starts playing + content_part = response.read(32*8192) + if not content_part: + break + yield content_part + + cleanup_func(response) + +def proxy_video(env, start_response): + yield from proxy_site(env, start_response, video=True) site_handlers = { 'youtube.com':yt_app, @@ -57,7 +85,7 @@ site_handlers = { 'ytimg.com': proxy_site, 'yt3.ggpht.com': proxy_site, 'lh3.googleusercontent.com': proxy_site, - + 'googlevideo.com': proxy_video, } def split_url(url): @@ -133,12 +161,15 @@ class FilteredRequestLog: if not self.filter_re.search(s): sys.stderr.write(s) +if __name__ == '__main__': + if settings.allow_foreign_addresses: + server = WSGIServer(('0.0.0.0', settings.port_number), site_dispatch, + log=FilteredRequestLog()) + else: + server = WSGIServer(('127.0.0.1', settings.port_number), site_dispatch, + log=FilteredRequestLog()) + print('Started httpserver on port' , settings.port_number) + server.serve_forever() -if settings.allow_foreign_addresses: - server = WSGIServer(('0.0.0.0', settings.port_number), site_dispatch, - log=FilteredRequestLog()) -else: - server = WSGIServer(('127.0.0.1', settings.port_number), site_dispatch, - log=FilteredRequestLog()) -print('Started httpserver on port ' , settings.port_number) -server.serve_forever() +# for uwsgi, gunicorn, etc. +application = site_dispatch diff --git a/settings.py b/settings.py index d8e8dce..942f79c 100644 --- a/settings.py +++ b/settings.py @@ -7,11 +7,24 @@ import collections import flask from flask import request -settings_info = collections.OrderedDict([ +SETTINGS_INFO = collections.OrderedDict([ ('route_tor', { - 'type': bool, - 'default': False, + 'type': int, + 'default': 0, 'label': 'Route Tor', + 'comment': '''0 - Off +1 - On, except video +2 - On, including video (see warnings)''', + 'options': [ + (0, 'Off'), + (1, 'On, except video'), + (2, 'On, including video (see warnings)'), + ], + }), + + ('tor_port', { + 'type': int, + 'default': 9150, 'comment': '', }), @@ -148,14 +161,14 @@ For security reasons, enabling this is not recommended.''', ('settings_version', { 'type': int, - 'default': 2, + 'default': 3, 'comment': '''Do not change, remove, or comment out this value, or else your settings may be lost or corrupted''', 'hidden': True, }), ]) program_directory = os.path.dirname(os.path.realpath(__file__)) -acceptable_targets = settings_info.keys() | {'enable_comments', 'enable_related_videos'} +acceptable_targets = SETTINGS_INFO.keys() | {'enable_comments', 'enable_related_videos'} def comment_string(comment): @@ -164,57 +177,49 @@ def comment_string(comment): result += '# ' + line + '\n' return result -def save_settings(settings): +def save_settings(settings_dict): with open(settings_file_path, 'w', encoding='utf-8') as file: - for setting_name, default_setting_dict in settings_info.items(): - file.write(comment_string(default_setting_dict['comment']) + setting_name + ' = ' + repr(settings[setting_name]) + '\n\n') + for setting_name, setting_info in SETTINGS_INFO.items(): + file.write(comment_string(setting_info['comment']) + setting_name + ' = ' + repr(settings_dict[setting_name]) + '\n\n') - -def create_missing_settings_string(current_settings): - result = '' - for setting_name, setting_dict in settings_info.items(): - if setting_name not in current_settings: - result += comment_string(setting_dict['comment']) + setting_name + ' = ' + repr(setting_dict['default']) + '\n\n' - return result - -def create_default_settings_string(): - return settings_to_string({}) - -def add_missing_settings(settings): +def add_missing_settings(settings_dict): result = default_settings() - result.update(settings) + result.update(settings_dict) return result def default_settings(): - return {key: setting_info['default'] for key, setting_info in settings_info.items()} - -def settings_to_string(settings): - '''Given a dictionary with the setting names/setting values for the keys/values, outputs a settings file string. - Fills in missing values from the defaults.''' - result = '' - for setting_name, default_setting_dict in settings_info.items(): - if setting_name in settings: - value = settings[setting_name] - else: - value = default_setting_dict['default'] - result += comment_string(default_setting_dict['comment']) + setting_name + ' = ' + repr(value) + '\n\n' - return result - + return {key: setting_info['default'] for key, setting_info in SETTINGS_INFO.items()} -def upgrade_to_2(current_settings): +def upgrade_to_2(settings_dict): '''Upgrade to settings version 2''' - new_settings = current_settings.copy() - if 'enable_comments' in current_settings: - new_settings['comments_mode'] = int(current_settings['enable_comments']) + new_settings = settings_dict.copy() + if 'enable_comments' in settings_dict: + new_settings['comments_mode'] = int(settings_dict['enable_comments']) del new_settings['enable_comments'] - if 'enable_related_videos' in current_settings: - new_settings['related_videos_mode'] = int(current_settings['enable_related_videos']) + if 'enable_related_videos' in settings_dict: + new_settings['related_videos_mode'] = int(settings_dict['enable_related_videos']) del new_settings['enable_related_videos'] + new_settings['settings_version'] = 2 return new_settings +def upgrade_to_3(settings_dict): + new_settings = settings_dict.copy() + if 'route_tor' in settings_dict: + new_settings['route_tor'] = int(settings_dict['route_tor']) + new_settings['settings_version'] = 3 + return new_settings + +upgrade_functions = { + 1: upgrade_to_2, + 2: upgrade_to_3, +} + def log_ignored_line(line_number, message): print("WARNING: Ignoring settings.txt line " + str(node.lineno) + " (" + message + ")") + + + if os.path.isfile("settings.txt"): print("Running in portable mode") settings_dir = os.path.normpath('./') @@ -232,15 +237,15 @@ try: with open(settings_file_path, 'r', encoding='utf-8') as file: settings_text = file.read() except FileNotFoundError: - settings = default_settings() - save_settings(settings) + current_settings_dict = default_settings() + save_settings(current_settings_dict) else: if re.fullmatch(r'\s*', settings_text): # blank file - settings = default_settings() - save_settings(settings) + current_settings_dict = default_settings() + save_settings(current_settings_dict) else: # parse settings in a safe way, without exec - settings = {} + current_settings_dict = {} attributes = { ast.Constant: 'value', ast.NameConstant: 'value', @@ -270,21 +275,27 @@ else: log_ignored_line(node.lineno, "only literals allowed for values") continue - settings[target.id] = node.value.__getattribute__(attributes[type(node.value)]) - + current_settings_dict[target.id] = node.value.__getattribute__(attributes[type(node.value)]) - if 'settings_version' not in settings: - print('Upgrading settings.txt') - settings = add_missing_settings(upgrade_to_2(settings)) - save_settings(settings) + # upgrades + latest_version = SETTINGS_INFO['settings_version']['default'] + while current_settings_dict.get('settings_version',1) < latest_version: + current_version = current_settings_dict.get('settings_version', 1) + print('Upgrading settings.txt to version', current_version+1) + upgrade_func = upgrade_functions[current_version] + # Must add missing settings here rather than below because + # save_settings needs all settings to be present + current_settings_dict = add_missing_settings( + upgrade_func(current_settings_dict)) + save_settings(current_settings_dict) # some settings not in the file, add those missing settings to the file - elif not settings.keys() >= settings_info.keys(): + if not current_settings_dict.keys() >= SETTINGS_INFO.keys(): print('Adding missing settings to settings.txt') - settings = add_missing_settings(settings) - save_settings(settings) + current_settings_dict = add_missing_settings(current_settings_dict) + save_settings(current_settings_dict) -locals().update(settings) +globals().update(current_settings_dict) @@ -294,6 +305,9 @@ if route_tor: else: print("Tor routing is OFF - your Youtube activity is NOT anonymous") + + + hooks = {} def add_setting_changed_hook(setting, func): '''Called right before new settings take effect''' @@ -306,43 +320,34 @@ def add_setting_changed_hook(setting, func): def settings_page(): if request.method == 'GET': return flask.render_template('settings.html', - settings = [(setting_name, setting_info, settings[setting_name]) for setting_name, setting_info in settings_info.items()] + settings = [(setting_name, setting_info, current_settings_dict[setting_name]) for setting_name, setting_info in SETTINGS_INFO.items()] ) elif request.method == 'POST': for key, value in request.values.items(): - if key in settings_info: - if settings_info[key]['type'] is bool and value == 'on': - settings[key] = True + if key in SETTINGS_INFO: + if SETTINGS_INFO[key]['type'] is bool and value == 'on': + current_settings_dict[key] = True else: - settings[key] = settings_info[key]['type'](value) + current_settings_dict[key] = SETTINGS_INFO[key]['type'](value) else: flask.abort(400) # need this bullshit because browsers don't send anything when an input is unchecked - expected_inputs = {setting_name for setting_name, setting_info in settings_info.items() if not settings_info[setting_name].get('hidden', False)} + expected_inputs = {setting_name for setting_name, setting_info in SETTINGS_INFO.items() if not SETTINGS_INFO[setting_name].get('hidden', False)} missing_inputs = expected_inputs - set(request.values.keys()) for setting_name in missing_inputs: - assert settings_info[setting_name]['type'] is bool, missing_inputs - settings[setting_name] = False + assert SETTINGS_INFO[setting_name]['type'] is bool, missing_inputs + current_settings_dict[setting_name] = False # call setting hooks - for setting_name, value in settings.items(): + for setting_name, value in current_settings_dict.items(): old_value = globals()[setting_name] if value != old_value and setting_name in hooks: for func in hooks[setting_name]: func(old_value, value) - globals().update(settings) - save_settings(settings) + globals().update(current_settings_dict) + save_settings(current_settings_dict) return flask.redirect(util.URL_ORIGIN + '/settings', 303) else: flask.abort(400) - - - - - - - - - diff --git a/youtube/__init__.py b/youtube/__init__.py index 8675c4b..6c2ec48 100644 --- a/youtube/__init__.py +++ b/youtube/__init__.py @@ -2,6 +2,7 @@ from youtube import util import flask import settings import traceback +import re from sys import exc_info yt_app = flask.Flask(__name__) yt_app.url_map.strict_slashes = False @@ -34,6 +35,22 @@ def commatize(num): num = int(num) return '{:,}'.format(num) +def timestamp_replacement(match): + time_seconds = 0 + for part in match.group(0).split(':'): + time_seconds = 60*time_seconds + int(part) + return ( + '<a href="#" onclick="document.querySelector(\'video\').currentTime=' + + str(time_seconds) + + '">' + match.group(0) + + '</a>' + ) + +TIMESTAMP_RE = re.compile(r'\b(\d?\d:)?\d?\d:\d\d\b') +@yt_app.template_filter('timestamps') +def timestamps(text): + return TIMESTAMP_RE.sub(timestamp_replacement, text) + @yt_app.errorhandler(500) def error_page(e): if (exc_info()[0] == util.FetchError diff --git a/youtube/channel.py b/youtube/channel.py index ba1a4d2..ad6db5b 100644 --- a/youtube/channel.py +++ b/youtube/channel.py @@ -44,7 +44,28 @@ generic_cookie = (('Cookie', 'VISITOR_INFO1_LIVE=ST1Ti53r4fU'),) # view: # grid: 0 or 1 # list: 2 -def channel_ctoken_desktop(channel_id, page, sort, tab, view=1): +def channel_ctoken_v3(channel_id, page, sort, tab, view=1): + # page > 1 doesn't work when sorting by oldest + offset = 30*(int(page) - 1) + page_token = proto.string(61, proto.unpadded_b64encode( + proto.string(1, proto.unpadded_b64encode(proto.uint(1,offset))) + )) + + tab = proto.string(2, tab ) + sort = proto.uint(3, int(sort)) + + shelf_view = proto.uint(4, 0) + view = proto.uint(6, int(view)) + continuation_info = proto.string(3, + proto.percent_b64encode(tab + sort + shelf_view + view + page_token) + ) + + channel_id = proto.string(2, channel_id ) + pointless_nest = proto.string(80226972, channel_id + continuation_info) + + return base64.urlsafe_b64encode(pointless_nest).decode('ascii') + +def channel_ctoken_v2(channel_id, page, sort, tab, view=1): # see https://github.com/iv-org/invidious/issues/1319#issuecomment-671732646 # page > 1 doesn't work when sorting by oldest offset = 30*(int(page) - 1) @@ -74,14 +95,14 @@ def channel_ctoken_desktop(channel_id, page, sort, tab, view=1): return base64.urlsafe_b64encode(pointless_nest).decode('ascii') -def channel_ctoken_mobile(channel_id, page, sort, tab, view=1): +def channel_ctoken_v1(channel_id, page, sort, tab, view=1): tab = proto.string(2, tab ) sort = proto.uint(3, int(sort)) page = proto.string(15, str(page) ) # example with shelves in videos tab: https://www.youtube.com/channel/UCNL1ZadSjHpjm4q9j2sVtOA/videos shelf_view = proto.uint(4, 0) view = proto.uint(6, int(view)) - continuation_info = proto.string( 3, proto.percent_b64encode(tab + view + sort + shelf_view + page) ) + continuation_info = proto.string(3, proto.percent_b64encode(tab + view + sort + shelf_view + page + proto.uint(23, 0)) ) channel_id = proto.string(2, channel_id ) pointless_nest = proto.string(80226972, channel_id + continuation_info) @@ -91,15 +112,16 @@ def channel_ctoken_mobile(channel_id, page, sort, tab, view=1): def get_channel_tab(channel_id, page="1", sort=3, tab='videos', view=1, print_status=True): message = 'Got channel tab' if print_status else None - if int(sort) == 2 and int(page) > 1: # use mobile endpoint - ctoken = channel_ctoken_mobile(channel_id, page, sort, tab, view) + if int(sort) == 2 and int(page) > 1: + ctoken = channel_ctoken_v1(channel_id, page, sort, tab, view) ctoken = ctoken.replace('=', '%3D') - url = ('https://m.youtube.com/channel/' + channel_id + '/' + tab - + '?ctoken=' + ctoken + '&pbj=1') - content = util.fetch_url(url, headers_mobile + real_cookie, + url = ('https://www.youtube.com/channel/' + channel_id + '/' + tab + + '?action_continuation=1&continuation=' + ctoken + + '&pbj=1') + content = util.fetch_url(url, headers_desktop + real_cookie, debug_name='channel_tab', report_text=message) - else: # use desktop endpoint - ctoken = channel_ctoken_desktop(channel_id, page, sort, tab, view) + else: + ctoken = channel_ctoken_v3(channel_id, page, sort, tab, view) ctoken = ctoken.replace('=', '%3D') url = 'https://www.youtube.com/browse_ajax?ctoken=' + ctoken content = util.fetch_url(url, diff --git a/youtube/templates/comments.html b/youtube/templates/comments.html index 396852a..f2cdf65 100644 --- a/youtube/templates/comments.html +++ b/youtube/templates/comments.html @@ -1,6 +1,6 @@ {% import "common_elements.html" as common_elements %} -{% macro render_comment(comment, include_avatar) %} +{% macro render_comment(comment, include_avatar, timestamp_links=False) %} <div class="comment-container"> <div class="comment"> <a class="author-avatar" href="{{ comment['author_url'] }}" title="{{ comment['author'] }}"> @@ -14,7 +14,11 @@ <a class="permalink" href="{{ comment['permalink'] }}" title="permalink"> <time datetime="">{{ comment['time_published'] }}</time> </a> - <span class="text">{{ common_elements.text_runs(comment['text']) }}</span> + {% if timestamp_links %} + <span class="text">{{ common_elements.text_runs(comment['text'])|timestamps|safe }}</span> + {% else %} + <span class="text">{{ common_elements.text_runs(comment['text']) }}</span> + {% endif %} <span class="likes">{{ comment['likes_text'] if comment['like_count'] else ''}}</span> <div class="bottom-row"> @@ -36,7 +40,7 @@ </div> <div class="comments"> {% for comment in comments_info['comments'] %} - {{ render_comment(comment, comments_info['include_avatars']) }} + {{ render_comment(comment, comments_info['include_avatars'], True) }} {% endfor %} </div> {% if 'more_comments_url' is in comments_info %} diff --git a/youtube/templates/settings.html b/youtube/templates/settings.html index 19a2461..5d1df5f 100644 --- a/youtube/templates/settings.html +++ b/youtube/templates/settings.html @@ -4,7 +4,7 @@ {% block style %} .settings-form { margin: auto; - width: 500px; + width: 600px; margin-top:10px; padding: 10px; display: block; diff --git a/youtube/templates/watch.html b/youtube/templates/watch.html index 8264eb8..e3c6fa0 100644 --- a/youtube/templates/watch.html +++ b/youtube/templates/watch.html @@ -413,7 +413,7 @@ Reload without invidious (for usage of new identity button).</a> <input class="checkbox" name="video_info_list" value="{{ video_info }}" form="playlist-edit" type="checkbox"> - <span class="description">{{ common_elements.text_runs(description)|urlize }}</span> + <span class="description">{{ common_elements.text_runs(description)|escape|urlize|timestamps|safe }}</span> <div class="music-list"> {% if music_list.__len__() != 0 %} <hr> diff --git a/youtube/util.py b/youtube/util.py index b19f91b..3c32ddb 100644 --- a/youtube/util.py +++ b/youtube/util.py @@ -54,7 +54,7 @@ URL_ORIGIN = "/https://www.youtube.com" connection_pool = urllib3.PoolManager(cert_reqs = 'CERT_REQUIRED') old_tor_connection_pool = None -tor_connection_pool = urllib3.contrib.socks.SOCKSProxyManager('socks5://127.0.0.1:9150/', cert_reqs = 'CERT_REQUIRED') +tor_connection_pool = urllib3.contrib.socks.SOCKSProxyManager('socks5://127.0.0.1:' + str(settings.tor_port) + '/', cert_reqs = 'CERT_REQUIRED') tor_pool_refresh_time = time.monotonic() # prevent problems due to clock changes @@ -74,7 +74,7 @@ def get_pool(use_tor): # Keep a reference for 5 min to avoid it getting garbage collected while sockets still in use old_tor_connection_pool = tor_connection_pool - tor_connection_pool = urllib3.contrib.socks.SOCKSProxyManager('socks5://127.0.0.1:9150/', cert_reqs = 'CERT_REQUIRED') + tor_connection_pool = urllib3.contrib.socks.SOCKSProxyManager('socks5://127.0.0.1:' + str(settings.tor_port) + '/', cert_reqs = 'CERT_REQUIRED') tor_pool_refresh_time = current_time return tor_connection_pool @@ -119,8 +119,11 @@ def decode_content(content, encoding_header): content = gzip.decompress(content) return content -def fetch_url(url, headers=(), timeout=15, report_text=None, data=None, cookiejar_send=None, cookiejar_receive=None, use_tor=True, return_response=False, debug_name=None): +def fetch_url_response(url, headers=(), timeout=15, data=None, + cookiejar_send=None, cookiejar_receive=None, + use_tor=True, max_redirects=None): ''' + returns response, cleanup_function When cookiejar_send is set to a CookieJar object, those cookies will be sent in the request (but cookies in response will not be merged into it) When cookiejar_receive is set to a CookieJar object, @@ -147,32 +150,51 @@ def fetch_url(url, headers=(), timeout=15, report_text=None, data=None, cookieja elif not isinstance(data, bytes): data = urllib.parse.urlencode(data).encode('ascii') - start_time = time.time() - if cookiejar_send is not None or cookiejar_receive is not None: # Use urllib req = urllib.request.Request(url, data=data, headers=headers) cookie_processor = HTTPAsymmetricCookieProcessor(cookiejar_send=cookiejar_send, cookiejar_receive=cookiejar_receive) if use_tor and settings.route_tor: - opener = urllib.request.build_opener(sockshandler.SocksiPyHandler(socks.PROXY_TYPE_SOCKS5, "127.0.0.1", 9150), cookie_processor) + opener = urllib.request.build_opener(sockshandler.SocksiPyHandler(socks.PROXY_TYPE_SOCKS5, "127.0.0.1", settings.tor_port), cookie_processor) else: opener = urllib.request.build_opener(cookie_processor) response = opener.open(req, timeout=timeout) - response_time = time.time() - - - content = response.read() + cleanup_func = (lambda r: None) else: # Use a urllib3 pool. Cookies can't be used since urllib3 doesn't have easy support for them. + # default: Retry.DEFAULT = Retry(3) + # (in connectionpool.py in urllib3) + # According to the documentation for urlopen, a redirect counts as a + # retry. So there are 3 redirects max by default. + if max_redirects: + retries = urllib3.Retry(3+max_redirects, redirect=max_redirects) + else: + retries = urllib3.Retry(3) pool = get_pool(use_tor and settings.route_tor) + response = pool.request(method, url, headers=headers, + timeout=timeout, preload_content=False, + decode_content=False, retries=retries) + cleanup_func = (lambda r: r.release_conn()) - response = pool.request(method, url, headers=headers, timeout=timeout, preload_content=False, decode_content=False) - response_time = time.time() + return response, cleanup_func - content = response.read() - response.release_conn() +def fetch_url(url, headers=(), timeout=15, report_text=None, data=None, + cookiejar_send=None, cookiejar_receive=None, use_tor=True, + debug_name=None): + start_time = time.time() + + response, cleanup_func = fetch_url_response( + url, headers, timeout=timeout, + cookiejar_send=cookiejar_send, cookiejar_receive=cookiejar_receive, + use_tor=use_tor) + response_time = time.time() + + content = response.read() + read_finish = time.time() + + cleanup_func(response) # release_connection for urllib3 if (response.status == 429 and content.startswith(b'<!DOCTYPE') @@ -185,7 +207,6 @@ def fetch_url(url, headers=(), timeout=15, report_text=None, data=None, cookieja elif response.status >= 400: raise FetchError(str(response.status), reason=response.reason, ip=None) - read_finish = time.time() if report_text: print(report_text, ' Latency:', round(response_time - start_time,3), ' Read time:', round(read_finish - response_time,3)) content = decode_content(content, response.getheader('Content-Encoding', default='identity')) @@ -198,8 +219,6 @@ def fetch_url(url, headers=(), timeout=15, report_text=None, data=None, cookieja with open(os.path.join(save_dir, debug_name), 'wb') as f: f.write(content) - if return_response: - return content, response return content def head(url, use_tor=False, report_text=None, max_redirects=10): @@ -209,7 +228,7 @@ def head(url, use_tor=False, report_text=None, max_redirects=10): # default: Retry.DEFAULT = Retry(3) # (in connectionpool.py in urllib3) # According to the documentation for urlopen, a redirect counts as a retry - # by default. So there are 3 redirects max by default. Let's change that + # So there are 3 redirects max by default. Let's change that # to 10 since googlevideo redirects a lot. retries = urllib3.Retry(3+max_redirects, redirect=max_redirects, raise_on_redirect=False) diff --git a/youtube/watch.py b/youtube/watch.py index c1f5e1e..cedf632 100644 --- a/youtube/watch.py +++ b/youtube/watch.py @@ -24,7 +24,7 @@ except FileNotFoundError: def get_video_sources(info): video_sources = [] - if not settings.theater_mode: + if (not settings.theater_mode) or settings.route_tor == 2: max_resolution = 360 else: max_resolution = settings.default_resolution @@ -270,10 +270,11 @@ def extract_info(video_id, use_invidious, playlist_id=None, index=None): else: info['hls_formats'] = [] - # check for 403 + # check for 403. Unnecessary for tor video routing b/c ip address is same info['invidious_used'] = False info['invidious_reload_button'] = False - if settings.route_tor and info['formats'] and info['formats'][0]['url']: + if (settings.route_tor == 1 + and info['formats'] and info['formats'][0]['url']): try: response = util.head(info['formats'][0]['url'], report_text='Checked for URL access') @@ -408,10 +409,10 @@ def get_watch_page(video_id=None): "author": info['author'], } + # prefix urls, and other post-processing not handled by yt_data_extract for item in info['related_videos']: util.prefix_urls(item) util.add_extra_html_info(item) - if info['playlist']: playlist_id = info['playlist']['id'] for item in info['playlist']['items']: @@ -423,6 +424,11 @@ def get_watch_page(video_id=None): item['url'] += '&index=' + str(item['index']) info['playlist']['author_url'] = util.prefix_url( info['playlist']['author_url']) + # Don't prefix hls_formats for now because the urls inside the manifest + # would need to be prefixed as well. + for fmt in info['formats']: + fmt['url'] = util.prefix_url(fmt['url']) + if settings.gather_googlevideo_domains: with open(os.path.join(settings.data_dir, 'googlevideo-domains.txt'), 'a+', encoding='utf-8') as f: |