From 8e8e5602b5c5cff7acdccaea9d87a5e5587eea07 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Tue, 18 Oct 2016 22:55:17 +0100 Subject: [PATCH] Add Plex notifications secure connect where available (PMS 1.1.4.2757 and newer with username and password). --- CHANGES.md | 1 + .../default/config_notifications.tmpl | 1 + sickbeard/helpers.py | 20 +- sickbeard/notifiers/plex.py | 176 +++++++++++------- sickbeard/webserve.py | 19 +- 5 files changed, 133 insertions(+), 84 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e3fc6625..ee7400e7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -184,6 +184,7 @@ * Change avi metadata extraction is more fault tolerant and the chance of hanging due to corrupt avi files is reduced * Change fuzzyMoment to handle air dates before ~1970 on display show page * Change limit availability of fuzzy date functions on General Config/Interface to English locale systems +* Add Plex notifications secure connect where available (PMS 1.1.4.2757 and newer with username and password) [develop changelog] * Change send nzb data to NZBGet for Anizb instead of url diff --git a/gui/slick/interfaces/default/config_notifications.tmpl b/gui/slick/interfaces/default/config_notifications.tmpl index e14a06fd..228e248e 100644 --- a/gui/slick/interfaces/default/config_notifications.tmpl +++ b/gui/slick/interfaces/default/config_notifications.tmpl @@ -20,6 +20,7 @@

$title

