diff options
Diffstat (limited to 'settings.py')
| -rw-r--r-- | settings.py | 540 |
1 files changed, 433 insertions, 107 deletions
diff --git a/settings.py b/settings.py index bd6710c..677a33a 100644 --- a/settings.py +++ b/settings.py @@ -1,4 +1,18 @@ from youtube import util +from youtube.i18n_strings import ( + AUTO, + AUTO_HLS_PREFERRED, + ENGLISH, + ESPANOL, + FORCE_DASH, + FORCE_HLS, + NEWEST, + PLAYBACK_MODE, + RANKING_1, + RANKING_2, + RANKING_3, + TOP, +) import ast import re import os @@ -7,26 +21,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 +102,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 +123,7 @@ For security reasons, enabling this is not recommended.''', (1, 'Always shown'), (2, 'Shown by clicking button'), ], + 'category': 'interface', }), ('comments_mode', { @@ -73,12 +137,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', { @@ -87,8 +153,8 @@ For security reasons, enabling this is not recommended.''', 'comment': '''0 to sort by top 1 to sort by newest''', 'options': [ - (0, 'Top'), - (1, 'Newest'), + (0, TOP), + (1, NEWEST), ], }), @@ -96,16 +162,151 @@ 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', { + 'type': str, + 'default': 'auto', + 'comment': '', + 'options': [ + ('auto', AUTO), + ('144', '144p'), + ('240', '240p'), + ('360', '360p'), + ('480', '480p'), + ('720', '720p'), + ('1080', '1080p'), + ('1440', '1440p'), + ('2160', '2160p'), + ], + 'category': 'playback', + }), + + ('playback_mode', { + 'type': str, + 'default': 'auto', + 'label': PLAYBACK_MODE, + 'comment': 'HLS uses hls.js (multi-audio). DASH uses av-merge (single audio).', + 'options': [ + ('auto', AUTO_HLS_PREFERRED), + ('hls', FORCE_HLS), + ('dash', FORCE_DASH), + ], + 'category': 'playback', + }), + + ('codec_rank_av1', { + 'type': int, + 'default': 1, + 'label': 'AV1 Codec Ranking', + 'comment': '', + 'options': [(1, RANKING_1), (2, RANKING_2), (3, RANKING_3)], + 'category': 'playback', + }), + + ('codec_rank_vp', { + 'type': int, + 'default': 2, + 'label': 'VP8/VP9 Codec Ranking', + 'comment': '', + 'options': [(1, RANKING_1), (2, RANKING_2), (3, RANKING_3)], + 'category': 'playback', + }), + + ('codec_rank_h264', { + 'type': int, + 'default': 3, + 'label': 'H.264 Codec Ranking', + 'comment': '', + 'options': [(1, RANKING_1), (2, RANKING_2), (3, RANKING_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': 720, + 'default': 1, 'comment': '', 'options': [ - (360, '360p'), - (720, '720p'), + (0, 'Prefer not'), + (1, 'Prefer'), + (2, 'Always'), + ], + 'category': 'playback', + 'hidden': True, + 'description': 'Deprecated: HLS is now used exclusively for all playback.', + }), + + ('use_video_player', { + 'type': int, + 'default': 1, + 'comment': '', + 'options': [ + (0, 'Native'), + (1, 'Native with hotkeys'), + (2, 'Plyr'), + ], + 'category': 'interface', + }), + + ('native_player_storyboard', { + 'type': bool, + 'default': False, + 'label': 'Storyboard preview (native)', + 'comment': '''Show thumbnail preview on hover (native player modes). + Positioning is heuristic; may misalign in Firefox/Safari. + Works best on Chromium browsers. + No effect in Plyr.''', + 'category': 'interface', + }), + + ('use_video_download', { + 'type': int, + 'default': 0, + '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 +318,40 @@ 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', + }), + + ('language', { + 'type': str, + 'default': 'en', + 'comment': 'Interface language', + 'options': [ + ('en', ENGLISH), + ('es', ESPANOL), + ], + 'category': 'interface', + }), + + ('embed_page_mode', { + 'type': bool, + 'label': 'Enable embed page', + 'default': True, + 'comment': '', + 'category': 'interface', }), ('autocheck_subscriptions', { @@ -125,11 +360,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,71 +381,108 @@ 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', + 'ytdlp_enabled', +} def comment_string(comment): result = '' for line in comment.splitlines(): - result += '# ' + line + '\n' + result += f'# {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 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 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(f"{comment_string(setting_info['comment'])}{setting_name} = {repr(settings_dict[setting_name])}\n\n") -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 + ")") + print(f'WARNING: Ignoring settings.txt line {line_number} ({message})') + if os.path.isfile("settings.txt"): print("Running in portable mode") @@ -213,10 +490,9 @@ if os.path.isfile("settings.txt"): 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")) - if not os.path.exists(settings_dir): - os.makedirs(settings_dir) + settings_dir = os.path.expanduser(os.path.normpath("~/.yt-local")) + data_dir = os.path.expanduser(os.path.normpath("~/.yt-local/data")) + os.makedirs(settings_dir, exist_ok=True) settings_file_path = os.path.join(settings_dir, 'settings.txt') @@ -224,101 +500,151 @@ 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 = {} + # Python 3.8+ uses ast.Constant; older versions use ast.Num, ast.Str, ast.NameConstant attributes = { - ast.NameConstant: 'value', - ast.Num: 'n', - ast.Str: 's', + ast.Constant: 'value', } + try: + attributes[ast.Num] = 'n' + attributes[ast.Str] = 's' + attributes[ast.NameConstant] = 'value' + except AttributeError: + pass # Removed in Python 3.12+ module_node = ast.parse(settings_text) for node in module_node.body: - if type(node) != ast.Assign: - log_ignored_line(node.lineno, "only assignments are allowed") + if not isinstance(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") + log_ignored_line(node.lineno, 'only simple single-variable assignments allowed') continue target = node.targets[0] - if type(target) != ast.Name: - log_ignored_line(node.lineno, "only simple single-variable assignments allowed") + if not isinstance(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") + log_ignored_line(node.lineno, f"{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: +if globals().get('route_tor', False): 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 = globals().get('proxy_images', False) + 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(f'{util.URL_ORIGIN}/settings', 303) + else: + flask.abort(400) |
