From ea6c6078df410f333a060016dfce18c21ad134c9 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Tue, 23 May 2023 10:03:36 -0700 Subject: [PATCH 01/20] v2.12.4 --- CHANGELOG.md | 11 +++++++++++ plexpy/version.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3c6c4a1..24baf072 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## v2.12.4 (2023-05-23) + +* History: + * Fix: Set view offset equal to duration if a stream is stopped within the last 10 sec. +* Other: + * Fix: Database import may fail for some older databases. + * Fix: Double-quoted strings for newer versions of SQLite. (#2015, #2057) +* API: + * Change: Return the ID for async API calls (export_metadata, notify, notify_newsletter). + + ## v2.12.3 (2023-04-14) * Activity: diff --git a/plexpy/version.py b/plexpy/version.py index 47ba56cb..119e0b07 100644 --- a/plexpy/version.py +++ b/plexpy/version.py @@ -18,4 +18,4 @@ from __future__ import unicode_literals PLEXPY_BRANCH = "master" -PLEXPY_RELEASE_VERSION = "v2.12.3" \ No newline at end of file +PLEXPY_RELEASE_VERSION = "v2.12.4" \ No newline at end of file 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 02/20] 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 03/20] 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 04/20] 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 05/20] 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 06/20] 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 07/20] 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 08/20] 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 09/20] 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()"> + @@ -14,9 +15,7 @@

