123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- from youtube import util
- import ast
- import re
- import os
- import collections
- import flask
- from flask import request
- 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': 9150,
- 'comment': '',
- 'category': 'network',
- }),
- ('tor_control_port', {
- 'type': int,
- 'default': 9151,
- 'comment': '',
- 'category': 'network',
- }),
- ('port_number', {
- 'type': int,
- 'default': 8080,
- 'comment': '',
- 'category': 'network',
- }),
- ('allow_foreign_addresses', {
- 'type': bool,
- 'default': False,
- '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', {
- 'type': int,
- 'default': 0,
- 'comment': '''0 - off by default
- 1 - only manually created subtitles on by default
- 2 - enable even if automatically generated is all that's available''',
- 'label': 'Default subtitles mode',
- 'options': [
- (0, 'Off'),
- (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', {
- 'type': int,
- 'default': 1,
- 'comment': '''0 - Related videos disabled
- 1 - Related videos always shown
- 2 - Related videos hidden; shown by clicking a button''',
- 'options': [
- (0, 'Disabled'),
- (1, 'Always shown'),
- (2, 'Shown by clicking button'),
- ],
- 'category': 'interface',
- }),
- ('comments_mode', {
- 'type': int,
- 'default': 1,
- 'comment': '''0 - Video comments disabled
- 1 - Video comments always shown
- 2 - Video comments hidden; shown by clicking a button''',
- 'options': [
- (0, 'Disabled'),
- (1, 'Always shown'),
- (2, 'Shown by clicking button'),
- ],
- 'category': 'interface',
- }),
- ('enable_comment_avatars', {
- 'type': bool,
- 'default': True,
- 'comment': '',
- 'category': 'interface',
- }),
- ('default_comment_sorting', {
- 'type': int,
- 'default': 0,
- 'comment': '''0 to sort by top
- 1 to sort by newest''',
- 'options': [
- (0, 'Top'),
- (1, 'Newest'),
- ],
- }),
- ('theater_mode', {
- 'type': bool,
- 'default': True,
- 'comment': '',
- 'category': 'interface',
- }),
- ('default_resolution', {
- 'type': int,
- 'default': 720,
- 'comment': '',
- 'options': [
- (360, '360p'),
- (480, '480p'),
- (720, '720p'),
- ],
- 'category': 'playback',
- }),
- ('use_video_player', {
- 'type': int,
- 'default': 1,
- 'comment': '',
- 'options': [
- (0, 'Native'),
- (1, 'Native with hotkeys'),
- (2, 'Plyr'),
- ],
- 'category': 'interface',
- }),
- ('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', {
- 'type': int,
- 'default': 0,
- 'comment': '',
- 'options': [
- (0, 'Light'),
- (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', {
- 'type': bool,
- 'default': 0,
- 'comment': '',
- }),
- ('gather_googlevideo_domains', {
- 'type': bool,
- 'default': False,
- 'comment': '''Developer use to debug 403s''',
- 'hidden': True,
- }),
- ('debugging_save_responses', {
- 'type': bool,
- 'default': False,
- 'comment': '''Save all responses from youtube for debugging''',
- 'hidden': True,
- }),
- ('settings_version', {
- 'type': int,
- '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'}
- def comment_string(comment):
- result = ''
- for line in comment.splitlines():
- result += '# ' + line + '\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(comment_string(setting_info['comment']) + setting_name + ' = ' + repr(settings_dict[setting_name]) + '\n\n')
- def add_missing_settings(settings_dict):
- result = default_settings()
- result.update(settings_dict)
- return result
- def default_settings():
- return {key: setting_info['default'] for key, setting_info in SETTINGS_INFO.items()}
- def upgrade_to_2(settings_dict):
- '''Upgrade to settings version 2'''
- 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 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('./')
- 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_file_path = os.path.join(settings_dir, 'settings.txt')
- try:
- with open(settings_file_path, 'r', encoding='utf-8') as file:
- settings_text = file.read()
- except FileNotFoundError:
- current_settings_dict = default_settings()
- save_settings(current_settings_dict)
- else:
- if re.fullmatch(r'\s*', settings_text): # blank file
- current_settings_dict = default_settings()
- save_settings(current_settings_dict)
- else:
- # parse settings in a safe way, without exec
- current_settings_dict = {}
- attributes = {
- ast.Constant: 'value',
- ast.NameConstant: 'value',
- ast.Num: 'n',
- ast.Str: 's',
- }
- 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")
- continue
- if len(node.targets) > 1:
- 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")
- 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 attributes:
- log_ignored_line(node.lineno, "only literals allowed for values")
- continue
- current_settings_dict[target.id] = node.value.__getattribute__(attributes[type(node.value)])
- # 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
- if not current_settings_dict.keys() >= SETTINGS_INFO.keys():
- print('Adding missing settings to settings.txt')
- 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")
- 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':
- 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':
- current_settings_dict[key] = True
- else:
- 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)}
- missing_inputs = expected_inputs - set(request.values.keys())
- for setting_name in missing_inputs:
- 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)
|