#!/usr/bin/env python3 import secrets import configparser import shutil import argparse import logging import threading import signal import sys from pathlib import Path from flask import Flask, g parser = argparse.ArgumentParser(description="jellyfin-accounts") parser.add_argument("-c", "--config", help="specifies path to configuration file.") parser.add_argument("-d", "--data", help=("specifies directory to store data in. " + "defaults to ~/.jf-accounts.")) parser.add_argument("--host", help="address to host web ui on.") parser.add_argument("-p", "--port", help="port to host web ui on.") parser.add_argument("-g", "--get_defaults", help=("tool to grab a JF users " + "policy (access, perms, etc.) and " + "homescreen layout and " + "output it as json to be used as a user template."), action='store_true') args, leftovers = parser.parse_known_args() if args.data is not None: data_dir = Path(args.data) else: data_dir = Path.home() / '.jf-accounts' local_dir = (Path(__file__).parents[2] / 'data').resolve() first_run = False if data_dir.exists() is False or (data_dir / 'config.ini').exists() is False: if not data_dir.exists(): Path.mkdir(data_dir) print(f'Config dir not found, so created at {str(data_dir)}') if args.config is None: config_path = data_dir / 'config.ini' shutil.copy(str(local_dir / 'config-default.ini'), str(config_path)) print("Setup through the web UI, or quit and edit the configuration manually.") first_run = True else: config_path = Path(args.config) print(f'config.ini can be found at {str(config_path)}') else: config_path = data_dir / 'config.ini' config = configparser.RawConfigParser() config.read(config_path) def create_log(name): log = logging.getLogger(name) handler = logging.StreamHandler(sys.stdout) if config.getboolean('ui', 'debug'): log.setLevel(logging.DEBUG) handler.setLevel(logging.DEBUG) else: log.setLevel(logging.INFO) handler.setLevel(logging.INFO) fmt = ' %(name)s - %(levelname)s - %(message)s' format = logging.Formatter(fmt) handler.setFormatter(format) log.addHandler(handler) log.propagate = False return log log = create_log('main') web_log = create_log('waitress') if not first_run: email_log = create_log('emails') auth_log = create_log('auth') if args.host is not None: log.debug(f'Using specified host {args.host}') config['ui']['host'] = args.host if args.port is not None: log.debug(f'Using specified port {args.port}') config['ui']['port'] = args.port for key in config['files']: if config['files'][key] == '': if key != 'custom_css': log.debug(f'Using default {key}') config['files'][key] = str(data_dir / (key + '.json')) for key in ['user_configuration', 'user_displayprefs']: if key not in config['files']: log.debug(f'Using default {key}') config['files'][key] = str(data_dir / (key + '.json')) def default_css(): css = {} css['href'] = "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" css['integrity'] = "sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" css['crossorigin'] = "anonymous" return css css = {} css = default_css() if 'custom_css' in config['files']: if config['files']['custom_css'] != '': try: shutil.copy(config['files']['custom_css'], (local_dir / 'static' / 'bootstrap.css')) log.debug('Loaded custom CSS') css['href'] = '/bootstrap.css' css['integrity'] = '' css['crossorigin'] = '' except FileNotFoundError: log.error(f'Custom CSS {config["files"]["custom_css"]} not found, using default.') if ('email_html' not in config['password_resets'] or config['password_resets']['email_html'] == ''): log.debug('Using default password reset email HTML template') config['password_resets']['email_html'] = str(local_dir / 'email.html') if ('email_text' not in config['password_resets'] or config['password_resets']['email_text'] == ''): log.debug('Using default password reset email plaintext template') config['password_resets']['email_text'] = str(local_dir / 'email.txt') if ('email_html' not in config['invite_emails'] or config['invite_emails']['email_html'] == ''): log.debug('Using default invite email HTML template') config['invite_emails']['email_html'] = str(local_dir / 'invite-email.html') if ('email_text' not in config['invite_emails'] or config['invite_emails']['email_text'] == ''): log.debug('Using default invite email plaintext template') config['invite_emails']['email_text'] = str(local_dir / 'invite-email.txt') if ('public_server' not in config['jellyfin'] or config['jellyfin']['public_server'] == ''): config['jellyfin']['public_server'] = config['jellyfin']['server'] if args.get_defaults: import json from jellyfin_accounts.jf_api import Jellyfin jf = Jellyfin(config['jellyfin']['server'], config['jellyfin']['client'], config['jellyfin']['version'], config['jellyfin']['device'], config['jellyfin']['device_id']) # No auth needed. print(""" This tool lets you grab various settings from a user, so that they can be applied every time a new account is created. """) print("Step 1: User Policy.") print(""" A user policy stores a users permissions (e.g access rights and most of the other settings in the 'Profile' and 'Access' tabs of a user). """) success = False msg = "Get public users only or all users? (requires auth) [public/all]: " public = False while not success: choice = input(msg) if choice == 'public': public = True print("Make sure the user is publicly visible!") success = True elif choice == 'all': jf.authenticate(config['jellyfin']['username'], config['jellyfin']['password']) public = False success = True users = jf.getUsers(public=public) for index, user in enumerate(users): print(f'{index+1}) {user["Name"]}') success = False while not success: try: user_index = int(input(">: "))-1 policy = users[user_index]['Policy'] success = True except (ValueError, IndexError): pass with open(config['files']['user_template'], 'w') as f: f.write(json.dumps(policy, indent=4)) print(f'Policy written to "{config["files"]["user_template"]}".') print('In future, this policy will be copied to all new users.') print('Step 2: Homescreen Layout') print(""" You may want to customize the default layout of a new user's home screen. These settings can be applied to an account through the 'Home' section in a user's settings. """) success = False while not success: choice = input("Grab the chosen user's homescreen layout? [y/n]: ") if choice.lower() == 'y': user_id = users[user_index]['Id'] configuration = users[user_index]['Configuration'] display_prefs = jf.getDisplayPreferences(user_id) with open(config['files']['user_configuration'], 'w') as f: f.write(json.dumps(configuration, indent=4)) print(f'Configuration written to "{config["files"]["user_configuration"]}".') with open(config['files']['user_displayprefs'], 'w') as f: f.write(json.dumps(display_prefs, indent=4)) print(f'Display Prefs written to "{config["files"]["user_displayprefs"]}".') success = True elif choice.lower() == 'n': success = True else: def signal_handler(sig, frame): print('Quitting...') sys.exit(0) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) app = Flask(__name__, root_path=str(local_dir)) app.config['DEBUG'] = config.getboolean('ui', 'debug') app.config['SECRET_KEY'] = secrets.token_urlsafe(16) if __name__ == '__main__': from waitress import serve if first_run: import jellyfin_accounts.setup host = config['ui']['host'] port = config['ui']['port'] log.info('Starting web UI for first run setup...') serve(app, host=host, port=port) else: import jellyfin_accounts.web_api import jellyfin_accounts.web host = config['ui']['host'] port = config['ui']['port'] log.info(f'Starting web UI on {host}:{port}') if config.getboolean('password_resets', 'enabled'): def start_pwr(): import jellyfin_accounts.pw_reset jellyfin_accounts.pw_reset.start() pwr = threading.Thread(target=start_pwr, daemon=True) log.info('Starting email thread') pwr.start() serve(app, host=host, port=int(port))