mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2024-12-22 09:00:14 +00:00
Harvey Tindall
ac60cc37da
live reloading was intended for previous release, but needed some tweaking. Settings that still require a restart are marked with an R. Fixed issue where default values weren't being filled in on reload of config that broke emails if settings were changed at all.
351 lines
11 KiB
Python
Executable File
351 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
__version__ = "0.2.6"
|
|
|
|
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, jsonify, 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__).parent / "data").resolve()
|
|
config_base_path = local_dir / "config-base.json"
|
|
|
|
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 generating at {str(data_dir)}")
|
|
if args.config is None:
|
|
config_path = data_dir / "config.ini"
|
|
from jellyfin_accounts.generate_ini import generate_ini
|
|
|
|
default_path = local_dir / "config-default.ini"
|
|
generate_ini(config_base_path, default_path, __version__)
|
|
shutil.copy(str(default_path), 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"
|
|
|
|
|
|
temp_config = configparser.RawConfigParser()
|
|
temp_config.read(config_path)
|
|
|
|
def create_log(name):
|
|
log = logging.getLogger(name)
|
|
handler = logging.StreamHandler(sys.stdout)
|
|
if temp_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")
|
|
|
|
def load_config(config_path, data_dir):
|
|
config = configparser.RawConfigParser()
|
|
config.read(config_path)
|
|
global log
|
|
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"))
|
|
|
|
if "no_username" not in config["email"]:
|
|
config["email"]["no_username"] = "false"
|
|
log.debug("Set no_username to false")
|
|
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"]
|
|
return config
|
|
|
|
config = load_config(config_path, data_dir)
|
|
|
|
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
|
|
|
|
|
|
try:
|
|
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))
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
|
|
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.'
|
|
)
|
|
|
|
|
|
|
|
|
|
def resp(success=True, code=500):
|
|
if success:
|
|
r = jsonify({"success": True})
|
|
if code == 500:
|
|
r.status_code = 200
|
|
else:
|
|
r.status_code = code
|
|
else:
|
|
r = jsonify({"success": False})
|
|
r.status_code = code
|
|
return r
|
|
|
|
|
|
def main():
|
|
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)
|
|
global app
|
|
app = Flask(__name__, root_path=str(local_dir))
|
|
app.config["DEBUG"] = config.getboolean("ui", "debug")
|
|
app.config["SECRET_KEY"] = secrets.token_urlsafe(16)
|
|
app.config["JSON_SORT_KEYS"] = False
|
|
|
|
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))
|