@@ -225,6 +224,7 @@ <%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':
@@ -121,6 +120,7 @@ <%def name="javascriptIncludes()"> + @@ -134,17 +134,40 @@ type: 'GET', dataType: 'json', success: function (data) { - var select = $('#history-user'); + let select = $('#history-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'); } }); + let history_user_last_id = undefined; + function loadHistoryTable(media_type, transcode_decision, selected_user_id) { history_table_options.ajax = { url: 'get_history', @@ -187,7 +210,16 @@ }); $('#history-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 === history_user_last_id) { + return; + } + history_user_last_id = selected_user_id; history_table.draw(); }); } diff --git a/data/interfaces/default/js/bootstrap-select.min.js b/data/interfaces/default/js/bootstrap-select.min.js new file mode 100644 index 00000000..92e3a32e --- /dev/null +++ b/data/interfaces/default/js/bootstrap-select.min.js @@ -0,0 +1,9 @@ +/*! + * 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) + */ + +!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){!function(z){"use strict";var d=["sanitize","whiteList","sanitizeFn"],r=["background","cite","href","itemtype","longdesc","poster","src","xlink:href"],e={"*":["class","dir","id","lang","role","tabindex","style",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},l=/^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi,a=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;function v(e,t){var i=e.nodeName.toLowerCase();if(-1!==z.inArray(i,t))return-1===z.inArray(i,r)||Boolean(e.nodeValue.match(l)||e.nodeValue.match(a));for(var s=z(t).filter(function(e,t){return t instanceof RegExp}),n=0,o=s.length;n]+>/g,"")),s&&(a=w(a)),a=a.toUpperCase(),o="contains"===i?0<=a.indexOf(t):a.startsWith(t)))break}return o}function L(e){return parseInt(e,10)||0}z.fn.triggerNative=function(e){var t,i=this[0];i.dispatchEvent?(u?t=new Event(e,{bubbles:!0}):(t=document.createEvent("Event")).initEvent(e,!0,!1),i.dispatchEvent(t)):i.fireEvent?((t=document.createEventObject()).eventType=e,i.fireEvent("on"+e,t)):this.trigger(e)};var f={"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A","\xe0":"a","\xe1":"a","\xe2":"a","\xe3":"a","\xe4":"a","\xe5":"a","\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I","\xcd":"I","\xce":"I","\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u","\xfb":"u","\xfc":"u","\xdd":"Y","\xfd":"y","\xff":"y","\xc6":"Ae","\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss","\u0100":"A","\u0102":"A","\u0104":"A","\u0101":"a","\u0103":"a","\u0105":"a","\u0106":"C","\u0108":"C","\u010a":"C","\u010c":"C","\u0107":"c","\u0109":"c","\u010b":"c","\u010d":"c","\u010e":"D","\u0110":"D","\u010f":"d","\u0111":"d","\u0112":"E","\u0114":"E","\u0116":"E","\u0118":"E","\u011a":"E","\u0113":"e","\u0115":"e","\u0117":"e","\u0119":"e","\u011b":"e","\u011c":"G","\u011e":"G","\u0120":"G","\u0122":"G","\u011d":"g","\u011f":"g","\u0121":"g","\u0123":"g","\u0124":"H","\u0126":"H","\u0125":"h","\u0127":"h","\u0128":"I","\u012a":"I","\u012c":"I","\u012e":"I","\u0130":"I","\u0129":"i","\u012b":"i","\u012d":"i","\u012f":"i","\u0131":"i","\u0134":"J","\u0135":"j","\u0136":"K","\u0137":"k","\u0138":"k","\u0139":"L","\u013b":"L","\u013d":"L","\u013f":"L","\u0141":"L","\u013a":"l","\u013c":"l","\u013e":"l","\u0140":"l","\u0142":"l","\u0143":"N","\u0145":"N","\u0147":"N","\u014a":"N","\u0144":"n","\u0146":"n","\u0148":"n","\u014b":"n","\u014c":"O","\u014e":"O","\u0150":"O","\u014d":"o","\u014f":"o","\u0151":"o","\u0154":"R","\u0156":"R","\u0158":"R","\u0155":"r","\u0157":"r","\u0159":"r","\u015a":"S","\u015c":"S","\u015e":"S","\u0160":"S","\u015b":"s","\u015d":"s","\u015f":"s","\u0161":"s","\u0162":"T","\u0164":"T","\u0166":"T","\u0163":"t","\u0165":"t","\u0167":"t","\u0168":"U","\u016a":"U","\u016c":"U","\u016e":"U","\u0170":"U","\u0172":"U","\u0169":"u","\u016b":"u","\u016d":"u","\u016f":"u","\u0171":"u","\u0173":"u","\u0174":"W","\u0175":"w","\u0176":"Y","\u0177":"y","\u0178":"Y","\u0179":"Z","\u017b":"Z","\u017d":"Z","\u017a":"z","\u017c":"z","\u017e":"z","\u0132":"IJ","\u0133":"ij","\u0152":"Oe","\u0153":"oe","\u0149":"'n","\u017f":"s"},m=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,g=RegExp("[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\u1ab0-\\u1aff\\u1dc0-\\u1dff]","g");function b(e){return f[e]}function w(e){return(e=e.toString())&&e.replace(m,b).replace(g,"")}var I,x,y,$,S=(I={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},x="(?:"+Object.keys(I).join("|")+")",y=RegExp(x),$=RegExp(x,"g"),function(e){return e=null==e?"":""+e,y.test(e)?e.replace($,E):e});function E(e){return I[e]}var C={32:" ",48:"0",49:"1",50:"2",51:"3",52:"4",53:"5",54:"6",55:"7",56:"8",57:"9",59:";",65:"A",66:"B",67:"C",68:"D",69:"E",70:"F",71:"G",72:"H",73:"I",74:"J",75:"K",76:"L",77:"M",78:"N",79:"O",80:"P",81:"Q",82:"R",83:"S",84:"T",85:"U",86:"V",87:"W",88:"X",89:"Y",90:"Z",96:"0",97:"1",98:"2",99:"3",100:"4",101:"5",102:"6",103:"7",104:"8",105:"9"},N=27,D=13,H=32,W=9,B=38,M=40,R={success:!1,major:"3"};try{R.full=(z.fn.dropdown.Constructor.VERSION||"").split(" ")[0].split("."),R.major=R.full[0],R.success=!0}catch(e){}var U=0,j=".bs.select",V={DISABLED:"disabled",DIVIDER:"divider",SHOW:"open",DROPUP:"dropup",MENU:"dropdown-menu",MENURIGHT:"dropdown-menu-right",MENULEFT:"dropdown-menu-left",BUTTONCLASS:"btn-default",POPOVERHEADER:"popover-title",ICONBASE:"glyphicon",TICKICON:"glyphicon-ok"},F={MENU:"."+V.MENU},_={span:document.createElement("span"),i:document.createElement("i"),subtext:document.createElement("small"),a:document.createElement("a"),li:document.createElement("li"),whitespace:document.createTextNode("\xa0"),fragment:document.createDocumentFragment()};_.a.setAttribute("role","option"),"4"===R.major&&(_.a.className="dropdown-item"),_.subtext.className="text-muted",_.text=_.span.cloneNode(!1),_.text.className="text",_.checkMark=_.span.cloneNode(!1);var G=new RegExp(B+"|"+M),q=new RegExp("^"+W+"$|"+N),K={li:function(e,t,i){var s=_.li.cloneNode(!1);return e&&(1===e.nodeType||11===e.nodeType?s.appendChild(e):s.innerHTML=e),void 0!==t&&""!==t&&(s.className=t),null!=i&&s.classList.add("optgroup-"+i),s},a:function(e,t,i){var s=_.a.cloneNode(!0);return e&&(11===e.nodeType?s.appendChild(e):s.insertAdjacentHTML("beforeend",e)),void 0!==t&&""!==t&&s.classList.add.apply(s.classList,t.split(" ")),i&&s.setAttribute("style",i),s},text:function(e,t){var i,s,n=_.text.cloneNode(!1);if(e.content)n.innerHTML=e.content;else{if(n.textContent=e.text,e.icon){var o=_.whitespace.cloneNode(!1);(s=(!0===t?_.i:_.span).cloneNode(!1)).className=this.options.iconBase+" "+e.icon,_.fragment.appendChild(s),_.fragment.appendChild(o)}e.subtext&&((i=_.subtext.cloneNode(!1)).textContent=e.subtext,n.appendChild(i))}if(!0===t)for(;0'},maxOptions:!1,mobile:!1,selectOnTab:!1,dropdownAlignRight:!1,windowPadding:0,virtualScroll:600,display:!1,sanitize:!0,sanitizeFn:null,whiteList:e},Y.prototype={constructor:Y,init:function(){var i=this,e=this.$element.attr("id");U++,this.selectId="bs-select-"+U,this.$element[0].classList.add("bs-select-hidden"),this.multiple=this.$element.prop("multiple"),this.autofocus=this.$element.prop("autofocus"),this.$element[0].classList.contains("show-tick")&&(this.options.showTick=!0),this.$newElement=this.createDropdown(),this.buildData(),this.$element.after(this.$newElement).prependTo(this.$newElement),this.$button=this.$newElement.children("button"),this.$menu=this.$newElement.children(F.MENU),this.$menuInner=this.$menu.children(".inner"),this.$searchbox=this.$menu.find("input"),this.$element[0].classList.remove("bs-select-hidden"),!0===this.options.dropdownAlignRight&&this.$menu[0].classList.add(V.MENURIGHT),void 0!==e&&this.$button.attr("data-id",e),this.checkDisabled(),this.clickListener(),this.options.liveSearch?(this.liveSearchListener(),this.focusedParent=this.$searchbox[0]):this.focusedParent=this.$menuInner[0],this.setStyle(),this.render(),this.setWidth(),this.options.container?this.selectPosition():this.$element.on("hide"+j,function(){if(i.isVirtual()){var e=i.$menuInner[0],t=e.firstChild.cloneNode(!1);e.replaceChild(t,e.firstChild),e.scrollTop=0}}),this.$menu.data("this",this),this.$newElement.data("this",this),this.options.mobile&&this.mobile(),this.$newElement.on({"hide.bs.dropdown":function(e){i.$element.trigger("hide"+j,e)},"hidden.bs.dropdown":function(e){i.$element.trigger("hidden"+j,e)},"show.bs.dropdown":function(e){i.$element.trigger("show"+j,e)},"shown.bs.dropdown":function(e){i.$element.trigger("shown"+j,e)}}),i.$element[0].hasAttribute("required")&&this.$element.on("invalid"+j,function(){i.$button[0].classList.add("bs-invalid"),i.$element.on("shown"+j+".invalid",function(){i.$element.val(i.$element.val()).off("shown"+j+".invalid")}).on("rendered"+j,function(){this.validity.valid&&i.$button[0].classList.remove("bs-invalid"),i.$element.off("rendered"+j)}),i.$button.on("blur"+j,function(){i.$element.trigger("focus").trigger("blur"),i.$button.off("blur"+j)})}),setTimeout(function(){i.buildList(),i.$element.trigger("loaded"+j)})},createDropdown:function(){var e=this.multiple||this.options.showTick?" show-tick":"",t=this.multiple?' aria-multiselectable="true"':"",i="",s=this.autofocus?" autofocus":"";R.major<4&&this.$element.parent().hasClass("input-group")&&(i=" input-group-btn");var n,o="",r="",l="",a="";return this.options.header&&(o='
'+this.options.header+"
"),this.options.liveSearch&&(r=''),this.multiple&&this.options.actionsBox&&(l='
"),this.multiple&&this.options.doneButton&&(a='
"),n='",z(n)},setPositionData:function(){this.selectpicker.view.canHighlight=[];for(var e=this.selectpicker.view.size=0;e=this.options.virtualScroll||!0===this.options.virtualScroll},createView:function(A,e,t){var L,N,D=this,i=0,H=[];if(this.selectpicker.isSearching=A,this.selectpicker.current=A?this.selectpicker.search:this.selectpicker.main,this.setPositionData(),e)if(t)i=this.$menuInner[0].scrollTop;else if(!D.multiple){var s=D.$element[0],n=(s.options[s.selectedIndex]||{}).liIndex;if("number"==typeof n&&!1!==D.options.size){var o=D.selectpicker.main.data[n],r=o&&o.position;r&&(i=r-(D.sizeInfo.menuInnerHeight+D.sizeInfo.liHeight)/2)}}function l(e,t){var i,s,n,o,r,l,a,c,d=D.selectpicker.current.elements.length,h=[],p=!0,u=D.isVirtual();D.selectpicker.view.scrollTop=e,i=Math.ceil(D.sizeInfo.menuInnerHeight/D.sizeInfo.liHeight*1.5),s=Math.round(d/i)||1;for(var f=0;fd-1?0:D.selectpicker.current.data[d-1].position-D.selectpicker.current.data[D.selectpicker.view.position1-1].position,b.firstChild.style.marginTop=v+"px",b.firstChild.style.marginBottom=g+"px"):(b.firstChild.style.marginTop=0,b.firstChild.style.marginBottom=0),b.firstChild.appendChild(w),!0===u&&D.sizeInfo.hasScrollBar){var C=b.firstChild.offsetWidth;if(t&&CD.sizeInfo.selectWidth)b.firstChild.style.minWidth=D.sizeInfo.menuInnerInnerWidth+"px";else if(C>D.sizeInfo.menuInnerInnerWidth){D.$menu[0].style.minWidth=0;var O=b.firstChild.offsetWidth;O>D.sizeInfo.menuInnerInnerWidth&&(D.sizeInfo.menuInnerInnerWidth=O,b.firstChild.style.minWidth=D.sizeInfo.menuInnerInnerWidth+"px"),D.$menu[0].style.minWidth=""}}}if(D.prevActiveIndex=D.activeIndex,D.options.liveSearch){if(A&&t){var z,T=0;D.selectpicker.view.canHighlight[T]||(T=1+D.selectpicker.view.canHighlight.slice(1).indexOf(!0)),z=D.selectpicker.view.visibleElements[T],D.defocusItem(D.selectpicker.view.currentActive),D.activeIndex=(D.selectpicker.current.data[T]||{}).index,D.focusItem(z)}}else D.$menuInner.trigger("focus")}l(i,!0),this.$menuInner.off("scroll.createView").on("scroll.createView",function(e,t){D.noScroll||l(this.scrollTop,t),D.noScroll=!1}),z(window).off("resize"+j+"."+this.selectId+".createView").on("resize"+j+"."+this.selectId+".createView",function(){D.$newElement.hasClass(V.SHOW)&&l(D.$menuInner[0].scrollTop)})},focusItem:function(e,t,i){if(e){t=t||this.selectpicker.main.data[this.activeIndex];var s=e.firstChild;s&&(s.setAttribute("aria-setsize",this.selectpicker.view.size),s.setAttribute("aria-posinset",t.posinset),!0!==i&&(this.focusedParent.setAttribute("aria-activedescendant",s.id),e.classList.add("active"),s.classList.add("active")))}},defocusItem:function(e){e&&(e.classList.remove("active"),e.firstChild&&e.firstChild.classList.remove("active"))},setPlaceholder:function(){var e=!1;if(this.options.title&&!this.multiple){this.selectpicker.view.titleOption||(this.selectpicker.view.titleOption=document.createElement("option")),e=!0;var t=this.$element[0],i=!1,s=!this.selectpicker.view.titleOption.parentNode;if(s)this.selectpicker.view.titleOption.className="bs-title-option",this.selectpicker.view.titleOption.value="",i=void 0===z(t.options[t.selectedIndex]).attr("selected")&&void 0===this.$element.data("selected");!s&&0===this.selectpicker.view.titleOption.index||t.insertBefore(this.selectpicker.view.titleOption,t.firstChild),i&&(t.selectedIndex=0)}return e},buildData:function(){var p=':not([hidden]):not([data-hidden="true"])',u=[],f=0,e=this.setPlaceholder()?1:0;this.options.hideDisabled&&(p+=":not(:disabled)");var t=this.$element[0].querySelectorAll("select > *"+p);function m(e){var t=u[u.length-1];t&&"divider"===t.type&&(t.optID||e.optID)||((e=e||{}).type="divider",u.push(e))}function v(e,t){if((t=t||{}).divider="true"===e.getAttribute("data-divider"),t.divider)m({optID:t.optID});else{var i=u.length,s=e.style.cssText,n=s?S(s):"",o=(e.className||"")+(t.optgroupClass||"");t.optID&&(o="opt "+o),t.optionClass=o.trim(),t.inlineStyle=n,t.text=e.textContent,t.content=e.getAttribute("data-content"),t.tokens=e.getAttribute("data-tokens"),t.subtext=e.getAttribute("data-subtext"),t.icon=e.getAttribute("data-icon"),e.liIndex=i,t.display=t.content||t.text,t.type="option",t.index=i,t.option=e,t.selected=!!e.selected,t.disabled=t.disabled||!!e.disabled,u.push(t)}}function i(e,t){var i=t[e],s=t[e-1],n=t[e+1],o=i.querySelectorAll("option"+p);if(o.length){var r,l,a={display:S(i.label),subtext:i.getAttribute("data-subtext"),icon:i.getAttribute("data-icon"),type:"optgroup-label",optgroupClass:" "+(i.className||"")};f++,s&&m({optID:f}),a.optID=f,u.push(a);for(var c=0,d=o.length;c li")},render:function(){var e,t=this,i=this.$element[0],s=this.setPlaceholder()&&0===i.selectedIndex,n=O(i,this.options.hideDisabled),o=n.length,r=this.$button[0],l=r.querySelector(".filter-option-inner-inner"),a=document.createTextNode(this.options.multipleSeparator),c=_.fragment.cloneNode(!1),d=!1;if(r.classList.toggle("bs-placeholder",t.multiple?!o:!T(i,n)),this.tabIndex(),"static"===this.options.selectedTextFormat)c=K.text.call(this,{text:this.options.title},!0);else if(!1===(this.multiple&&-1!==this.options.selectedTextFormat.indexOf("count")&&1")).length&&o>e[1]||1===e.length&&2<=o))){if(!s){for(var h=0;h option"+m+", optgroup"+m+" option"+m).length,g="function"==typeof this.options.countSelectedText?this.options.countSelectedText(o,v):this.options.countSelectedText;c=K.text.call(this,{text:g.replace("{0}",o.toString()).replace("{1}",v.toString())},!0)}if(null==this.options.title&&(this.options.title=this.$element.attr("title")),c.childNodes.length||(c=K.text.call(this,{text:void 0!==this.options.title?this.options.title:this.options.noneSelectedText},!0)),r.title=c.textContent.replace(/<[^>]*>?/g,"").trim(),this.options.sanitize&&d&&P([c],t.options.whiteList,t.options.sanitizeFn),l.innerHTML="",l.appendChild(c),R.major<4&&this.$newElement[0].classList.contains("bs3-has-addon")){var b=r.querySelector(".filter-expand"),w=l.cloneNode(!0);w.className="filter-expand",b?r.replaceChild(w,b):r.appendChild(w)}this.$element.trigger("rendered"+j)},setStyle:function(e,t){var i,s=this.$button[0],n=this.$newElement[0],o=this.options.style.trim();this.$element.attr("class")&&this.$newElement.addClass(this.$element.attr("class").replace(/selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi,"")),R.major<4&&(n.classList.add("bs3"),n.parentNode.classList.contains("input-group")&&(n.previousElementSibling||n.nextElementSibling)&&(n.previousElementSibling||n.nextElementSibling).classList.contains("input-group-addon")&&n.classList.add("bs3-has-addon")),i=e?e.trim():o,"add"==t?i&&s.classList.add.apply(s.classList,i.split(" ")):"remove"==t?i&&s.classList.remove.apply(s.classList,i.split(" ")):(o&&s.classList.remove.apply(s.classList,o.split(" ")),i&&s.classList.add.apply(s.classList,i.split(" ")))},liHeight:function(e){if(e||!1!==this.options.size&&!Object.keys(this.sizeInfo).length){var t=document.createElement("div"),i=document.createElement("div"),s=document.createElement("div"),n=document.createElement("ul"),o=document.createElement("li"),r=document.createElement("li"),l=document.createElement("li"),a=document.createElement("a"),c=document.createElement("span"),d=this.options.header&&0this.sizeInfo.menuExtras.vert&&l+this.sizeInfo.menuExtras.vert+50>this.sizeInfo.selectOffsetBot,!0===this.selectpicker.isSearching&&(a=this.selectpicker.dropup),this.$newElement.toggleClass(V.DROPUP,a),this.selectpicker.dropup=a),"auto"===this.options.size)n=3this.options.size){for(var b=0;bthis.sizeInfo.menuInnerHeight&&(this.sizeInfo.hasScrollBar=!0,this.sizeInfo.totalMenuWidth=this.sizeInfo.menuWidth+this.sizeInfo.scrollBarWidth),"auto"===this.options.dropdownAlignRight&&this.$menu.toggleClass(V.MENURIGHT,this.sizeInfo.selectOffsetLeft>this.sizeInfo.selectOffsetRight&&this.sizeInfo.selectOffsetRightthis.options.size&&i.off("resize"+j+"."+this.selectId+".setMenuSize scroll"+j+"."+this.selectId+".setMenuSize")}this.createView(!1,!0,e)},setWidth:function(){var i=this;"auto"===this.options.width?requestAnimationFrame(function(){i.$menu.css("min-width","0"),i.$element.on("loaded"+j,function(){i.liHeight(),i.setMenuSize();var e=i.$newElement.clone().appendTo("body"),t=e.css("width","auto").children("button").outerWidth();e.remove(),i.sizeInfo.selectWidth=Math.max(i.sizeInfo.totalMenuWidth,t),i.$newElement.css("width",i.sizeInfo.selectWidth+"px")})}):"fit"===this.options.width?(this.$menu.css("min-width",""),this.$newElement.css("width","").addClass("fit-width")):this.options.width?(this.$menu.css("min-width",""),this.$newElement.css("width",this.options.width)):(this.$menu.css("min-width",""),this.$newElement.css("width","")),this.$newElement.hasClass("fit-width")&&"fit"!==this.options.width&&this.$newElement[0].classList.remove("fit-width")},selectPosition:function(){this.$bsContainer=z('
');function e(e){var t={},i=r.options.display||!!z.fn.dropdown.Constructor.Default&&z.fn.dropdown.Constructor.Default.display;r.$bsContainer.addClass(e.attr("class").replace(/form-control|fit-width/gi,"")).toggleClass(V.DROPUP,e.hasClass(V.DROPUP)),s=e.offset(),l.is("body")?n={top:0,left:0}:((n=l.offset()).top+=parseInt(l.css("borderTopWidth"))-l.scrollTop(),n.left+=parseInt(l.css("borderLeftWidth"))-l.scrollLeft()),o=e.hasClass(V.DROPUP)?0:e[0].offsetHeight,(R.major<4||"static"===i)&&(t.top=s.top-n.top+o,t.left=s.left-n.left),t.width=e[0].offsetWidth,r.$bsContainer.css(t)}var s,n,o,r=this,l=z(this.options.container);this.$button.on("click.bs.dropdown.data-api",function(){r.isDisabled()||(e(r.$newElement),r.$bsContainer.appendTo(r.options.container).toggleClass(V.SHOW,!r.$button.hasClass(V.SHOW)).append(r.$menu))}),z(window).off("resize"+j+"."+this.selectId+" scroll"+j+"."+this.selectId).on("resize"+j+"."+this.selectId+" scroll"+j+"."+this.selectId,function(){r.$newElement.hasClass(V.SHOW)&&e(r.$newElement)}),this.$element.on("hide"+j,function(){r.$menu.data("height",r.$menu.height()),r.$bsContainer.detach()})},setOptionStatus:function(e){var t=this;if(t.noScroll=!1,t.selectpicker.view.visibleElements&&t.selectpicker.view.visibleElements.length)for(var i=0;i
');y[2]&&($=$.replace("{var}",y[2][1"+$+"
")),d=!1,C.$element.trigger("maxReached"+j)),g&&w&&(E.append(z("
"+S+"
")),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 10/20] 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?

This make take a long time for large databases.
Regrouping will continue in the background.
'; var url = 'regroup_history'; - confirmAjaxCall(url, msg, null, 'Regrouping play history...'); + confirmAjaxCall(url, msg); }); $("#delete_temp_sessions").click(function () { diff --git a/plexpy/activity_processor.py b/plexpy/activity_processor.py index b1558a56..0437d2d5 100644 --- a/plexpy/activity_processor.py +++ b/plexpy/activity_processor.py @@ -719,8 +719,14 @@ class ActivityProcessor(object): "JOIN session_history_metadata ON session_history.id = session_history_metadata.id" ) results = self.db.select(query) + count = len(results) + progress = 0 + + for i, session in enumerate(results, start=1): + if int(i / count * 10) > progress: + progress = int(i / count * 10) + logger.info("Tautulli ActivityProcessor :: Regrouping session history: %d%%", progress * 10) - for session in results: try: self.group_history(session['id'], session) except Exception as e: @@ -729,3 +735,7 @@ class ActivityProcessor(object): logger.info("Tautulli ActivityProcessor :: Regrouping session history complete.") return True + + +def regroup_history(): + ActivityProcessor().regroup_history() diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 7549aefa..b98c9e7c 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -443,12 +443,10 @@ class WebInterface(object): def regroup_history(self, **kwargs): """ Regroup play history in the database.""" - result = activity_processor.ActivityProcessor().regroup_history() + threading.Thread(target=activity_processor.regroup_history).start() - if result: - return {'result': 'success', 'message': 'Regrouped play history.'} - else: - return {'result': 'error', 'message': 'Regrouping play history failed.'} + return {'result': 'success', + 'message': 'Regrouping play history started. Check the logs to monitor any problems.'} @cherrypy.expose @cherrypy.tools.json_out() From 6010e406c817ecfb11d873824e0adcc22f5bcc80 Mon Sep 17 00:00:00 2001 From: David Pooley Date: Sun, 9 Jul 2023 00:32:42 +0100 Subject: [PATCH 11/20] Fix simultaneous streams per IP not behaving as expected with IPv6 (#2096) * Fix IPv6 comparisson for concurrent streams * Update regex to allow numbers in config variables * Remove additional logging for local testing * Update plexpy/notification_handler.py Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> --------- Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> --- plexpy/config.py | 3 ++- plexpy/helpers.py | 16 ++++++++++++++++ plexpy/notification_handler.py | 9 ++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/plexpy/config.py b/plexpy/config.py index 7b583d8d..6f2926d9 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -177,6 +177,7 @@ _CONFIG_DEFINITIONS = { 'NOTIFY_RECENTLY_ADDED_UPGRADE': (int, 'Monitoring', 0), 'NOTIFY_REMOTE_ACCESS_THRESHOLD': (int, 'Monitoring', 60), 'NOTIFY_CONCURRENT_BY_IP': (int, 'Monitoring', 0), + 'NOTIFY_CONCURRENT_IPV6_CIDR': (str, 'Monitoring', '/64'), 'NOTIFY_CONCURRENT_THRESHOLD': (int, 'Monitoring', 2), 'NOTIFY_NEW_DEVICE_INITIAL_ONLY': (int, 'Monitoring', 1), 'NOTIFY_SERVER_CONNECTION_THRESHOLD': (int, 'Monitoring', 60), @@ -536,7 +537,7 @@ class Config(object): Returns something from the ini unless it is a real property of the configuration object or is not all caps. """ - if not re.match(r'[A-Z_]+$', name): + if not re.match(r'[A-Z0-9_]+$', name): return super(Config, self).__getattr__(name) else: return self.check_setting(name) diff --git a/plexpy/helpers.py b/plexpy/helpers.py index 9cfb9c45..085dfc12 100644 --- a/plexpy/helpers.py +++ b/plexpy/helpers.py @@ -33,6 +33,7 @@ from functools import reduce, wraps import hashlib import imghdr from future.moves.itertools import islice, zip_longest +from ipaddress import ip_address, ip_network, IPv4Address import ipwhois import ipwhois.exceptions import ipwhois.utils @@ -1777,3 +1778,18 @@ def check_watched(media_type, view_offset, duration, marker_credits_first=None, def pms_name(): return plexpy.CONFIG.PMS_NAME_OVERRIDE or plexpy.CONFIG.PMS_NAME + + +def ip_type(ip: str) -> str: + try: + return "IPv4" if type(ip_address(ip)) is IPv4Address else "IPv6" + except ValueError: + return "Invalid" + + +def get_ipv6_network_address(ip: str) -> str: + cidr = "/64" + cidr_pattern = re.compile(r'^/(1([0-1]\d|2[0-8]))$|^/(\d\d)$|^/[1-9]$') + if cidr_pattern.match(plexpy.CONFIG.NOTIFY_CONCURRENT_IPV6_CIDR): + cidr = plexpy.CONFIG.NOTIFY_CONCURRENT_IPV6_CIDR + return str(ip_network(ip+cidr, strict=False).network_address) diff --git a/plexpy/notification_handler.py b/plexpy/notification_handler.py index 7dd81627..8b4b8583 100644 --- a/plexpy/notification_handler.py +++ b/plexpy/notification_handler.py @@ -160,6 +160,7 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti def notify_conditions(notify_action=None, stream_data=None, timeline_data=None, **kwargs): logger.debug("Tautulli NotificationHandler :: Checking global notification conditions.") + evaluated = False # Activity notifications if stream_data: @@ -187,7 +188,13 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None, user_sessions = [s for s in result['sessions'] if s['user_id'] == stream_data['user_id']] if plexpy.CONFIG.NOTIFY_CONCURRENT_BY_IP: - evaluated = len(Counter(s['ip_address'] for s in user_sessions)) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD + ip_addresses = set() + for s in user_sessions: + if helpers.ip_type(s['ip_address']) == 'IPv6': + ip_addresses.add(helpers.get_ipv6_network_address(s['ip_address'])) + elif helpers.ip_type(s['ip_address']) == 'IPv4': + ip_addresses.add(s['ip_address']) + evaluated = len(ip_addresses) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD else: evaluated = len(user_sessions) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD From 571a6b6d2df91d209907800af3f1b5c7356e2577 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Mon, 10 Jul 2023 08:56:27 -0700 Subject: [PATCH 12/20] Cast view_offset to int for regrouping history --- plexpy/activity_processor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plexpy/activity_processor.py b/plexpy/activity_processor.py index 0437d2d5..9115f332 100644 --- a/plexpy/activity_processor.py +++ b/plexpy/activity_processor.py @@ -519,12 +519,12 @@ class ActivityProcessor(object): if len(result) > 1: new_session = {'id': result[0]['id'], 'rating_key': result[0]['rating_key'], - 'view_offset': result[0]['view_offset'], + 'view_offset': helpers.cast_to_int(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'], + 'view_offset': helpers.cast_to_int(result[1]['view_offset']), 'reference_id': result[1]['reference_id']} if metadata: From b953b951fb46400f7d30a22cdc2791faa8d6e233 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Thu, 13 Jul 2023 15:50:39 -0700 Subject: [PATCH 13/20] v2.12.6 --- CHANGELOG.md | 17 +++++++++++++++++ plexpy/version.py | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24baf072..974e69ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## v2.12.5 (2023-07-13) + +* Activity: + * New: Added d3d11va to list of hardware decoders. +* History: + * Fix: Incorrect grouping of play history. + * New: Added button in settings to regroup play history. +* Notifications: + * Fix: Incorrect concurrent streams notifications by IP addresss for IPv6 addresses (#2096) (Thanks @pooley182) +* UI: + * Fix: Occasional UI crashing on Python 3.11. + * New: Added multiselect user filters to History and Graphs pages. (#2090) (Thanks @zdimension) +* API: + * New: Added regroup_history API command. + * Change: Updated graph API commands to accept a comma separated list of user IDs. + + ## v2.12.4 (2023-05-23) * History: diff --git a/plexpy/version.py b/plexpy/version.py index 119e0b07..116f4687 100644 --- a/plexpy/version.py +++ b/plexpy/version.py @@ -18,4 +18,4 @@ from __future__ import unicode_literals PLEXPY_BRANCH = "master" -PLEXPY_RELEASE_VERSION = "v2.12.4" \ No newline at end of file +PLEXPY_RELEASE_VERSION = "v2.12.5" \ No newline at end of file From 765804c93b8304629b19140f1ed58b7dbace1c4e Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Thu, 20 Jul 2023 14:19:05 -0700 Subject: [PATCH 14/20] Don't expose do_state_change --- plexpy/webserve.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plexpy/webserve.py b/plexpy/webserve.py index b98c9e7c..b643f84b 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -4302,8 +4302,6 @@ class WebInterface(object): return update - @cherrypy.expose - @requireAuth(member_of("admin")) def do_state_change(self, signal, title, timer, **kwargs): message = title quote = self.random_arnold_quotes() From d701d18a813246eff84034205c6ced99f7c09c63 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Thu, 27 Jul 2023 20:04:03 -0700 Subject: [PATCH 15/20] Update workflows action version refs --- .github/workflows/publish-installers.yml | 8 ++++---- .github/workflows/publish-snap.yml | 2 +- .github/workflows/pull-requests.yml | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-installers.yml b/.github/workflows/publish-installers.yml index 49d53233..0b6eec36 100644 --- a/.github/workflows/publish-installers.yml +++ b/.github/workflows/publish-installers.yml @@ -68,7 +68,7 @@ jobs: pyinstaller -y ./package/Tautulli-${{ matrix.os }}.spec - name: Create Windows Installer - uses: joncloud/makensis-action@v3.7 + uses: joncloud/makensis-action@v4 if: matrix.os == 'windows' with: script-file: ./package/Tautulli.nsi @@ -100,10 +100,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Get Build Job Status - uses: technote-space/workflow-conclusion-action@v3.0 + uses: technote-space/workflow-conclusion-action@v3 - name: Checkout Code - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3 - name: Set Release Version id: get_version @@ -168,7 +168,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Get Build Job Status - uses: technote-space/workflow-conclusion-action@v3.0 + uses: technote-space/workflow-conclusion-action@v3 - name: Combine Job Status id: status diff --git a/.github/workflows/publish-snap.yml b/.github/workflows/publish-snap.yml index 9df4d2fd..dd74c3a3 100644 --- a/.github/workflows/publish-snap.yml +++ b/.github/workflows/publish-snap.yml @@ -70,7 +70,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Get Build Job Status - uses: technote-space/workflow-conclusion-action@v3.0 + uses: technote-space/workflow-conclusion-action@v3 - name: Combine Job Status id: status diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 58cb4ee4..1a24cf24 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -18,7 +18,6 @@ jobs: with: message: Pull requests must be made to the `nightly` branch. Thanks. repo-token: ${{ secrets.GITHUB_TOKEN }} - repo-token-user-login: 'github-actions[bot]' - name: Fail Workflow if: github.base_ref != 'nightly' From b984a99d512b32795031fd6ed3816afe11d516db Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Thu, 27 Jul 2023 20:04:27 -0700 Subject: [PATCH 16/20] Update workflows action version refs --- .github/workflows/publish-docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 6480575f..6d91bbf6 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -95,7 +95,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Get Build Job Status - uses: technote-space/workflow-conclusion-action@v3.0 + uses: technote-space/workflow-conclusion-action@v3 - name: Combine Job Status id: status From e2cb15ef49ed6df5493b35e95f51835829879446 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 2 Aug 2023 16:51:20 -0700 Subject: [PATCH 17/20] Add notification image option for iOS Tautulli Remote App --- plexpy/notifiers.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plexpy/notifiers.py b/plexpy/notifiers.py index a2fa6341..2611aaea 100644 --- a/plexpy/notifiers.py +++ b/plexpy/notifiers.py @@ -3967,6 +3967,14 @@ class TAUTULLIREMOTEAPP(Notifier): 2: 'Large image (Non-expandable text)' } }) + elif platform == 'ios': + config_option.append({ + 'label': 'Include Poster Image', + 'value': self.config['notification_type'], + 'name': 'remoteapp_notification_type', + 'description': 'Include a poster with the notifications.', + 'input_type': 'checkbox' + }) return config_option From 842e36485a6b4e718ad870cf0024c23bdc7d8d16 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Aug 2023 08:42:01 -0700 Subject: [PATCH 18/20] Bump actions/stale from 7 to 8 (#2025) Bumps [actions/stale](https://github.com/actions/stale) from 7 to 8. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/v7...v8) --- updated-dependencies: - dependency-name: actions/stale dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> [skip ci] --- .github/workflows/issues-stale.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issues-stale.yml b/.github/workflows/issues-stale.yml index 0643cb0a..26b8aa5f 100644 --- a/.github/workflows/issues-stale.yml +++ b/.github/workflows/issues-stale.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Stale - uses: actions/stale@v7 + uses: actions/stale@v8 with: stale-issue-message: > This issue is stale because it has been open for 30 days with no activity. @@ -30,7 +30,7 @@ jobs: days-before-close: 5 - name: Invalid Template - uses: actions/stale@v7 + uses: actions/stale@v8 with: stale-issue-message: > Invalid issues template. From 31543d267f4f793689be599fa66a1ce17879bab6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Aug 2023 21:19:39 -0700 Subject: [PATCH 19/20] Bump pywin32 from 305 to 306 (#2028) Bumps [pywin32](https://github.com/mhammond/pywin32) from 305 to 306. - [Release notes](https://github.com/mhammond/pywin32/releases) - [Changelog](https://github.com/mhammond/pywin32/blob/main/CHANGES.txt) - [Commits](https://github.com/mhammond/pywin32/commits) --- updated-dependencies: - dependency-name: pywin32 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> [skip ci] --- package/requirements-package.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/requirements-package.txt b/package/requirements-package.txt index e8ccd0b8..d3c6d57b 100644 --- a/package/requirements-package.txt +++ b/package/requirements-package.txt @@ -8,4 +8,4 @@ pycryptodomex==3.17 pyobjc-core==9.0.1; platform_system == "Darwin" pyobjc-framework-Cocoa==9.0.1; platform_system == "Darwin" -pywin32==305; platform_system == "Windows" +pywin32==306; platform_system == "Windows" From 6fa8bb376828cdb954ecf08eda804d70747e7502 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Aug 2023 21:20:05 -0700 Subject: [PATCH 20/20] Bump pyopenssl from 23.0.0 to 23.2.0 (#2081) Bumps [pyopenssl](https://github.com/pyca/pyopenssl) from 23.0.0 to 23.2.0. - [Changelog](https://github.com/pyca/pyopenssl/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/pyopenssl/compare/23.0.0...23.2.0) --- updated-dependencies: - dependency-name: pyopenssl dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> [skip ci] --- package/requirements-package.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/requirements-package.txt b/package/requirements-package.txt index d3c6d57b..f432c59b 100644 --- a/package/requirements-package.txt +++ b/package/requirements-package.txt @@ -2,7 +2,7 @@ apscheduler==3.10.1 importlib-metadata==6.0.0 importlib-resources==5.12.0 pyinstaller==5.8.0 -pyopenssl==23.0.0 +pyopenssl==23.2.0 pycryptodomex==3.17 pyobjc-core==9.0.1; platform_system == "Darwin"