From 2a48e3375a5ab3d8f9a031ef0949664b4444dc39 Mon Sep 17 00:00:00 2001
From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
Date: Sun, 25 Jun 2023 17:35:16 -0700
Subject: [PATCH 01/21] Add `d3d11va` hardware decoder
---
plexpy/common.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/plexpy/common.py b/plexpy/common.py
index cf1180dc..889d3f73 100644
--- a/plexpy/common.py
+++ b/plexpy/common.py
@@ -216,6 +216,7 @@ AUDIO_QUALITY_PROFILES = {
AUDIO_QUALITY_PROFILES = OrderedDict(sorted(list(AUDIO_QUALITY_PROFILES.items()), key=lambda k: k[0], reverse=True))
HW_DECODERS = [
+ 'd3d11va',
'dxva2',
'videotoolbox',
'mediacodecndk',
From d9b3b311b947ccf8430fadee3c051a49bda0750c Mon Sep 17 00:00:00 2001
From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
Date: Tue, 27 Jun 2023 14:23:02 -0700
Subject: [PATCH 02/21] Only initialize mako TemplateLookup once
* Ref: sqlalchemy/mako#378
---
plexpy/webserve.py | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/plexpy/webserve.py b/plexpy/webserve.py
index 88b65174..b893b661 100644
--- a/plexpy/webserve.py
+++ b/plexpy/webserve.py
@@ -119,12 +119,16 @@ else:
from plexpy import macos
-def serve_template(templatename, **kwargs):
- interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/')
- template_dir = os.path.join(str(interface_dir), plexpy.CONFIG.INTERFACE)
+TEMPLATE_LOOKUP = None
- _hplookup = TemplateLookup(directories=[template_dir], default_filters=['unicode', 'h'],
- error_handler=mako_error_handler)
+
+def serve_template(template_name, **kwargs):
+ global TEMPLATE_LOOKUP
+ if TEMPLATE_LOOKUP is None:
+ interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/')
+ template_dir = os.path.join(str(interface_dir), plexpy.CONFIG.INTERFACE)
+ TEMPLATE_LOOKUP = TemplateLookup(directories=[template_dir], default_filters=['unicode', 'h'],
+ error_handler=mako_error_handler)
http_root = plexpy.HTTP_ROOT
server_name = helpers.pms_name()
@@ -133,7 +137,7 @@ def serve_template(templatename, **kwargs):
_session = get_session_info()
try:
- template = _hplookup.get_template(templatename)
+ template = TEMPLATE_LOOKUP.get_template(template_name)
return template.render(http_root=http_root, server_name=server_name, cache_param=cache_param,
_session=_session, **kwargs)
except Exception as e:
From 7ff3abe8b73921ce5a5a05fd7a5821b1b6575869 Mon Sep 17 00:00:00 2001
From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
Date: Tue, 27 Jun 2023 17:20:21 -0700
Subject: [PATCH 03/21] Rename template_name argument
---
plexpy/newsletters.py | 8 +-
plexpy/webserve.py | 170 +++++++++++++++++++++---------------------
2 files changed, 89 insertions(+), 89 deletions(-)
diff --git a/plexpy/newsletters.py b/plexpy/newsletters.py
index 94f73c8f..661a2b42 100644
--- a/plexpy/newsletters.py
+++ b/plexpy/newsletters.py
@@ -318,7 +318,7 @@ def blacklist_logger():
logger.blacklist_config(email_config)
-def serve_template(templatename, **kwargs):
+def serve_template(template_name, **kwargs):
if plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR:
logger.info("Tautulli Newsletters :: Using custom newsletter template directory.")
template_dir = plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR
@@ -327,12 +327,12 @@ def serve_template(templatename, **kwargs):
template_dir = os.path.join(str(interface_dir), plexpy.CONFIG.NEWSLETTER_TEMPLATES)
if not plexpy.CONFIG.NEWSLETTER_INLINE_STYLES:
- templatename = templatename.replace('.html', '.internal.html')
+ template_name = template_name.replace('.html', '.internal.html')
_hplookup = TemplateLookup(directories=[template_dir], default_filters=['unicode', 'h'])
try:
- template = _hplookup.get_template(templatename)
+ template = _hplookup.get_template(template_name)
return template.render(**kwargs), False
except:
return exceptions.html_error_template().render(), True
@@ -477,7 +477,7 @@ class Newsletter(object):
logger.info("Tautulli Newsletters :: Generating newsletter%s." % (' preview' if self.is_preview else ''))
newsletter_rendered, self.template_error = serve_template(
- templatename=self._TEMPLATE,
+ template_name=self._TEMPLATE,
uuid=self.uuid,
subject=self.subject_formatted,
body=self.body_formatted,
diff --git a/plexpy/webserve.py b/plexpy/webserve.py
index b893b661..72eb9170 100644
--- a/plexpy/webserve.py
+++ b/plexpy/webserve.py
@@ -226,7 +226,7 @@ class WebInterface(object):
plexpy.initialize_scheduler()
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "home")
else:
- return serve_template(templatename="welcome.html", title="Welcome", config=config)
+ return serve_template(template_name="welcome.html", title="Welcome", config=config)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -291,7 +291,7 @@ class WebInterface(object):
"update_show_changelog": plexpy.CONFIG.UPDATE_SHOW_CHANGELOG,
"first_run_complete": plexpy.CONFIG.FIRST_RUN_COMPLETE
}
- return serve_template(templatename="index.html", title="Home", config=config)
+ return serve_template(template_name="index.html", title="Home", config=config)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -336,10 +336,10 @@ class WebInterface(object):
result = pms_connect.get_current_activity()
if result:
- return serve_template(templatename="current_activity.html", data=result)
+ return serve_template(template_name="current_activity.html", data=result)
else:
logger.warn("Unable to retrieve data for get_current_activity.")
- return serve_template(templatename="current_activity.html", data=None)
+ return serve_template(template_name="current_activity.html", data=None)
@cherrypy.expose
@requireAuth()
@@ -350,9 +350,9 @@ class WebInterface(object):
if result:
session = next((s for s in result['sessions'] if s['session_key'] == session_key), None)
- return serve_template(templatename="current_activity_instance.html", session=session)
+ return serve_template(template_name="current_activity_instance.html", session=session)
else:
- return serve_template(templatename="current_activity_instance.html", session=None)
+ return serve_template(template_name="current_activity_instance.html", session=None)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -395,7 +395,7 @@ class WebInterface(object):
endpoint = endpoint.format(machine_id=plexpy.CONFIG.PMS_IDENTIFIER)
url = base_url + endpoint + ('?' + urlencode(kwargs) if kwargs else '')
- return serve_template(templatename="xml_shortcut.html", title="Plex XML", url=url)
+ return serve_template(template_name="xml_shortcut.html", title="Plex XML", url=url)
@cherrypy.expose
@requireAuth()
@@ -405,7 +405,7 @@ class WebInterface(object):
stats_type=stats_type,
stats_count=stats_count)
- return serve_template(templatename="home_stats.html", title="Stats", data=stats_data)
+ return serve_template(template_name="home_stats.html", title="Stats", data=stats_data)
@cherrypy.expose
@requireAuth()
@@ -416,7 +416,7 @@ class WebInterface(object):
stats_data = data_factory.get_library_stats(library_cards=library_cards)
- return serve_template(templatename="library_stats.html", title="Library Stats", data=stats_data)
+ return serve_template(template_name="library_stats.html", title="Library Stats", data=stats_data)
@cherrypy.expose
@requireAuth()
@@ -426,13 +426,13 @@ class WebInterface(object):
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_recently_added_details(count=count, media_type=media_type)
except IOError as e:
- return serve_template(templatename="recently_added.html", data=None)
+ return serve_template(template_name="recently_added.html", data=None)
if result and 'recently_added' in result:
- return serve_template(templatename="recently_added.html", data=result['recently_added'])
+ return serve_template(template_name="recently_added.html", data=result['recently_added'])
else:
logger.warn("Unable to retrieve data for get_recently_added.")
- return serve_template(templatename="recently_added.html", data=None)
+ return serve_template(template_name="recently_added.html", data=None)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -467,7 +467,7 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth()
def libraries(self, **kwargs):
- return serve_template(templatename="libraries.html", title="Libraries")
+ return serve_template(template_name="libraries.html", title="Libraries")
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -620,12 +620,12 @@ class WebInterface(object):
library_details = library_data.get_details(section_id=section_id)
except:
logger.warn("Unable to retrieve library details for section_id %s " % section_id)
- return serve_template(templatename="library.html", title="Library", data=None, config=config)
+ return serve_template(template_name="library.html", title="Library", data=None, config=config)
else:
logger.debug("Library page requested but no section_id received.")
- return serve_template(templatename="library.html", title="Library", data=None, config=config)
+ return serve_template(template_name="library.html", title="Library", data=None, config=config)
- return serve_template(templatename="library.html", title="Library", data=library_details, config=config)
+ return serve_template(template_name="library.html", title="Library", data=library_details, config=config)
@cherrypy.expose
@requireAuth(member_of("admin"))
@@ -638,7 +638,7 @@ class WebInterface(object):
result = None
status_message = 'An error occured.'
- return serve_template(templatename="edit_library.html", title="Edit Library",
+ return serve_template(template_name="edit_library.html", title="Edit Library",
data=result, server_id=plexpy.CONFIG.PMS_IDENTIFIER, status_message=status_message)
@cherrypy.expose
@@ -685,7 +685,7 @@ class WebInterface(object):
@requireAuth()
def library_watch_time_stats(self, section_id=None, **kwargs):
if not allow_session_library(section_id):
- return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats")
+ return serve_template(template_name="user_watch_time_stats.html", data=None, title="Watch Stats")
if section_id:
library_data = libraries.Libraries()
@@ -694,16 +694,16 @@ class WebInterface(object):
result = None
if result:
- return serve_template(templatename="user_watch_time_stats.html", data=result, title="Watch Stats")
+ return serve_template(template_name="user_watch_time_stats.html", data=result, title="Watch Stats")
else:
logger.warn("Unable to retrieve data for library_watch_time_stats.")
- return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats")
+ return serve_template(template_name="user_watch_time_stats.html", data=None, title="Watch Stats")
@cherrypy.expose
@requireAuth()
def library_user_stats(self, section_id=None, **kwargs):
if not allow_session_library(section_id):
- return serve_template(templatename="library_user_stats.html", data=None, title="Player Stats")
+ return serve_template(template_name="library_user_stats.html", data=None, title="Player Stats")
if section_id:
library_data = libraries.Libraries()
@@ -712,16 +712,16 @@ class WebInterface(object):
result = None
if result:
- return serve_template(templatename="library_user_stats.html", data=result, title="Player Stats")
+ return serve_template(template_name="library_user_stats.html", data=result, title="Player Stats")
else:
logger.warn("Unable to retrieve data for library_user_stats.")
- return serve_template(templatename="library_user_stats.html", data=None, title="Player Stats")
+ return serve_template(template_name="library_user_stats.html", data=None, title="Player Stats")
@cherrypy.expose
@requireAuth()
def library_recently_watched(self, section_id=None, limit='10', **kwargs):
if not allow_session_library(section_id):
- return serve_template(templatename="user_recently_watched.html", data=None, title="Recently Watched")
+ return serve_template(template_name="user_recently_watched.html", data=None, title="Recently Watched")
if section_id:
library_data = libraries.Libraries()
@@ -730,16 +730,16 @@ class WebInterface(object):
result = None
if result:
- return serve_template(templatename="user_recently_watched.html", data=result, title="Recently Watched")
+ return serve_template(template_name="user_recently_watched.html", data=result, title="Recently Watched")
else:
logger.warn("Unable to retrieve data for library_recently_watched.")
- return serve_template(templatename="user_recently_watched.html", data=None, title="Recently Watched")
+ return serve_template(template_name="user_recently_watched.html", data=None, title="Recently Watched")
@cherrypy.expose
@requireAuth()
def library_recently_added(self, section_id=None, limit='10', **kwargs):
if not allow_session_library(section_id):
- return serve_template(templatename="library_recently_added.html", data=None, title="Recently Added")
+ return serve_template(template_name="library_recently_added.html", data=None, title="Recently Added")
if section_id:
pms_connect = pmsconnect.PmsConnect()
@@ -748,10 +748,10 @@ class WebInterface(object):
result = None
if result and result['recently_added']:
- return serve_template(templatename="library_recently_added.html", data=result['recently_added'], title="Recently Added")
+ return serve_template(template_name="library_recently_added.html", data=result['recently_added'], title="Recently Added")
else:
logger.warn("Unable to retrieve data for library_recently_added.")
- return serve_template(templatename="library_recently_added.html", data=None, title="Recently Added")
+ return serve_template(template_name="library_recently_added.html", data=None, title="Recently Added")
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -1243,7 +1243,7 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth()
def users(self, **kwargs):
- return serve_template(templatename="users.html", title="Users")
+ return serve_template(template_name="users.html", title="Users")
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -1359,12 +1359,12 @@ class WebInterface(object):
user_details = user_data.get_details(user_id=user_id)
except:
logger.warn("Unable to retrieve user details for user_id %s " % user_id)
- return serve_template(templatename="user.html", title="User", data=None)
+ return serve_template(template_name="user.html", title="User", data=None)
else:
logger.debug("User page requested but no user_id received.")
- return serve_template(templatename="user.html", title="User", data=None)
+ return serve_template(template_name="user.html", title="User", data=None)
- return serve_template(templatename="user.html", title="User", data=user_details)
+ return serve_template(template_name="user.html", title="User", data=user_details)
@cherrypy.expose
@requireAuth(member_of("admin"))
@@ -1377,7 +1377,7 @@ class WebInterface(object):
result = None
status_message = 'An error occured.'
- return serve_template(templatename="edit_user.html", title="Edit User", data=result, status_message=status_message)
+ return serve_template(template_name="edit_user.html", title="Edit User", data=result, status_message=status_message)
@cherrypy.expose
@requireAuth(member_of("admin"))
@@ -1425,7 +1425,7 @@ class WebInterface(object):
@requireAuth()
def user_watch_time_stats(self, user=None, user_id=None, **kwargs):
if not allow_session_user(user_id):
- return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats")
+ return serve_template(template_name="user_watch_time_stats.html", data=None, title="Watch Stats")
if user_id or user:
user_data = users.Users()
@@ -1434,16 +1434,16 @@ class WebInterface(object):
result = None
if result:
- return serve_template(templatename="user_watch_time_stats.html", data=result, title="Watch Stats")
+ return serve_template(template_name="user_watch_time_stats.html", data=result, title="Watch Stats")
else:
logger.warn("Unable to retrieve data for user_watch_time_stats.")
- return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats")
+ return serve_template(template_name="user_watch_time_stats.html", data=None, title="Watch Stats")
@cherrypy.expose
@requireAuth()
def user_player_stats(self, user=None, user_id=None, **kwargs):
if not allow_session_user(user_id):
- return serve_template(templatename="user_player_stats.html", data=None, title="Player Stats")
+ return serve_template(template_name="user_player_stats.html", data=None, title="Player Stats")
if user_id or user:
user_data = users.Users()
@@ -1452,16 +1452,16 @@ class WebInterface(object):
result = None
if result:
- return serve_template(templatename="user_player_stats.html", data=result, title="Player Stats")
+ return serve_template(template_name="user_player_stats.html", data=result, title="Player Stats")
else:
logger.warn("Unable to retrieve data for user_player_stats.")
- return serve_template(templatename="user_player_stats.html", data=None, title="Player Stats")
+ return serve_template(template_name="user_player_stats.html", data=None, title="Player Stats")
@cherrypy.expose
@requireAuth()
def get_user_recently_watched(self, user=None, user_id=None, limit='10', **kwargs):
if not allow_session_user(user_id):
- return serve_template(templatename="user_recently_watched.html", data=None, title="Recently Watched")
+ return serve_template(template_name="user_recently_watched.html", data=None, title="Recently Watched")
if user_id or user:
user_data = users.Users()
@@ -1470,10 +1470,10 @@ class WebInterface(object):
result = None
if result:
- return serve_template(templatename="user_recently_watched.html", data=result, title="Recently Watched")
+ return serve_template(template_name="user_recently_watched.html", data=result, title="Recently Watched")
else:
logger.warn("Unable to retrieve data for get_user_recently_watched.")
- return serve_template(templatename="user_recently_watched.html", data=None, title="Recently Watched")
+ return serve_template(template_name="user_recently_watched.html", data=None, title="Recently Watched")
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -1879,7 +1879,7 @@ class WebInterface(object):
"database_is_importing": database.IS_IMPORTING,
}
- return serve_template(templatename="history.html", title="History", config=config)
+ return serve_template(template_name="history.html", title="History", config=config)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -2067,7 +2067,7 @@ class WebInterface(object):
data_factory = datafactory.DataFactory()
stream_data = data_factory.get_stream_details(row_id, session_key)
- return serve_template(templatename="stream_data.html", title="Stream Data", data=stream_data, user=user)
+ return serve_template(template_name="stream_data.html", title="Stream Data", data=stream_data, user=user)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -2158,7 +2158,7 @@ class WebInterface(object):
public = helpers.is_public_ip(ip_address)
- return serve_template(templatename="ip_address_modal.html", title="IP Address Details",
+ return serve_template(template_name="ip_address_modal.html", title="IP Address Details",
data=ip_address, public=public, kwargs=kwargs)
@cherrypy.expose
@@ -2197,7 +2197,7 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth()
def graphs(self, **kwargs):
- return serve_template(templatename="graphs.html", title="Graphs")
+ return serve_template(template_name="graphs.html", title="Graphs")
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -2711,9 +2711,9 @@ class WebInterface(object):
@requireAuth()
def history_table_modal(self, **kwargs):
if kwargs.get('user_id') and not allow_session_user(kwargs['user_id']):
- return serve_template(templatename="history_table_modal.html", title="History Data", data=None)
+ return serve_template(template_name="history_table_modal.html", title="History Data", data=None)
- return serve_template(templatename="history_table_modal.html", title="History Data", data=kwargs)
+ return serve_template(template_name="history_table_modal.html", title="History Data", data=kwargs)
##### Sync #####
@@ -2721,7 +2721,7 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth()
def sync(self, **kwargs):
- return serve_template(templatename="sync.html", title="Synced Items")
+ return serve_template(template_name="sync.html", title="Synced Items")
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -2780,7 +2780,7 @@ class WebInterface(object):
@requireAuth(member_of("admin"))
def logs(self, **kwargs):
plex_log_files = log_reader.list_plex_logs()
- return serve_template(templatename="logs.html", title="Log", plex_log_files=plex_log_files)
+ return serve_template(template_name="logs.html", title="Log", plex_log_files=plex_log_files)
@cherrypy.expose
@requireAuth(member_of("admin"))
@@ -3174,7 +3174,7 @@ class WebInterface(object):
for key in ('home_sections', 'home_stats_cards', 'home_library_cards'):
settings_dict[key] = json.dumps(settings_dict[key])
- return serve_template(templatename="settings.html", title="Settings", config=settings_dict)
+ return serve_template(template_name="settings.html", title="Settings", config=settings_dict)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -3365,17 +3365,17 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth(member_of("admin"))
def get_configuration_table(self, **kwargs):
- return serve_template(templatename="configuration_table.html")
+ return serve_template(template_name="configuration_table.html")
@cherrypy.expose
@requireAuth(member_of("admin"))
def get_scheduler_table(self, **kwargs):
- return serve_template(templatename="scheduler_table.html")
+ return serve_template(template_name="scheduler_table.html")
@cherrypy.expose
@requireAuth(member_of("admin"))
def get_queue_modal(self, queue=None, **kwargs):
- return serve_template(templatename="queue_modal.html", queue=queue)
+ return serve_template(template_name="queue_modal.html", queue=queue)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -3439,7 +3439,7 @@ class WebInterface(object):
@requireAuth(member_of("admin"))
def get_notifiers_table(self, **kwargs):
result = notifiers.get_notifiers()
- return serve_template(templatename="notifiers_table.html", notifiers_list=result)
+ return serve_template(template_name="notifiers_table.html", notifiers_list=result)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -3522,7 +3522,7 @@ class WebInterface(object):
for category in common.NOTIFICATION_PARAMETERS for param in category['parameters']
]
- return serve_template(templatename="notifier_config.html", notifier=result, parameters=parameters)
+ return serve_template(template_name="notifier_config.html", notifier=result, parameters=parameters)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -3605,7 +3605,7 @@ class WebInterface(object):
text.append({'media_type': media_type, 'subject': test_subject, 'body': test_body})
- return serve_template(templatename="notifier_text_preview.html", text=text, agent=agent_name)
+ return serve_template(template_name="notifier_text_preview.html", text=text, agent=agent_name)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -3783,7 +3783,7 @@ class WebInterface(object):
@requireAuth(member_of("admin"))
def get_mobile_devices_table(self, **kwargs):
result = mobile_app.get_mobile_devices()
- return serve_template(templatename="mobile_devices_table.html", devices_list=result)
+ return serve_template(template_name="mobile_devices_table.html", devices_list=result)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -3806,7 +3806,7 @@ class WebInterface(object):
def get_mobile_device_config_modal(self, mobile_device_id=None, **kwargs):
result = mobile_app.get_mobile_device_config(mobile_device_id=mobile_device_id)
- return serve_template(templatename="mobile_device_config.html", device=result)
+ return serve_template(template_name="mobile_device_config.html", device=result)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -4016,11 +4016,11 @@ class WebInterface(object):
@requireAuth(member_of("admin"))
def import_database_tool(self, app=None, **kwargs):
if app == 'tautulli':
- return serve_template(templatename="app_import.html", title="Import Tautulli Database", app="Tautulli")
+ return serve_template(template_name="app_import.html", title="Import Tautulli Database", app="Tautulli")
elif app == 'plexwatch':
- return serve_template(templatename="app_import.html", title="Import PlexWatch Database", app="PlexWatch")
+ return serve_template(template_name="app_import.html", title="Import PlexWatch Database", app="PlexWatch")
elif app == 'plexivity':
- return serve_template(templatename="app_import.html", title="Import Plexivity Database", app="Plexivity")
+ return serve_template(template_name="app_import.html", title="Import Plexivity Database", app="Plexivity")
logger.warn("No app specified for import.")
return
@@ -4028,7 +4028,7 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth(member_of("admin"))
def import_config_tool(self, **kwargs):
- return serve_template(templatename="config_import.html", title="Import Tautulli Configuration")
+ return serve_template(template_name="config_import.html", title="Import Tautulli Configuration")
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -4301,7 +4301,7 @@ class WebInterface(object):
else:
new_http_root = '/'
- return serve_template(templatename="shutdown.html", signal=signal, title=title,
+ return serve_template(template_name="shutdown.html", signal=signal, title=title,
new_http_root=new_http_root, message=message, timer=timer, quote=quote)
@cherrypy.expose
@@ -4411,7 +4411,7 @@ class WebInterface(object):
if metadata['section_id'] and not allow_session_library(metadata['section_id']):
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
- return serve_template(templatename="info.html", metadata=metadata, title="Info",
+ return serve_template(template_name="info.html", metadata=metadata, title="Info",
config=config, source=source, user_info=user_info)
else:
if get_session_user_id():
@@ -4427,11 +4427,11 @@ class WebInterface(object):
result = pms_connect.get_item_children(rating_key=rating_key, media_type=media_type)
if result:
- return serve_template(templatename="info_children_list.html", data=result,
+ return serve_template(template_name="info_children_list.html", data=result,
media_type=media_type, title="Children List")
else:
logger.warn("Unable to retrieve data for get_item_children.")
- return serve_template(templatename="info_children_list.html", data=None, title="Children List")
+ return serve_template(template_name="info_children_list.html", data=None, title="Children List")
@cherrypy.expose
@requireAuth()
@@ -4441,9 +4441,9 @@ class WebInterface(object):
result = pms_connect.get_item_children_related(rating_key=rating_key)
if result:
- return serve_template(templatename="info_collection_list.html", data=result, title=title)
+ return serve_template(template_name="info_collection_list.html", data=result, title=title)
else:
- return serve_template(templatename="info_collection_list.html", data=None, title=title)
+ return serve_template(template_name="info_collection_list.html", data=None, title=title)
@cherrypy.expose
@requireAuth()
@@ -4455,10 +4455,10 @@ class WebInterface(object):
result = None
if result:
- return serve_template(templatename="user_watch_time_stats.html", data=result, title="Watch Stats")
+ return serve_template(template_name="user_watch_time_stats.html", data=result, title="Watch Stats")
else:
logger.warn("Unable to retrieve data for item_watch_time_stats.")
- return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats")
+ return serve_template(template_name="user_watch_time_stats.html", data=None, title="Watch Stats")
@cherrypy.expose
@requireAuth()
@@ -4470,10 +4470,10 @@ class WebInterface(object):
result = None
if result:
- return serve_template(templatename="library_user_stats.html", data=result, title="Player Stats")
+ return serve_template(template_name="library_user_stats.html", data=result, title="Player Stats")
else:
logger.warn("Unable to retrieve data for item_user_stats.")
- return serve_template(templatename="library_user_stats.html", data=None, title="Player Stats")
+ return serve_template(template_name="library_user_stats.html", data=None, title="Player Stats")
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -5095,7 +5095,7 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth()
def search(self, query='', **kwargs):
- return serve_template(templatename="search.html", title="Search", query=query)
+ return serve_template(template_name="search.html", title="Search", query=query)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -5152,10 +5152,10 @@ class WebInterface(object):
if season['media_index'] == season_index]
if result:
- return serve_template(templatename="info_search_results_list.html", data=result, title="Search Result List")
+ return serve_template(template_name="info_search_results_list.html", data=result, title="Search Result List")
else:
logger.warn("Unable to retrieve data for get_search_results_children.")
- return serve_template(templatename="info_search_results_list.html", data=None, title="Search Result List")
+ return serve_template(template_name="info_search_results_list.html", data=None, title="Search Result List")
##### Update Metadata #####
@@ -5172,10 +5172,10 @@ class WebInterface(object):
query['query_string'] = query_string
if query:
- return serve_template(templatename="update_metadata.html", query=query, update=update, title="Info")
+ return serve_template(template_name="update_metadata.html", query=query, update=update, title="Info")
else:
logger.warn("Unable to retrieve data for update_metadata.")
- return serve_template(templatename="update_metadata.html", query=query, update=update, title="Info")
+ return serve_template(template_name="update_metadata.html", query=query, update=update, title="Info")
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -6578,7 +6578,7 @@ class WebInterface(object):
@requireAuth(member_of("admin"))
def get_newsletters_table(self, **kwargs):
result = newsletters.get_newsletters()
- return serve_template(templatename="newsletters_table.html", newsletters_list=result)
+ return serve_template(template_name="newsletters_table.html", newsletters_list=result)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -6653,7 +6653,7 @@ class WebInterface(object):
@requireAuth(member_of("admin"))
def get_newsletter_config_modal(self, newsletter_id=None, **kwargs):
result = newsletters.get_newsletter_config(newsletter_id=newsletter_id, mask_passwords=True)
- return serve_template(templatename="newsletter_config.html", newsletter=result)
+ return serve_template(template_name="newsletter_config.html", newsletter=result)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -6761,7 +6761,7 @@ class WebInterface(object):
elif kwargs.pop('key', None) == plexpy.CONFIG.NEWSLETTER_PASSWORD:
return self.newsletter_auth(*args, **kwargs)
else:
- return serve_template(templatename="newsletter_auth.html",
+ return serve_template(template_name="newsletter_auth.html",
title="Newsletter Login",
uri=request_uri)
@@ -6798,7 +6798,7 @@ class WebInterface(object):
@requireAuth(member_of("admin"))
def newsletter_preview(self, **kwargs):
kwargs['preview'] = 'true'
- return serve_template(templatename="newsletter_preview.html",
+ return serve_template(template_name="newsletter_preview.html",
title="Newsletter",
kwargs=kwargs)
@@ -6837,7 +6837,7 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth(member_of("admin"))
def support(self, **kwargs):
- return serve_template(templatename="support.html", title="Support")
+ return serve_template(template_name="support.html", title="Support")
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -6992,7 +6992,7 @@ class WebInterface(object):
if media_type == 'photo_album':
media_type = 'photoalbum'
- return serve_template(templatename="export_modal.html", title="Export Metadata",
+ return serve_template(template_name="export_modal.html", title="Export Metadata",
section_id=section_id, user_id=user_id, rating_key=rating_key,
media_type=media_type, sub_media_type=sub_media_type,
export_type=export_type, file_formats=file_formats)
From c761e6e8d09491886bcfc8b056da0abcb70b401d Mon Sep 17 00:00:00 2001
From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
Date: Thu, 29 Jun 2023 00:07:43 -0700
Subject: [PATCH 04/21] Fix `template_name` argument for login page
---
plexpy/webauth.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plexpy/webauth.py b/plexpy/webauth.py
index d105a8c2..5487f2ea 100644
--- a/plexpy/webauth.py
+++ b/plexpy/webauth.py
@@ -314,7 +314,7 @@ class AuthController(object):
def get_loginform(self, redirect_uri=''):
from plexpy.webserve import serve_template
- return serve_template(templatename="login.html", title="Login", redirect_uri=unquote(redirect_uri))
+ return serve_template(template_name="login.html", title="Login", redirect_uri=unquote(redirect_uri))
@cherrypy.expose
def index(self, *args, **kwargs):
From 085cfa4bef90e7c3847a51789f2c18ed174c5bee Mon Sep 17 00:00:00 2001
From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
Date: Fri, 7 Jul 2023 11:51:54 -0700
Subject: [PATCH 05/21] Fix history grouping incorrect for watched content
---
plexpy/activity_processor.py | 21 +++++++++++----------
1 file changed, 11 insertions(+), 10 deletions(-)
diff --git a/plexpy/activity_processor.py b/plexpy/activity_processor.py
index 588e91ce..2d752104 100644
--- a/plexpy/activity_processor.py
+++ b/plexpy/activity_processor.py
@@ -1,4 +1,4 @@
-# This file is part of Tautulli.
+# This file is part of Tautulli.
#
# Tautulli is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -327,7 +327,7 @@ class ActivityProcessor(object):
# Get the last insert row id
last_id = self.db.last_insert_id()
new_session = prev_session = None
- watched = False
+ prev_watched = None
if session['live']:
# Check if we should group the session, select the last guid from the user
@@ -370,24 +370,25 @@ class ActivityProcessor(object):
'reference_id': result[1]['reference_id']}
marker_first, marker_final = helpers.get_first_final_marker(metadata['markers'])
- watched = helpers.check_watched(
- session['media_type'], session['view_offset'], session['duration'],
+ prev_watched = helpers.check_watched(
+ session['media_type'], prev_session['view_offset'], session['duration'],
marker_first, marker_final
)
query = "UPDATE session_history SET reference_id = ? WHERE id = ? "
- # If previous session view offset less than watched percent,
+ # If previous session view offset less than watched threshold,
# and new session view offset is greater,
# then set the reference_id to the previous row,
# else set the reference_id to the new id
- if prev_session is None and new_session is None:
- args = [last_id, last_id]
- elif watched and prev_session['view_offset'] <= new_session['view_offset'] or \
- session['live'] and prev_session['guid'] == new_session['guid']:
+ if (prev_watched is False and prev_session['view_offset'] <= new_session['view_offset'] or
+ session['live'] and prev_session['guid'] == new_session['guid']):
+ logger.debug("Tautulli ActivityProcessor :: Grouping history for sessionKey %s", session['session_key'])
args = [prev_session['reference_id'], new_session['id']]
+
else:
- args = [new_session['id'], new_session['id']]
+ logger.debug("Tautulli ActivityProcessor :: Not grouping history for sessionKey %s", session['session_key'])
+ args = [last_id, last_id]
self.db.action(query=query, args=args)
From 1fe6d1505f5524c2706934093b77034b00c94374 Mon Sep 17 00:00:00 2001
From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
Date: Fri, 7 Jul 2023 13:02:45 -0700
Subject: [PATCH 06/21] Add method to regroup history
---
plexpy/activity_processor.py | 159 ++++++++++++++++++++---------------
1 file changed, 93 insertions(+), 66 deletions(-)
diff --git a/plexpy/activity_processor.py b/plexpy/activity_processor.py
index 2d752104..6851263b 100644
--- a/plexpy/activity_processor.py
+++ b/plexpy/activity_processor.py
@@ -1,4 +1,4 @@
-# This file is part of Tautulli.
+# This file is part of Tautulli.
#
# Tautulli is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -326,71 +326,7 @@ class ActivityProcessor(object):
# Get the last insert row id
last_id = self.db.last_insert_id()
- new_session = prev_session = None
- prev_watched = None
-
- if session['live']:
- # Check if we should group the session, select the last guid from the user
- query = "SELECT session_history.id, session_history_metadata.guid, session_history.reference_id " \
- "FROM session_history " \
- "JOIN session_history_metadata ON session_history.id == session_history_metadata.id " \
- "WHERE session_history.user_id = ? ORDER BY session_history.id DESC LIMIT 1 "
-
- args = [session['user_id']]
-
- result = self.db.select(query=query, args=args)
-
- if len(result) > 0:
- new_session = {'id': last_id,
- 'guid': metadata['guid'],
- 'reference_id': last_id}
-
- prev_session = {'id': result[0]['id'],
- 'guid': result[0]['guid'],
- 'reference_id': result[0]['reference_id']}
-
- else:
- # Check if we should group the session, select the last two rows from the user
- query = "SELECT id, rating_key, view_offset, reference_id FROM session_history " \
- "WHERE user_id = ? AND rating_key = ? ORDER BY id DESC LIMIT 2 "
-
- args = [session['user_id'], session['rating_key']]
-
- result = self.db.select(query=query, args=args)
-
- if len(result) > 1:
- new_session = {'id': result[0]['id'],
- 'rating_key': result[0]['rating_key'],
- 'view_offset': result[0]['view_offset'],
- 'reference_id': result[0]['reference_id']}
-
- prev_session = {'id': result[1]['id'],
- 'rating_key': result[1]['rating_key'],
- 'view_offset': result[1]['view_offset'],
- 'reference_id': result[1]['reference_id']}
-
- marker_first, marker_final = helpers.get_first_final_marker(metadata['markers'])
- prev_watched = helpers.check_watched(
- session['media_type'], prev_session['view_offset'], session['duration'],
- marker_first, marker_final
- )
-
- query = "UPDATE session_history SET reference_id = ? WHERE id = ? "
-
- # If previous session view offset less than watched threshold,
- # and new session view offset is greater,
- # then set the reference_id to the previous row,
- # else set the reference_id to the new id
- if (prev_watched is False and prev_session['view_offset'] <= new_session['view_offset'] or
- session['live'] and prev_session['guid'] == new_session['guid']):
- logger.debug("Tautulli ActivityProcessor :: Grouping history for sessionKey %s", session['session_key'])
- args = [prev_session['reference_id'], new_session['id']]
-
- else:
- logger.debug("Tautulli ActivityProcessor :: Not grouping history for sessionKey %s", session['session_key'])
- args = [last_id, last_id]
-
- self.db.action(query=query, args=args)
+ self.group_history(last_id, session, metadata)
# logger.debug("Tautulli ActivityProcessor :: Successfully written history item, last id for session_history is %s"
# % last_id)
@@ -547,6 +483,80 @@ class ActivityProcessor(object):
# Return the session row id when the session is successfully written to the database
return session['id']
+ def group_history(self, last_id, session, metadata=None):
+ new_session = prev_session = None
+ prev_watched = None
+
+ if session['live']:
+ # Check if we should group the session, select the last guid from the user
+ query = "SELECT session_history.id, session_history_metadata.guid, session_history.reference_id " \
+ "FROM session_history " \
+ "JOIN session_history_metadata ON session_history.id == session_history_metadata.id " \
+ "WHERE session_history.id <= ? AND session_history.user_id = ? ORDER BY session_history.id DESC LIMIT 1 "
+
+ args = [last_id, session['user_id']]
+
+ result = self.db.select(query=query, args=args)
+
+ if len(result) > 0:
+ new_session = {'id': last_id,
+ 'guid': metadata['guid'] if metadata else session['guid'],
+ 'reference_id': last_id}
+
+ prev_session = {'id': result[0]['id'],
+ 'guid': result[0]['guid'],
+ 'reference_id': result[0]['reference_id']}
+
+ else:
+ # Check if we should group the session, select the last two rows from the user
+ query = "SELECT id, rating_key, view_offset, reference_id FROM session_history " \
+ "WHERE id <= ? AND user_id = ? AND rating_key = ? ORDER BY id DESC LIMIT 2 "
+
+ args = [last_id, session['user_id'], session['rating_key']]
+
+ result = self.db.select(query=query, args=args)
+
+ if len(result) > 1:
+ new_session = {'id': result[0]['id'],
+ 'rating_key': result[0]['rating_key'],
+ 'view_offset': result[0]['view_offset'],
+ 'reference_id': result[0]['reference_id']}
+
+ prev_session = {'id': result[1]['id'],
+ 'rating_key': result[1]['rating_key'],
+ 'view_offset': result[1]['view_offset'],
+ 'reference_id': result[1]['reference_id']}
+
+ if metadata:
+ marker_first, marker_final = helpers.get_first_final_marker(metadata['markers'])
+ else:
+ marker_first = session['marker_credits_first']
+ marker_final = session['marker_credits_final']
+
+ prev_watched = helpers.check_watched(
+ session['media_type'], prev_session['view_offset'], session['duration'],
+ marker_first, marker_final
+ )
+
+ query = "UPDATE session_history SET reference_id = ? WHERE id = ? "
+
+ # If previous session view offset less than watched threshold,
+ # and new session view offset is greater,
+ # then set the reference_id to the previous row,
+ # else set the reference_id to the new id
+ if (prev_watched is False and prev_session['view_offset'] <= new_session['view_offset'] or
+ session['live'] and prev_session['guid'] == new_session['guid']):
+ if metadata:
+ logger.debug("Tautulli ActivityProcessor :: Grouping history for sessionKey %s", session['session_key'])
+ args = [prev_session['reference_id'], new_session['id']]
+
+ else:
+ if metadata:
+ logger.debug("Tautulli ActivityProcessor :: Not grouping history for sessionKey %s", session['session_key'])
+ args = [last_id, last_id]
+
+ self.db.action(query=query, args=args)
+
def get_sessions(self, user_id=None, ip_address=None):
query = "SELECT * FROM sessions"
args = []
@@ -696,3 +706,20 @@ class ActivityProcessor(object):
"ORDER BY stopped DESC",
[user_id, machine_id, media_type])
return int(started - last_session.get('stopped', 0) >= plexpy.CONFIG.NOTIFY_CONTINUED_SESSION_THRESHOLD)
+
+ def regroup_history(self):
+ logger.info("Tautulli ActivityProcessor :: Creating database backup...")
+ database.make_backup()
+
+ logger.info("Tautulli ActivityProcessor :: Regrouping session history...")
+
+ query = (
+ "SELECT * FROM session_history "
+ "JOIN session_history_metadata ON session_history.id = session_history_metadata.id"
+ )
+ results = self.db.select(query)
+
+ for session in results:
+ self.group_history(session['id'], session)
+
+ logger.info("Tautulli ActivityProcessor :: Regrouping session history complete.")
From b144e6527fabb13cad9eb90bac49985f8dc89ac8 Mon Sep 17 00:00:00 2001
From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
Date: Fri, 7 Jul 2023 13:26:11 -0700
Subject: [PATCH 07/21] Add button to regroup play history
---
data/interfaces/default/settings.html | 31 +++++++++++++++++++++------
plexpy/activity_processor.py | 10 +++++++--
plexpy/webserve.py | 16 ++++++++++++++
3 files changed, 49 insertions(+), 8 deletions(-)
diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html
index fd234da2..7cd614e0 100644
--- a/data/interfaces/default/settings.html
+++ b/data/interfaces/default/settings.html
@@ -132,12 +132,6 @@
Change the "Play by day of week" graph to start on Monday. Default is start on Sunday.
-
-
-
Group play history for the same item and user as a single entry when progress is less than the watched percent.
-
Decide whether to use end credits markers to determine the 'watched' state of video items. When markers are not available the selected threshold percentage will be used.
+
+
+
Group play history for the same item and user as a single entry when progress is less than the watched percent.
+
+
+
+
+ Fix grouping of play history in the database.
+
+
+
+
+
+
+
+
+
@@ -2484,6 +2497,12 @@ $(document).ready(function() {
confirmAjaxCall(url, msg);
});
+ $("#regroup_history").click(function () {
+ var msg = 'Are you sure you want to regroup play history in the database?';
+ var url = 'regroup_history';
+ confirmAjaxCall(url, msg, null, 'Regrouping play history...');
+ });
+
$("#delete_temp_sessions").click(function () {
var msg = 'Are you sure you want to flush the temporary sessions?
This will reset all currently active sessions.';
var url = 'delete_temp_sessions';
diff --git a/plexpy/activity_processor.py b/plexpy/activity_processor.py
index 6851263b..b1558a56 100644
--- a/plexpy/activity_processor.py
+++ b/plexpy/activity_processor.py
@@ -709,7 +709,8 @@ class ActivityProcessor(object):
def regroup_history(self):
logger.info("Tautulli ActivityProcessor :: Creating database backup...")
- database.make_backup()
+ if not database.make_backup():
+ return False
logger.info("Tautulli ActivityProcessor :: Regrouping session history...")
@@ -720,6 +721,11 @@ class ActivityProcessor(object):
results = self.db.select(query)
for session in results:
- self.group_history(session['id'], session)
+ try:
+ self.group_history(session['id'], session)
+ except Exception as e:
+ logger.error("Tautulli ActivityProcessor :: Error regrouping session history: %s", e)
+ return False
logger.info("Tautulli ActivityProcessor :: Regrouping session history complete.")
+ return True
diff --git a/plexpy/webserve.py b/plexpy/webserve.py
index 72eb9170..06a9a802 100644
--- a/plexpy/webserve.py
+++ b/plexpy/webserve.py
@@ -51,6 +51,7 @@ if sys.version_info >= (3, 6):
import plexpy
if plexpy.PYTHON2:
import activity_pinger
+ import activity_processor
import common
import config
import database
@@ -85,6 +86,7 @@ if plexpy.PYTHON2:
import macos
else:
from plexpy import activity_pinger
+ from plexpy import activity_processor
from plexpy import common
from plexpy import config
from plexpy import database
@@ -434,6 +436,20 @@ class WebInterface(object):
logger.warn("Unable to retrieve data for get_recently_added.")
return serve_template(template_name="recently_added.html", data=None)
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ @requireAuth(member_of("admin"))
+ @addtoapi()
+ def regroup_history(self, **kwargs):
+ """ Regroup play history in the database."""
+
+ result = activity_processor.ActivityProcessor().regroup_history()
+
+ if result:
+ return {'result': 'success', 'message': 'Regrouped play history.'}
+ else:
+ return {'result': 'error', 'message': 'Regrouping play history failed.'}
+
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
From 343a3e928169843d458b435a81637b736b5261e6 Mon Sep 17 00:00:00 2001
From: Tom Niget
Date: Sat, 8 Jul 2023 02:15:16 +0200
Subject: [PATCH 08/21] Multiselect user filters (#2090)
* Extract user filter generation code into method
* Extend make_user_cond to allow lists of user IDs
* Update documentation for stats APIs to indicate handling of ID lists
* Use multiselect dropdown for user filter on graphs page
Use standard concatenation
Fix select style
Move settings to JS constructor
Change text for no users checked
Don't call selectAll on page init
Add it back
Remove attributes
Fix emptiness check
Allow deselect all
Only refresh if user id changed
* Show "N users" starting at 2 users
Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
* Use helper function split_strip
Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
* Move make_user_cond at bottom and make private
* Add new user picker to history page
* Fix copy-paste error
* Again
* Add CSS for bootstrap-select
---------
Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
---
.../default/css/bootstrap-select.min.css | 6 ++
data/interfaces/default/css/tautulli.css | 2 +-
data/interfaces/default/graphs.html | 47 ++++++++---
data/interfaces/default/history.html | 42 ++++++++--
.../default/js/bootstrap-select.min.js | 9 +++
plexpy/graphs.py | 79 ++++++-------------
plexpy/webserve.py | 22 +++---
7 files changed, 125 insertions(+), 82 deletions(-)
create mode 100644 data/interfaces/default/css/bootstrap-select.min.css
create mode 100644 data/interfaces/default/js/bootstrap-select.min.js
diff --git a/data/interfaces/default/css/bootstrap-select.min.css b/data/interfaces/default/css/bootstrap-select.min.css
new file mode 100644
index 00000000..d22faa63
--- /dev/null
+++ b/data/interfaces/default/css/bootstrap-select.min.css
@@ -0,0 +1,6 @@
+/*!
+ * Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select)
+ *
+ * Copyright 2012-2020 SnapAppointments, LLC
+ * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
+ */@-webkit-keyframes bs-notify-fadeOut{0%{opacity:.9}100%{opacity:0}}@-o-keyframes bs-notify-fadeOut{0%{opacity:.9}100%{opacity:0}}@keyframes bs-notify-fadeOut{0%{opacity:.9}100%{opacity:0}}.bootstrap-select>select.bs-select-hidden,select.bs-select-hidden,select.selectpicker{display:none!important}.bootstrap-select{width:220px\0;vertical-align:middle}.bootstrap-select>.dropdown-toggle{position:relative;width:100%;text-align:right;white-space:nowrap;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.bootstrap-select>.dropdown-toggle:after{margin-top:-1px}.bootstrap-select>.dropdown-toggle.bs-placeholder,.bootstrap-select>.dropdown-toggle.bs-placeholder:active,.bootstrap-select>.dropdown-toggle.bs-placeholder:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder:hover{color:#999}.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:hover{color:rgba(255,255,255,.5)}.bootstrap-select>select{position:absolute!important;bottom:0;left:50%;display:block!important;width:.5px!important;height:100%!important;padding:0!important;opacity:0!important;border:none;z-index:0!important}.bootstrap-select>select.mobile-device{top:0;left:0;display:block!important;width:100%!important;z-index:2!important}.bootstrap-select.is-invalid .dropdown-toggle,.error .bootstrap-select .dropdown-toggle,.has-error .bootstrap-select .dropdown-toggle,.was-validated .bootstrap-select select:invalid+.dropdown-toggle{border-color:#b94a48}.bootstrap-select.is-valid .dropdown-toggle,.was-validated .bootstrap-select select:valid+.dropdown-toggle{border-color:#28a745}.bootstrap-select.fit-width{width:auto!important}.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn){width:220px}.bootstrap-select .dropdown-toggle:focus,.bootstrap-select>select.mobile-device:focus+.dropdown-toggle{outline:thin dotted #333!important;outline:5px auto -webkit-focus-ring-color!important;outline-offset:-2px}.bootstrap-select.form-control{margin-bottom:0;padding:0;border:none;height:auto}:not(.input-group)>.bootstrap-select.form-control:not([class*=col-]){width:100%}.bootstrap-select.form-control.input-group-btn{float:none;z-index:auto}.form-inline .bootstrap-select,.form-inline .bootstrap-select.form-control:not([class*=col-]){width:auto}.bootstrap-select:not(.input-group-btn),.bootstrap-select[class*=col-]{float:none;display:inline-block;margin-left:0}.bootstrap-select.dropdown-menu-right,.bootstrap-select[class*=col-].dropdown-menu-right,.row .bootstrap-select[class*=col-].dropdown-menu-right{float:right}.form-group .bootstrap-select,.form-horizontal .bootstrap-select,.form-inline .bootstrap-select{margin-bottom:0}.form-group-lg .bootstrap-select.form-control,.form-group-sm .bootstrap-select.form-control{padding:0}.form-group-lg .bootstrap-select.form-control .dropdown-toggle,.form-group-sm .bootstrap-select.form-control .dropdown-toggle{height:100%;font-size:inherit;line-height:inherit;border-radius:inherit}.bootstrap-select.form-control-lg .dropdown-toggle,.bootstrap-select.form-control-sm .dropdown-toggle{font-size:inherit;line-height:inherit;border-radius:inherit}.bootstrap-select.form-control-sm .dropdown-toggle{padding:.25rem .5rem}.bootstrap-select.form-control-lg .dropdown-toggle{padding:.5rem 1rem}.form-inline .bootstrap-select .form-control{width:100%}.bootstrap-select.disabled,.bootstrap-select>.disabled{cursor:not-allowed}.bootstrap-select.disabled:focus,.bootstrap-select>.disabled:focus{outline:0!important}.bootstrap-select.bs-container{position:absolute;top:0;left:0;height:0!important;padding:0!important}.bootstrap-select.bs-container .dropdown-menu{z-index:1060}.bootstrap-select .dropdown-toggle .filter-option{position:static;top:0;left:0;float:left;height:100%;width:100%;text-align:left;overflow:hidden;-webkit-box-flex:0;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto}.bs3.bootstrap-select .dropdown-toggle .filter-option{padding-right:inherit}.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option{position:absolute;padding-top:inherit;padding-bottom:inherit;padding-left:inherit;float:none}.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option .filter-option-inner{padding-right:inherit}.bootstrap-select .dropdown-toggle .filter-option-inner-inner{overflow:hidden}.bootstrap-select .dropdown-toggle .filter-expand{width:0!important;float:left;opacity:0!important;overflow:hidden}.bootstrap-select .dropdown-toggle .caret{position:absolute;top:50%;right:12px;margin-top:-2px;vertical-align:middle}.input-group .bootstrap-select.form-control .dropdown-toggle{border-radius:inherit}.bootstrap-select[class*=col-] .dropdown-toggle{width:100%}.bootstrap-select .dropdown-menu{min-width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select .dropdown-menu>.inner:focus{outline:0!important}.bootstrap-select .dropdown-menu.inner{position:static;float:none;border:0;padding:0;margin:0;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.bootstrap-select .dropdown-menu li{position:relative}.bootstrap-select .dropdown-menu li.active small{color:rgba(255,255,255,.5)!important}.bootstrap-select .dropdown-menu li.disabled a{cursor:not-allowed}.bootstrap-select .dropdown-menu li a{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.bootstrap-select .dropdown-menu li a.opt{position:relative;padding-left:2.25em}.bootstrap-select .dropdown-menu li a span.check-mark{display:none}.bootstrap-select .dropdown-menu li a span.text{display:inline-block}.bootstrap-select .dropdown-menu li small{padding-left:.5em}.bootstrap-select .dropdown-menu .notify{position:absolute;bottom:5px;width:96%;margin:0 2%;min-height:26px;padding:3px 5px;background:#f5f5f5;border:1px solid #e3e3e3;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05);pointer-events:none;opacity:.9;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select .dropdown-menu .notify.fadeOut{-webkit-animation:.3s linear 750ms forwards bs-notify-fadeOut;-o-animation:.3s linear 750ms forwards bs-notify-fadeOut;animation:.3s linear 750ms forwards bs-notify-fadeOut}.bootstrap-select .no-results{padding:3px;background:#f5f5f5;margin:0 5px;white-space:nowrap}.bootstrap-select.fit-width .dropdown-toggle .filter-option{position:static;display:inline;padding:0}.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner,.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner-inner{display:inline}.bootstrap-select.fit-width .dropdown-toggle .bs-caret:before{content:'\00a0'}.bootstrap-select.fit-width .dropdown-toggle .caret{position:static;top:auto;margin-top:-1px}.bootstrap-select.show-tick .dropdown-menu .selected span.check-mark{position:absolute;display:inline-block;right:15px;top:5px}.bootstrap-select.show-tick .dropdown-menu li a span.text{margin-right:34px}.bootstrap-select .bs-ok-default:after{content:'';display:block;width:.5em;height:1em;border-style:solid;border-width:0 .26em .26em 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle{z-index:1061}.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:before{content:'';border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid rgba(204,204,204,.2);position:absolute;bottom:-4px;left:9px;display:none}.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:after{content:'';border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;bottom:-4px;left:10px;display:none}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:before{bottom:auto;top:-4px;border-top:7px solid rgba(204,204,204,.2);border-bottom:0}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:after{bottom:auto;top:-4px;border-top:6px solid #fff;border-bottom:0}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:before{right:12px;left:auto}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:after{right:13px;left:auto}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle .filter-option:after,.bootstrap-select.show-menu-arrow.open>.dropdown-toggle .filter-option:before,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle .filter-option:after,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle .filter-option:before{display:block}.bs-actionsbox,.bs-donebutton,.bs-searchbox{padding:4px 8px}.bs-actionsbox{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-actionsbox .btn-group button{width:50%}.bs-donebutton{float:left;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-donebutton .btn-group button{width:100%}.bs-searchbox+.bs-actionsbox{padding:0 8px 4px}.bs-searchbox .form-control{margin-bottom:0;width:100%;float:none}
\ No newline at end of file
diff --git a/data/interfaces/default/css/tautulli.css b/data/interfaces/default/css/tautulli.css
index 5f1d90a0..e256d2d7 100644
--- a/data/interfaces/default/css/tautulli.css
+++ b/data/interfaces/default/css/tautulli.css
@@ -2914,7 +2914,7 @@ a .home-platforms-list-cover-face:hover
margin-bottom: -20px;
width: 100%;
max-width: 1750px;
- overflow: hidden;
+ display: flow-root;
}
.table-card-back td {
font-size: 12px;
diff --git a/data/interfaces/default/graphs.html b/data/interfaces/default/graphs.html
index 3f189112..8435df20 100644
--- a/data/interfaces/default/graphs.html
+++ b/data/interfaces/default/graphs.html
@@ -1,6 +1,7 @@
<%inherit file="base.html"/>
<%def name="headIncludes()">
+
%def>
@@ -14,9 +15,7 @@
@@ -225,6 +224,7 @@
%def>
<%def name="javascriptIncludes()">
+
@@ -373,14 +373,35 @@
type: 'get',
dataType: "json",
success: function (data) {
- var select = $('#graph-user');
+ let select = $('#graph-user');
+ let by_id = {};
data.sort(function(a, b) {
return a.friendly_name.localeCompare(b.friendly_name);
});
data.forEach(function(item) {
select.append('');
+ by_id[item.user_id] = item.friendly_name;
});
+ select.selectpicker({
+ countSelectedText: function(sel, total) {
+ if (sel === 0 || sel === total) {
+ return 'All users';
+ } else if (sel > 1) {
+ return sel + ' users';
+ } else {
+ return select.val().map(function(id) {
+ return by_id[id];
+ }).join(', ');
+ }
+ },
+ style: 'btn-dark',
+ actionsBox: true,
+ selectedTextFormat: 'count',
+ noneSelectedText: 'All users'
+ });
+ select.selectpicker('render');
+ select.selectpicker('selectAll');
}
});
@@ -602,11 +623,6 @@
$('#nav-tabs-total').tab('show');
}
- // Set initial state
- if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
- if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
- if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
-
// Tab1 opened
$('#nav-tabs-plays').on('shown.bs.tab', function (e) {
e.preventDefault();
@@ -652,9 +668,20 @@
$('.months').text(current_month_range);
});
+ let graph_user_last_id = undefined;
+
// User changed
$('#graph-user').on('change', function() {
- selected_user_id = $(this).val() || null;
+ let val = $(this).val();
+ if (val.length === 0 || val.length === $(this).children().length) {
+ selected_user_id = null; // if all users are selected, just send an empty list
+ } else {
+ selected_user_id = val.join(",");
+ }
+ if (selected_user_id === graph_user_last_id) {
+ return;
+ }
+ graph_user_last_id = selected_user_id;
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
diff --git a/data/interfaces/default/history.html b/data/interfaces/default/history.html
index 327b99b7..8ab8b19e 100644
--- a/data/interfaces/default/history.html
+++ b/data/interfaces/default/history.html
@@ -1,6 +1,7 @@
<%inherit file="base.html"/>
<%def name="headIncludes()">
+
@@ -31,9 +32,7 @@
% if _session['user_group'] == 'admin':
")),d=!1,C.$element.trigger("maxReachedGrp"+j)),setTimeout(function(){C.setSelected(r,!1)},10),E[0].classList.add("fadeOut"),setTimeout(function(){E.remove()},1050)}}}else c&&(c.selected=!1),h.selected=!0,C.setSelected(r,!0);!C.multiple||C.multiple&&1===C.options.maxOptions?C.$button.trigger("focus"):C.options.liveSearch&&C.$searchbox.trigger("focus"),d&&(!C.multiple&&a===s.selectedIndex||(A=[h.index,p.prop("selected"),l],C.$element.triggerNative("change")))}}),this.$menu.on("click","li."+V.DISABLED+" a, ."+V.POPOVERHEADER+", ."+V.POPOVERHEADER+" :not(.close)",function(e){e.currentTarget==this&&(e.preventDefault(),e.stopPropagation(),C.options.liveSearch&&!z(e.target).hasClass("close")?C.$searchbox.trigger("focus"):C.$button.trigger("focus"))}),this.$menuInner.on("click",".divider, .dropdown-header",function(e){e.preventDefault(),e.stopPropagation(),C.options.liveSearch?C.$searchbox.trigger("focus"):C.$button.trigger("focus")}),this.$menu.on("click","."+V.POPOVERHEADER+" .close",function(){C.$button.trigger("click")}),this.$searchbox.on("click",function(e){e.stopPropagation()}),this.$menu.on("click",".actions-btn",function(e){C.options.liveSearch?C.$searchbox.trigger("focus"):C.$button.trigger("focus"),e.preventDefault(),e.stopPropagation(),z(this).hasClass("bs-select-all")?C.selectAll():C.deselectAll()}),this.$element.on("change"+j,function(){C.render(),C.$element.trigger("changed"+j,A),A=null}).on("focus"+j,function(){C.options.mobile||C.$button.trigger("focus")})},liveSearchListener:function(){var u=this,f=document.createElement("li");this.$button.on("click.bs.dropdown.data-api",function(){u.$searchbox.val()&&u.$searchbox.val("")}),this.$searchbox.on("click.bs.dropdown.data-api focus.bs.dropdown.data-api touchend.bs.dropdown.data-api",function(e){e.stopPropagation()}),this.$searchbox.on("input propertychange",function(){var e=u.$searchbox.val();if(u.selectpicker.search.elements=[],u.selectpicker.search.data=[],e){var t=[],i=e.toUpperCase(),s={},n=[],o=u._searchStyle(),r=u.options.liveSearchNormalize;r&&(i=w(i));for(var l=0;l=a.selectpicker.view.canHighlight.length&&(t=0),a.selectpicker.view.canHighlight[t+f]||(t=t+1+a.selectpicker.view.canHighlight.slice(t+f+1).indexOf(!0))),e.preventDefault();var m=f+t;e.which===B?0===f&&t===c.length-1?(a.$menuInner[0].scrollTop=a.$menuInner[0].scrollHeight,m=a.selectpicker.current.elements.length-1):d=(o=(n=a.selectpicker.current.data[m]).position-n.height)u+a.sizeInfo.menuInnerHeight),s=a.selectpicker.main.elements[v],a.activeIndex=b[x],a.focusItem(s),s&&s.firstChild.focus(),d&&(a.$menuInner[0].scrollTop=o),r.trigger("focus")}}i&&(e.which===H&&!a.selectpicker.keydown.keyHistory||e.which===D||e.which===W&&a.options.selectOnTab)&&(e.which!==H&&e.preventDefault(),a.options.liveSearch&&e.which===H||(a.$menuInner.find(".active a").trigger("click",!0),r.trigger("focus"),a.options.liveSearch||(e.preventDefault(),z(document).data("spaceSelect",!0))))}},mobile:function(){this.$element[0].classList.add("mobile-device")},refresh:function(){var e=z.extend({},this.options,this.$element.data());this.options=e,this.checkDisabled(),this.setStyle(),this.render(),this.buildData(),this.buildList(),this.setWidth(),this.setSize(!0),this.$element.trigger("refreshed"+j)},hide:function(){this.$newElement.hide()},show:function(){this.$newElement.show()},remove:function(){this.$newElement.remove(),this.$element.remove()},destroy:function(){this.$newElement.before(this.$element).remove(),this.$bsContainer?this.$bsContainer.remove():this.$menu.remove(),this.$element.off(j).removeData("selectpicker").removeClass("bs-select-hidden selectpicker"),z(window).off(j+"."+this.selectId)}};var J=z.fn.selectpicker;z.fn.selectpicker=Z,z.fn.selectpicker.Constructor=Y,z.fn.selectpicker.noConflict=function(){return z.fn.selectpicker=J,this};var Q=z.fn.dropdown.Constructor._dataApiKeydownHandler||z.fn.dropdown.Constructor.prototype.keydown;z(document).off("keydown.bs.dropdown.data-api").on("keydown.bs.dropdown.data-api",':not(.bootstrap-select) > [data-toggle="dropdown"]',Q).on("keydown.bs.dropdown.data-api",":not(.bootstrap-select) > .dropdown-menu",Q).on("keydown"+j,'.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bootstrap-select .bs-searchbox input',Y.prototype.keydown).on("focusin.modal",'.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bootstrap-select .bs-searchbox input',function(e){e.stopPropagation()}),z(window).on("load"+j+".data-api",function(){z(".selectpicker").each(function(){var e=z(this);Z.call(e,e.data())})})}(e)});
+//# sourceMappingURL=bootstrap-select.min.js.map
\ No newline at end of file
diff --git a/plexpy/graphs.py b/plexpy/graphs.py
index 49dfee57..58a199c0 100644
--- a/plexpy/graphs.py
+++ b/plexpy/graphs.py
@@ -51,11 +51,7 @@ class Graphs(object):
time_range = helpers.cast_to_int(time_range) or 30
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
- user_cond = ''
- if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
- user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
- elif user_id and user_id.isdigit():
- user_cond = 'AND session_history.user_id = %s ' % user_id
+ user_cond = self._make_user_cond(user_id)
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
@@ -171,11 +167,7 @@ class Graphs(object):
time_range = helpers.cast_to_int(time_range) or 30
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
- user_cond = ''
- if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
- user_cond = "AND session_history.user_id = %s " % session.get_session_user_id()
- elif user_id and user_id.isdigit():
- user_cond = "AND session_history.user_id = %s " % user_id
+ user_cond = self._make_user_cond(user_id)
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
@@ -308,11 +300,7 @@ class Graphs(object):
time_range = helpers.cast_to_int(time_range) or 30
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
- user_cond = ''
- if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
- user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
- elif user_id and user_id.isdigit():
- user_cond = 'AND session_history.user_id = %s ' % user_id
+ user_cond = self._make_user_cond(user_id)
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
@@ -427,11 +415,7 @@ class Graphs(object):
time_range = helpers.cast_to_int(time_range) or 12
timestamp = arrow.get(helpers.timestamp()).shift(months=-time_range).floor('month').timestamp()
- user_cond = ''
- if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
- user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
- elif user_id and user_id.isdigit():
- user_cond = 'AND session_history.user_id = %s ' % user_id
+ user_cond = self._make_user_cond(user_id)
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
@@ -554,11 +538,7 @@ class Graphs(object):
time_range = helpers.cast_to_int(time_range) or 30
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
- user_cond = ''
- if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
- user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
- elif user_id and user_id.isdigit():
- user_cond = 'AND session_history.user_id = %s ' % user_id
+ user_cond = self._make_user_cond(user_id)
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
@@ -653,11 +633,7 @@ class Graphs(object):
time_range = helpers.cast_to_int(time_range) or 30
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
- user_cond = ''
- if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
- user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
- elif user_id and user_id.isdigit():
- user_cond = 'AND session_history.user_id = %s ' % user_id
+ user_cond = self._make_user_cond(user_id)
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
@@ -763,11 +739,7 @@ class Graphs(object):
time_range = helpers.cast_to_int(time_range) or 30
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
- user_cond = ''
- if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
- user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
- elif user_id and user_id.isdigit():
- user_cond = 'AND session_history.user_id = %s ' % user_id
+ user_cond = self._make_user_cond(user_id)
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
@@ -860,11 +832,7 @@ class Graphs(object):
time_range = helpers.cast_to_int(time_range) or 30
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
- user_cond = ''
- if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
- user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
- elif user_id and user_id.isdigit():
- user_cond = 'AND session_history.user_id = %s ' % user_id
+ user_cond = self._make_user_cond(user_id)
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
@@ -941,11 +909,7 @@ class Graphs(object):
time_range = helpers.cast_to_int(time_range) or 30
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
- user_cond = ''
- if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
- user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
- elif user_id and user_id.isdigit():
- user_cond = 'AND session_history.user_id = %s ' % user_id
+ user_cond = self._make_user_cond(user_id)
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
@@ -1048,11 +1012,7 @@ class Graphs(object):
time_range = helpers.cast_to_int(time_range) or 30
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
- user_cond = ''
- if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
- user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
- elif user_id and user_id.isdigit():
- user_cond = 'AND session_history.user_id = %s ' % user_id
+ user_cond = self._make_user_cond(user_id)
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
@@ -1128,11 +1088,7 @@ class Graphs(object):
time_range = helpers.cast_to_int(time_range) or 30
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
- user_cond = ''
- if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
- user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
- elif user_id and user_id.isdigit():
- user_cond = 'AND session_history.user_id = %s ' % user_id
+ user_cond = self._make_user_cond(user_id)
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
@@ -1212,3 +1168,16 @@ class Graphs(object):
'series': [series_1_output, series_2_output, series_3_output]}
return output
+
+ def _make_user_cond(self, user_id):
+ """
+ Expects user_id to be a comma-separated list of ints.
+ """
+ user_cond = ''
+ if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
+ user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
+ elif user_id:
+ user_ids = helpers.split_strip(user_id)
+ if all(id.isdigit() for id in user_ids):
+ user_cond = 'AND session_history.user_id IN (%s) ' % ','.join(user_ids)
+ return user_cond
diff --git a/plexpy/webserve.py b/plexpy/webserve.py
index 06a9a802..7549aefa 100644
--- a/plexpy/webserve.py
+++ b/plexpy/webserve.py
@@ -2258,7 +2258,7 @@ class WebInterface(object):
Optional parameters:
time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration"
- user_id (str): The user id to filter the data
+ user_id (str): Comma separated list of user id to filter the data
grouping (int): 0 or 1
Returns:
@@ -2302,7 +2302,7 @@ class WebInterface(object):
Optional parameters:
time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration"
- user_id (str): The user id to filter the data
+ user_id (str): Comma separated list of user id to filter the data
grouping (int): 0 or 1
Returns:
@@ -2346,7 +2346,7 @@ class WebInterface(object):
Optional parameters:
time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration"
- user_id (str): The user id to filter the data
+ user_id (str): Comma separated list of user id to filter the data
grouping (int): 0 or 1
Returns:
@@ -2390,7 +2390,7 @@ class WebInterface(object):
Optional parameters:
time_range (str): The number of months of data to return
y_axis (str): "plays" or "duration"
- user_id (str): The user id to filter the data
+ user_id (str): Comma separated list of user id to filter the data
grouping (int): 0 or 1
Returns:
@@ -2434,7 +2434,7 @@ class WebInterface(object):
Optional parameters:
time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration"
- user_id (str): The user id to filter the data
+ user_id (str): Comma separated list of user id to filter the data
grouping (int): 0 or 1
Returns:
@@ -2478,7 +2478,7 @@ class WebInterface(object):
Optional parameters:
time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration"
- user_id (str): The user id to filter the data
+ user_id (str): Comma separated list of user id to filter the data
grouping (int): 0 or 1
Returns:
@@ -2522,7 +2522,7 @@ class WebInterface(object):
Optional parameters:
time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration"
- user_id (str): The user id to filter the data
+ user_id (str): Comma separated list of user id to filter the data
grouping (int): 0 or 1
Returns:
@@ -2565,7 +2565,7 @@ class WebInterface(object):
Optional parameters:
time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration"
- user_id (str): The user id to filter the data
+ user_id (str): Comma separated list of user id to filter the data
grouping (int): 0 or 1
Returns:
@@ -2608,7 +2608,7 @@ class WebInterface(object):
Optional parameters:
time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration"
- user_id (str): The user id to filter the data
+ user_id (str): Comma separated list of user id to filter the data
grouping (int): 0 or 1
Returns:
@@ -2651,7 +2651,7 @@ class WebInterface(object):
Optional parameters:
time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration"
- user_id (str): The user id to filter the data
+ user_id (str): Comma separated list of user id to filter the data
grouping (int): 0 or 1
Returns:
@@ -2694,7 +2694,7 @@ class WebInterface(object):
Optional parameters:
time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration"
- user_id (str): The user id to filter the data
+ user_id (str): Comma separated list of user id to filter the data
grouping (int): 0 or 1
Returns:
From d91e561a56a14728970b3f581a3329a40cff0abf Mon Sep 17 00:00:00 2001
From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
Date: Fri, 7 Jul 2023 17:47:38 -0700
Subject: [PATCH 09/21] Regroup history in separate thread and improve logging
---
data/interfaces/default/settings.html | 4 ++--
plexpy/activity_processor.py | 12 +++++++++++-
plexpy/webserve.py | 8 +++-----
3 files changed, 16 insertions(+), 8 deletions(-)
diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html
index 7cd614e0..c5d8fe37 100644
--- a/data/interfaces/default/settings.html
+++ b/data/interfaces/default/settings.html
@@ -2498,9 +2498,9 @@ $(document).ready(function() {
});
$("#regroup_history").click(function () {
- var msg = 'Are you sure you want to regroup play history in the database?';
+ var msg = 'Are you sure you want to regroup play history in the database?