mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-11 07:46:07 -07:00
A new first run setup wizard (WIP)
Move all user related links to use user_id instead of username. Remove excess debug loggin. Catch more exceptions on PW importer.
This commit is contained in:
parent
96f48291e5
commit
4f00ecc070
20 changed files with 7975 additions and 94 deletions
|
@ -53,6 +53,7 @@ _CONFIG_DEFINITIONS = {
|
|||
'EMAIL_SMTP_PORT': (int, 'Email', 25),
|
||||
'EMAIL_TLS': (int, 'Email', 0),
|
||||
'ENABLE_HTTPS': (int, 'General', 0),
|
||||
'FIRST_RUN_COMPLETE': (int, 'General', 0),
|
||||
'FREEZE_DB': (int, 'General', 0),
|
||||
'GIT_BRANCH': (str, 'General', 'master'),
|
||||
'GIT_PATH': (str, 'General', ''),
|
||||
|
|
|
@ -73,13 +73,13 @@ class DataFactory(object):
|
|||
search_value=search_value,
|
||||
search_regex=search_regex,
|
||||
custom_where='',
|
||||
group_by=(t1 + '.user'),
|
||||
group_by=(t1 + '.user_id'),
|
||||
join_type=['LEFT OUTER JOIN'],
|
||||
join_table=['users'],
|
||||
join_evals=[[t1 + '.user', 'users.username']],
|
||||
join_evals=[[t1 + '.user_id', 'users.user_id']],
|
||||
kwargs=kwargs)
|
||||
except:
|
||||
logger.warn("Unable to open session_history table.")
|
||||
logger.warn("Unable to execute database query.")
|
||||
return {'recordsFiltered': 0,
|
||||
'recordsTotal': 0,
|
||||
'data': 'null'},
|
||||
|
@ -159,7 +159,8 @@ class DataFactory(object):
|
|||
t1 + '.rating_key as rating_key',
|
||||
t1 + '.user',
|
||||
t2 + '.media_type',
|
||||
t4 + '.video_decision'
|
||||
t4 + '.video_decision',
|
||||
t1 + '.user_id as user_id'
|
||||
]
|
||||
try:
|
||||
query = data_tables.ssp_query(table_name=t1,
|
||||
|
@ -179,7 +180,7 @@ class DataFactory(object):
|
|||
[t1 + '.id', t4 + '.id']],
|
||||
kwargs=kwargs)
|
||||
except:
|
||||
logger.warn("Unable to open PlexWatch database.")
|
||||
logger.warn("Unable to execute database query.")
|
||||
return {'recordsFiltered': 0,
|
||||
'recordsTotal': 0,
|
||||
'data': 'null'},
|
||||
|
@ -205,6 +206,7 @@ class DataFactory(object):
|
|||
"user": item["user"],
|
||||
"media_type": item["media_type"],
|
||||
"video_decision": item["video_decision"],
|
||||
"user_id": item["user_id"]
|
||||
}
|
||||
|
||||
if item['paused_counter'] > 0:
|
||||
|
@ -260,7 +262,8 @@ class DataFactory(object):
|
|||
'COUNT(session_history.ip_address) as play_count',
|
||||
'session_history.player as platform',
|
||||
'session_history_metadata.full_title as last_watched',
|
||||
'session_history.user as user'
|
||||
'session_history.user as user',
|
||||
'session_history.user_id as user_id'
|
||||
]
|
||||
|
||||
try:
|
||||
|
@ -304,7 +307,19 @@ class DataFactory(object):
|
|||
|
||||
return dict
|
||||
|
||||
def set_user_friendly_name(self, user=None, friendly_name=None):
|
||||
def set_user_friendly_name(self, user=None, user_id=None, friendly_name=None):
|
||||
if user_id:
|
||||
if friendly_name.strip() == '':
|
||||
friendly_name = None
|
||||
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
control_value_dict = {"user_id": user_id}
|
||||
new_value_dict = {"friendly_name": friendly_name}
|
||||
try:
|
||||
monitor_db.upsert('users', new_value_dict, control_value_dict)
|
||||
except Exception, e:
|
||||
logger.debug(u"Uncaught exception %s" % e)
|
||||
if user:
|
||||
if friendly_name.strip() == '':
|
||||
friendly_name = None
|
||||
|
@ -318,18 +333,51 @@ class DataFactory(object):
|
|||
except Exception, e:
|
||||
logger.debug(u"Uncaught exception %s" % e)
|
||||
|
||||
def get_user_friendly_name(self, user=None):
|
||||
if user:
|
||||
def get_user_friendly_name(self, user=None, user_id=None):
|
||||
if user_id:
|
||||
try:
|
||||
monitor_db = database.MonitorDatabase()
|
||||
query = 'select friendly_name FROM users WHERE username = ?'
|
||||
result = monitor_db.select_single(query, args=[user])
|
||||
query = 'select username, ' \
|
||||
'(CASE WHEN friendly_name IS NULL THEN username ELSE friendly_name END) ' \
|
||||
'FROM users WHERE user_id = ?'
|
||||
result = monitor_db.select(query, args=[user_id])
|
||||
if result:
|
||||
return result
|
||||
user_detail = {'user_id': user_id,
|
||||
'user': result[0][0],
|
||||
'friendly_name': result[0][1]}
|
||||
return user_detail
|
||||
else:
|
||||
return user
|
||||
user_detail = {'user_id': user_id,
|
||||
'user': '',
|
||||
'friendly_name': ''}
|
||||
return user_detail
|
||||
except:
|
||||
return user
|
||||
user_detail = {'user_id': user_id,
|
||||
'user': '',
|
||||
'friendly_name': ''}
|
||||
return user_detail
|
||||
elif user:
|
||||
try:
|
||||
monitor_db = database.MonitorDatabase()
|
||||
query = 'select user_id, ' \
|
||||
'(CASE WHEN friendly_name IS NULL THEN username ELSE friendly_name END) ' \
|
||||
'FROM users WHERE username = ?'
|
||||
result = monitor_db.select(query, args=[user])
|
||||
if result:
|
||||
user_detail = {'user_id': result[0][0],
|
||||
'user': user,
|
||||
'friendly_name': result[0][1]}
|
||||
return user_detail
|
||||
else:
|
||||
user_detail = {'user_id': None,
|
||||
'user': user,
|
||||
'friendly_name': ''}
|
||||
return user_detail
|
||||
except:
|
||||
user_detail = {'user_id': None,
|
||||
'user': user,
|
||||
'friendly_name': ''}
|
||||
return user_detail
|
||||
|
||||
return None
|
||||
|
||||
|
@ -608,7 +656,7 @@ class DataFactory(object):
|
|||
|
||||
return stream_output
|
||||
|
||||
def get_recently_watched(self, user=None, limit='10'):
|
||||
def get_recently_watched(self, user=None, user_id=None, limit='10'):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
recently_watched = []
|
||||
|
||||
|
@ -616,7 +664,14 @@ class DataFactory(object):
|
|||
limit = '10'
|
||||
|
||||
try:
|
||||
if user:
|
||||
if user_id:
|
||||
query = 'SELECT session_history.media_type, session_history.rating_key, title, thumb, parent_thumb, ' \
|
||||
'media_index, parent_media_index, year, started, user ' \
|
||||
'FROM session_history_metadata ' \
|
||||
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
|
||||
'WHERE user_id = ? ORDER BY started DESC LIMIT ?'
|
||||
result = monitor_db.select(query, args=[user_id, limit])
|
||||
elif user:
|
||||
query = 'SELECT session_history.media_type, session_history.rating_key, title, thumb, parent_thumb, ' \
|
||||
'media_index, parent_media_index, year, started, user ' \
|
||||
'FROM session_history_metadata ' \
|
||||
|
@ -654,7 +709,7 @@ class DataFactory(object):
|
|||
|
||||
return recently_watched
|
||||
|
||||
def get_user_watch_time_stats(self, user=None):
|
||||
def get_user_watch_time_stats(self, user=None, user_id=None):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
time_queries = [1, 7, 30, 0]
|
||||
|
@ -662,13 +717,22 @@ class DataFactory(object):
|
|||
|
||||
for days in time_queries:
|
||||
if days > 0:
|
||||
query = 'SELECT (SUM(stopped - started) - ' \
|
||||
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
|
||||
'COUNT(id) AS total_plays ' \
|
||||
'FROM session_history ' \
|
||||
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
|
||||
'AND user = ?' % days
|
||||
result = monitor_db.select(query, args=[user])
|
||||
if user_id:
|
||||
query = 'SELECT (SUM(stopped - started) - ' \
|
||||
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
|
||||
'COUNT(id) AS total_plays ' \
|
||||
'FROM session_history ' \
|
||||
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
|
||||
'AND user_id = ?' % days
|
||||
result = monitor_db.select(query, args=[user_id])
|
||||
elif user:
|
||||
query = 'SELECT (SUM(stopped - started) - ' \
|
||||
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
|
||||
'COUNT(id) AS total_plays ' \
|
||||
'FROM session_history ' \
|
||||
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
|
||||
'AND user = ?' % days
|
||||
result = monitor_db.select(query, args=[user])
|
||||
else:
|
||||
query = 'SELECT (SUM(stopped - started) - ' \
|
||||
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
|
||||
|
@ -694,19 +758,27 @@ class DataFactory(object):
|
|||
|
||||
return user_watch_time_stats
|
||||
|
||||
def get_user_platform_stats(self, user=None):
|
||||
def get_user_platform_stats(self, user=None, user_id=None):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
platform_stats = []
|
||||
result_id = 0
|
||||
|
||||
try:
|
||||
query = 'SELECT player, COUNT(player) as player_count, platform ' \
|
||||
'FROM session_history ' \
|
||||
'WHERE user = ? ' \
|
||||
'GROUP BY player ' \
|
||||
'ORDER BY player_count DESC'
|
||||
result = monitor_db.select(query, args=[user])
|
||||
if user_id:
|
||||
query = 'SELECT player, COUNT(player) as player_count, platform ' \
|
||||
'FROM session_history ' \
|
||||
'WHERE user_id = ? ' \
|
||||
'GROUP BY player ' \
|
||||
'ORDER BY player_count DESC'
|
||||
result = monitor_db.select(query, args=[user_id])
|
||||
else:
|
||||
query = 'SELECT player, COUNT(player) as player_count, platform ' \
|
||||
'FROM session_history ' \
|
||||
'WHERE user = ? ' \
|
||||
'GROUP BY player ' \
|
||||
'ORDER BY player_count DESC'
|
||||
result = monitor_db.select(query, args=[user])
|
||||
except:
|
||||
logger.warn("Unable to execute database query.")
|
||||
return None
|
||||
|
|
|
@ -27,7 +27,6 @@ class DataTables(object):
|
|||
|
||||
def __init__(self):
|
||||
self.ssp_db = database.MonitorDatabase()
|
||||
logger.debug(u"Database initilised!")
|
||||
|
||||
# TODO: Pass all parameters via kwargs
|
||||
def ssp_query(self, table_name,
|
||||
|
@ -71,8 +70,6 @@ class DataTables(object):
|
|||
join_iter += 1
|
||||
join += join_item
|
||||
|
||||
logger.debug(u"join string = %s" % join)
|
||||
|
||||
# TODO: custom_where is ugly and causes issues with reported total results
|
||||
if custom_where != '':
|
||||
custom_where = 'WHERE (' + custom_where + ')'
|
||||
|
@ -96,7 +93,7 @@ class DataTables(object):
|
|||
% (column_data['column_string'], table_name, join, where,
|
||||
order, custom_where)
|
||||
|
||||
logger.debug(u"Query string: %s" % query)
|
||||
# logger.debug(u"Query string: %s" % query)
|
||||
filtered = self.ssp_db.select(query)
|
||||
|
||||
if search_value == '':
|
||||
|
|
|
@ -206,6 +206,9 @@ def validate_database(database=None, table_name=None):
|
|||
except ValueError:
|
||||
logger.error('PlexPy Importer :: Invalid database specified.')
|
||||
return 'Invalid database specified.'
|
||||
except:
|
||||
logger.error('PlexPy Importer :: Uncaught exception.')
|
||||
return 'Uncaught exception.'
|
||||
|
||||
try:
|
||||
connection.execute('SELECT ratingKey from %s' % table_name)
|
||||
|
@ -213,6 +216,9 @@ def validate_database(database=None, table_name=None):
|
|||
except sqlite3.OperationalError:
|
||||
logger.error('PlexPy Importer :: Invalid database specified.')
|
||||
return 'Invalid database specified.'
|
||||
except:
|
||||
logger.error('PlexPy Importer :: Uncaught exception.')
|
||||
return 'Uncaught exception.'
|
||||
|
||||
return 'success'
|
||||
|
||||
|
|
|
@ -57,12 +57,42 @@ class WebInterface(object):
|
|||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
raise cherrypy.HTTPRedirect("home")
|
||||
if plexpy.CONFIG.FIRST_RUN_COMPLETE:
|
||||
raise cherrypy.HTTPRedirect("home")
|
||||
else:
|
||||
raise cherrypy.HTTPRedirect("welcome")
|
||||
|
||||
@cherrypy.expose
|
||||
def home(self):
|
||||
return serve_template(templatename="index.html", title="Home")
|
||||
|
||||
@cherrypy.expose
|
||||
def welcome(self, **kwargs):
|
||||
config = {
|
||||
"launch_browser": checked(plexpy.CONFIG.LAUNCH_BROWSER),
|
||||
"refresh_users_on_startup": checked(plexpy.CONFIG.REFRESH_USERS_ON_STARTUP),
|
||||
"pms_ip": plexpy.CONFIG.PMS_IP,
|
||||
"pms_port": plexpy.CONFIG.PMS_PORT,
|
||||
"pms_token": plexpy.CONFIG.PMS_TOKEN,
|
||||
"pms_uuid": plexpy.CONFIG.PMS_UUID,
|
||||
"tv_notify_enable": checked(plexpy.CONFIG.TV_NOTIFY_ENABLE),
|
||||
"movie_notify_enable": checked(plexpy.CONFIG.MOVIE_NOTIFY_ENABLE),
|
||||
"music_notify_enable": checked(plexpy.CONFIG.MUSIC_NOTIFY_ENABLE),
|
||||
"tv_notify_on_start": checked(plexpy.CONFIG.TV_NOTIFY_ON_START),
|
||||
"movie_notify_on_start": checked(plexpy.CONFIG.MOVIE_NOTIFY_ON_START),
|
||||
"music_notify_on_start": checked(plexpy.CONFIG.MUSIC_NOTIFY_ON_START),
|
||||
"video_logging_enable": checked(plexpy.CONFIG.VIDEO_LOGGING_ENABLE),
|
||||
"music_logging_enable": checked(plexpy.CONFIG.MUSIC_LOGGING_ENABLE),
|
||||
"logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL
|
||||
}
|
||||
|
||||
# The setup wizard just refreshes the page on submit so we must redirect to home if config set.
|
||||
# Also redirecting to home if a PMS token already exists - will remove this in future.
|
||||
if plexpy.CONFIG.FIRST_RUN_COMPLETE or plexpy.CONFIG.PMS_TOKEN:
|
||||
raise cherrypy.HTTPRedirect("home")
|
||||
else:
|
||||
return serve_template(templatename="welcome.html", title="Welcome", config=config)
|
||||
|
||||
@cherrypy.expose
|
||||
def get_date_formats(self):
|
||||
if plexpy.CONFIG.DATE_FORMAT:
|
||||
|
@ -104,40 +134,57 @@ class WebInterface(object):
|
|||
return serve_template(templatename="sync.html", title="Synced Items")
|
||||
|
||||
@cherrypy.expose
|
||||
def user(self, user=None):
|
||||
try:
|
||||
data_factory = datafactory.DataFactory()
|
||||
user_details = data_factory.get_user_details(user=user)
|
||||
except:
|
||||
logger.warn("Unable to retrieve friendly name for user %s " % user)
|
||||
def user(self, user=None, user_id=None):
|
||||
if user_id:
|
||||
try:
|
||||
data_factory = datafactory.DataFactory()
|
||||
user_details = data_factory.get_user_details(user_id=user_id)
|
||||
except:
|
||||
logger.warn("Unable to retrieve friendly name for user_id %s " % user_id)
|
||||
elif user:
|
||||
try:
|
||||
data_factory = datafactory.DataFactory()
|
||||
user_details = data_factory.get_user_details(user=user)
|
||||
except:
|
||||
logger.warn("Unable to retrieve friendly name for user %s " % user)
|
||||
else:
|
||||
logger.debug(u"User page requested but no parameters received.")
|
||||
raise cherrypy.HTTPRedirect("home")
|
||||
|
||||
return serve_template(templatename="user.html", title="User", data=user_details)
|
||||
|
||||
@cherrypy.expose
|
||||
def edit_user_dialog(self, user=None, **kwargs):
|
||||
if user:
|
||||
try:
|
||||
data_factory = datafactory.DataFactory()
|
||||
result = {'user': user,
|
||||
'friendly_name': data_factory.get_user_friendly_name(user)
|
||||
}
|
||||
status_message = ""
|
||||
except:
|
||||
result = {'user': user,
|
||||
'friendly_name': ''
|
||||
}
|
||||
status_message = "There was an error."
|
||||
|
||||
return serve_template(templatename="edit_user.html", title="Edit User", data=result, status_message=status_message)
|
||||
def edit_user_dialog(self, user=None, user_id=None, **kwargs):
|
||||
if user_id:
|
||||
data_factory = datafactory.DataFactory()
|
||||
result = data_factory.get_user_friendly_name(user_id=user_id)
|
||||
status_message = ''
|
||||
elif user:
|
||||
data_factory = datafactory.DataFactory()
|
||||
result = data_factory.get_user_friendly_name(user=user)
|
||||
status_message = ''
|
||||
else:
|
||||
return serve_template(templatename="edit_user.html", title="Edit User", data=user, status_message='Unknown error.')
|
||||
result = None
|
||||
status_message = 'An error occured.'
|
||||
|
||||
return serve_template(templatename="edit_user.html", title="Edit User", data=result, status_message=status_message)
|
||||
|
||||
@cherrypy.expose
|
||||
def edit_user(self, user=None, friendly_name=None, **kwargs):
|
||||
def edit_user(self, user=None, user_id=None, friendly_name=None, **kwargs):
|
||||
if user_id:
|
||||
try:
|
||||
data_factory = datafactory.DataFactory()
|
||||
data_factory.set_user_friendly_name(user_id=user_id, friendly_name=friendly_name)
|
||||
|
||||
status_message = "Successfully updated user."
|
||||
return status_message
|
||||
except:
|
||||
status_message = "Failed to update user."
|
||||
return status_message
|
||||
if user:
|
||||
try:
|
||||
data_factory = datafactory.DataFactory()
|
||||
data_factory.set_user_friendly_name(user, friendly_name)
|
||||
data_factory.set_user_friendly_name(user=user, friendly_name=friendly_name)
|
||||
|
||||
status_message = "Successfully updated user."
|
||||
return status_message
|
||||
|
@ -376,8 +423,9 @@ class WebInterface(object):
|
|||
kwargs[checked_config] = 0
|
||||
|
||||
# If http password exists in config, do not overwrite when blank value received
|
||||
if kwargs['http_password'] == ' ' and plexpy.CONFIG.HTTP_PASSWORD != '':
|
||||
kwargs['http_password'] = plexpy.CONFIG.HTTP_PASSWORD
|
||||
if 'http_password' in kwargs:
|
||||
if kwargs['http_password'] == ' ' and plexpy.CONFIG.HTTP_PASSWORD != '':
|
||||
kwargs['http_password'] = plexpy.CONFIG.HTTP_PASSWORD
|
||||
|
||||
for plain_config, use_config in [(x[4:], x) for x in kwargs if x.startswith('use_')]:
|
||||
# the use prefix is fairly nice in the html, but does not match the actual config
|
||||
|
@ -405,10 +453,12 @@ class WebInterface(object):
|
|||
message=message, timer=timer)
|
||||
|
||||
@cherrypy.expose
|
||||
def get_history(self, start=0, length=100, custom_where='', **kwargs):
|
||||
def get_history(self, start=0, length=100, user=None, user_id=None, **kwargs):
|
||||
|
||||
if 'user' in kwargs:
|
||||
user = kwargs.get('user', "")
|
||||
custom_where=''
|
||||
if user_id:
|
||||
custom_where = 'user_id = "%s"' % user_id
|
||||
elif user:
|
||||
custom_where = 'user = "%s"' % user
|
||||
if 'rating_key' in kwargs:
|
||||
rating_key = kwargs.get('rating_key', "")
|
||||
|
@ -603,10 +653,10 @@ class WebInterface(object):
|
|||
logger.warn('Unable to retrieve data.')
|
||||
|
||||
@cherrypy.expose
|
||||
def get_user_recently_watched(self, user=None, limit='10', **kwargs):
|
||||
def get_user_recently_watched(self, user=None, user_id=None, limit='10', **kwargs):
|
||||
|
||||
data_factory = datafactory.DataFactory()
|
||||
result = data_factory.get_recently_watched(user=user, limit=limit)
|
||||
result = data_factory.get_recently_watched(user_id=user_id, user=user, limit=limit)
|
||||
|
||||
if result:
|
||||
return serve_template(templatename="user_recently_watched.html", data=result,
|
||||
|
@ -617,10 +667,10 @@ class WebInterface(object):
|
|||
logger.warn('Unable to retrieve data.')
|
||||
|
||||
@cherrypy.expose
|
||||
def get_user_watch_time_stats(self, user=None, **kwargs):
|
||||
def get_user_watch_time_stats(self, user=None, user_id=None, **kwargs):
|
||||
|
||||
data_factory = datafactory.DataFactory()
|
||||
result = data_factory.get_user_watch_time_stats(user=user)
|
||||
result = data_factory.get_user_watch_time_stats(user_id=user_id, user=user)
|
||||
|
||||
if result:
|
||||
return serve_template(templatename="user_watch_time_stats.html", data=result, title="Watch Stats")
|
||||
|
@ -629,10 +679,10 @@ class WebInterface(object):
|
|||
logger.warn('Unable to retrieve data.')
|
||||
|
||||
@cherrypy.expose
|
||||
def get_user_platform_stats(self, user=None, **kwargs):
|
||||
def get_user_platform_stats(self, user=None, user_id=None, **kwargs):
|
||||
|
||||
data_factory = datafactory.DataFactory()
|
||||
result = data_factory.get_user_platform_stats(user=user)
|
||||
result = data_factory.get_user_platform_stats(user_id=user_id, user=user)
|
||||
|
||||
if result:
|
||||
return serve_template(templatename="user_platform_stats.html", data=result,
|
||||
|
@ -702,10 +752,12 @@ class WebInterface(object):
|
|||
logger.warn('Unable to retrieve data.')
|
||||
|
||||
@cherrypy.expose
|
||||
def get_user_ips(self, start=0, length=100, custom_where='', **kwargs):
|
||||
def get_user_ips(self, start=0, length=100, user_id=None, user=None, **kwargs):
|
||||
|
||||
if 'user' in kwargs:
|
||||
user = kwargs.get('user', "")
|
||||
custom_where=''
|
||||
if user_id:
|
||||
custom_where = 'user_id = "%s"' % user_id
|
||||
elif user:
|
||||
custom_where = 'user = "%s"' % user
|
||||
|
||||
data_factory = datafactory.DataFactory()
|
||||
|
@ -939,4 +991,4 @@ class WebInterface(object):
|
|||
|
||||
@cherrypy.expose
|
||||
def plexwatch_import(self, **kwargs):
|
||||
return serve_template(templatename="plexwatch_import.html", title="Import PlexWatch Database")
|
||||
return serve_template(templatename="plexwatch_import.html", title="Import PlexWatch Database")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue