diff --git a/CHANGES.md b/CHANGES.md index 97a92352..b9e865ea 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -34,6 +34,7 @@ * Add py2/3 regression testing for exception clauses * Change py2 exception clauses to py2/3 compatible clauses * Change py2 print statements to py2/3 compatible functions +* Change Kodi notifier to use requests as opposed to urllib [develop changelog] * Update Requests library 2.7.0 (ab1f493) to 2.7.0 (8b5e457) diff --git a/sickbeard/notifiers/kodi.py b/sickbeard/notifiers/kodi.py index 3655a7b2..ea3bfa5c 100644 --- a/sickbeard/notifiers/kodi.py +++ b/sickbeard/notifiers/kodi.py @@ -16,24 +16,13 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -import urllib -import urllib2 -import socket import base64 -import time - +import requests import sickbeard - -from sickbeard import logger -from sickbeard import common +from sickbeard import logger, common from sickbeard.exceptions import ex from sickbeard.encodingKludge import fixStupidEncodings -try: - import xml.etree.cElementTree as etree -except ImportError: - import xml.etree.ElementTree as etree - try: import json except ImportError: @@ -41,7 +30,8 @@ except ImportError: class KODINotifier: - sg_logo_url = 'https://raw.githubusercontent.com/SickGear/SickGear/master/gui/slick/images/ico/apple-touch-icon-precomposed.png' + sg_logo_url = 'https://raw.githubusercontent.com/SickGear/SickGear/master/gui/slick/images/ico/apple-touch-icon' \ + '-precomposed.png' def _notify_kodi(self, message, title='SickGear', host=None, username=None, password=None, force=False): @@ -55,21 +45,19 @@ class KODINotifier: # suppress notifications if the notifier is disabled but the notify options are checked if not sickbeard.USE_KODI and not force: - logger.log(u'KODI: Notifications are not enabled, skipping this notification', logger.DEBUG) return False result = '' for curHost in [x.strip() for x in host.split(',')]: - logger.log(u'KODI: Sending Kodi notification to \'%s\' - %s' % (curHost, message), logger.MESSAGE) - - command = '{"jsonrpc":"2.0","method":"GUI.ShowNotification","params":{"title":"%s","message":"%s", "image": "%s"},"id":1}' % (title.encode('utf-8'), message.encode('utf-8'), self.sg_logo_url) + logger.log(u'KODI: Sending Kodi notification to "%s" - %s' % (curHost, message), logger.MESSAGE) + command = {'jsonrpc': '2.0', 'method': 'GUI.ShowNotification', + 'params': {'title': title, 'message': message, 'image': self.sg_logo_url}, 'id': 1} notifyResult = self._send_to_kodi(command, curHost, username, password) if notifyResult: - result += curHost + ':' + notifyResult['result'].decode(sickbeard.SYS_ENCODING) + result += '%s:%s' % (curHost, notifyResult['result']) else: if sickbeard.KODI_ALWAYS_ON or force: - result += curHost + ':False' - + result += '%s:False' % curHost return result def _send_to_kodi(self, command, host=None, username=None, password=None): @@ -84,40 +72,38 @@ class KODINotifier: logger.log(u'KODI: No host specified, check your settings', logger.ERROR) return False - command = command.encode('utf-8') - logger.log(u'KODI: JSON command: ' + command, logger.DEBUG) + data = json.dumps(command) + logger.log(u'KODI: JSON command: %s' % data, logger.DEBUG) + + url = 'http://%s/jsonrpc' % host + + headers = {'Content-type': 'application/json'} + # if we have a password, use authentication + if password: + base64string = base64.encodestring('%s:%s' % (username, password))[:-1] + authheader = 'Basic %s' % base64string + headers['Authorization'] = authheader + logger.log(u'KODI: Contacting (with auth header) via url: %s' % fixStupidEncodings(url), logger.DEBUG) + else: + logger.log(u'KODI: Contacting via url: %s' % fixStupidEncodings(url), logger.DEBUG) - url = 'http://%s/jsonrpc' % (host) try: - req = urllib2.Request(url, command) - req.add_header('Content-type', 'application/json') - # if we have a password, use authentication - if password: - base64string = base64.encodestring('%s:%s' % (username, password))[:-1] - authheader = 'Basic %s' % base64string - req.add_header('Authorization', authheader) - logger.log(u'KODI: Contacting (with auth header) via url: ' + fixStupidEncodings(url), logger.DEBUG) - else: - logger.log(u'KODI: Contacting via url: ' + fixStupidEncodings(url), logger.DEBUG) + response = requests.post(url, data=data, headers=headers) + except Exception as e: + logger.log(u'KODI: Warning: Couldn\'t contact Kodi at %s - %s' % (host, ex(e)), logger.WARNING) + return False - try: - response = urllib2.urlopen(req) - except urllib2.URLError as e: - logger.log(u'KODI: Warning: Couldn\'t contact Kodi at ' + host + '- ' + ex(e), logger.WARNING) - return False + if response.status_code == 401: + logger.log(u'KODI: Invalid login credentials', logger.ERROR) + return False - # parse the json result - try: - result = json.load(response) - response.close() - logger.log(u'KODI: JSON response: ' + str(result), logger.DEBUG) - return result # need to return response for parsing - except ValueError as e: - logger.log(u'KODI: Unable to decode JSON response: ' + response, logger.WARNING) - return False - - except IOError as e: - logger.log(u'KODI: Warning: Couldn\'t contact Kodi at ' + host + ' - ' + ex(e), logger.WARNING) + # parse the json result + try: + result = response.json() + logger.log(u'KODI: JSON response: %s' % result, logger.DEBUG) + return result # need to return response for parsing + except ValueError as e: + logger.log(u'KODI: Unable to decode JSON response: %s' % response.text, logger.WARNING) return False def _update_library(self, host=None, showName=None): @@ -126,70 +112,66 @@ class KODINotifier: logger.log(u'KODI: No host specified, check your settings', logger.DEBUG) return False - logger.log(u'KODI: Updating library on host: ' + host, logger.MESSAGE) + logger.log(u'KODI: Updating library on host: %s' % host, logger.MESSAGE) # if we're doing per-show if showName: - tvshowid = -1 - logger.log(u'KODI: Updating library for show ' + showName, logger.DEBUG) + logger.log(u'KODI: Updating library for show %s' % showName, logger.DEBUG) # get tvshowid by showName - showsCommand = '{"jsonrpc":"2.0","method":"VideoLibrary.GetTVShows","id":1}' + showsCommand = {'jsonrpc': '2.0', 'method': 'VideoLibrary.GetTVShows', 'id': 1} showsResponse = self._send_to_kodi(showsCommand, host) - if showsResponse and 'result' in showsResponse and 'tvshows' in showsResponse['result']: + try: shows = showsResponse['result']['tvshows'] - else: + except: logger.log(u'KODI: No TV shows in Kodi TV show list', logger.DEBUG) return False - for show in shows: - if (show['label'] == showName): - tvshowid = show['tvshowid'] - break # exit out of loop otherwise the label and showname will not match up - - # this can be big, so free some memory - del shows - - # we didn't find the show (exact match), thus revert to just doing a full update if enabled - if (tvshowid == -1): - logger.log(u'KODI: Exact show name not matched in KODI TV show list', logger.DEBUG) + try: + tvshowid = next((show['tvshowid'] for show in shows if show['label'] == showName)) + except StopIteration: + logger.log(u'XBMC: Exact show name not matched in XBMC TV show list', logger.DEBUG) return False # lookup tv-show path - pathCommand = '{"jsonrpc":"2.0","method":"VideoLibrary.GetTVShowDetails","params":{"tvshowid":%d, "properties": ["file"]},"id":1}' % (tvshowid) + pathCommand = {'jsonrpc': '2.0', 'method': 'VideoLibrary.GetTVShowDetails', + 'params': {'tvshowid': tvshowid, 'properties': ['file']}, 'id': 1} pathResponse = self._send_to_kodi(pathCommand, host) path = pathResponse['result']['tvshowdetails']['file'] - logger.log(u'KODI: Received Show: ' + showName + ' with ID: ' + str(tvshowid) + ' Path: ' + path, logger.DEBUG) + logger.log(u'KODI: Received Show: %s with ID: %s Path: %s' % (showName, tvshowid, path), logger.DEBUG) - if (len(path) < 1): - logger.log(u'KODI: No valid path found for ' + showName + ' with ID: ' + str(tvshowid) + ' on ' + host, logger.WARNING) + if len(path) < 1: + logger.log(u'KODI: No valid path found for %s with ID: %s on %s' % (showName, tvshowid, host), + logger.WARNING) return False - logger.log(u'KODI: Updating ' + showName + ' on ' + host + ' at ' + path, logger.DEBUG) - updateCommand = '{"jsonrpc":"2.0","method":"VideoLibrary.Scan","params":{"directory":%s},"id":1}' % (json.dumps(path)) + logger.log(u'KODI: Updating %s on %s at %s' % (showName, host, path), logger.DEBUG) + updateCommand = {'jsonrpc': '2.0', 'method': 'VideoLibrary.Scan', + 'params': {'directory': json.dumps(path)}, 'id': 1} request = self._send_to_kodi(updateCommand, host) if not request: - logger.log(u'KODI: Update of show directory failed on ' + showName + ' on ' + host + ' at ' + path, logger.ERROR) + logger.log(u'KODI: Update of show directory failed on %s on %s at %s' % (showName, host, path), + logger.ERROR) return False # catch if there was an error in the returned request for r in request: if 'error' in r: - logger.log(u'KODI: Error while attempting to update show directory for ' + showName + ' on ' + host + ' at ' + path, logger.ERROR) + logger.log(u'KODI: Error while attempting to update show directory for %s on %s at %s' + % (showName, host, path), logger.ERROR) return False # do a full update if requested else: - logger.log(u'KODI: Performing full library update on host: ' + host, logger.DEBUG) - updateCommand = '{"jsonrpc":"2.0","method":"VideoLibrary.Scan","id":1}' + logger.log(u'KODI: Performing full library update on host: %s' % host, logger.DEBUG) + updateCommand = {'jsonrpc': '2.0', 'method': 'VideoLibrary.Scan', 'id': 1} request = self._send_to_kodi(updateCommand, host, sickbeard.KODI_USERNAME, sickbeard.KODI_PASSWORD) if not request: - logger.log(u'KODI: Full library update failed on host: ' + host, logger.ERROR) + logger.log(u'KODI: Full library update failed on host: %s' % host, logger.ERROR) return False - return True def notify_snatch(self, ep_name): @@ -202,16 +184,17 @@ class KODINotifier: def notify_subtitle_download(self, ep_name, lang): if sickbeard.KODI_NOTIFY_ONSUBTITLEDOWNLOAD: - self._notify_kodi(ep_name + ': ' + lang, common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD]) - - def notify_git_update(self, new_version = '??'): + self._notify_kodi('%s: %s' % (ep_name, lang), common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD]) + + def notify_git_update(self, new_version='??'): if sickbeard.USE_KODI: - update_text=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] - title=common.notifyStrings[common.NOTIFY_GIT_UPDATE] - self._notify_kodi(update_text + new_version, title) + update_text = common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] + title = common.notifyStrings[common.NOTIFY_GIT_UPDATE] + self._notify_kodi('%s %s' % (update_text, new_version), title) def test_notify(self, host, username, password): - return self._notify_kodi('Testing Kodi notifications from SickGear', 'Test', host, username, password, force=True) + return self._notify_kodi( + 'Testing Kodi notifications from SickGear', 'Test', host, username, password, force=True) def update_library(self, showName=None): @@ -225,17 +208,19 @@ class KODINotifier: for host in [x.strip() for x in sickbeard.KODI_HOST.split(',')]: if self._update_library(host, showName): if sickbeard.KODI_UPDATE_ONLYFIRST: - logger.log(u'KODI: Update first host successful on host ' + host + ', stopped sending library update commands', logger.DEBUG) + logger.log( + u'KODI: Update first host successful on host %s , stopped sending library update commands' + % host, logger.DEBUG) return True else: if sickbeard.KODI_ALWAYS_ON: result = result + 1 - # needed for the 'update kodi' submenu command - # as it only cares of the final result vs the individual ones + # needed for the 'update kodi' submenu command as it only cares of the final result vs the individual ones if result == 0: return True else: return False + notifier = KODINotifier