diff options
Diffstat (limited to 'settings.py')
-rw-r--r-- | settings.py | 448 |
1 files changed, 360 insertions, 88 deletions
diff --git a/settings.py b/settings.py index bd6710c..2de5efa 100644 --- a/settings.py +++ b/settings.py @@ -7,26 +7,73 @@ import collections import flask from flask import request -settings_info = collections.OrderedDict([ - ('route_tor', { +SETTINGS_INFO = collections.OrderedDict([ + ('app_public', { 'type': bool, 'default': False, + 'comment': '''Set app public mode, disabled by default''', + 'hidden': True, + 'category': 'network', + }), + ('app_url', { + 'type': str, + 'default': 'http://localhost', + 'comment': '''Set URL of app 'http://localhost' by default''', + 'hidden': True, + 'category': 'network', + }), + ('route_tor', { + '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)'), + ], + 'category': 'network', + }), + + ('tor_port', { + 'type': int, + 'default': 9050, + 'comment': '', + 'category': 'network', + }), + + ('tor_control_port', { + 'type': int, + 'default': 9151, 'comment': '', + 'category': 'network', }), ('port_number', { 'type': int, - 'default': 8080, + 'default': 9010, 'comment': '', + 'category': 'network', }), ('allow_foreign_addresses', { 'type': bool, 'default': False, - 'comment': '''This will allow others to connect to your Youtube Local instance as a website. + 'comment': '''This will allow others to connect to your YT Local instance as a website. For security reasons, enabling this is not recommended.''', 'hidden': True, + 'category': 'network', + }), + + ('allow_foreign_post_requests', { + 'type': bool, + 'default': False, + 'comment': '''Enables requests from foreign addresses to make post requests. +For security reasons, enabling this is not recommended.''', + 'hidden': True, + 'category': 'network', }), ('subtitles_mode', { @@ -41,12 +88,14 @@ For security reasons, enabling this is not recommended.''', (1, 'Manually created only'), (2, 'Automatic if manual unavailable'), ], + 'category': 'playback', }), ('subtitles_language', { 'type': str, 'default': 'en', 'comment': '''ISO 639 language code: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes''', + 'category': 'playback', }), ('related_videos_mode', { @@ -60,6 +109,7 @@ For security reasons, enabling this is not recommended.''', (1, 'Always shown'), (2, 'Shown by clicking button'), ], + 'category': 'interface', }), ('comments_mode', { @@ -73,12 +123,14 @@ For security reasons, enabling this is not recommended.''', (1, 'Always shown'), (2, 'Shown by clicking button'), ], + 'category': 'interface', }), ('enable_comment_avatars', { 'type': bool, 'default': True, 'comment': '', + 'category': 'interface', }), ('default_comment_sorting', { @@ -96,6 +148,14 @@ For security reasons, enabling this is not recommended.''', 'type': bool, 'default': True, 'comment': '', + 'category': 'interface', + }), + + ('autoplay_videos', { + 'type': bool, + 'default': False, + 'comment': '', + 'category': 'playback', }), ('default_resolution', { @@ -103,9 +163,111 @@ For security reasons, enabling this is not recommended.''', 'default': 720, 'comment': '', 'options': [ + (144, '144p'), + (240, '240p'), (360, '360p'), + (480, '480p'), (720, '720p'), + (1080, '1080p'), + (1440, '1440p'), + (2160, '2160p'), ], + 'category': 'playback', + }), + + ('codec_rank_av1', { + 'type': int, + 'default': 1, + 'label': 'AV1 Codec Ranking', + 'comment': '', + 'options': [(1, '#1'), (2, '#2'), (3, '#3')], + 'category': 'playback', + }), + + ('codec_rank_vp', { + 'type': int, + 'default': 2, + 'label': 'VP8/VP9 Codec Ranking', + 'comment': '', + 'options': [(1, '#1'), (2, '#2'), (3, '#3')], + 'category': 'playback', + }), + + ('codec_rank_h264', { + 'type': int, + 'default': 3, + 'label': 'H.264 Codec Ranking', + 'comment': '', + 'options': [(1, '#1'), (2, '#2'), (3, '#3')], + 'category': 'playback', + 'description': ( + 'Which video codecs to prefer. Codecs given the same ' + 'ranking will use smaller file size as a tiebreaker.' + ) + }), + + ('prefer_uni_sources', { + 'label': 'Use integrated sources', + 'type': int, + 'default': 1, + 'comment': '', + 'options': [ + (0, 'Prefer not'), + (1, 'Prefer'), + (2, 'Always'), + ], + 'category': 'playback', + 'description': 'If set to Prefer or Always and the default resolution is set to 360p or 720p, uses the unified (integrated) video files which contain audio and video, with buffering managed by the browser. If set to prefer not, uses the separate audio and video files through custom buffer management in av-merge via MediaSource unless they are unavailable.', + }), + + ('use_video_player', { + 'type': int, + 'default': 1, + 'comment': '', + 'options': [ + (0, 'Native'), + (1, 'Native with hotkeys'), + (2, 'Plyr'), + ], + 'category': 'interface', + }), + + ('use_video_download', { + 'type': int, + 'default': 0, + 'comment': '', + 'options': [ + (0, 'Disabled'), + (1, 'Enabled'), + ], + 'category': 'interface', + 'comment': '''If enabled, you may incur legal issues with RIAA. Disabled by default. +More info: https://torrentfreak.com/riaa-thwarts-youts-attempt-to-declare-youtube-ripping-legal-221002/ +Archive: https://archive.ph/OZQbN''', + }), + + ('proxy_images', { + 'label': 'Route images', + 'type': bool, + 'default': True, + 'comment': '', + 'category': 'network', + }), + + ('use_comments_js', { + 'label': 'Enable comments.js', + 'type': bool, + 'default': True, + 'comment': '', + 'category': 'interface', + }), + + ('use_sponsorblock_js', { + 'label': 'Enable SponsorBlock', + 'type': bool, + 'default': False, + 'comment': '', + 'category': 'playback', }), ('theme', { @@ -117,6 +279,29 @@ For security reasons, enabling this is not recommended.''', (1, 'Gray'), (2, 'Dark'), ], + 'category': 'interface', + }), + + ('font', { + 'type': int, + 'default': 1, + 'comment': '', + 'options': [ + (0, 'Browser default'), + (1, 'Liberation Serif'), + (2, 'Arial'), + (3, 'Verdana'), + (4, 'Tahoma'), + ], + 'category': 'interface', + }), + + ('embed_page_mode', { + 'type': bool, + 'label': 'Enable embed page', + 'default': True, + 'comment': '', + 'category': 'interface', }), ('autocheck_subscriptions', { @@ -125,11 +310,16 @@ For security reasons, enabling this is not recommended.''', 'comment': '', }), - ('gather_googlevideo_domains', { + ('include_shorts_in_subscriptions', { 'type': bool, - 'default': False, - 'comment': '''Developer use to debug 403s''', - 'hidden': True, + 'default': 0, + 'comment': '', + }), + + ('include_shorts_in_channel', { + 'type': bool, + 'default': 1, + 'comment': '', }), ('debugging_save_responses', { @@ -141,13 +331,16 @@ For security reasons, enabling this is not recommended.''', ('settings_version', { 'type': int, - 'default': 2, + 'default': 6, 'comment': '''Do not change, remove, or comment out this value, or else your settings may be lost or corrupted''', 'hidden': True, }), ]) -acceptable_targets = settings_info.keys() | {'enable_comments', 'enable_related_videos'} +program_directory = os.path.dirname(os.path.realpath(__file__)) +acceptable_targets = SETTINGS_INFO.keys() | { + 'enable_comments', 'enable_related_videos', 'preferred_video_codec' +} def comment_string(comment): @@ -156,65 +349,98 @@ def comment_string(comment): result += '# ' + line + '\n' return result -def save_settings(settings): - 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') +def save_settings(settings_dict): + with open(settings_file_path, 'w', encoding='utf-8') as file: + 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 +def default_settings(): + 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 + +def upgrade_to_4(settings_dict): + new_settings = settings_dict.copy() + if 'preferred_video_codec' in settings_dict: + pref = settings_dict['preferred_video_codec'] + if pref == 0: + new_settings['codec_rank_h264'] = 1 + new_settings['codec_rank_vp'] = 2 + new_settings['codec_rank_av1'] = 3 + else: + new_settings['codec_rank_h264'] = 3 + new_settings['codec_rank_vp'] = 2 + new_settings['codec_rank_av1'] = 1 + del new_settings['preferred_video_codec'] + new_settings['settings_version'] = 4 + return new_settings + + +def upgrade_to_5(settings_dict): + new_settings = settings_dict.copy() + if 'prefer_uni_sources' in settings_dict: + new_settings['prefer_uni_sources'] = int(settings_dict['prefer_uni_sources']) + new_settings['settings_version'] = 5 + return new_settings + + +def upgrade_to_6(settings_dict): + new_settings = settings_dict.copy() + if 'gather_googlevideo_domains' in new_settings: + del new_settings['gather_googlevideo_domains'] + new_settings['settings_version'] = 6 + return new_settings + + +upgrade_functions = { + 1: upgrade_to_2, + 2: upgrade_to_3, + 3: upgrade_to_4, + 4: upgrade_to_5, + 5: upgrade_to_6, +} + + 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('./') data_dir = os.path.normpath('./data') else: print("Running in non-portable mode") - settings_dir = os.path.expanduser(os.path.normpath("~/.youtube-local")) - data_dir = os.path.expanduser(os.path.normpath("~/.youtube-local/data")) + settings_dir = os.path.expanduser(os.path.normpath("~/.yt-local")) + data_dir = os.path.expanduser(os.path.normpath("~/.yt-local/data")) if not os.path.exists(settings_dir): os.makedirs(settings_dir) @@ -224,16 +450,17 @@ 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', ast.Num: 'n', ast.Str: 's', @@ -243,7 +470,7 @@ else: if type(node) != ast.Assign: log_ignored_line(node.lineno, "only assignments are allowed") continue - + if len(node.targets) > 1: log_ignored_line(node.lineno, "only simple single-variable assignments allowed") continue @@ -252,73 +479,118 @@ else: if type(target) != ast.Name: log_ignored_line(node.lineno, "only simple single-variable assignments allowed") continue - + if target.id not in acceptable_targets: log_ignored_line(node.lineno, target.id + " is not a valid setting") continue - - if type(node.value) not in (ast.NameConstant, ast.Num, ast.Str): + + if type(node.value) not in attributes: 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) - -locals().update(settings) - + current_settings_dict = add_missing_settings(current_settings_dict) + save_settings(current_settings_dict) +globals().update(current_settings_dict) if route_tor: print("Tor routing is ON") else: - print("Tor routing is OFF - your Youtube activity is NOT anonymous") + print("Tor routing is OFF - your YouTube activity is NOT anonymous") + + +hooks = {} + + +def add_setting_changed_hook(setting, func): + '''Called right after new settings take effect''' + if setting in hooks: + hooks[setting].append(func) + else: + hooks[setting] = [func] + + +def set_img_prefix(old_value=None, value=None): + global img_prefix + if value is None: + value = proxy_images + if value: + img_prefix = '/' + else: + img_prefix = '' + + +set_img_prefix() + +add_setting_changed_hook('proxy_images', set_img_prefix) + + +categories = ['network', 'interface', 'playback', 'other'] 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_by_category = {categ: [] for categ in categories} + for setting_name, setting_info in SETTINGS_INFO.items(): + categ = setting_info.get('category', 'other') + settings_by_category[categ].append( + (setting_name, setting_info, current_settings_dict[setting_name]) + ) + return flask.render_template( + 'settings.html', + categories=categories, + settings_by_category=settings_by_category, ) 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 - - globals().update(settings) - save_settings(settings) - return flask.redirect(util.URL_ORIGIN + '/settings', 303) - else: - flask.abort(400) - - - - - + assert SETTINGS_INFO[setting_name]['type'] is bool, missing_inputs + current_settings_dict[setting_name] = False + # find settings that have changed to prepare setting hook calls + to_call = [] + 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]: + to_call.append((func, old_value, value)) + globals().update(current_settings_dict) + save_settings(current_settings_dict) + # call setting hooks + for func, old_value, value in to_call: + func(old_value, value) + return flask.redirect(util.URL_ORIGIN + '/settings', 303) + else: + flask.abort(400) |