2020-04-11 14:20:25 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
import secrets
|
|
|
|
import configparser
|
|
|
|
import shutil
|
|
|
|
import argparse
|
2020-04-12 20:25:27 +00:00
|
|
|
import logging
|
|
|
|
import threading
|
|
|
|
import signal
|
|
|
|
import sys
|
2020-04-11 14:20:25 +00:00
|
|
|
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()
|
|
|
|
|
2020-05-02 17:32:58 +00:00
|
|
|
first_run = False
|
2020-05-02 18:09:02 +00:00
|
|
|
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)}')
|
2020-04-11 14:20:25 +00:00
|
|
|
if args.config is None:
|
|
|
|
config_path = data_dir / 'config.ini'
|
|
|
|
shutil.copy(str(local_dir / 'config-default.ini'),
|
|
|
|
str(config_path))
|
2020-05-02 17:32:58 +00:00
|
|
|
print("Setup through the web UI, or quit and edit the configuration manually.")
|
|
|
|
first_run = True
|
2020-04-11 14:20:25 +00:00
|
|
|
else:
|
|
|
|
config_path = Path(args.config)
|
|
|
|
print(f'config.ini can be found at {str(config_path)}')
|
|
|
|
else:
|
|
|
|
config_path = data_dir / 'config.ini'
|
|
|
|
|
2020-04-12 20:25:27 +00:00
|
|
|
config = configparser.RawConfigParser()
|
2020-04-11 14:20:25 +00:00
|
|
|
config.read(config_path)
|
|
|
|
|
2020-04-12 20:25:27 +00:00
|
|
|
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')
|
2020-05-02 17:32:58 +00:00
|
|
|
if not first_run:
|
|
|
|
email_log = create_log('emails')
|
|
|
|
auth_log = create_log('auth')
|
2020-04-12 20:25:27 +00:00
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
if args.host is not None:
|
2020-04-12 20:25:27 +00:00
|
|
|
log.debug(f'Using specified host {args.host}')
|
2020-04-11 14:20:25 +00:00
|
|
|
config['ui']['host'] = args.host
|
|
|
|
if args.port is not None:
|
2020-04-12 20:25:27 +00:00
|
|
|
log.debug(f'Using specified port {args.port}')
|
2020-04-11 14:20:25 +00:00
|
|
|
config['ui']['port'] = args.port
|
|
|
|
|
|
|
|
for key in config['files']:
|
|
|
|
if config['files'][key] == '':
|
2020-05-17 14:22:50 +00:00
|
|
|
if key != 'custom_css':
|
|
|
|
log.debug(f'Using default {key}')
|
|
|
|
config['files'][key] = str(data_dir / (key + '.json'))
|
2020-04-11 14:20:25 +00:00
|
|
|
|
2020-05-17 14:22:50 +00:00
|
|
|
|
|
|
|
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()
|
2020-06-03 11:17:29 +00:00
|
|
|
if 'custom_css' in config['files']:
|
2020-05-17 14:22:50 +00:00
|
|
|
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.')
|
2020-06-03 11:17:29 +00:00
|
|
|
|
|
|
|
|
2020-04-19 21:35:51 +00:00
|
|
|
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')
|
2020-06-03 11:17:29 +00:00
|
|
|
if ('public_server' not in config['jellyfin'] or
|
|
|
|
config['jellyfin']['public_server'] == ''):
|
|
|
|
config['jellyfin']['public_server'] = config['jellyfin']['server']
|
2020-04-19 21:35:51 +00:00
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
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:
|
2020-04-12 20:25:27 +00:00
|
|
|
def signal_handler(sig, frame):
|
|
|
|
print('Quitting...')
|
|
|
|
sys.exit(0)
|
2020-06-03 11:17:29 +00:00
|
|
|
|
2020-04-12 20:25:27 +00:00
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
2020-04-11 14:20:25 +00:00
|
|
|
app = Flask(__name__, root_path=str(local_dir))
|
|
|
|
app.config['DEBUG'] = config.getboolean('ui', 'debug')
|
|
|
|
app.config['SECRET_KEY'] = secrets.token_urlsafe(16)
|
2020-06-03 11:17:29 +00:00
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
if __name__ == '__main__':
|
|
|
|
from waitress import serve
|
2020-05-02 17:32:58 +00:00
|
|
|
if first_run:
|
|
|
|
import jellyfin_accounts.setup
|
2020-05-24 14:24:23 +00:00
|
|
|
host = config['ui']['host']
|
|
|
|
port = config['ui']['port']
|
2020-05-02 17:32:58 +00:00
|
|
|
log.info('Starting web UI for first run setup...')
|
|
|
|
serve(app,
|
|
|
|
host=host,
|
|
|
|
port=port)
|
2020-06-03 11:17:29 +00:00
|
|
|
else:
|
2020-05-02 17:32:58 +00:00
|
|
|
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))
|