2020-07-12 18:53:04 +00:00
# Runs it!
2020-07-24 11:04:57 +00:00
__version__ = " 0.3.9 "
2020-06-16 19:07:47 +00:00
2020-11-13 18:23:48 +00:00
print ( " Note: jellyfin-accounts has been deprecated. Try jfa-go, a rewrite thats fast, portable, and has more features. Find it at \n https://github.com/hrfee/jfa-go \n " )
2020-08-31 13:13:53 +00:00
2020-06-16 19:07:47 +00:00
import secrets
import configparser
import shutil
import argparse
import logging
import threading
import signal
import sys
import json
from pathlib import Path
2020-06-29 21:05:40 +00:00
from flask import Flask , jsonify , g
2020-06-16 19:07:47 +00:00
from jellyfin_accounts . data_store import JSONStorage
2020-07-12 18:53:04 +00:00
from jellyfin_accounts . config import Config
2020-06-21 19:29:53 +00:00
2020-06-16 19:07:47 +00:00
parser = argparse . ArgumentParser ( description = " jellyfin-accounts " )
2020-06-21 19:29:53 +00:00
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 " ,
)
2020-07-04 21:17:49 +00:00
parser . add_argument (
" -i " , " --install " , help = " attempt to install a system service. " , action = " store_true "
)
2020-06-16 19:07:47 +00:00
args , leftovers = parser . parse_known_args ( )
if args . data is not None :
data_dir = Path ( args . data )
else :
2020-06-21 19:29:53 +00:00
data_dir = Path . home ( ) / " .jf-accounts "
2020-06-16 19:07:47 +00:00
2020-06-21 19:29:53 +00:00
local_dir = ( Path ( __file__ ) . parent / " data " ) . resolve ( )
2020-06-29 22:23:43 +00:00
config_base_path = local_dir / " config-base.json "
2020-06-16 19:07:47 +00:00
first_run = False
2020-06-21 19:29:53 +00:00
if data_dir . exists ( ) is False or ( data_dir / " config.ini " ) . exists ( ) is False :
2020-06-16 19:07:47 +00:00
if not data_dir . exists ( ) :
Path . mkdir ( data_dir )
2020-06-29 22:06:58 +00:00
print ( f " Config dir not found, so generating at { str ( data_dir ) } " )
2020-06-16 19:07:47 +00:00
if args . config is None :
2020-06-21 19:29:53 +00:00
config_path = data_dir / " config.ini "
2020-06-29 22:06:58 +00:00
from jellyfin_accounts . generate_ini import generate_ini
2020-06-29 22:23:43 +00:00
2020-06-29 22:06:58 +00:00
default_path = local_dir / " config-default.ini "
2020-06-30 18:58:06 +00:00
generate_ini ( config_base_path , default_path , __version__ )
2020-06-29 22:06:58 +00:00
shutil . copy ( str ( default_path ) , str ( config_path ) )
2020-06-16 19:07:47 +00:00
print ( " Setup through the web UI, or quit and edit the configuration manually. " )
first_run = True
else :
config_path = Path ( args . config )
2020-06-21 19:29:53 +00:00
print ( f " config.ini can be found at { str ( config_path ) } " )
2020-06-16 19:07:47 +00:00
else :
2020-06-21 19:29:53 +00:00
config_path = data_dir / " config.ini "
2020-06-16 19:07:47 +00:00
2020-07-12 18:53:04 +00:00
# Temp config so logger knows whether to use debug mode or not
2020-06-30 20:24:07 +00:00
temp_config = configparser . RawConfigParser ( )
2020-08-04 00:29:29 +00:00
temp_config . read ( str ( config_path . resolve ( ) ) )
2020-06-21 19:29:53 +00:00
2020-07-04 21:17:49 +00:00
2020-06-16 19:07:47 +00:00
def create_log ( name ) :
log = logging . getLogger ( name )
handler = logging . StreamHandler ( sys . stdout )
2020-07-04 21:17:49 +00:00
if temp_config . getboolean ( " ui " , " debug " ) :
2020-06-16 19:07:47 +00:00
log . setLevel ( logging . DEBUG )
handler . setLevel ( logging . DEBUG )
else :
log . setLevel ( logging . INFO )
handler . setLevel ( logging . INFO )
2020-06-21 19:29:53 +00:00
fmt = " %(name)s - %(levelname)s - %(message)s "
2020-06-16 19:07:47 +00:00
format = logging . Formatter ( fmt )
handler . setFormatter ( format )
log . addHandler ( handler )
log . propagate = False
return log
2020-06-21 19:29:53 +00:00
log = create_log ( " main " )
2020-06-30 20:24:07 +00:00
2020-07-12 18:53:04 +00:00
config = Config ( config_path , secrets . token_urlsafe ( 16 ) , data_dir , local_dir , log )
2020-06-30 20:24:07 +00:00
2020-06-21 19:29:53 +00:00
web_log = create_log ( " waitress " )
2020-06-16 19:07:47 +00:00
if not first_run :
2020-08-02 01:33:34 +00:00
email_log = create_log ( " email " )
pwr_log = create_log ( " pwr " )
2020-06-21 19:29:53 +00:00
auth_log = create_log ( " auth " )
2020-06-16 19:07:47 +00:00
if args . host is not None :
2020-06-21 19:29:53 +00:00
log . debug ( f " Using specified host { args . host } " )
config [ " ui " ] [ " host " ] = args . host
2020-06-16 19:07:47 +00:00
if args . port is not None :
2020-06-21 19:29:53 +00:00
log . debug ( f " Using specified port { args . port } " )
config [ " ui " ] [ " port " ] = args . port
2020-06-16 19:07:47 +00:00
2020-06-28 23:35:51 +00:00
2020-06-29 21:05:40 +00:00
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
2020-06-16 19:07:47 +00:00
2020-06-21 19:29:53 +00:00
data_store = JSONStorage (
config [ " files " ] [ " emails " ] ,
config [ " files " ] [ " invites " ] ,
config [ " files " ] [ " user_template " ] ,
config [ " files " ] [ " user_displayprefs " ] ,
config [ " files " ] [ " user_configuration " ] ,
)
2020-06-16 19:07:47 +00:00
2020-07-05 13:38:07 +00:00
if config . getboolean ( " ui " , " bs5 " ) :
css_file = " bs5-jf.css "
2020-07-05 14:09:42 +00:00
log . debug ( " Using Bootstrap 5 " )
2020-07-05 13:38:07 +00:00
else :
css_file = " bs4-jf.css "
2020-06-16 19:07:47 +00:00
2020-07-05 14:09:42 +00:00
with open ( config_base_path , " r " ) as f :
themes = json . load ( f ) [ " ui " ] [ " theme " ]
theme_options = themes [ " options " ]
if " theme " not in config [ " ui " ] or config [ " ui " ] [ " theme " ] not in theme_options :
config [ " ui " ] [ " theme " ] = themes [ " value " ]
if config . getboolean ( " ui " , " bs5 " ) :
num = 5
else :
num = 4
current_theme = config [ " ui " ] [ " theme " ]
if " Bootstrap " in current_theme :
css_file = f " bs { num } .css "
elif " Jellyfin " in current_theme :
css_file = f " bs { num } -jf.css "
elif " Custom " in current_theme and " custom_css " in config [ " files " ] :
2020-06-21 19:29:53 +00:00
if config [ " files " ] [ " custom_css " ] != " " :
2020-06-16 19:07:47 +00:00
try :
2020-07-04 21:17:49 +00:00
css_path = Path ( config [ " files " ] [ " custom_css " ] )
shutil . copy ( css_path , ( local_dir / " static " / css_path . name ) )
2020-07-06 19:53:14 +00:00
log . debug ( f ' Loaded custom CSS " { css_path . name } " ' )
2020-07-04 21:17:49 +00:00
css_file = css_path . name
2020-06-16 19:07:47 +00:00
except FileNotFoundError :
2020-06-21 19:29:53 +00:00
log . error (
f ' Custom CSS { config [ " files " ] [ " custom_css " ] } not found, using default. '
)
2020-06-29 21:05:40 +00:00
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
2020-08-02 01:33:34 +00:00
app = Flask ( __name__ , root_path = str ( local_dir ) )
2020-06-29 22:23:43 +00:00
2020-06-16 19:07:47 +00:00
def main ( ) :
2020-07-04 21:17:49 +00:00
if args . install :
executable = sys . argv [ 0 ]
print ( f ' Assuming executable path " { executable } " . ' )
options = [ " systemd " ]
for i , opt in enumerate ( options ) :
print ( f " { i + 1 } : { opt } " )
success = False
while not success :
try :
method = options [ int ( input ( " >: " ) ) - 1 ]
success = True
except IndexError :
pass
if method == " systemd " :
with open ( local_dir / " services " / " jf-accounts.service " , " r " ) as f :
data = f . read ( )
data = data . replace ( " {executable} " , executable )
service_path = str ( Path ( " jf-accounts.service " ) . resolve ( ) )
with open ( service_path , " w " ) as f :
f . write ( data )
print ( f " service written to the current directory \n ( { service_path } ). " )
print ( " Place this in the appropriate directory, and reload daemons. " )
elif args . get_defaults :
2020-06-16 19:07:47 +00:00
import json
from jellyfin_accounts . jf_api import Jellyfin
2020-06-21 19:29:53 +00:00
jf = Jellyfin (
config [ " jellyfin " ] [ " server " ] ,
config [ " jellyfin " ] [ " client " ] ,
config [ " jellyfin " ] [ " version " ] ,
config [ " jellyfin " ] [ " device " ] ,
config [ " jellyfin " ] [ " device_id " ] ,
)
2020-06-16 19:07:47 +00:00
print ( " NOTE: This can now be done through the web ui. " )
2020-06-21 19:29:53 +00:00
print (
"""
2020-06-16 19:07:47 +00:00
This tool lets you grab various settings from a user ,
so that they can be applied every time a new account is
2020-06-21 19:29:53 +00:00
created . """
)
2020-06-16 19:07:47 +00:00
print ( " Step 1: User Policy. " )
2020-06-21 19:29:53 +00:00
print (
"""
2020-06-21 19:21:33 +00:00
A user policy stores a users permissions ( e . g access rights and
2020-06-16 19:07:47 +00:00
most of the other settings in the ' Profile ' and ' Access ' tabs
2020-06-21 19:29:53 +00:00
of a user ) . """
)
2020-06-16 19:07:47 +00:00
success = False
2020-06-21 19:21:33 +00:00
msg = " Get public users only or all users? (requires auth) [public/all]: "
2020-06-16 19:07:47 +00:00
public = False
while not success :
choice = input ( msg )
2020-06-21 19:29:53 +00:00
if choice == " public " :
2020-06-16 19:07:47 +00:00
public = True
print ( " Make sure the user is publicly visible! " )
success = True
2020-06-21 19:29:53 +00:00
elif choice == " all " :
jf . authenticate (
config [ " jellyfin " ] [ " username " ] , config [ " jellyfin " ] [ " password " ]
)
2020-06-16 19:07:47 +00:00
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 :
2020-06-21 19:29:53 +00:00
user_index = int ( input ( " >: " ) ) - 1
policy = users [ user_index ] [ " Policy " ]
2020-06-16 19:07:47 +00:00
success = True
except ( ValueError , IndexError ) :
pass
data_store . user_template = policy
print ( f ' Policy written to " { config [ " files " ] [ " user_template " ] } " . ' )
2020-06-21 19:29:53 +00:00
print ( " In future, this policy will be copied to all new users. " )
print ( " Step 2: Homescreen Layout " )
print (
"""
2020-06-16 19:07:47 +00:00
You may want to customize the default layout of a new user ' s
home screen . These settings can be applied to an account through
2020-06-21 19:29:53 +00:00
the ' Home ' section in a user ' s settings. " " "
)
2020-06-16 19:07:47 +00:00
success = False
while not success :
choice = input ( " Grab the chosen user ' s homescreen layout? [y/n]: " )
2020-06-21 19:29:53 +00:00
if choice . lower ( ) == " y " :
user_id = users [ user_index ] [ " Id " ]
configuration = users [ user_index ] [ " Configuration " ]
2020-06-16 19:07:47 +00:00
display_prefs = jf . getDisplayPreferences ( user_id )
data_store . user_configuration = configuration
2020-06-21 19:29:53 +00:00
print (
f ' Configuration written to " { config [ " files " ] [ " user_configuration " ] } " . '
)
2020-06-16 19:07:47 +00:00
data_store . user_displayprefs = display_prefs
2020-06-21 19:29:53 +00:00
print (
f ' Display Prefs written to " { config [ " files " ] [ " user_displayprefs " ] } " . '
)
2020-06-16 19:07:47 +00:00
success = True
2020-06-21 19:29:53 +00:00
elif choice . lower ( ) == " n " :
2020-06-16 19:07:47 +00:00
success = True
else :
2020-06-21 19:29:53 +00:00
app . config [ " DEBUG " ] = config . getboolean ( " ui " , " debug " )
app . config [ " SECRET_KEY " ] = secrets . token_urlsafe ( 16 )
2020-06-29 22:23:43 +00:00
app . config [ " JSON_SORT_KEYS " ] = False
2020-06-16 19:07:47 +00:00
from waitress import serve
2020-06-21 19:29:53 +00:00
2020-06-16 19:07:47 +00:00
if first_run :
2020-07-17 15:08:36 +00:00
def signal_handler ( sig , frame ) :
print ( " Quitting... " )
sys . exit ( 0 )
signal . signal ( signal . SIGINT , signal_handler )
signal . signal ( signal . SIGTERM , signal_handler )
2020-06-16 19:07:47 +00:00
import jellyfin_accounts . setup
2020-06-21 19:29:53 +00:00
host = config [ " ui " ] [ " host " ]
port = config [ " ui " ] [ " port " ]
log . info ( " Starting web UI for first run setup... " )
serve ( app , host = host , port = port )
2020-06-16 19:07:47 +00:00
else :
import jellyfin_accounts . web_api
import jellyfin_accounts . web
2020-07-17 15:08:36 +00:00
import jellyfin_accounts . invite_daemon
2020-06-21 19:29:53 +00:00
host = config [ " ui " ] [ " host " ]
port = config [ " ui " ] [ " port " ]
log . info ( f " Starting web UI on { host } : { port } " )
if config . getboolean ( " password_resets " , " enabled " ) :
2020-06-16 19:07:47 +00:00
def start_pwr ( ) :
import jellyfin_accounts . pw_reset
2020-06-21 19:29:53 +00:00
2020-06-16 19:07:47 +00:00
jellyfin_accounts . pw_reset . start ( )
2020-06-21 19:29:53 +00:00
2020-06-16 19:07:47 +00:00
pwr = threading . Thread ( target = start_pwr , daemon = True )
2020-08-02 01:33:34 +00:00
log . info ( " Starting password reset thread " )
2020-06-16 19:07:47 +00:00
pwr . start ( )
2020-07-17 15:08:36 +00:00
def signal_handler ( sig , frame ) :
print ( " Quitting... " )
if config . getboolean ( " notifications " , " enabled " ) :
jellyfin_accounts . invite_daemon . inviteDaemon . stop ( )
sys . exit ( 0 )
signal . signal ( signal . SIGINT , signal_handler )
signal . signal ( signal . SIGTERM , signal_handler )
2020-06-21 19:29:53 +00:00
serve ( app , host = host , port = int ( port ) )