From 7193b6518b6c04da91d391428023ab5d039a8381 Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Sat, 30 Jan 2016 00:40:06 -0800 Subject: [PATCH 01/13] Fix removing unique constraints from database --- CHANGELOG.md | 6 +++--- plexpy/__init__.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d928908..f073bb24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,10 @@ * Fix: Libraries and Users lists not refreshing. * Fix: Server verification in settings. * Fix: Empty libraries not added to database. -* Add: Unique identifier to notification options. -* Remove: Media type toggles for recently added notifications. +* Add: Unique identifiers to notification options. +* Remove: Requirement of media type toggles for recently added notifications. * Remove: Built in Twitter key and secret. -* Remove: Unnecessary quoting of script arguments. +* Change: Unnecessary quoting of script arguments. * Change: Facebook notification instructions. diff --git a/plexpy/__init__.py b/plexpy/__init__.py index 6827df72..806833b6 100644 --- a/plexpy/__init__.py +++ b/plexpy/__init__.py @@ -713,8 +713,8 @@ def dbcheck(): # Upgrade library_sections table from earlier versions (remove UNIQUE constraint on section_id) try: - result = c_db.execute('PRAGMA index_xinfo("sqlite_autoindex_library_sections_1")') - if result and 'server_id' not in [row[2] for row in result]: + result = c_db.execute('SELECT SQL FROM sqlite_master WHERE type="table" AND name="library_sections"').fetchone() + if 'section_id INTEGER UNIQUE' in result[0]: logger.debug(u"Altering database. Removing unique constraint on section_id from library_sections table.") c_db.execute( 'CREATE TABLE library_sections_temp (id INTEGER PRIMARY KEY AUTOINCREMENT, ' @@ -760,8 +760,8 @@ def dbcheck(): # Upgrade users table from earlier versions (remove UNIQUE constraint on username) try: - result = c_db.execute('PRAGMA index_xinfo("sqlite_autoindex_users_2")') - if result and 'username' in [row[2] for row in result]: + result = c_db.execute('SELECT SQL FROM sqlite_master WHERE type="table" AND name="users"').fetchone() + if 'username TEXT NOT NULL UNIQUE' in result[0]: logger.debug(u"Altering database. Removing unique constraint on username from users table.") c_db.execute( 'CREATE TABLE users_temp (id INTEGER PRIMARY KEY AUTOINCREMENT, ' From 4d156a8911334da4c2730e022e7ed5ae47bdf7a0 Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Sat, 30 Jan 2016 00:48:51 -0800 Subject: [PATCH 02/13] Allow expanding of media info table when missing added at date --- data/interfaces/default/js/tables/media_info_table.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data/interfaces/default/js/tables/media_info_table.js b/data/interfaces/default/js/tables/media_info_table.js index d9d745cc..87615398 100644 --- a/data/interfaces/default/js/tables/media_info_table.js +++ b/data/interfaces/default/js/tables/media_info_table.js @@ -34,9 +34,12 @@ media_info_table_options = { "targets": [0], "data": "added_at", "createdCell": function (td, cellData, rowData, row, col) { - if (cellData !== null && cellData !== '') { + if (rowData) { var expand_details = ''; - var date = moment(cellData, "X").format(date_format); + var date = ''; + if (cellData !== null && cellData !== '') { + date = moment(cellData, "X").format(date_format); + } if (rowData['media_type'] === 'show') { expand_details = ''; $(td).html('
' + expand_details + ' ' + date + '
'); From b2292e98c145b960a3afb93f79b05cd17c542258 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Sat, 30 Jan 2016 23:09:48 +0100 Subject: [PATCH 03/13] add support for powershell --- plexpy/notifiers.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/plexpy/notifiers.py b/plexpy/notifiers.py index 1623cd8d..85b5cf1a 100644 --- a/plexpy/notifiers.py +++ b/plexpy/notifiers.py @@ -34,7 +34,7 @@ from pynma import pynma import gntp.notifier import oauth2 as oauth import pythontwitter as twitter -import pythonfacebook as facebook +import pythonfacebook as facebook import plexpy from plexpy import logger, helpers, request @@ -58,7 +58,7 @@ AGENT_IDS = {"Growl": 0, "Scripts": 15, "Facebook": 16} - + def available_notification_agents(): agents = [{'name': 'Growl', 'id': AGENT_IDS['Growl'], @@ -1777,7 +1777,7 @@ class SLACK(object): class Scripts(object): def __init__(self, **kwargs): - self.script_exts = ('.bat', '.cmd', '.exe', '.php', '.pl', '.py', '.pyw', '.rb', '.sh') + self.script_exts = ('.bat', '.cmd', '.exe', '.php', '.pl', '.py', '.pyw', '.rb', '.sh', '.ps1') def conf(self, options): return cherrypy.config['config'].get('Scripts', options) @@ -1807,7 +1807,7 @@ class Scripts(object): return scripts - def notify(self, subject='', message='', notify_action='', script_args=[], *args, **kwargs): + def notify(self, subject='', message='', notify_action='', script_args=None, *args, **kwargs): """ Args: subject(string, optional): Head text, @@ -1817,7 +1817,10 @@ class Scripts(object): """ logger.debug(u"PlexPy Notifiers :: Trying to run notify script, action: %s, arguments: %s" % (notify_action if notify_action else None, script_args if script_args else None)) - + + if script_args is None: + script_args = [] + if not plexpy.CONFIG.SCRIPTS_FOLDER: return @@ -1879,6 +1882,8 @@ class Scripts(object): prefix = 'perl' elif ext == '.rb': prefix = 'ruby' + elif ext == '.ps1': + prefix = 'powershell -executionPolicy bypass -file' else: prefix = '' @@ -1886,7 +1891,10 @@ class Scripts(object): script = script.encode(plexpy.SYS_ENCODING, 'ignore') if prefix: - script = [prefix, script] + if ext == '.ps1': + script = prefix.split() + [script] + else: + script = [prefix, script] else: script = [script] @@ -2025,7 +2033,7 @@ class Scripts(object): return config_option - + class FacebookNotifier(object): def __init__(self): @@ -2050,7 +2058,7 @@ class FacebookNotifier(object): def _get_credentials(self, code): logger.info(u"PlexPy Notifiers :: Requesting access token from Facebook") - + try: # Request user access token api = facebook.GraphAPI(version='2.5') @@ -2059,19 +2067,19 @@ class FacebookNotifier(object): app_id=self.app_id, app_secret=self.app_secret) access_token = response['access_token'] - + # Request extended user access token api = facebook.GraphAPI(access_token=access_token, version='2.5') response = api.extend_access_token(app_id=self.app_id, app_secret=self.app_secret) access_token = response['access_token'] - + plexpy.CONFIG.FACEBOOK_TOKEN = access_token plexpy.CONFIG.write() except Exception as e: logger.error(u"PlexPy Notifiers :: Error requesting Facebook access token: %s" % e) return False - + return True def _post_facebook(self, message=None): From 1ff1270bfa814db7b0fdafa21e0ce1b0689dbeb2 Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Sat, 30 Jan 2016 16:18:45 -0800 Subject: [PATCH 04/13] Clean up powershell for scripts --- plexpy/notifiers.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/plexpy/notifiers.py b/plexpy/notifiers.py index 85b5cf1a..092eb9b9 100644 --- a/plexpy/notifiers.py +++ b/plexpy/notifiers.py @@ -1777,7 +1777,7 @@ class SLACK(object): class Scripts(object): def __init__(self, **kwargs): - self.script_exts = ('.bat', '.cmd', '.exe', '.php', '.pl', '.py', '.pyw', '.rb', '.sh', '.ps1') + self.script_exts = ('.bat', '.cmd', '.exe', '.php', '.pl', '.ps1', '.py', '.pyw', '.rb', '.sh') def conf(self, options): return cherrypy.config['config'].get('Scripts', options) @@ -1872,18 +1872,18 @@ class Scripts(object): name, ext = os.path.splitext(script) - if ext == '.py': - prefix = 'python' - elif ext == '.pyw': - prefix = 'pythonw' - elif ext == '.php': + if ext == '.php': prefix = 'php' elif ext == '.pl': prefix = 'perl' - elif ext == '.rb': - prefix = 'ruby' elif ext == '.ps1': prefix = 'powershell -executionPolicy bypass -file' + elif ext == '.py': + prefix = 'python' + elif ext == '.pyw': + prefix = 'pythonw' + elif ext == '.rb': + prefix = 'ruby' else: prefix = '' @@ -1891,10 +1891,7 @@ class Scripts(object): script = script.encode(plexpy.SYS_ENCODING, 'ignore') if prefix: - if ext == '.ps1': - script = prefix.split() + [script] - else: - script = [prefix, script] + script = prefix.split() + [script] else: script = [script] From c17bf79d7929a9f8e10e2c20064c0d3021de27a0 Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Sun, 31 Jan 2016 11:32:44 -0800 Subject: [PATCH 05/13] Fix server verification for unpublished servers --- data/interfaces/default/settings.html | 19 ++++++----- data/interfaces/default/welcome.html | 7 ++-- plexpy/http_handler.py | 9 ++--- plexpy/webserve.py | 47 +++++++++++++++------------ 4 files changed, 45 insertions(+), 37 deletions(-) diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index f05d4ba7..f67bdd59 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -1569,11 +1569,11 @@ $(document).ready(function() { }); function verifyServer(_callback) { - var pms_ip = $("#pms_ip").val() - var pms_port = $("#pms_port").val() - var pms_identifier = $("#pms_identifier").val() - var pms_ssl = $("#pms_ssl").val() - var pms_is_remote = $("#pms_is_remote").val() + var pms_ip = $("#pms_ip").val(); + var pms_port = $("#pms_port").val(); + var pms_identifier = $("#pms_identifier").val(); + var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0; + var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0; if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) { $("#pms-verify").html(''); $('#pms-verify').fadeIn('fast'); @@ -1582,15 +1582,16 @@ $(document).ready(function() { data : { hostname: pms_ip, port: pms_port, identifier: pms_identifier, ssl: pms_ssl, remote: pms_is_remote }, cache: true, async: true, - timeout: 5000, + timeout: 10000, error: function(jqXHR, textStatus, errorThrown) { $("#pms-verify").html(''); $('#pms-verify').fadeIn('fast'); $("#pms-ip-group").addClass("has-error"); }, - success: function (xml) { - if ($(xml).find('MediaContainer').attr('machineIdentifier')) { - $("#pms_identifier").val($(xml).find('MediaContainer').attr('machineIdentifier')); + success: function (json) { + var machine_identifier = json; + if (machine_identifier) { + $("#pms_identifier").val(machine_identifier); $("#pms-verify").html(''); $('#pms-verify').fadeIn('fast'); $("#pms-ip-group").removeClass("has-error"); diff --git a/data/interfaces/default/welcome.html b/data/interfaces/default/welcome.html index a717ad0e..5174ade1 100644 --- a/data/interfaces/default/welcome.html +++ b/data/interfaces/default/welcome.html @@ -393,9 +393,10 @@ from plexpy import common $("#pms-verify-status").html(' This is not a Plex Server!'); $('#pms-verify-status').fadeIn('fast'); }, - success: function (xml) { - if ($(xml).find('MediaContainer').attr('machineIdentifier')) { - $("#pms_identifier").val($(xml).find('MediaContainer').attr('machineIdentifier')); + success: function (json) { + var machine_identifier = json; + if (machine_identifier) { + $("#pms_identifier").val(machine_identifier); $("#pms-verify-status").html(' Server found!'); $('#pms-verify-status').fadeIn('fast'); pms_verified = true; diff --git a/plexpy/http_handler.py b/plexpy/http_handler.py index c85d2958..013512ba 100644 --- a/plexpy/http_handler.py +++ b/plexpy/http_handler.py @@ -44,7 +44,8 @@ class HTTPHandler(object): headers=None, output_format='raw', return_type=False, - no_token=False): + no_token=False, + timeout=20): valid_request_types = ['GET', 'POST', 'PUT', 'DELETE'] @@ -56,12 +57,12 @@ class HTTPHandler(object): if proto.upper() == 'HTTPS': if not self.ssl_verify and hasattr(ssl, '_create_unverified_context'): context = ssl._create_unverified_context() - handler = HTTPSConnection(host=self.host, port=self.port, timeout=20, context=context) + handler = HTTPSConnection(host=self.host, port=self.port, timeout=timeout, context=context) logger.warn(u"PlexPy HTTP Handler :: Unverified HTTPS request made. This connection is not secure.") else: - handler = HTTPSConnection(host=self.host, port=self.port, timeout=20) + handler = HTTPSConnection(host=self.host, port=self.port, timeout=timeout) else: - handler = HTTPConnection(host=self.host, port=self.port, timeout=20) + handler = HTTPConnection(host=self.host, port=self.port, timeout=timeout) token_string = '' if not no_token: diff --git a/plexpy/webserve.py b/plexpy/webserve.py index a70b8252..a73b5576 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -1382,7 +1382,11 @@ class WebInterface(object): @cherrypy.expose def get_server_id(self, hostname=None, port=None, identifier=None, ssl=0, remote=0, **kwargs): - if not identifier: + from plexpy import http_handler + + # Attempt to get the pms_identifier from plex.tv if the server is published + # Works for all PMS SSL settings + if not identifier and hostname and port: plex_tv = plextv.PlexTV() servers = plex_tv.discover() @@ -1391,27 +1395,28 @@ class WebInterface(object): identifier = server['clientIdentifier'] break - if identifier and hostname and port: - # Set PMS attributes to get the real PMS url - plexpy.CONFIG.__setattr__('PMS_IP', hostname) - plexpy.CONFIG.__setattr__('PMS_PORT', port) - plexpy.CONFIG.__setattr__('PMS_IDENTIFIER', identifier) - plexpy.CONFIG.__setattr__('PMS_SSL', ssl) - plexpy.CONFIG.__setattr__('PMS_IS_REMOTE', remote) - plexpy.CONFIG.write() - - plextv.get_real_pms_url() - - pms_connect = pmsconnect.PmsConnect() - request = pms_connect.get_local_server_identity() - - if request: - cherrypy.response.headers['Content-type'] = 'application/xml' - return request - else: - logger.warn(u"Unable to retrieve data for get_server_id.") - return None + # Fallback to checking /identity endpoint is server is unpublished + # Cannot set SSL settings on the PMS if unpublished so 'http' is okay + if not identifier: + request_handler = http_handler.HTTPHandler(host=hostname, + port=port, + token=None) + uri = '/identity' + request = request_handler.make_request(uri=uri, + proto='http', + request_type='GET', + output_format='xml', + no_token=True, + timeout=10) + if request: + xml_head = request.getElementsByTagName('MediaContainer')[0] + identifier = xml_head.getAttribute('machineIdentifier') + + if identifier: + cherrypy.response.headers['Content-type'] = 'application/json' + return json.dumps(identifier) else: + logger.warn('Unable to retrieve the PMS identifier.') return None @cherrypy.expose From 3248e6500e8043f5165160dd458032fdbd3f462d Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Sun, 31 Jan 2016 13:34:51 -0800 Subject: [PATCH 06/13] Clean up build_notify_text * session is now a dict, so no need for "default values" --- plexpy/notification_handler.py | 158 ++++++++++++--------------------- 1 file changed, 57 insertions(+), 101 deletions(-) diff --git a/plexpy/notification_handler.py b/plexpy/notification_handler.py index 2e400ca8..ab8d20f9 100644 --- a/plexpy/notification_handler.py +++ b/plexpy/notification_handler.py @@ -340,6 +340,11 @@ def set_notify_state(session, state, agent_info): def build_notify_text(session=None, timeline=None, state=None): + # Get time formats + date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','').replace('zz','') + time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz','') + duration_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz','').replace('a','').replace('A','') + # Get the server name server_name = plexpy.CONFIG.PMS_NAME @@ -428,79 +433,26 @@ def build_notify_text(session=None, timeline=None, state=None): else: full_title = metadata['title'] - duration = helpers.convert_milliseconds_to_minutes(metadata['duration']) - - # Default values - user = '' - platform = '' - player = '' - ip_address = 'N/A' - stream_duration = 0 - view_offset = 0 - container = '' - video_codec = '' - video_bitrate = '' - video_width = '' - video_height = '' - video_resolution = '' - video_framerate = '' - aspect_ratio = '' - audio_codec = '' - audio_channels = '' - transcode_decision = '' - video_decision = '' - audio_decision = '' - transcode_container = '' - transcode_video_codec = '' - transcode_video_width = '' - transcode_video_height = '' - transcode_audio_codec = '' - transcode_audio_channels = '' - user_id = '' - # Session values - if session: - # Generate a combined transcode decision value - video_decision = session['video_decision'].title() - audio_decision = session['audio_decision'].title() + if session is None: + session = {} - if session['video_decision'] == 'transcode' or session['audio_decision'] == 'transcode': - transcode_decision = 'Transcode' - elif session['video_decision'] == 'copy' or session['audio_decision'] == 'copy': - transcode_decision = 'Direct Stream' - else: - transcode_decision = 'Direct Play' - - if state != 'play': - if session['paused_counter']: - stream_duration = int((time.time() - helpers.cast_to_float(session['started']) - - helpers.cast_to_float(session['paused_counter'])) / 60) - else: - stream_duration = int((time.time() - helpers.cast_to_float(session['started'])) / 60) - - view_offset = helpers.convert_milliseconds_to_minutes(session['view_offset']) - user = session['friendly_name'] - platform = session['platform'] - player = session['player'] - ip_address = session['ip_address'] if session['ip_address'] else 'N/A' - container = session['container'] - video_codec = session['video_codec'] - video_bitrate = session['bitrate'] - video_width = session['width'] - video_height = session['height'] - video_resolution = session['video_resolution'] - video_framerate = session['video_framerate'] - aspect_ratio = session['aspect_ratio'] - audio_codec = session['audio_codec'] - audio_channels = session['audio_channels'] - transcode_container = session['transcode_container'] - transcode_video_codec = session['transcode_video_codec'] - transcode_video_width = session['transcode_width'] - transcode_video_height = session['transcode_height'] - transcode_audio_codec = session['transcode_audio_codec'] - transcode_audio_channels = session['transcode_audio_channels'] - user_id = session['user_id'] + # Generate a combined transcode decision value + if session.get('video_decision','') == 'transcode' or session.get('audio_decision','') == 'transcode': + transcode_decision = 'Transcode' + elif session.get('video_decision','') == 'copy' or session.get('audio_decision','') == 'copy': + transcode_decision = 'Direct Stream' + else: + transcode_decision = 'Direct Play' + + if state != 'play': + stream_duration = int((time.time() - helpers.cast_to_float(session.get('started', 0)) - + helpers.cast_to_float(session.get('paused_counter', 0))) / 60) + else: + stream_duration = 0 + view_offset = helpers.convert_milliseconds_to_minutes(session.get('view_offset', 0)) + duration = helpers.convert_milliseconds_to_minutes(metadata['duration']) progress_percent = helpers.get_percent(view_offset, duration) # Fix metadata params for notify recently added grandparent @@ -516,42 +468,46 @@ def build_notify_text(session=None, timeline=None, state=None): artist_name = metadata['grandparent_title'] album_name = metadata['parent_title'] track_name = metadata['title'] - - available_params = {'server_name': server_name, + + available_params = {# Global paramaters + 'server_name': server_name, 'server_uptime': server_uptime, - 'action': state, - 'datestamp': arrow.now().format(plexpy.CONFIG.DATE_FORMAT.replace('Do','').replace('zz','')), - 'timestamp': arrow.now().format(plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz','')), + 'action': state.title(), + 'datestamp': arrow.now().format(date_format), + 'timestamp': arrow.now().format(time_format), + # Stream parameters 'streams': stream_count, - 'user': user, - 'platform': platform, - 'player': player, - 'ip_address': ip_address, - 'media_type': metadata['media_type'], + 'user': session.get('friendly_name',''), + 'platform': session.get('platform',''), + 'player': session.get('player',''), + 'ip_address': session.get('ip_address','N/A'), 'stream_duration': stream_duration, 'remaining_duration': duration - view_offset, 'progress': view_offset, 'progress_percent': progress_percent, - 'container': container, - 'video_codec': video_codec, - 'video_bitrate': video_bitrate, - 'video_width': video_width, - 'video_height': video_height, - 'video_resolution': video_resolution, - 'video_framerate': video_framerate, - 'aspect_ratio': aspect_ratio, - 'audio_codec': audio_codec, - 'audio_channels': audio_channels, + 'container': session.get('container',''), + 'video_codec': session.get('video_codec',''), + 'video_bitrate': session.get('bitrate',''), + 'video_width': session.get('width',''), + 'video_height': session.get('height',''), + 'video_resolution': session.get('video_resolution',''), + 'video_framerate': session.get('video_framerate',''), + 'aspect_ratio': session.get('aspect_ratio',''), + 'audio_codec': session.get('audio_codec',''), + 'audio_channels': session.get('audio_channels',''), 'transcode_decision': transcode_decision, - 'video_decision': video_decision, - 'audio_decision': audio_decision, - 'transcode_container': transcode_container, - 'transcode_video_codec': transcode_video_codec, - 'transcode_video_width': transcode_video_width, - 'transcode_video_height': transcode_video_height, - 'transcode_audio_codec': transcode_audio_codec, - 'transcode_audio_channels': transcode_audio_channels, - 'user_id': user_id, + 'video_decision': session.get('video_decision','').title(), + 'audio_decision': session.get('audio_decision','').title(), + 'transcode_container': session.get('transcode_container',''), + 'transcode_video_codec': session.get('transcode_video_codec',''), + 'transcode_video_width': session.get('transcode_width',''), + 'transcode_video_height': session.get('transcode_height',''), + 'transcode_audio_codec': session.get('transcode_audio_codec',''), + 'transcode_audio_channels': session.get('transcode_audio_channels',''), + 'session_key': session.get('session_key',''), + 'user_id': session.get('user_id',''), + # Metadata parameters + 'media_type': metadata['media_type'], 'title': full_title, 'library_name': metadata['library_name'], 'show_name': show_name, @@ -575,7 +531,7 @@ def build_notify_text(session=None, timeline=None, state=None): 'summary': metadata['summary'], 'tagline': metadata['tagline'], 'rating': metadata['rating'], - 'duration': duration, + 'duration': metadata['duration'], 'section_id': metadata['section_id'], 'rating_key': metadata['rating_key'], 'parent_rating_key': metadata['parent_rating_key'], @@ -907,4 +863,4 @@ def build_server_notify_text(state=None): def strip_tag(data): p = re.compile(r'<.*?>') - return p.sub('', data) + return p.sub('', data) \ No newline at end of file From fae9bc618a0610d08ae7a70a05ec5ba614086615 Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Sun, 31 Jan 2016 15:13:35 -0800 Subject: [PATCH 07/13] Initialize PlexPy after daemonizing --- PlexPy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PlexPy.py b/PlexPy.py index cafb7804..15e3212e 100755 --- a/PlexPy.py +++ b/PlexPy.py @@ -153,12 +153,12 @@ def main(): # Put the database in the DATA_DIR plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, 'plexpy.db') - # Read config and start logging - plexpy.initialize(config_file) - if plexpy.DAEMON: plexpy.daemonize() + # Read config and start logging + plexpy.initialize(config_file) + # Force the http port if neccessary if args.port: http_port = args.port From 14a90d84ec62756f764e12eeeda3f943c5a448ac Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Sun, 31 Jan 2016 16:15:06 -0800 Subject: [PATCH 08/13] Add {stream_time}, {remaining_time}, and {progress_time} to notification options --- data/interfaces/default/settings.html | 22 +++++++++++++++++++--- plexpy/config.py | 19 ++++++++++++++++++- plexpy/helpers.py | 9 +++++++++ plexpy/notification_handler.py | 16 +++++++++++----- 4 files changed, 57 insertions(+), 9 deletions(-) diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index f67bdd59..bbcec5f6 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -1093,11 +1093,11 @@ available_notification_agents = sorted(notifiers.available_notification_agents() {datestamp} - The date the notification was triggered. + The date (in date format) the notification was triggered. {timestamp} - The time the notification was triggered. + The time (in time format) the notification was triggered. @@ -1134,14 +1134,26 @@ available_notification_agents = sorted(notifiers.available_notification_agents() {stream_duration} The stream duration (in minutes) for the item. + + {stream_time} + The stream duration (in time format) for the item. + {remaining_duration} The remaining duration (in minutes) for the item. - {progress} + {remaining_time} + The remaining duration (in time format) for the item. + + + {progress_duration} The last reported offset (in minutes) for the item. + + {progress_time} + The last reported offset (in time format) for the item. + {progress_percent} The last reported progress percent for the item. @@ -1222,6 +1234,10 @@ available_notification_agents = sorted(notifiers.available_notification_agents() {transcode_audio_channels} The audio channels of the transcoded media. + + {session_key} + The unique identifier for the session. + {user_id} The unique identifier for the user. diff --git a/plexpy/config.py b/plexpy/config.py index 9cc23068..45eb1c79 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -512,6 +512,7 @@ class Config(object): self.MOVIE_LOGGING_ENABLE = 0 self.TV_LOGGING_ENABLE = 0 self.CONFIG_VERSION = '1' + if self.CONFIG_VERSION == '1': # Change home_stats_cards to list if self.HOME_STATS_CARDS: @@ -525,4 +526,20 @@ class Config(object): if 'library_statistics' in home_library_cards: home_library_cards.remove('library_statistics') self.HOME_LIBRARY_CARDS = home_library_cards - self.CONFIG_VERSION = '2' \ No newline at end of file + self.CONFIG_VERSION = '2' + + if self.CONFIG_VERSION == '2': + self.NOTIFY_ON_START_SUBJECT_TEXT = self.NOTIFY_ON_START_SUBJECT_TEXT.replace('{progress}','{progress_duration}') + self.NOTIFY_ON_START_BODY_TEXT = self.NOTIFY_ON_START_BODY_TEXT.replace('{progress}','{progress_duration}') + self.NOTIFY_ON_STOP_SUBJECT_TEXT = self.NOTIFY_ON_STOP_SUBJECT_TEXT.replace('{progress}','{progress_duration}') + self.NOTIFY_ON_STOP_BODY_TEXT = self.NOTIFY_ON_STOP_BODY_TEXT.replace('{progress}','{progress_duration}') + self.NOTIFY_ON_PAUSE_SUBJECT_TEXT = self.NOTIFY_ON_PAUSE_SUBJECT_TEXT.replace('{progress}','{progress_duration}') + self.NOTIFY_ON_PAUSE_BODY_TEXT = self.NOTIFY_ON_PAUSE_BODY_TEXT.replace('{progress}','{progress_duration}') + self.NOTIFY_ON_RESUME_SUBJECT_TEXT = self.NOTIFY_ON_RESUME_SUBJECT_TEXT.replace('{progress}','{progress_duration}') + self.NOTIFY_ON_RESUME_BODY_TEXT = self.NOTIFY_ON_RESUME_BODY_TEXT.replace('{progress}','{progress_duration}') + self.NOTIFY_ON_BUFFER_SUBJECT_TEXT = self.NOTIFY_ON_BUFFER_SUBJECT_TEXT.replace('{progress}','{progress_duration}') + self.NOTIFY_ON_BUFFER_BODY_TEXT = self.NOTIFY_ON_BUFFER_BODY_TEXT.replace('{progress}','{progress_duration}') + self.NOTIFY_ON_WATCHED_SUBJECT_TEXT = self.NOTIFY_ON_WATCHED_SUBJECT_TEXT.replace('{progress}','{progress_duration}') + self.NOTIFY_ON_WATCHED_BODY_TEXT = self.NOTIFY_ON_WATCHED_BODY_TEXT.replace('{progress}','{progress_duration}') + self.NOTIFY_SCRIPTS_ARGS_TEXT = self.NOTIFY_SCRIPTS_ARGS_TEXT.replace('{progress}','{progress_duration}') + self.CONFIG_VERSION = '3' diff --git a/plexpy/helpers.py b/plexpy/helpers.py index cb11e87d..a5d9e2d4 100644 --- a/plexpy/helpers.py +++ b/plexpy/helpers.py @@ -135,6 +135,15 @@ def convert_seconds(s): return minutes +def convert_seconds_to_minutes(s): + + if str(s).isdigit(): + minutes = round(float(s) / 60, 0) + + return math.trunc(minutes) + + return 0 + def today(): today = datetime.date.today() diff --git a/plexpy/notification_handler.py b/plexpy/notification_handler.py index ab8d20f9..05e19798 100644 --- a/plexpy/notification_handler.py +++ b/plexpy/notification_handler.py @@ -446,14 +446,17 @@ def build_notify_text(session=None, timeline=None, state=None): transcode_decision = 'Direct Play' if state != 'play': - stream_duration = int((time.time() - helpers.cast_to_float(session.get('started', 0)) - - helpers.cast_to_float(session.get('paused_counter', 0))) / 60) + stream_duration = helpers.convert_seconds_to_minutes( + time.time() - + helpers.cast_to_float(session.get('started', 0)) - + helpers.cast_to_float(session.get('paused_counter', 0))) else: stream_duration = 0 view_offset = helpers.convert_milliseconds_to_minutes(session.get('view_offset', 0)) duration = helpers.convert_milliseconds_to_minutes(metadata['duration']) progress_percent = helpers.get_percent(view_offset, duration) + remaining_duration = duration - view_offset # Fix metadata params for notify recently added grandparent if state == 'created' and plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_GRANDPARENT: @@ -468,7 +471,7 @@ def build_notify_text(session=None, timeline=None, state=None): artist_name = metadata['grandparent_title'] album_name = metadata['parent_title'] track_name = metadata['title'] - + available_params = {# Global paramaters 'server_name': server_name, 'server_uptime': server_uptime, @@ -482,8 +485,11 @@ def build_notify_text(session=None, timeline=None, state=None): 'player': session.get('player',''), 'ip_address': session.get('ip_address','N/A'), 'stream_duration': stream_duration, - 'remaining_duration': duration - view_offset, - 'progress': view_offset, + 'stream_time': arrow.get(stream_duration * 60).format(duration_format), + 'remaining_duration': remaining_duration, + 'remaining_time': arrow.get(remaining_duration * 60).format(duration_format), + 'progress_duration': view_offset, + 'progress_time': arrow.get(view_offset * 60).format(duration_format), 'progress_percent': progress_percent, 'container': session.get('container',''), 'video_codec': session.get('video_codec',''), From a957e8eb4fd5d29205210ab5207897c1619e3c07 Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Tue, 2 Feb 2016 20:33:08 -0800 Subject: [PATCH 09/13] Clean up time formats for server notifications --- plexpy/notification_handler.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/plexpy/notification_handler.py b/plexpy/notification_handler.py index 05e19798..6962c298 100644 --- a/plexpy/notification_handler.py +++ b/plexpy/notification_handler.py @@ -560,9 +560,9 @@ def build_notify_text(session=None, timeline=None, state=None): if state == 'play': # Default body text - body_text = '%s (%s) is watching %s' % (session['friendly_name'], - session['player'], - full_title) + body_text = '%s (%s) started playing %s' % (session['friendly_name'], + session['player'], + full_title) if on_start_subject and on_start_body: try: @@ -729,6 +729,10 @@ def build_notify_text(session=None, timeline=None, state=None): def build_server_notify_text(state=None): + # Get time formats + date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','').replace('zz','') + time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz','') + # Get the server name server_name = plexpy.CONFIG.PMS_NAME @@ -753,11 +757,12 @@ def build_server_notify_text(state=None): on_intup_body = plexpy.CONFIG.NOTIFY_ON_INTUP_BODY_TEXT script_args_text = plexpy.CONFIG.NOTIFY_SCRIPTS_ARGS_TEXT - available_params = {'server_name': server_name, + available_params = {# Global paramaters + 'server_name': server_name, 'server_uptime': server_uptime, - 'action': state, - 'datestamp': arrow.now().format(plexpy.CONFIG.DATE_FORMAT.replace('Do','').replace('zz','')), - 'timestamp': arrow.now().format(plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz',''))} + 'action': state.title(), + 'datestamp': arrow.now().format(date_format), + 'timestamp': arrow.now().format(time_format)} # Default text subject_text = 'PlexPy (%s)' % server_name From 36de20dd7525ae8e95a98889b209e5abddb902cd Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Tue, 2 Feb 2016 20:33:47 -0800 Subject: [PATCH 10/13] Fix getting new pms_identifier for server only --- plexpy/plextv.py | 68 +++++++++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/plexpy/plextv.py b/plexpy/plextv.py index c45db03f..b3774342 100644 --- a/plexpy/plextv.py +++ b/plexpy/plextv.py @@ -383,7 +383,6 @@ class PlexTV(object): return [] plextv_resources = self.get_plextv_resources(include_https=include_https) - server_urls = [] try: xml_parse = minidom.parseString(plextv_resources) @@ -400,36 +399,51 @@ class PlexTV(object): logger.warn(u"PlexPy PlexTV :: Unable to parse XML for get_server_urls: %s." % e) return [] + # Function to get all connections for a device + def get_connections(device): + conn = [] + connections = device.getElementsByTagName('Connection') + + for c in connections: + server_details = {"protocol": helpers.get_xml_attr(c, 'protocol'), + "address": helpers.get_xml_attr(c, 'address'), + "port": helpers.get_xml_attr(c, 'port'), + "uri": helpers.get_xml_attr(c, 'uri'), + "local": helpers.get_xml_attr(c, 'local') + } + conn.append(server_details) + + return conn + + server_urls = [] + + # Try to match the device for a in xml_head: if helpers.get_xml_attr(a, 'clientIdentifier') == server_id: - connections = a.getElementsByTagName('Connection') - for connection in connections: - server_details = {"protocol": helpers.get_xml_attr(connection, 'protocol'), - "address": helpers.get_xml_attr(connection, 'address'), - "port": helpers.get_xml_attr(connection, 'port'), - "uri": helpers.get_xml_attr(connection, 'uri'), - "local": helpers.get_xml_attr(connection, 'local') - } + server_urls = get_connections(a) + break + + # Else no device match found + if not server_urls: + # Try to match the PMS_IP and PMS_PORT + for a in xml_head: + if helpers.get_xml_attr(a, 'provides') == 'server': + connections = a.getElementsByTagName('Connection') - server_urls.append(server_details) - # Else try to match the PMS_IP and PMS_PORT - else: - connections = a.getElementsByTagName('Connection') - for connection in connections: - if helpers.get_xml_attr(connection, 'address') == plexpy.CONFIG.PMS_IP and \ - int(helpers.get_xml_attr(connection, 'port')) == plexpy.CONFIG.PMS_PORT: + for connection in connections: + if helpers.get_xml_attr(connection, 'address') == plexpy.CONFIG.PMS_IP and \ + int(helpers.get_xml_attr(connection, 'port')) == plexpy.CONFIG.PMS_PORT: + + plexpy.CONFIG.PMS_IDENTIFIER = helpers.get_xml_attr(a, 'clientIdentifier') + plexpy.CONFIG.write() + + logger.info(u"PlexPy PlexTV :: PMS identifier changed from %s to %s." % \ + (server_id, plexpy.CONFIG.PMS_IDENTIFIER)) + + server_urls = get_connections(a) + break - plexpy.CONFIG.PMS_IDENTIFIER = helpers.get_xml_attr(a, 'clientIdentifier') - - logger.info(u"PlexPy PlexTV :: PMS identifier changed from %s to %s." % \ - (server_id, plexpy.CONFIG.PMS_IDENTIFIER)) - - server_details = {"protocol": helpers.get_xml_attr(connection, 'protocol'), - "address": helpers.get_xml_attr(connection, 'address'), - "port": helpers.get_xml_attr(connection, 'port'), - "uri": helpers.get_xml_attr(connection, 'uri'), - "local": helpers.get_xml_attr(connection, 'local') - } + if server_urls: break return server_urls From ee754ea53321c2eda6b2cbb757584e4d0542c925 Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Tue, 2 Feb 2016 20:38:16 -0800 Subject: [PATCH 11/13] Remove trailing slash from Facebook redirect URI --- data/interfaces/default/notification_config.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/interfaces/default/notification_config.html b/data/interfaces/default/notification_config.html index 6ae4a0c6..d071ce77 100644 --- a/data/interfaces/default/notification_config.html +++ b/data/interfaces/default/notification_config.html @@ -181,6 +181,10 @@ from plexpy import helpers }); $('#facebookStep1').click(function () { + // Remove trailing '/' from Facebook redirect URI + if ($('#facebook_redirect_uri') && $('#facebook_redirect_uri').val().endsWith('/')) { + $('#facebook_redirect_uri').val($('#facebook_redirect_uri').val().slice(0, -1)); + } doAjaxCall('set_notification_config', $(this), 'tabs', true); $.get('facebookStep1', function (data) { window.open(data); }) .done(function () { showMsg(' Confirm Authorization. Check pop-up blocker if no response.', false, true, 3000); }); From 9cd6396c352631205b6737e0d93fbd3664c38617 Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Tue, 2 Feb 2016 20:54:34 -0800 Subject: [PATCH 12/13] Add method to delete duplicate libraries --- plexpy/libraries.py | 18 ++++++++++++++++++ plexpy/webserve.py | 15 ++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/plexpy/libraries.py b/plexpy/libraries.py index b764077b..cbe2e33e 100644 --- a/plexpy/libraries.py +++ b/plexpy/libraries.py @@ -879,3 +879,21 @@ class Libraries(object): return 'Unable to delete media info table cache, section_id not valid.' except Exception as e: logger.warn(u"PlexPy Libraries :: Unable to delete media info table cache: %s." % e) + + def delete_duplicate_libraries(self): + from plexpy import plextv + + monitor_db = database.MonitorDatabase() + + # Refresh the PMS_URL to make sure the server_id is updated + plextv.get_real_pms_url() + + server_id = plexpy.CONFIG.PMS_IDENTIFIER + + try: + logger.debug(u"PlexPy Libraries :: Deleting libraries where server_id does not match %s." % server_id) + monitor_db.action('DELETE FROM library_sections WHERE server_id != ?', [server_id]) + + return 'Deleted duplicate libraries from the database.' + except Exception as e: + logger.warn(u"PlexPy Libraries :: Unable to delete duplicate libraries: %s." % e) \ No newline at end of file diff --git a/plexpy/webserve.py b/plexpy/webserve.py index a73b5576..831aaef0 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -492,7 +492,20 @@ class WebInterface(object): cherrypy.response.headers['Content-type'] = 'application/json' return json.dumps({'message': 'no data received'}) else: - return json.dumps({'message': 'Cannot refresh library while getting file sizes.'}) + return json.dumps({'message': 'Cannot refresh library while getting file sizes.'}) + + @cherrypy.expose + def delete_duplicate_libraries(self): + library_data = libraries.Libraries() + + result = library_data.delete_duplicate_libraries() + + if result: + cherrypy.response.headers['Content-type'] = 'application/json' + return json.dumps({'message': result}) + else: + cherrypy.response.headers['Content-type'] = 'application/json' + return json.dumps({'message': 'Unable to delete duplicate libraries from the database.'}) ##### Users ##### From f409dda2ef46c2710f8b42160b44ec5168462217 Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Tue, 2 Feb 2016 21:10:59 -0800 Subject: [PATCH 13/13] v1.3.5 --- CHANGELOG.md | 12 ++++++++++++ plexpy/version.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f073bb24..474c778d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## v1.3.5 (2016-02-02) + +* Fix: Removing unique constraints from database. +* Fix: Unable to expand media info table when missing "Added At" date. +* Fix: Server verification for unpublished servers. +* Fix: Updating PMS identifier for server change. +* Add: {stream_time}, {remaining_time}, and {progress_time} to notification options. +* Add: Powershell script support. (Thanks @Hellowlol) +* Add: Method to delete duplicate libraries. +* Change: Daemonize before running start up tasks. + + ## v1.3.4 (2016-01-29) * Fix: Activity checker not starting with library update (history not logging). diff --git a/plexpy/version.py b/plexpy/version.py index 74c338e8..67235cab 100644 --- a/plexpy/version.py +++ b/plexpy/version.py @@ -1,2 +1,2 @@ PLEXPY_VERSION = "master" -PLEXPY_RELEASE_VERSION = "1.3.4" +PLEXPY_RELEASE_VERSION = "1.3.5"