#!/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_policy", help=("tool to grab a JF users " + "policy (access, perms, etc.) and " + "output 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')) 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 args.get_policy: 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("Make sure the user is publicly visible!") users = jf.getUsers() for index, user in enumerate(users): print(f'{index+1}) {user["Name"]}') success = False while not success: try: policy = users[int(input(">: "))-1]['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.') 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 = '0.0.0.0' port = 8056 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))