Merge pull request #804 from JackDandy/feature/AddPlexSecureConnect

Add Plex notifications secure connect where available (PMS 1.1.4.2757…
This commit is contained in:
JackDandy 2016-10-19 04:51:26 +01:00 committed by GitHub
commit ab0eabc34a
5 changed files with 133 additions and 84 deletions

View file

@ -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 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 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 * 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] [develop changelog]
* Change send nzb data to NZBGet for Anizb instead of url * Change send nzb data to NZBGet for Anizb instead of url

View file

@ -20,6 +20,7 @@
<h1 class="title">$title</h1> <h1 class="title">$title</h1>
#end if #end if
<img src="$sbRoot/images/loading16#echo ('', '-dark')['dark' == $sickbeard.THEME_NAME]#.gif" height="16" width="16" style="display:none" />
<div id="config"> <div id="config">
<div id="config-content"> <div id="config-content">
<form id="configForm" action="saveNotifications" method="post"> <form id="configForm" action="saveNotifications" method="post">

View file

@ -1107,9 +1107,11 @@ def getURL(url, post_data=None, params=None, headers=None, timeout=30, session=N
req_headers.update(headers) req_headers.update(headers)
session.headers.update(req_headers) session.headers.update(req_headers)
mute_connect_err = kwargs.get('mute_connect_err') mute = []
if mute_connect_err: for muted in filter(
del(kwargs['mute_connect_err']) lambda x: kwargs.get(x, False), ['mute_connect_err', 'mute_read_timeout', 'mute_connect_timeout']):
mute += [muted]
del kwargs[muted]
# request session ssl verify # request session ssl verify
session.verify = False 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) e.errno, _maybe_request_url(e)), logger.WARNING)
return return
except requests.exceptions.ConnectionError as e: 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' % ( logger.log(u'Connection error msg:%s while loading URL%s' % (
e.message, _maybe_request_url(e)), logger.WARNING) e.message, _maybe_request_url(e)), logger.WARNING)
return return
except requests.exceptions.ReadTimeout as e: except requests.exceptions.ReadTimeout as e:
logger.log(u'Read timed out msg:%s while loading URL%s' % ( if 'mute_read_timeout' not in mute:
e.message, _maybe_request_url(e)), logger.WARNING) logger.log(u'Read timed out msg:%s while loading URL%s' % (
e.message, _maybe_request_url(e)), logger.WARNING)
return return
except (requests.exceptions.Timeout, socket.timeout) as e: except (requests.exceptions.Timeout, socket.timeout) as e:
logger.log(u'Connection timed out msg:%s while loading URL %s' % ( if 'mute_connect_timeout' not in mute:
e.message, _maybe_request_url(e, url)), logger.WARNING) logger.log(u'Connection timed out msg:%s while loading URL %s' % (
e.message, _maybe_request_url(e, url)), logger.WARNING)
return return
except Exception as e: except Exception as e:
if e.message: if e.message:

View file

@ -23,8 +23,7 @@ import re
import sickbeard import sickbeard
from sickbeard import logger from sickbeard import common, logger
from sickbeard import common
from sickbeard.exceptions import ex from sickbeard.exceptions import ex
from sickbeard.encodingKludge import fixStupidEncodings from sickbeard.encodingKludge import fixStupidEncodings
@ -36,6 +35,14 @@ except ImportError:
class PLEXNotifier: 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): def _send_to_plex(self, command, host, username=None, password=None):
"""Handles communication to Plex hosts via HTTP API """Handles communication to Plex hosts via HTTP API
@ -57,7 +64,7 @@ class PLEXNotifier:
password = sickbeard.PLEX_PASSWORD password = sickbeard.PLEX_PASSWORD
if not host: 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 return False
for key in command: for key in command:
@ -65,7 +72,7 @@ class PLEXNotifier:
command[key] = command[key].encode('utf-8') command[key] = command[key].encode('utf-8')
enc_command = urllib.urlencode(command) 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) url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, enc_command)
try: try:
@ -75,21 +82,21 @@ class PLEXNotifier:
base64string = base64.encodestring('%s:%s' % (username, password))[:-1] base64string = base64.encodestring('%s:%s' % (username, password))[:-1]
authheader = 'Basic %s' % base64string authheader = 'Basic %s' % base64string
req.add_header('Authorization', authheader) 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: else:
logger.log(u'PLEX: Contacting via url: ' + url, logger.DEBUG) self.log(u'Contacting via url: ' + url, logger.DEBUG)
response = urllib2.urlopen(req) response = urllib2.urlopen(req)
result = response.read().decode(sickbeard.SYS_ENCODING) result = response.read().decode(sickbeard.SYS_ENCODING)
response.close() 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('<html><li>(.+\w)</html>').findall(result) # could return result response = re.compile('<html><li>(.+\w)</html>').findall(result)
return 'OK' return 'OK'
except (urllib2.URLError, IOError) as e: 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 return False
def _notify_pmc(self, message, title='SickGear', host=None, username=None, password=None, force=False): def _notify_pmc(self, message, title='SickGear', host=None, username=None, password=None, force=False):
@ -123,9 +130,10 @@ class PLEXNotifier:
result = '' result = ''
for curHost in [x.strip() for x in host.split(',')]: 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) notify_result = self._send_to_plex(command, curHost, username, password)
if notify_result: if notify_result:
result += '%s:%s' % (curHost, str(notify_result)) result += '%s:%s' % (curHost, str(notify_result))
@ -154,13 +162,29 @@ class PLEXNotifier:
title = common.notifyStrings[common.NOTIFY_GIT_UPDATE] title = common.notifyStrings[common.NOTIFY_GIT_UPDATE]
self._notify_pmc(update_text + new_version, title) self._notify_pmc(update_text + new_version, title)
def test_notify_pmc(self, host, username, password): def test_notify(self, host, username, password, server=False):
return self._notify_pmc('This is a test notification from SickGear', 'Test', host, username, password, force=True) 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): @staticmethod
return self.update_library(host=host, username=username, password=password, force=False) 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 """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. 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: if not sickbeard.PLEX_SERVER_HOST and not any([host]):
logger.log(u'PLEX: No Plex Media Server host specified, check your settings', logger.DEBUG) msg = u'No Plex Media Server host specified, check your settings'
return False self.log(msg, logger.DEBUG)
return '%sFail: %s' % (('', '<br />')[test], msg)
if not host: if not host:
host = sickbeard.PLEX_SERVER_HOST host = sickbeard.PLEX_SERVER_HOST
@ -184,10 +209,10 @@ class PLEXNotifier:
password = sickbeard.PLEX_PASSWORD password = sickbeard.PLEX_PASSWORD
# if username and password were provided, fetch the auth token from plex.tv # if username and password were provided, fetch the auth token from plex.tv
token_arg = '' token_arg = None
if username and password: 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='') req = urllib2.Request('https://plex.tv/users/sign_in.xml', data='')
authheader = 'Basic %s' % base64.encodestring('%s:%s' % (username, password))[:-1] authheader = 'Basic %s' % base64.encodestring('%s:%s' % (username, password))[:-1]
req.add_header('Authorization', authheader) req.add_header('Authorization', authheader)
@ -195,6 +220,7 @@ class PLEXNotifier:
req.add_header('X-Plex-Product', 'SickGear Notifier') req.add_header('X-Plex-Product', 'SickGear Notifier')
req.add_header('X-Plex-Client-Identifier', '5f48c063eaf379a565ff56c9bb2b401e') req.add_header('X-Plex-Client-Identifier', '5f48c063eaf379a565ff56c9bb2b401e')
req.add_header('X-Plex-Version', '1.0') req.add_header('X-Plex-Version', '1.0')
token_arg = False
try: try:
response = urllib2.urlopen(req) response = urllib2.urlopen(req)
@ -203,67 +229,89 @@ class PLEXNotifier:
token_arg = '?X-Plex-Token=' + token token_arg = '?X-Plex-Token=' + token
except urllib2.URLError as e: 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: 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 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_all = {}
hosts_match = {} hosts_match = {}
hosts_failed = [] hosts_failed = []
for cur_host in host_list: for cur_host in host_validate:
response = sickbeard.helpers.getURL(
url = 'http://%s/library/sections%s' % (cur_host, token_arg) '%s/library/sections%s' % (cur_host, token_arg or ''), timeout=10,
try: mute_connect_err=True, mute_read_timeout=True, mute_connect_timeout=True)
xml_tree = etree.parse(urllib.urlopen(url)) if response:
media_container = xml_tree.getroot() response = sickbeard.helpers.parse_xml(response)
except IOError as e: if not response:
logger.log(u'PLEX: Error while trying to contact Plex Media Server: ' + ex(e), logger.ERROR)
hosts_failed.append(cur_host) hosts_failed.append(cur_host)
continue continue
sections = media_container.findall('.//Directory') sections = response.findall('.//Directory')
if not sections: 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) hosts_failed.append(cur_host)
continue continue
for section in sections: for section in filter(lambda x: 'show' == x.attrib['type'], sections):
if 'show' == section.attrib['type']: 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)] for section_location in section.findall('.//Location'):
hosts_all.update(keyed_host) section_path = re.sub(r'[/\\]+', '/', section_location.attrib['path'].lower())
if not file_location: section_path = re.sub(r'^(.{,2})[/\\]', '', section_path)
continue location_path = re.sub(r'[/\\]+', '/', file_location.lower())
location_path = re.sub(r'^(.{,2})[/\\]', '', location_path)
for section_location in section.findall('.//Location'): if section_path in location_path:
section_path = re.sub(r'[/\\]+', '/', section_location.attrib['path'].lower()) hosts_match.update(keyed_host)
section_path = re.sub(r'^(.{,2})[/\\]', '', section_path) break
location_path = re.sub(r'[/\\]+', '/', file_location.lower())
location_path = re.sub(r'^(.{,2})[/\\]', '', location_path)
if section_path in location_path: if not test:
hosts_match.update(keyed_host) 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)] if len(hosts_failed) == len(host_validate):
host_list = [] self.log(u'No successful Plex host updated')
for section_key, cur_host in hosts_try.items(): 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) hosts = [
try: host.replace('http://', '') for host in filter(lambda x: x.startswith('http:'), hosts_all.values())]
force and urllib.urlopen(url) secured = [
host_list.append(cur_host) host.replace('https://', '') for host in filter(lambda x: x.startswith('https:'), hosts_all.values())]
except Exception as e: failed = [
logger.log(u'PLEX: Error updating library section for Plex Media Server: ' + ex(e), logger.ERROR) host.replace('http://', '') for host in filter(lambda x: x.startswith('http:'), hosts_failed)]
hosts_failed.append(cur_host) failed_secured = ', '.join(filter(
lambda x: x not in hosts,
if len(hosts_match): [host.replace('https://', '') for host in filter(lambda x: x.startswith('https:'), hosts_failed)]))
logger.log(u'PLEX: Updating hosts where TV section paths match the downloaded show: ' + ', '.join(set(host_list)), logger.MESSAGE) return '<br />' + '<br />'.join(result for result in [
else: ('', 'Fail: username/password when fetching credentials from plex.tv')[False is token_arg],
logger.log(u'PLEX: Updating all hosts with TV sections: ' + ', '.join(set(host_list)), logger.MESSAGE) ('', 'OK (secure connect): %s' % ', '.join(secured))[any(secured)],
('', 'OK%s: %s' % ((' (legacy connect)', '')[None is token_arg], ', '.join(hosts)))[any(hosts)],
return (', '.join(set(hosts_failed)), None)[not len(hosts_failed)] ('', '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 notifier = PLEXNotifier

View file

@ -846,7 +846,7 @@ class Home(MainHandler):
finalResult = '' finalResult = ''
for curHost in [x.strip() for x in host.split(',')]: 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]: if len(curResult.split(':')) > 2 and 'OK' in curResult.split(':')[2]:
finalResult += 'Successful test notice sent to Plex client ... ' + urllib.unquote_plus(curHost) finalResult += 'Successful test notice sent to Plex client ... ' + urllib.unquote_plus(curHost)
else: else:
@ -863,18 +863,13 @@ class Home(MainHandler):
if None is not password and set('*') == set(password): if None is not password and set('*') == set(password):
password = sickbeard.PLEX_PASSWORD password = sickbeard.PLEX_PASSWORD
finalResult = '' 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]
curResult = notifiers.plex_notifier.test_notify_pms(urllib.unquote_plus(host), username, password) + ' Plex server(s) ... %s<br />\n' % cur_result)
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 += '<br />' + '\n'
ui.notifications.message('Tested Plex Media Server host(s): ', urllib.unquote_plus(host.replace(',', ', '))) ui.notifications.message('Tested Plex Media Server host(s): ', urllib.unquote_plus(host.replace(',', ', ')))
return finalResult return final_result
def testLibnotify(self, *args, **kwargs): def testLibnotify(self, *args, **kwargs):
self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') self.set_header('Cache-Control', 'max-age=0,no-cache,no-store')
@ -1815,11 +1810,11 @@ class Home(MainHandler):
def updatePLEX(self, *args, **kwargs): def updatePLEX(self, *args, **kwargs):
result = notifiers.plex_notifier.update_library() result = notifiers.plex_notifier.update_library()
if None is result: if 'Fail' not in result:
ui.notifications.message( ui.notifications.message(
'Library update command sent to', 'Plex Media Server host(s): ' + sickbeard.PLEX_SERVER_HOST.replace(',', ', ')) 'Library update command sent to', 'Plex Media Server host(s): ' + sickbeard.PLEX_SERVER_HOST.replace(',', ', '))
else: 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/') self.redirect('/home/')
def setStatus(self, show=None, eps=None, status=None, direct=False): def setStatus(self, show=None, eps=None, status=None, direct=False):