From cb5053476dc44fa252df7364df81ae0f113e55e9 Mon Sep 17 00:00:00 2001 From: Sander Ploegsma Date: Wed, 25 May 2016 00:05:48 +0200 Subject: [PATCH 01/19] Add log level filter for plex server and media logs --- data/interfaces/default/logs.html | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/data/interfaces/default/logs.html b/data/interfaces/default/logs.html index c1aff6f6..6a899ffe 100644 --- a/data/interfaces/default/logs.html +++ b/data/interfaces/default/logs.html @@ -21,6 +21,18 @@ Logs
+ @@ -143,6 +155,34 @@ clearSearchButton('log_table', log_table); }); + var log_levels = ['DEBUG', 'INFO', 'WARN', 'ERROR']; + + function bindLogLevelFilter() { + clearLogLevelFilter(); + var log_level_column = this.api().column(1); + var select = $('#log-filter'); + select.on('change', function() { + var val = $.fn.dataTable.util.escapeRegex( + $(this).val() + ); + var search_string = ''; + var levelIndex = log_levels.indexOf(val); + if (levelIndex >= 0) { + search_string = '^' + log_levels + .slice(levelIndex) + .join('|') + '$'; + } + + log_level_column + .search(search_string, true, false ) + .draw(); + }).change(); + } + + function clearLogLevelFilter() { + $('#log-filter').off('change'); + } + function loadPlexPyLogs() { log_table_options.ajax = { url: "getLog" @@ -154,6 +194,7 @@ plex_log_table_options.ajax = { url: "get_plex_log?log_type=server" } + plex_log_table_options.initComplete = bindLogLevelFilter; plex_log_table = $('#plex_log_table').DataTable(plex_log_table_options); } @@ -161,6 +202,7 @@ plex_log_table_options.ajax = { url: "get_plex_log?log_type=scanner" } + plex_log_table_options.initComplete = bindLogLevelFilter; plex_scanner_log_table = $('#plex_scanner_log_table').DataTable(plex_log_table_options); } @@ -190,6 +232,7 @@ } $("#plexpy-logs-btn").click(function () { + $("#filter-logs").hide(); $("#clear-logs").show(); $("#download-plexpylog").show() $("#clear-notify-logs").hide(); @@ -199,6 +242,7 @@ }); $("#plex-logs-btn").click(function () { + $("#filter-logs").show(); $("#clear-logs").hide(); $("#download-plexpylog").hide() $("#clear-notify-logs").hide(); @@ -208,6 +252,7 @@ }); $("#plex-scanner-logs-btn").click(function () { + $("#filter-logs").show(); $("#clear-logs").hide(); $("#download-plexpylog").hide() $("#clear-notify-logs").hide(); @@ -217,6 +262,7 @@ }); $("#notification-logs-btn").click(function () { + $("#filter-logs").hide(); $("#clear-logs").hide(); $("#download-plexpylog").hide() $("#clear-notify-logs").show(); @@ -226,6 +272,7 @@ }); $("#login-logs-btn").click(function () { + $("#filter-logs").hide(); $("#clear-logs").hide(); $("#download-plexpylog").hide() $("#clear-notify-logs").hide(); From d8112e7628b5ce51a8277f6b564d719711c37d4b Mon Sep 17 00:00:00 2001 From: Nick N Date: Thu, 26 May 2016 11:44:03 -0700 Subject: [PATCH 02/19] Reflect path suggested in installation guide --- init-scripts/init.freebsd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/init-scripts/init.freebsd b/init-scripts/init.freebsd index b3a62fee..2cfb472f 100755 --- a/init-scripts/init.freebsd +++ b/init-scripts/init.freebsd @@ -14,7 +14,7 @@ # default. Do not sets it as empty or it will run # as root. # plexpy_dir: Directory where PlexPy lives. -# Default: /usr/local/plexpy +# Default: /usr/local/share/plexpy # plexpy_chdir: Change to this directory before running PlexPy. # Default is same as plexpy_dir. # plexpy_pid: The name of the pidfile to create. @@ -30,7 +30,7 @@ load_rc_config ${name} : ${plexpy_enable:="NO"} : ${plexpy_user:="_sabnzbd"} -: ${plexpy_dir:="/usr/local/plexpy"} +: ${plexpy_dir:="/usr/local/share/plexpy"} : ${plexpy_chdir:="${plexpy_dir}"} : ${plexpy_pid:="${plexpy_dir}/plexpy.pid"} : ${plexpy_flags:=""} From 77bd52b2aee0f5b50f1f84bc3b41e43478167ff9 Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Thu, 26 May 2016 19:25:14 -0700 Subject: [PATCH 03/19] Add user details and stats to API --- API.md | 89 +++++++++++++++++++ data/interfaces/default/user.html | 4 +- plexpy/webserve.py | 136 +++++++++++++++++++++++++++++- 3 files changed, 223 insertions(+), 6 deletions(-) diff --git a/API.md b/API.md index 9a62bc95..a88d8aa5 100644 --- a/API.md +++ b/API.md @@ -1311,6 +1311,35 @@ Returns: ``` +### get_user +Get a user's details. + +``` +Required parameters: + user_id (str): The id of the Plex user + +Optional parameters: + None + +Returns: + json: + {"allow_guest": 1, + "deleted_user": 0, + "do_notify": 1, + "email": "Jon.Snow.1337@CastleBlack.com", + "friendly_name": "Jon Snow", + "is_allow_sync": 1, + "is_home_user": 1, + "is_restricted": 0, + "keep_history": 1, + "shared_libraries": ["10", "1", "4", "5", "15", "20", "2"], + "user_id": 133788, + "user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar", + "username": "LordCommanderSnow" + } +``` + + ### get_user_ips Get the data on PlexPy users IP table. @@ -1415,6 +1444,66 @@ Returns: ``` +### get_user_player_stats +Get a user's player statistics. + +``` +Required parameters: + user_id (str): The id of the Plex user + +Optional parameters: + None + +Returns: + json: + [{"platform_type": "Chrome", + "player_name": "Plex Web (Chrome)", + "result_id": 1, + "total_plays": 170 + }, + {"platform_type": "Chromecast", + "player_name": "Chromecast", + "result_id": 2, + "total_plays": 42 + }, + {...}, + {...} + ] +``` + + +### get_user_watch_time_stats +Get a user's watch time statistics. + +``` +Required parameters: + user_id (str): The id of the Plex user + +Optional parameters: + None + +Returns: + json: + [{"query_days": 1, + "total_plays": 0, + "total_time": 0 + }, + {"query_days": 7, + "total_plays": 3, + "total_time": 15694 + }, + {"query_days": 30, + "total_plays": 35, + "total_time": 63054 + }, + {"query_days": 0, + "total_plays": 508, + "total_time": 1183080 + } + ] +``` + + ### get_users Get a list of all users that have access to your server. diff --git a/data/interfaces/default/user.html b/data/interfaces/default/user.html index a9fda930..79cacc58 100644 --- a/data/interfaces/default/user.html +++ b/data/interfaces/default/user.html @@ -383,7 +383,7 @@ DOCUMENTATION :: END // Populate watch time stats $.ajax({ - url: 'get_user_watch_time_stats', + url: 'user_watch_time_stats', async: true, data: { user_id: user_id, user: username }, complete: function(xhr, status) { @@ -393,7 +393,7 @@ DOCUMENTATION :: END // Populate platform stats $.ajax({ - url: 'get_user_player_stats', + url: 'user_player_stats', async: true, data: { user_id: user_id, user: username }, complete: function(xhr, status) { diff --git a/plexpy/webserve.py b/plexpy/webserve.py index d5cbcb00..06a9126f 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -1047,7 +1047,7 @@ class WebInterface(object): @cherrypy.expose @requireAuth() - def get_user_watch_time_stats(self, user=None, user_id=None, **kwargs): + 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") @@ -1060,12 +1060,12 @@ class WebInterface(object): if result: return serve_template(templatename="user_watch_time_stats.html", data=result, title="Watch Stats") else: - logger.warn(u"Unable to retrieve data for get_user_watch_time_stats.") + logger.warn(u"Unable to retrieve data for user_watch_time_stats.") return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats") @cherrypy.expose @requireAuth() - def get_user_player_stats(self, user=None, user_id=None, **kwargs): + 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") @@ -1078,7 +1078,7 @@ class WebInterface(object): if result: return serve_template(templatename="user_player_stats.html", data=result, title="Player Stats") else: - logger.warn(u"Unable to retrieve data for get_user_player_stats.") + logger.warn(u"Unable to retrieve data for user_player_stats.") return serve_template(templatename="user_player_stats.html", data=None, title="Player Stats") @cherrypy.expose @@ -1222,6 +1222,134 @@ class WebInterface(object): return history + @cherrypy.expose + @cherrypy.tools.json_out() + @requireAuth(member_of("admin")) + @addtoapi() + def get_user(self, user_id=None, **kwargs): + """ Get a user's details. + + ``` + Required parameters: + user_id (str): The id of the Plex user + + Optional parameters: + None + + Returns: + json: + {"allow_guest": 1, + "deleted_user": 0, + "do_notify": 1, + "email": "Jon.Snow.1337@CastleBlack.com", + "friendly_name": "Jon Snow", + "is_allow_sync": 1, + "is_home_user": 1, + "is_restricted": 0, + "keep_history": 1, + "shared_libraries": ["10", "1", "4", "5", "15", "20", "2"], + "user_id": 133788, + "user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar", + "username": "LordCommanderSnow" + } + ``` + """ + user_data = users.Users() + if user_id: + user_details = user_data.get_details(user_id=user_id) + if user_details: + return user_details + else: + logger.warn(u"Unable to retrieve data for get_user.") + else: + logger.warn(u"User details requested but no user_id received.") + + @cherrypy.expose + @cherrypy.tools.json_out() + @requireAuth(member_of("admin")) + @addtoapi() + def get_user_watch_time_stats(self, user_id=None, **kwargs): + """ Get a user's watch time statistics. + + ``` + Required parameters: + user_id (str): The id of the Plex user + + Optional parameters: + None + + Returns: + json: + [{"query_days": 1, + "total_plays": 0, + "total_time": 0 + }, + {"query_days": 7, + "total_plays": 3, + "total_time": 15694 + }, + {"query_days": 30, + "total_plays": 35, + "total_time": 63054 + }, + {"query_days": 0, + "total_plays": 508, + "total_time": 1183080 + } + ] + ``` + """ + user_data = users.Users() + if user_id: + result = user_data.get_watch_time_stats(user_id=user_id) + if result: + return result + else: + logger.warn(u"Unable to retrieve data for get_user_watch_time_stats.") + else: + logger.warn(u"User watch time stats requested but no user_id received.") + + @cherrypy.expose + @cherrypy.tools.json_out() + @requireAuth(member_of("admin")) + @addtoapi() + def get_user_player_stats(self, user_id=None, **kwargs): + """ Get a user's player statistics. + + ``` + Required parameters: + user_id (str): The id of the Plex user + + Optional parameters: + None + + Returns: + json: + [{"platform_type": "Chrome", + "player_name": "Plex Web (Chrome)", + "result_id": 1, + "total_plays": 170 + }, + {"platform_type": "Chromecast", + "player_name": "Chromecast", + "result_id": 2, + "total_plays": 42 + }, + {...}, + {...} + ] + ``` + """ + user_data = users.Users() + if user_id: + result = user_data.get_player_stats(user_id=user_id) + if result: + return result + else: + logger.warn(u"Unable to retrieve data for get_user_player_stats.") + else: + logger.warn(u"User watch time stats requested but no user_id received.") + @cherrypy.expose @cherrypy.tools.json_out() @requireAuth(member_of("admin")) From 29522428dec43adf97b349b61c3d387c91059840 Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Sat, 28 May 2016 14:01:29 -0700 Subject: [PATCH 04/19] Catch exception when retrieving current activity --- plexpy/webserve.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 06a9126f..20ab5477 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -3738,19 +3738,22 @@ class WebInterface(object): } ``` """ - pms_connect = pmsconnect.PmsConnect(token=plexpy.CONFIG.PMS_TOKEN) - result = pms_connect.get_current_activity() + try: + pms_connect = pmsconnect.PmsConnect(token=plexpy.CONFIG.PMS_TOKEN) + result = pms_connect.get_current_activity() - if result: - data_factory = datafactory.DataFactory() - for session in result['sessions']: - if not session['ip_address']: - ip_address = data_factory.get_session_ip(session['session_key']) - session['ip_address'] = ip_address + if result: + data_factory = datafactory.DataFactory() + for session in result['sessions']: + if not session['ip_address']: + ip_address = data_factory.get_session_ip(session['session_key']) + session['ip_address'] = ip_address - return result - else: - logger.warn(u"Unable to retrieve data for get_activity.") + return result + else: + logger.warn(u"Unable to retrieve data for get_activity.") + except Exception as e: + logger.exception(u"Unable to retrieve data for get_activity: %s" % e) @cherrypy.expose @cherrypy.tools.json_out() From b3a7fbd9b59e9639a0687263f311152376343e09 Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Sat, 28 May 2016 14:02:21 -0700 Subject: [PATCH 05/19] Add pms token to loopback url when retrieving images --- plexpy/plextv.py | 8 +++++--- plexpy/pmsconnect.py | 10 ++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/plexpy/plextv.py b/plexpy/plextv.py index a9b2b784..ee46aa95 100644 --- a/plexpy/plextv.py +++ b/plexpy/plextv.py @@ -147,13 +147,15 @@ class PlexTV(object): if session.get_session_user_id(): user_data = users.Users() user_tokens = user_data.get_tokens(user_id=session.get_session_user_id()) - token = user_tokens['server_token'] + self.token = user_tokens['server_token'] else: - token = plexpy.CONFIG.PMS_TOKEN + self.token = plexpy.CONFIG.PMS_TOKEN + else: + self.token = token self.request_handler = http_handler.HTTPHandler(host='plex.tv', port=443, - token=token, + token=self.token, ssl_verify=self.ssl_verify) def get_plex_auth(self, output_format='raw'): diff --git a/plexpy/pmsconnect.py b/plexpy/pmsconnect.py index c0678044..d01fa4a2 100644 --- a/plexpy/pmsconnect.py +++ b/plexpy/pmsconnect.py @@ -121,13 +121,15 @@ class PmsConnect(object): if session.get_session_user_id(): user_data = users.Users() user_tokens = user_data.get_tokens(user_id=session.get_session_user_id()) - token = user_tokens['server_token'] + self.token = user_tokens['server_token'] else: - token = plexpy.CONFIG.PMS_TOKEN + self.token = plexpy.CONFIG.PMS_TOKEN + else: + self.token = token self.request_handler = http_handler.HTTPHandler(host=hostname, port=port, - token=token) + token=self.token) def get_sessions(self, output_format=''): """ @@ -1904,7 +1906,7 @@ class PmsConnect(object): """ if img: - params = {'url': 'http://127.0.0.1:32400%s' % img} + params = {'url': 'http://127.0.0.1:32400%s?%s' % (img, urllib.urlencode({'X-Plex-Token': self.token}))} if width.isdigit() and height.isdigit(): params['width'] = width params['height'] = height From c67aedceb1013d27c8b870b256bc8e03f8f8da8c Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Sat, 28 May 2016 14:09:59 -0700 Subject: [PATCH 06/19] Add library statistics to API --- API.md | 87 ++++++++++++++ data/interfaces/default/library.html | 8 +- plexpy/webserve.py | 173 +++++++++++++++++++++++---- 3 files changed, 238 insertions(+), 30 deletions(-) diff --git a/API.md b/API.md index a88d8aa5..1895f99a 100644 --- a/API.md +++ b/API.md @@ -543,6 +543,33 @@ Returns: ``` +### get_library +Get a library's details. + +``` +Required parameters: + section_id (str): The id of the Plex library section + +Optional parameters: + None + +Returns: + json: + {"child_count": null, + "count": 887, + "do_notify": 1, + "do_notify_created": 1, + "keep_history": 1, + "library_art": "/:/resources/movie-fanart.jpg", + "library_thumb": "/:/resources/movie.png", + "parent_count": null, + "section_id": 1, + "section_name": "Movies", + "section_type": "movie" + } +``` + + ### get_library_media_info Get the data on the PlexPy media info tables. @@ -619,6 +646,66 @@ Returns: ``` +### get_library_user_stats +Get a library's user statistics. + +``` +Required parameters: + section_id (str): The id of the Plex library section + +Optional parameters: + None + +Returns: + json: + [{"friendly_name": "Jon Snow", + "total_plays": 170, + "user_id": 133788, + "user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar" + }, + {"platform_type": "DanyKhaleesi69", + "total_plays": 42, + "user_id": 8008135, + "user_thumb": "https://plex.tv/users/568gwwoib5t98a3a/avatar" + }, + {...}, + {...} + ] +``` + + +### get_library_watch_time_stats +Get a library's watch time statistics. + +``` +Required parameters: + section_id (str): The id of the Plex library section + +Optional parameters: + None + +Returns: + json: + [{"query_days": 1, + "total_plays": 0, + "total_time": 0 + }, + {"query_days": 7, + "total_plays": 3, + "total_time": 15694 + }, + {"query_days": 30, + "total_plays": 35, + "total_time": 63054 + }, + {"query_days": 0, + "total_plays": 508, + "total_time": 1183080 + } + ] +``` + + ### get_logs Get the PlexPy logs. diff --git a/data/interfaces/default/library.html b/data/interfaces/default/library.html index 230ad0ec..86fbf9f3 100644 --- a/data/interfaces/default/library.html +++ b/data/interfaces/default/library.html @@ -362,7 +362,7 @@ DOCUMENTATION :: END // Populate watch time stats $.ajax({ - url: 'get_library_watch_time_stats', + url: 'library_watch_time_stats', async: true, data: { section_id: section_id }, complete: function(xhr, status) { @@ -372,7 +372,7 @@ DOCUMENTATION :: END // Populate user stats $.ajax({ - url: 'get_library_user_stats', + url: 'library_user_stats', async: true, data: { section_id: section_id }, complete: function(xhr, status) { @@ -498,7 +498,7 @@ DOCUMENTATION :: END function recentlyWatched() { // Populate recently watched $.ajax({ - url: 'get_library_recently_watched', + url: 'library_recently_watched', async: true, data: { section_id: section_id, @@ -514,7 +514,7 @@ DOCUMENTATION :: END function recentlyAdded() { // Populate recently added $.ajax({ - url: 'get_library_recently_added', + url: 'library_recently_added', async: true, data: { section_id: section_id, diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 20ab5477..5a0228ea 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -477,9 +477,9 @@ class WebInterface(object): "get_file_sizes_hold": plexpy.CONFIG.GET_FILE_SIZES_HOLD } - library_data = libraries.Libraries() if section_id: try: + library_data = libraries.Libraries() library_details = library_data.get_details(section_id=section_id) except: logger.warn(u"Unable to retrieve library details for section_id %s " % section_id) @@ -493,8 +493,8 @@ class WebInterface(object): @cherrypy.expose @requireAuth(member_of("admin")) def edit_library_dialog(self, section_id=None, **kwargs): - library_data = libraries.Libraries() if section_id: + library_data = libraries.Libraries() result = library_data.get_details(section_id=section_id) status_message = '' else: @@ -528,9 +528,9 @@ class WebInterface(object): do_notify_created = kwargs.get('do_notify_created', 0) keep_history = kwargs.get('keep_history', 0) - library_data = libraries.Libraries() if section_id: try: + library_data = libraries.Libraries() library_data.set_config(section_id=section_id, custom_thumb=custom_thumb, do_notify=do_notify, @@ -543,7 +543,7 @@ class WebInterface(object): @cherrypy.expose @requireAuth() - def get_library_watch_time_stats(self, section_id=None, **kwargs): + 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") @@ -556,12 +556,12 @@ class WebInterface(object): if result: return serve_template(templatename="user_watch_time_stats.html", data=result, title="Watch Stats") else: - logger.warn(u"Unable to retrieve data for get_library_watch_time_stats.") + logger.warn(u"Unable to retrieve data for library_watch_time_stats.") return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats") @cherrypy.expose @requireAuth() - def get_library_user_stats(self, section_id=None, **kwargs): + 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") @@ -574,12 +574,12 @@ class WebInterface(object): if result: return serve_template(templatename="library_user_stats.html", data=result, title="Player Stats") else: - logger.warn(u"Unable to retrieve data for get_library_user_stats.") + logger.warn(u"Unable to retrieve data for library_user_stats.") return serve_template(templatename="library_user_stats.html", data=None, title="Player Stats") @cherrypy.expose @requireAuth() - def get_library_recently_watched(self, section_id=None, limit='10', **kwargs): + 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") @@ -592,12 +592,12 @@ class WebInterface(object): if result: return serve_template(templatename="user_recently_watched.html", data=result, title="Recently Watched") else: - logger.warn(u"Unable to retrieve data for get_library_recently_watched.") + logger.warn(u"Unable to retrieve data for library_recently_watched.") return serve_template(templatename="user_recently_watched.html", data=None, title="Recently Watched") @cherrypy.expose @requireAuth() - def get_library_recently_added(self, section_id=None, limit='10', **kwargs): + 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") @@ -610,7 +610,7 @@ class WebInterface(object): if result: return serve_template(templatename="library_recently_added.html", data=result['recently_added'], title="Recently Added") else: - logger.warn(u"Unable to retrieve data for get_library_recently_added.") + logger.warn(u"Unable to retrieve data for library_recently_added.") return serve_template(templatename="library_recently_added.html", data=None, title="Recently Added") @cherrypy.expose @@ -733,6 +733,132 @@ class WebInterface(object): return {'success': result} + @cherrypy.expose + @cherrypy.tools.json_out() + @requireAuth(member_of("admin")) + @addtoapi() + def get_library(self, section_id=None, **kwargs): + """ Get a library's details. + + ``` + Required parameters: + section_id (str): The id of the Plex library section + + Optional parameters: + None + + Returns: + json: + {"child_count": null, + "count": 887, + "do_notify": 1, + "do_notify_created": 1, + "keep_history": 1, + "library_art": "/:/resources/movie-fanart.jpg", + "library_thumb": "/:/resources/movie.png", + "parent_count": null, + "section_id": 1, + "section_name": "Movies", + "section_type": "movie" + } + ``` + """ + if section_id: + library_data = libraries.Libraries() + library_details = library_data.get_details(section_id=section_id) + if library_details: + return library_details + else: + logger.warn(u"Unable to retrieve data for get_library.") + else: + logger.warn(u"Library details requested but no section_id received.") + + @cherrypy.expose + @cherrypy.tools.json_out() + @requireAuth(member_of("admin")) + @addtoapi() + def get_library_watch_time_stats(self, section_id=None, **kwargs): + """ Get a library's watch time statistics. + + ``` + Required parameters: + section_id (str): The id of the Plex library section + + Optional parameters: + None + + Returns: + json: + [{"query_days": 1, + "total_plays": 0, + "total_time": 0 + }, + {"query_days": 7, + "total_plays": 3, + "total_time": 15694 + }, + {"query_days": 30, + "total_plays": 35, + "total_time": 63054 + }, + {"query_days": 0, + "total_plays": 508, + "total_time": 1183080 + } + ] + ``` + """ + if section_id: + library_data = libraries.Libraries() + result = library_data.get_watch_time_stats(section_id=section_id) + if result: + return result + else: + logger.warn(u"Unable to retrieve data for get_library_watch_time_stats.") + else: + logger.warn(u"Library watch time stats requested but no section_id received.") + + @cherrypy.expose + @cherrypy.tools.json_out() + @requireAuth(member_of("admin")) + @addtoapi() + def get_library_user_stats(self, section_id=None, **kwargs): + """ Get a library's user statistics. + + ``` + Required parameters: + section_id (str): The id of the Plex library section + + Optional parameters: + None + + Returns: + json: + [{"friendly_name": "Jon Snow", + "total_plays": 170, + "user_id": 133788, + "user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar" + }, + {"platform_type": "DanyKhaleesi69", + "total_plays": 42, + "user_id": 8008135, + "user_thumb": "https://plex.tv/users/568gwwoib5t98a3a/avatar" + }, + {...}, + {...} + ] + ``` + """ + if section_id: + library_data = libraries.Libraries() + result = library_data.get_user_stats(section_id=section_id) + if result: + return result + else: + logger.warn(u"Unable to retrieve data for get_library_user_stats.") + else: + logger.warn(u"Library user stats requested but no section_id received.") + @cherrypy.expose @cherrypy.tools.json_out() @requireAuth(member_of("admin")) @@ -977,9 +1103,9 @@ class WebInterface(object): if not allow_session_user(user_id): raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT) - user_data = users.Users() if user_id: try: + user_data = users.Users() user_details = user_data.get_details(user_id=user_id) except: logger.warn(u"Unable to retrieve user details for user_id %s " % user_id) @@ -993,8 +1119,8 @@ class WebInterface(object): @cherrypy.expose @requireAuth(member_of("admin")) def edit_user_dialog(self, user=None, user_id=None, **kwargs): - user_data = users.Users() if user_id: + user_data = users.Users() result = user_data.get_details(user_id=user_id) status_message = '' else: @@ -1030,9 +1156,9 @@ class WebInterface(object): keep_history = kwargs.get('keep_history', 0) allow_guest = kwargs.get('allow_guest', 0) - user_data = users.Users() if user_id: try: + user_data = users.Users() user_data.set_config(user_id=user_id, friendly_name=friendly_name, custom_thumb=custom_thumb, @@ -1254,8 +1380,8 @@ class WebInterface(object): } ``` """ - user_data = users.Users() if user_id: + user_data = users.Users() user_details = user_data.get_details(user_id=user_id) if user_details: return user_details @@ -1299,8 +1425,8 @@ class WebInterface(object): ] ``` """ - user_data = users.Users() if user_id: + user_data = users.Users() result = user_data.get_watch_time_stats(user_id=user_id) if result: return result @@ -1340,8 +1466,8 @@ class WebInterface(object): ] ``` """ - user_data = users.Users() if user_id: + user_data = users.Users() result = user_data.get_player_stats(user_id=user_id) if result: return result @@ -1368,9 +1494,8 @@ class WebInterface(object): None ``` """ - user_data = users.Users() - if user_id: + user_data = users.Users() delete_row = user_data.delete_all_history(user_id=user_id) if delete_row: return {'message': delete_row} @@ -1395,11 +1520,9 @@ class WebInterface(object): None ``` """ - user_data = users.Users() - if user_id: + user_data = users.Users() delete_row = user_data.delete(user_id=user_id) - if delete_row: return {'message': delete_row} else: @@ -1424,16 +1547,14 @@ class WebInterface(object): None ``` """ - user_data = users.Users() - if user_id: + user_data = users.Users() delete_row = user_data.undelete(user_id=user_id) - if delete_row: return {'message': delete_row} elif username: + user_data = users.Users() delete_row = user_data.undelete(username=username) - if delete_row: return {'message': delete_row} else: From 8f0ba5ba4fc42944d37408d82bc50140d1996d8c Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Sun, 29 May 2016 16:04:27 -0700 Subject: [PATCH 07/19] Add notification agent ids to the API docs --- API.md | 18 ++++++++++++++++++ plexpy/webserve.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/API.md b/API.md index 1895f99a..3e2465a8 100644 --- a/API.md +++ b/API.md @@ -1696,6 +1696,24 @@ Send a notification using PlexPy. ``` Required parameters: agent_id(str): The id of the notification agent to use + 9 # Boxcar2 + 17 # Browser + 10 # Email + 16 # Facebook + 0 # Growl + 12 # IFTTT + 18 # Join + 4 # NotifyMyAndroid + 3 # Plex Home Theater + 1 # Prowl + 5 # Pushalot + 6 # Pushbullet + 7 # Pushover + 15 # Scripts + 14 # Slack + 13 # Telegram + 11 # Twitter + 2 # XBMC subject(str): The subject of the message body(str): The body of the message diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 5a0228ea..9084b00a 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -2819,6 +2819,24 @@ class WebInterface(object): ``` Required parameters: agent_id(str): The id of the notification agent to use + 9 # Boxcar2 + 17 # Browser + 10 # Email + 16 # Facebook + 0 # Growl + 12 # IFTTT + 18 # Join + 4 # NotifyMyAndroid + 3 # Plex Home Theater + 1 # Prowl + 5 # Pushalot + 6 # Pushbullet + 7 # Pushover + 15 # Scripts + 14 # Slack + 13 # Telegram + 11 # Twitter + 2 # XBMC subject(str): The subject of the message body(str): The body of the message From 38e04bd42ad5768f9382b36f59657e911ddf6884 Mon Sep 17 00:00:00 2001 From: Tim Van Date: Mon, 30 May 2016 15:59:01 +0200 Subject: [PATCH 08/19] Allow global config setting for PMS log window size. --- plexpy/config.py | 1 + plexpy/webserve.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/plexpy/config.py b/plexpy/config.py index e1b70d53..92bc4ee0 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -46,6 +46,7 @@ _CONFIG_DEFINITIONS = { 'PMS_IP': (str, 'PMS', '127.0.0.1'), 'PMS_IS_REMOTE': (int, 'PMS', 0), 'PMS_LOGS_FOLDER': (str, 'PMS', ''), + 'PMS_LOGS_LINE_CAP': (int, 'PMS', 1000), 'PMS_NAME': (unicode, 'PMS', ''), 'PMS_PORT': (int, 'PMS', 32400), 'PMS_TOKEN': (str, 'PMS', ''), diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 9084b00a..74af8d95 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -2287,7 +2287,7 @@ class WebInterface(object): @cherrypy.tools.json_out() @requireAuth(member_of("admin")) @addtoapi() - def get_plex_log(self, window=1000, **kwargs): + def get_plex_log(self, **kwargs): """ Get the PMS logs. ``` @@ -2309,6 +2309,7 @@ class WebInterface(object): ] ``` """ + window = int(kwargs.get('window', plexpy.CONFIG.PMS_LOGS_LINE_CAP)) log_lines = [] log_type = kwargs.get('log_type', 'server') From 552a428985a57a73d9751f7eaa425ff1742dc71c Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Thu, 2 Jun 2016 17:27:23 +0200 Subject: [PATCH 09/19] allow refresh of images Allow refresh of images and fix bug where a disabled cache still would use the cache --- plexpy/webserve.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 74af8d95..4896b334 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -749,16 +749,16 @@ class WebInterface(object): Returns: json: - {"child_count": null, - "count": 887, - "do_notify": 1, - "do_notify_created": 1, - "keep_history": 1, - "library_art": "/:/resources/movie-fanart.jpg", - "library_thumb": "/:/resources/movie.png", - "parent_count": null, - "section_id": 1, - "section_name": "Movies", + {"child_count": null, + "count": 887, + "do_notify": 1, + "do_notify_created": 1, + "keep_history": 1, + "library_art": "/:/resources/movie-fanart.jpg", + "library_thumb": "/:/resources/movie.png", + "parent_count": null, + "section_id": 1, + "section_name": "Movies", "section_type": "movie" } ``` @@ -1373,7 +1373,7 @@ class WebInterface(object): "is_home_user": 1, "is_restricted": 0, "keep_history": 1, - "shared_libraries": ["10", "1", "4", "5", "15", "20", "2"], + "shared_libraries": ["10", "1", "4", "5", "15", "20", "2"], "user_id": 133788, "user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar", "username": "LordCommanderSnow" @@ -3200,7 +3200,9 @@ class WebInterface(object): @cherrypy.expose @requireAuth() - def pms_image_proxy(self, img='', rating_key=None, width='0', height='0', fallback=None, **kwargs): + def pms_image_proxy(self, img='', rating_key=None, width='0', height='0', + fallback=None, refresh=False, **kwargs): + """ Gets an image from the PMS and saves it to the image cache directory. """ if not img and not rating_key: @@ -3221,8 +3223,9 @@ class WebInterface(object): os.mkdir(c_dir) try: - if 'indexes' in img: + if not plexpy.CONFIG.CACHE_IMAGES or refresh or 'indexes' in img: raise NotFound + return serve_file(path=ffp, content_type='image/jpeg') except NotFound: From 6a58895d3772aadc7dd7acb264b0267c61a57992 Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Sat, 4 Jun 2016 08:48:58 -0700 Subject: [PATCH 10/19] Reflect path suggested in installation guide (FreeNAS) --- init-scripts/init.freenas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init-scripts/init.freenas b/init-scripts/init.freenas index 54587aa8..c19b824b 100755 --- a/init-scripts/init.freenas +++ b/init-scripts/init.freenas @@ -14,7 +14,7 @@ # default. Do not sets it as empty or it will run # as root. # plexpy_dir: Directory where PlexPy lives. -# Default: /usr/local/plexpy +# Default: /usr/local/share/plexpy # plexpy_chdir: Change to this directory before running PlexPy. # Default is same as plexpy_dir. # plexpy_pid: The name of the pidfile to create. From 0f92dc0fdfe11c52445bc989edacabf522e0ba3c Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Sat, 4 Jun 2016 12:43:45 -0700 Subject: [PATCH 11/19] Add refresh button to images --- data/interfaces/default/css/plexpy.css | 40 +++++++++++++++++++ data/interfaces/default/current_activity.html | 1 + .../default/current_activity_instance.html | 1 + data/interfaces/default/home_stats.html | 14 +++++++ data/interfaces/default/info.html | 4 ++ .../default/info_children_list.html | 3 ++ .../default/info_search_results_list.html | 7 ++++ data/interfaces/default/js/script.js | 25 +++++++++++- data/interfaces/default/library.html | 1 + .../default/library_recently_added.html | 1 + data/interfaces/default/recently_added.html | 3 ++ .../default/user_recently_watched.html | 1 + plexpy/webserve.py | 4 +- plexpy/webstart.py | 22 +++++----- 14 files changed, 114 insertions(+), 13 deletions(-) diff --git a/data/interfaces/default/css/plexpy.css b/data/interfaces/default/css/plexpy.css index ffc79a39..be3d66c9 100644 --- a/data/interfaces/default/css/plexpy.css +++ b/data/interfaces/default/css/plexpy.css @@ -1184,6 +1184,7 @@ a:hover .dashboard-recent-media-cover { margin: 0 40px 0 25px; height: 100px; overflow: visible; + position: relative; } .summary-poster-face { background-position: center; @@ -1922,6 +1923,7 @@ a .library-user-instance-box:hover { .home-platforms-instance-poster { margin-left: 0px; position: absolute; + overflow: hidden; } .home-platforms-instance-poster .home-platforms-poster-face { background-position: center; @@ -2079,6 +2081,7 @@ a .library-user-instance-box:hover { .home-platforms-instance-list-poster { position: absolute; left: 20px; + overflow: hidden; } .home-platforms-instance-list-poster .home-platforms-list-poster-face { background-position: center; @@ -2964,4 +2967,41 @@ a.no-highlight:hover { background-color: #555; border: 0px solid #444; border-radius: 3px; +} +.overlay-refresh-image { + opacity: 0; + color: #000; + font-size: 16px; + float: left; + position: absolute; + top: 0px; + right: 10px; + z-index: 1; + transition: all .1s cubic-bezier(.4,0,1,1); + -webkit-transition: all .1s cubic-bezier(.4,0,1,1); + -moz-transition: all .1s cubic-bezier(.4,0,1,1); + -o-transition: all .1s cubic-bezier(.4,0,1,1); + text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff; +} +.overlay-refresh-image.left { + left: 10px; +} +.overlay-refresh-image.info-art { + color: #999; + top: 15px; + right: 25px; + opacity: 1; + text-shadow: none; + cursor: pointer; +} +.overlay-refresh-image.info-art:hover { + color: #fff; + text-shadow: none; +} +a:hover .overlay-refresh-image { + opacity: .25; + top: 8px; +} +a:hover .overlay-refresh-image:hover { + opacity: .9; } \ No newline at end of file diff --git a/data/interfaces/default/current_activity.html b/data/interfaces/default/current_activity.html index 675904a5..8446d7c6 100644 --- a/data/interfaces/default/current_activity.html +++ b/data/interfaces/default/current_activity.html @@ -106,6 +106,7 @@ DOCUMENTATION :: END % else:
% endif +
+ @@ -52,24 +64,23 @@
- - - - - + + + + + - - +
TimestampLevelMessage
TimestampLevelMessage
- - - - - + + + + + @@ -126,7 +137,8 @@
-
Refresh rate: +
+ Refresh rate:
TimestampLevelMessage
TimestampLevelMessage