jellyfin-accounts/jf-accounts
Harvey Tindall f4f18d41ea Modularized JSON storage
user_template and other files are now accessed via JSONStorage, which
has dictionary like attributes for each file which can be used like a
dictionary, without the need to manually read and write the file. This
was done so that other storage types (e.g a database) can be added in
future.
2020-06-14 17:58:18 +01:00

272 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
import secrets
import configparser
import shutil
import argparse
import logging
import threading
import signal
import sys
import json
from pathlib import Path
from flask import Flask, g
from jellyfin_accounts.data_store import JSONStorage
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'))
with open(config['files']['invites'], 'r') as f:
temp_invites = json.load(f)
if 'invites' in temp_invites:
new_invites = {}
log.info('Converting invites.json to new format, temporary.')
for el in temp_invites['invites']:
i = {'valid_till': el['valid_till']}
if 'email' in el:
i['email'] = el['email']
new_invites[el['code']] = i
with open(config['files']['invites'], 'w') as f:
f.write(json.dumps(new_invites, indent=4, default=str))
data_store = JSONStorage(config['files']['emails'],
config['files']['invites'],
config['files']['user_template'],
config['files']['user_displayprefs'],
config['files']['user_configuration'])
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'])
print("NOTE: This can now be done through the web ui.")
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
data_store.user_template = policy
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)
data_store.user_configuration = configuration
print(f'Configuration written to "{config["files"]["user_configuration"]}".')
data_store.user_displayprefs = display_prefs
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))