#end if +
diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py index cc8c57c9..a7b741fb 100644 --- a/sickbeard/helpers.py +++ b/sickbeard/helpers.py @@ -1107,9 +1107,11 @@ def getURL(url, post_data=None, params=None, headers=None, timeout=30, session=N req_headers.update(headers) session.headers.update(req_headers) - mute_connect_err = kwargs.get('mute_connect_err') - if mute_connect_err: - del(kwargs['mute_connect_err']) + mute = [] + for muted in filter( + lambda x: kwargs.get(x, False), ['mute_connect_err', 'mute_read_timeout', 'mute_connect_timeout']): + mute += [muted] + del kwargs[muted] # request session ssl verify session.verify = False @@ -1176,17 +1178,19 @@ def getURL(url, post_data=None, params=None, headers=None, timeout=30, session=N e.errno, _maybe_request_url(e)), logger.WARNING) return except requests.exceptions.ConnectionError as e: - if not mute_connect_err: + if 'mute_connect_err' not in mute: logger.log(u'Connection error msg:%s while loading URL%s' % ( e.message, _maybe_request_url(e)), logger.WARNING) return except requests.exceptions.ReadTimeout as e: - logger.log(u'Read timed out msg:%s while loading URL%s' % ( - e.message, _maybe_request_url(e)), logger.WARNING) + if 'mute_read_timeout' not in mute: + logger.log(u'Read timed out msg:%s while loading URL%s' % ( + e.message, _maybe_request_url(e)), logger.WARNING) return except (requests.exceptions.Timeout, socket.timeout) as e: - logger.log(u'Connection timed out msg:%s while loading URL %s' % ( - e.message, _maybe_request_url(e, url)), logger.WARNING) + if 'mute_connect_timeout' not in mute: + logger.log(u'Connection timed out msg:%s while loading URL %s' % ( + e.message, _maybe_request_url(e, url)), logger.WARNING) return except Exception as e: if e.message: diff --git a/sickbeard/notifiers/plex.py b/sickbeard/notifiers/plex.py index 694fcb37..ba6d8548 100644 --- a/sickbeard/notifiers/plex.py +++ b/sickbeard/notifiers/plex.py @@ -23,8 +23,7 @@ import re import sickbeard -from sickbeard import logger -from sickbeard import common +from sickbeard import common, logger from sickbeard.exceptions import ex from sickbeard.encodingKludge import fixStupidEncodings @@ -36,6 +35,14 @@ except ImportError: class PLEXNotifier: + def __init__(self): + + self.name = 'PLEX' + + def log(self, msg, level=logger.MESSAGE): + + logger.log(u'%s: %s' % (self.name, msg), level) + def _send_to_plex(self, command, host, username=None, password=None): """Handles communication to Plex hosts via HTTP API @@ -57,7 +64,7 @@ class PLEXNotifier: password = sickbeard.PLEX_PASSWORD if not host: - logger.log(u'PLEX: No host specified, check your settings', logger.ERROR) + self.log(u'No host specified, check your settings', logger.ERROR) return False for key in command: @@ -65,7 +72,7 @@ class PLEXNotifier: command[key] = command[key].encode('utf-8') enc_command = urllib.urlencode(command) - logger.log(u'PLEX: Encoded API command: ' + enc_command, logger.DEBUG) + self.log(u'Encoded API command: ' + enc_command, logger.DEBUG) url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, enc_command) try: @@ -75,21 +82,21 @@ class PLEXNotifier: base64string = base64.encodestring('%s:%s' % (username, password))[:-1] authheader = 'Basic %s' % base64string req.add_header('Authorization', authheader) - logger.log(u'PLEX: Contacting (with auth header) via url: ' + url, logger.DEBUG) + self.log(u'Contacting (with auth header) via url: ' + url, logger.DEBUG) else: - logger.log(u'PLEX: Contacting via url: ' + url, logger.DEBUG) + self.log(u'Contacting via url: ' + url, logger.DEBUG) response = urllib2.urlopen(req) result = response.read().decode(sickbeard.SYS_ENCODING) response.close() - logger.log(u'PLEX: HTTP response: ' + result.replace('\n', ''), logger.DEBUG) + self.log(u'HTTP response: ' + result.replace('\n', ''), logger.DEBUG) # could return result response = re.compile('
  • (.+\w)').findall(result) return 'OK' except (urllib2.URLError, IOError) as e: - logger.log(u'PLEX: Warning: Couldn\'t contact Plex at ' + fixStupidEncodings(url) + ' ' + ex(e), logger.WARNING) + self.log(u'Couldn\'t contact Plex at ' + fixStupidEncodings(url) + ' ' + ex(e), logger.WARNING) return False def _notify_pmc(self, message, title='SickGear', host=None, username=None, password=None, force=False): @@ -123,9 +130,10 @@ class PLEXNotifier: result = '' for curHost in [x.strip() for x in host.split(',')]: - logger.log(u'PLEX: Sending notification to \'%s\' - %s' % (curHost, message), logger.MESSAGE) + self.log(u'Sending notification to \'%s\' - %s' % (curHost, message)) - command = {'command': 'ExecBuiltIn', 'parameter': 'Notification(%s,%s)' % (title.encode('utf-8'), message.encode('utf-8'))} + command = {'command': 'ExecBuiltIn', + 'parameter': 'Notification(%s,%s)' % (title.encode('utf-8'), message.encode('utf-8'))} notify_result = self._send_to_plex(command, curHost, username, password) if notify_result: result += '%s:%s' % (curHost, str(notify_result)) @@ -154,13 +162,29 @@ class PLEXNotifier: title = common.notifyStrings[common.NOTIFY_GIT_UPDATE] self._notify_pmc(update_text + new_version, title) - def test_notify_pmc(self, host, username, password): - return self._notify_pmc('This is a test notification from SickGear', 'Test', host, username, password, force=True) + def test_notify(self, host, username, password, server=False): + if server: + return self.update_library(host=host, username=username, password=password, force=False, test=True) + return self._notify_pmc( + 'This is a test notification from SickGear', 'Test', host, username, password, force=True) - def test_notify_pms(self, host, username, password): - return self.update_library(host=host, username=username, password=password, force=False) + @staticmethod + def _get_host_list(host='', enable_secure=False): + """ + Return a list of hosts from a host CSV string + """ + host_list = [] - def update_library(self, ep_obj=None, host=None, username=None, password=None, force=True): + user_list = [x.strip().lower() for x in host.split(',')] + for cur_host in user_list: + if cur_host.startswith('https://'): + host_list += ([], [cur_host])[enable_secure] + else: + host_list += ([], ['https://%s' % cur_host])[enable_secure] + ['http://%s' % cur_host] + + return host_list + + def update_library(self, ep_obj=None, host=None, username=None, password=None, force=True, test=False): """Handles updating the Plex Media Server host via HTTP API Plex Media Server currently only supports updating the whole video library and not a specific path. @@ -170,11 +194,12 @@ class PLEXNotifier: """ - if sickbeard.USE_PLEX and sickbeard.PLEX_UPDATE_LIBRARY: + if sickbeard.USE_PLEX and sickbeard.PLEX_UPDATE_LIBRARY or test: - if not sickbeard.PLEX_SERVER_HOST: - logger.log(u'PLEX: No Plex Media Server host specified, check your settings', logger.DEBUG) - return False + if not sickbeard.PLEX_SERVER_HOST and not any([host]): + msg = u'No Plex Media Server host specified, check your settings' + self.log(msg, logger.DEBUG) + return '%sFail: %s' % (('', '
    ')[test], msg) if not host: host = sickbeard.PLEX_SERVER_HOST @@ -184,10 +209,10 @@ class PLEXNotifier: password = sickbeard.PLEX_PASSWORD # if username and password were provided, fetch the auth token from plex.tv - token_arg = '' + token_arg = None if username and password: - logger.log(u'PLEX: fetching plex.tv credentials for user: ' + username, logger.DEBUG) + self.log(u'fetching plex.tv credentials for user: ' + username, logger.DEBUG) req = urllib2.Request('https://plex.tv/users/sign_in.xml', data='') authheader = 'Basic %s' % base64.encodestring('%s:%s' % (username, password))[:-1] req.add_header('Authorization', authheader) @@ -195,6 +220,7 @@ class PLEXNotifier: req.add_header('X-Plex-Product', 'SickGear Notifier') req.add_header('X-Plex-Client-Identifier', '5f48c063eaf379a565ff56c9bb2b401e') req.add_header('X-Plex-Version', '1.0') + token_arg = False try: response = urllib2.urlopen(req) @@ -203,67 +229,89 @@ class PLEXNotifier: token_arg = '?X-Plex-Token=' + token except urllib2.URLError as e: - logger.log(u'PLEX: Error fetching credentials from from plex.tv for user %s: %s' % (username, ex(e)), logger.MESSAGE) + self.log(u'Error fetching credentials from plex.tv for user %s: %s' % (username, ex(e))) except (ValueError, IndexError) as e: - logger.log(u'PLEX: Error parsing plex.tv response: ' + ex(e), logger.MESSAGE) + self.log(u'Error parsing plex.tv response: ' + ex(e)) file_location = '' if None is ep_obj else ep_obj.location - host_list = [x.strip() for x in host.split(',')] + host_validate = self._get_host_list(host, all([token_arg])) hosts_all = {} hosts_match = {} hosts_failed = [] - for cur_host in host_list: - - url = 'http://%s/library/sections%s' % (cur_host, token_arg) - try: - xml_tree = etree.parse(urllib.urlopen(url)) - media_container = xml_tree.getroot() - except IOError as e: - logger.log(u'PLEX: Error while trying to contact Plex Media Server: ' + ex(e), logger.ERROR) + for cur_host in host_validate: + response = sickbeard.helpers.getURL( + '%s/library/sections%s' % (cur_host, token_arg or ''), timeout=10, + mute_connect_err=True, mute_read_timeout=True, mute_connect_timeout=True) + if response: + response = sickbeard.helpers.parse_xml(response) + if not response: hosts_failed.append(cur_host) continue - sections = media_container.findall('.//Directory') + sections = response.findall('.//Directory') if not sections: - logger.log(u'PLEX: Plex Media Server not running on: ' + cur_host, logger.MESSAGE) + self.log(u'Plex Media Server not running on: ' + cur_host) hosts_failed.append(cur_host) continue - for section in sections: - if 'show' == section.attrib['type']: + for section in filter(lambda x: 'show' == x.attrib['type'], sections): + if str(section.attrib['key']) in hosts_all: + continue + keyed_host = [(str(section.attrib['key']), cur_host)] + hosts_all.update(keyed_host) + if not file_location: + continue - keyed_host = [(str(section.attrib['key']), cur_host)] - hosts_all.update(keyed_host) - if not file_location: - continue + for section_location in section.findall('.//Location'): + section_path = re.sub(r'[/\\]+', '/', section_location.attrib['path'].lower()) + section_path = re.sub(r'^(.{,2})[/\\]', '', section_path) + location_path = re.sub(r'[/\\]+', '/', file_location.lower()) + location_path = re.sub(r'^(.{,2})[/\\]', '', location_path) - for section_location in section.findall('.//Location'): - section_path = re.sub(r'[/\\]+', '/', section_location.attrib['path'].lower()) - section_path = re.sub(r'^(.{,2})[/\\]', '', section_path) - location_path = re.sub(r'[/\\]+', '/', file_location.lower()) - location_path = re.sub(r'^(.{,2})[/\\]', '', location_path) + if section_path in location_path: + hosts_match.update(keyed_host) + break - if section_path in location_path: - hosts_match.update(keyed_host) + if not test: + hosts_try = (hosts_all.copy(), hosts_match.copy())[any(hosts_match)] + host_list = [] + for section_key, cur_host in hosts_try.items(): + refresh_result = None + if force: + refresh_result = sickbeard.helpers.getURL( + '%s/library/sections/%s/refresh%s' % (cur_host, section_key, token_arg or '')) + if (force and '' == refresh_result) or not force: + host_list.append(cur_host) + else: + hosts_failed.append(cur_host) + self.log(u'Error updating library section for Plex Media Server: %s' % cur_host, logger.ERROR) - hosts_try = (hosts_all.copy(), hosts_match.copy())[any(hosts_match)] - host_list = [] - for section_key, cur_host in hosts_try.items(): + if len(hosts_failed) == len(host_validate): + self.log(u'No successful Plex host updated') + return 'Fail no successful Plex host updated: %s' % ', '.join(host for host in hosts_failed) + else: + hosts = ', '.join(set(host_list)) + if len(hosts_match): + self.log(u'Hosts updating where TV section paths match the downloaded show: %s' % hosts) + else: + self.log(u'Updating all hosts with TV sections: %s' % hosts) + return '' - url = 'http://%s/library/sections/%s/refresh%s' % (cur_host, section_key, token_arg) - try: - force and urllib.urlopen(url) - host_list.append(cur_host) - except Exception as e: - logger.log(u'PLEX: Error updating library section for Plex Media Server: ' + ex(e), logger.ERROR) - hosts_failed.append(cur_host) - - if len(hosts_match): - logger.log(u'PLEX: Updating hosts where TV section paths match the downloaded show: ' + ', '.join(set(host_list)), logger.MESSAGE) - else: - logger.log(u'PLEX: Updating all hosts with TV sections: ' + ', '.join(set(host_list)), logger.MESSAGE) - - return (', '.join(set(hosts_failed)), None)[not len(hosts_failed)] + hosts = [ + host.replace('http://', '') for host in filter(lambda x: x.startswith('http:'), hosts_all.values())] + secured = [ + host.replace('https://', '') for host in filter(lambda x: x.startswith('https:'), hosts_all.values())] + failed = [ + host.replace('http://', '') for host in filter(lambda x: x.startswith('http:'), hosts_failed)] + failed_secured = ', '.join(filter( + lambda x: x not in hosts, + [host.replace('https://', '') for host in filter(lambda x: x.startswith('https:'), hosts_failed)])) + return '
    ' + '
    '.join(result for result in [ + ('', 'Fail: username/password when fetching credentials from plex.tv')[False is token_arg], + ('', 'OK (secure connect): %s' % ', '.join(secured))[any(secured)], + ('', 'OK%s: %s' % ((' (legacy connect)', '')[None is token_arg], ', '.join(hosts)))[any(hosts)], + ('', 'Fail (secure connect): %s' % failed_secured)[any(failed_secured)], + ('', 'Fail%s: %s' % ((' (legacy connect)', '')[None is token_arg], failed))[any(failed)]] if result) notifier = PLEXNotifier diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 943d1ce2..85fad43b 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -846,7 +846,7 @@ class Home(MainHandler): finalResult = '' for curHost in [x.strip() for x in host.split(',')]: - curResult = notifiers.plex_notifier.test_notify_pmc(urllib.unquote_plus(curHost), username, password) + curResult = notifiers.plex_notifier.test_notify(urllib.unquote_plus(curHost), username, password) if len(curResult.split(':')) > 2 and 'OK' in curResult.split(':')[2]: finalResult += 'Successful test notice sent to Plex client ... ' + urllib.unquote_plus(curHost) else: @@ -863,18 +863,13 @@ class Home(MainHandler): if None is not password and set('*') == set(password): password = sickbeard.PLEX_PASSWORD - finalResult = '' - - curResult = notifiers.plex_notifier.test_notify_pms(urllib.unquote_plus(host), username, password) - if None is curResult: - finalResult += 'Successful test of Plex server(s) ... ' + urllib.unquote_plus(host.replace(',', ', ')) - else: - finalResult += 'Test failed for Plex server(s) ... ' + urllib.unquote_plus(curResult.replace(',', ', ')) - finalResult += '
    ' + '\n' + cur_result = notifiers.plex_notifier.test_notify(urllib.unquote_plus(host), username, password, server=True) + final_result = (('Test result for', 'Successful test of')['Fail' not in cur_result] + + ' Plex server(s) ... %s
    \n' % cur_result) ui.notifications.message('Tested Plex Media Server host(s): ', urllib.unquote_plus(host.replace(',', ', '))) - return finalResult + return final_result def testLibnotify(self, *args, **kwargs): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -1815,11 +1810,11 @@ class Home(MainHandler): def updatePLEX(self, *args, **kwargs): result = notifiers.plex_notifier.update_library() - if None is result: + if 'Fail' not in result: ui.notifications.message( 'Library update command sent to', 'Plex Media Server host(s): ' + sickbeard.PLEX_SERVER_HOST.replace(',', ', ')) else: - ui.notifications.error('Unable to contact', 'Plex Media Server host(s): ' + result.replace(',', ', ')) + ui.notifications.error('Unable to contact', 'Plex Media Server host(s): ' + result) self.redirect('/home/') def setStatus(self, show=None, eps=None, status=None, direct=False):