aboutsummaryrefslogtreecommitdiffstats
path: root/settings.py
diff options
context:
space:
mode:
Diffstat (limited to 'settings.py')
-rw-r--r--settings.py540
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)