From a68f64327fda2706773122914886340e56213ee0 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Fri, 20 Nov 2015 22:52:19 +0000 Subject: [PATCH] Change overhaul Kodi notifier and tidy up config/notification/KodiNotifier ui. Add passthru of param "post_json" to Requests() "json" in helpers.getURL. --- CHANGES.md | 2 + .../default/config_notifications.tmpl | 47 +- sickbeard/helpers.py | 30 +- sickbeard/notifiers/__init__.py | 6 +- sickbeard/notifiers/kodi.py | 538 +++++++++++++----- sickbeard/notifiers/libnotify.py | 8 +- sickbeard/webserve.py | 18 +- 7 files changed, 467 insertions(+), 182 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index def25ec2..08e28371 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,6 +30,8 @@ * Allow episode status "Skipped" to be changed to "Downloaded" * Allow found "Skipped" episode files to be set "Unknown" quality * Add CPU throttling preset "Disabled" to config/General/Advanced Settings +* Change overhaul Kodi notifier and tidy up config/notification/KodiNotifier ui +* Add passthru of param "post_json" to Requests() "json" in helpers.getURL ### 0.11.6 (2016-02-18 23:10:00 UTC) diff --git a/gui/slick/interfaces/default/config_notifications.tmpl b/gui/slick/interfaces/default/config_notifications.tmpl index 1de8e626..c18af884 100644 --- a/gui/slick/interfaces/default/config_notifications.tmpl +++ b/gui/slick/interfaces/default/config_notifications.tmpl @@ -211,19 +211,19 @@
@@ -238,36 +238,31 @@
- -
-
-
Click below to test.
diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py index 72078156..8167335e 100644 --- a/sickbeard/helpers.py +++ b/sickbeard/helpers.py @@ -1152,8 +1152,13 @@ def getURL(url, post_data=None, params=None, headers=None, timeout=30, session=N } # decide if we get or post data to server + if 'post_json' in kwargs: + kwargs.setdefault('json', kwargs.get('post_json')) + del(kwargs['post_json']) if post_data: - resp = session.post(url, data=post_data, timeout=timeout, **kwargs) + kwargs.setdefault('data', post_data) + if 'data' in kwargs or 'json' in kwargs: + resp = session.post(url, timeout=timeout, **kwargs) else: resp = session.get(url, timeout=timeout, **kwargs) @@ -1169,26 +1174,35 @@ def getURL(url, post_data=None, params=None, headers=None, timeout=30, session=N return except requests.exceptions.HTTPError as e: - logger.log(u'HTTP error %s while loading URL %s' % (e.errno, url), logger.WARNING) + logger.log(u'HTTP error %s while loading URL %s' % (e.errno, e.request.url), logger.WARNING) return except requests.exceptions.ConnectionError as e: - logger.log(u'Internet connection error msg:%s while loading URL %s' % (str(e.message), url), logger.WARNING) + if not kwargs.get('mute_connect_err'): + logger.log(u'Connection error msg:%s while loading URL %s' % (e.message, e.request.url), logger.WARNING) return except requests.exceptions.ReadTimeout as e: - logger.log(u'Read timed out msg:%s while loading URL %s' % (str(e.message), url), logger.WARNING) + logger.log(u'Read timed out msg:%s while loading URL %s' % (e.message, e.request.url), logger.WARNING) return except (requests.exceptions.Timeout, socket.timeout) as e: - logger.log(u'Connection timed out msg:%s while loading URL %s' % (str(e.message), url), logger.WARNING) + logger.log(u'Connection timed out msg:%s while loading URL %s' + % (e.message, hasattr(e, 'request') and e.request.url or url), logger.WARNING) return except Exception as e: + url = hasattr(e, 'request') and e.request.url or url if e.message: - logger.log(u'Exception caught while loading URL %s\r\nDetail... %s\r\n%s' % (url, str(e.message), traceback.format_exc()), logger.WARNING) + logger.log(u'Exception caught while loading URL %s\r\nDetail... %s\r\n%s' + % (url, e.message, traceback.format_exc()), logger.WARNING) else: - logger.log(u'Unknown exception while loading URL %s\r\nDetail... %s' % (url, traceback.format_exc()), logger.WARNING) + logger.log(u'Unknown exception while loading URL %s\r\nDetail... %s' + % (url, traceback.format_exc()), logger.WARNING) return if json: - return resp.json() + try: + return resp.json() + except (TypeError, Exception) as e: + logger.log(u'JSON data issue from URL %s\r\nDetail... %s' % (url, e.message), logger.WARNING) + return None return resp.content diff --git a/sickbeard/notifiers/__init__.py b/sickbeard/notifiers/__init__.py index ee7dbf8c..122eb3d4 100644 --- a/sickbeard/notifiers/__init__.py +++ b/sickbeard/notifiers/__init__.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -import sickbeard - import xbmc import kodi import plex @@ -41,11 +39,9 @@ import tweet from lib import libtrakt import emailnotify -from sickbeard.common import * - # home theater / nas xbmc_notifier = xbmc.XBMCNotifier() -kodi_notifier = kodi.KODINotifier() +kodi_notifier = kodi.KodiNotifier() plex_notifier = plex.PLEXNotifier() nmj_notifier = nmj.NMJNotifier() nmjv2_notifier = nmjv2.NMJv2Notifier() diff --git a/sickbeard/notifiers/kodi.py b/sickbeard/notifiers/kodi.py index ea3bfa5c..f24ed53f 100644 --- a/sickbeard/notifiers/kodi.py +++ b/sickbeard/notifiers/kodi.py @@ -1,7 +1,8 @@ -# Author: Nic Wolfe -# URL: http://code.google.com/p/sickbeard/ +# coding=utf-8 # # This file is part of SickGear. +# Author: SickGear +# Thanks to: Nic Wolfe # # SickGear is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -16,12 +17,20 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -import base64 -import requests +import time +import urllib + import sickbeard -from sickbeard import logger, common +import sickbeard.helpers from sickbeard.exceptions import ex -from sickbeard.encodingKludge import fixStupidEncodings +from sickbeard import logger, common + +try: + # noinspection PyPep8Naming + import xml.etree.cElementTree as etree +except ImportError: + # noinspection PyPep8Naming + import xml.etree.ElementTree as etree try: import json @@ -29,151 +38,397 @@ except ImportError: from lib import simplejson as json -class KODINotifier: - sg_logo_url = 'https://raw.githubusercontent.com/SickGear/SickGear/master/gui/slick/images/ico/apple-touch-icon' \ - '-precomposed.png' +class KodiNotifier: + def __init__(self): + self.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): + self.username, self.password = (None, None) + self.response = None + self.prefix = '' + self.test_mode = False - # fill in omitted parameters - if not host: - host = sickbeard.KODI_HOST - if not username: - username = sickbeard.KODI_USERNAME - if not password: - password = sickbeard.KODI_PASSWORD + def _get_kodi_version(self, host): + """ Return Kodi JSON-RPC API version (odd # = dev, even # = stable) - # suppress notifications if the notifier is disabled but the notify options are checked - if not sickbeard.USE_KODI and not force: + Communicate with Kodi hosts using JSON-RPC to determine whether to use the legacy API or the JSON-RPC API. + + Fallback to testing legacy HTTP API before assuming it is a badly configured host. + + Returns: + Returns API number or False + + API | Kodi Version + -----+--------------- + 2 | v10 (Dharma) + 3 | (pre Eden) + 4 | v11 (Eden) + 5 | (pre Frodo) + 6 | v12 (Frodo) / v13 (Gotham) + """ + + response = self._send_to_kodi_json(host, dict(method='JSONRPC.Version'), 10) + if self.response and 401 == self.response.get('status_code'): 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': title, 'message': message, 'image': self.sg_logo_url}, 'id': 1} - notifyResult = self._send_to_kodi(command, curHost, username, password) - if notifyResult: - result += '%s:%s' % (curHost, notifyResult['result']) + if response.get('version'): + version = response.get('version') + return isinstance(version, dict) and version.get('major') or version + + # fallback to legacy HTTPAPI method + test_command = {'command': 'Help'} + if self._send_to_kodi(host, test_command): + # return fake version number to use the legacy method + return 1 + + if self.response and 404 == self.response.get('status_code'): + self.prefix = 'xbmc' + if self._send_to_kodi(host, test_command): + # return fake version number to use the legacy method + return 1 + + return False + + def _notify_kodi(self, msg, title='SickGear', kodi_hosts=None): + """ Internal wrapper for the notify_snatch and notify_download functions + + Call either the JSON-RPC over HTTP or the legacy HTTP API methods depending on the Kodi API version. + + Args: + msg: Message body of the notice to send + title: Title of the notice to send + + Return: + A list of results in the format of host:ip:result, where result will either be 'OK' or False. + """ + + # fill in omitted parameters + if not kodi_hosts: + kodi_hosts = sickbeard.KODI_HOST + + if not sickbeard.USE_KODI and not self.test_mode: + self._log(u'Notification not enabled, skipping this notification', logger.DEBUG) + return False, None + + total_success = True + message = [] + for host in [x.strip() for x in kodi_hosts.split(',')]: + cur_host = urllib.unquote_plus(host) + + self._log(u'Sending notification to "%s" - %s' % (cur_host, message), logger.DEBUG) + + api_version = self._get_kodi_version(cur_host) + if self.response and 401 == self.response.get('status_code'): + total_success = False + message += ['Fail: Cannot authenticate with %s' % cur_host] + self._log(u'Failed to authenticate with %s' % cur_host, logger.DEBUG) + elif not api_version: + total_success = False + message += ['Fail: No supported Kodi found at %s' % cur_host] + self._maybe_log_failed_detection(cur_host) else: - if sickbeard.KODI_ALWAYS_ON or force: - result += '%s:False' % curHost - return result + if 4 >= api_version: + self._log(u'Detected %sversion <= 11, using HTTP API' + % self.prefix and ' ' + self.prefix.capitalize(), logger.DEBUG) + __method_send = self._send_to_kodi + command = dict(command='ExecBuiltIn', + parameter='Notification(%s,%s)' % (title, msg)) + else: + self._log(u'Detected version >= 12, using JSON API', logger.DEBUG) + __method_send = self._send_to_kodi_json + command = dict(method='GUI.ShowNotification', + params={'title': '%s' % title, + 'message': '%s' % msg, + 'image': '%s' % self.sg_logo_url}) - def _send_to_kodi(self, command, host=None, username=None, password=None): + response_notify = __method_send(cur_host, command) + if response_notify: + message += ['%s: %s' % ((response_notify, 'OK')['OK' in response_notify], cur_host)] - # fill in omitted parameters - if not username: - username = sickbeard.KODI_USERNAME - if not password: - password = sickbeard.KODI_PASSWORD + return total_success, '
\n'.join(message) + + def _send_update_library(self, host, show_name=None): + """ Internal wrapper for the update library function + + Call either the JSON-RPC over HTTP or the legacy HTTP API methods depending on the Kodi API version. + + Args: + show_name: Name of a TV show to specifically target the library update for + + Return: + True if the update was successful else False + """ + + self._log(u'Sending request to update library for host: "%s"' % host, logger.DEBUG) + + api_version = self._get_kodi_version(host) + if api_version: + # try to update just the show, if it fails, do full update if enabled + __method_update = (self._update_library, self._update_library_json)[4 < api_version] + if __method_update(host, show_name): + return True + + failed_msg = 'Single show update failed,' + if sickbeard.KODI_UPDATE_FULL: + self._log(u'%s falling back to full update' % failed_msg, logger.DEBUG) + return __method_update(host) + + self._log(u'%s consider enabling "Perform full library update" in config/notifications' % failed_msg, + logger.DEBUG) + return False + + ############################################################################## + # Legacy HTTP API (pre Kodi 12) methods + ############################################################################## + + def _send_to_kodi(self, host, command): + """ Handle communication to Kodi servers via HTTP API + + Args: + command: Dictionary encoded via urllib and passed to the Kodi API via HTTP + + Return: + response.result for successful commands or False if there was an error + """ if not host: - logger.log(u'KODI: No host specified, check your settings', logger.ERROR) + self._log(u'No host specified, aborting update', logger.WARNING) return False - data = json.dumps(command) - logger.log(u'KODI: JSON command: %s' % data, logger.DEBUG) + args = {} + if not sickbeard.KODI_ALWAYS_ON and not self.test_mode: + args['mute_connect_err'] = True - url = 'http://%s/jsonrpc' % host + if self.password or sickbeard.KODI_PASSWORD: + args['auth'] = (self.username or sickbeard.KODI_USERNAME, self.password or sickbeard.KODI_PASSWORD) - 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/%sCmds/%sHttp' % (host, self.prefix or 'kodi', self.prefix or 'kodi') + response = sickbeard.helpers.getURL(url=url, params=command, hooks=dict(response=self.cb_response), **args) - try: - 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 + return response or False - if response.status_code == 401: - logger.log(u'KODI: Invalid login credentials', logger.ERROR) - return False + def _update_library(self, host=None, show_name=None): + """ Handle updating Kodi host via HTTP API - # 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 + Update the video library for a specific tv show if passed, otherwise update the whole library if option enabled. - def _update_library(self, host=None, showName=None): + Args: + show_name: Name of a TV show to target for a library update + + Return: + True or False + """ if not host: - logger.log(u'KODI: No host specified, check your settings', logger.DEBUG) + self._log(u'No host specified, aborting update', logger.WARNING) return False - logger.log(u'KODI: Updating library on host: %s' % host, logger.MESSAGE) + self._log(u'Updating library via HTTP method for host: %s' % host, logger.DEBUG) # if we're doing per-show - if showName: - logger.log(u'KODI: Updating library for show %s' % showName, logger.DEBUG) + if show_name: + self._log(u'Updating library via HTTP method for show %s' % show_name, logger.DEBUG) - # get tvshowid by showName - showsCommand = {'jsonrpc': '2.0', 'method': 'VideoLibrary.GetTVShows', 'id': 1} - showsResponse = self._send_to_kodi(showsCommand, host) + path_sql = 'SELECT path.strPath FROM path, tvshow, tvshowlinkpath WHERE ' \ + 'tvshow.c00 = "%s"' % show_name \ + + ' AND tvshowlinkpath.idShow = tvshow.idShow AND tvshowlinkpath.idPath = path.idPath' - try: - shows = showsResponse['result']['tvshows'] - except: - logger.log(u'KODI: No TV shows in Kodi TV show list', logger.DEBUG) + # set xml response format, if this fails then don't bother with the rest + if not self._send_to_kodi( + host, {'command': 'SetResponseFormat(webheader;false;webfooter;false;header;;footer;;' + + 'opentag;;closetag;;closefinaltag;false)'}): + return False + + # sql used to grab path(s) + response = self._send_to_kodi(host, {'command': 'QueryVideoDatabase(%s)' % path_sql}) + if not response: + self._log(u'Invalid response for %s on %s' % (show_name, host), logger.DEBUG) return False 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) + et = etree.fromstring(urllib.quote(response, ':\\/<>')) + except SyntaxError as e: + self._log(u'Unable to parse XML in response: %s' % ex(e), logger.ERROR) return False - # lookup tv-show path - 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: %s with ID: %s Path: %s' % (showName, tvshowid, path), logger.DEBUG) - - if len(path) < 1: - logger.log(u'KODI: No valid path found for %s with ID: %s on %s' % (showName, tvshowid, host), - logger.WARNING) + paths = et.findall('.//field') + if not paths: + self._log(u'No valid path found for %s on %s' % (show_name, host), logger.DEBUG) return False - 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 %s on %s at %s' % (showName, host, path), - logger.ERROR) - return False + for path in paths: + # we do not need it double-encoded, gawd this is dumb + un_enc_path = urllib.unquote(path.text).decode(sickbeard.SYS_ENCODING) + self._log(u'Updating %s on %s at %s' % (show_name, host, un_enc_path), logger.DEBUG) - # 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 %s on %s at %s' - % (showName, host, path), logger.ERROR) + if not self._send_to_kodi( + host, {'command': 'ExecBuiltIn', 'parameter': 'Kodi.updatelibrary(video, %s)' % un_enc_path}): + self._log(u'Update of show directory failed for %s on %s at %s' + % (show_name, host, un_enc_path), logger.ERROR) return False + # sleep for a few seconds just to be sure kodi has a chance to finish each directory + if 1 < len(paths): + time.sleep(5) + # do a full update if requested + else: + self._log(u'Full library update on host: %s' % host, logger.DEBUG) + + if not self._send_to_kodi(host, {'command': 'ExecBuiltIn', 'parameter': 'Kodi.updatelibrary(video)'}): + self._log(u'Failed full library update on: %s' % host, logger.ERROR) + return False + + return True + + ############################################################################## + # JSON-RPC API (Kodi 12+) methods + ############################################################################## + + def _send_to_kodi_json(self, host, command, timeout=30): + """ Handle communication to Kodi installations via JSONRPC + + Args: + command: Kodi JSON-RPC command to send via HTTP + + Return: + response.result dict for successful commands or empty dict if there was an error + """ + + result = {} + if not host: + self._log(u'No host specified, aborting update', logger.WARNING) + return result + + if isinstance(command, dict): + command.setdefault('jsonrpc', '2.0') + command.setdefault('id', 'SickGear') + args = dict(post_json=command) + else: + args = dict(data=command) + + if not sickbeard.KODI_ALWAYS_ON and not self.test_mode: + args['mute_connect_err'] = True + + if self.password or sickbeard.KODI_PASSWORD: + args['auth'] = (self.username or sickbeard.KODI_USERNAME, self.password or sickbeard.KODI_PASSWORD) + + response = sickbeard.helpers.getURL(url='http://%s/jsonrpc' % host, timeout=timeout, + headers={'Content-type': 'application/json'}, json=True, + hooks=dict(response=self.cb_response), **args) + if response: + if not response.get('error'): + return 'OK' == response.get('result') and {'OK': True} or response.get('result') + + self._log(u'API error; %s from %s in response to command: %s' + % (json.dumps(response['error']), host, json.dumps(command)), logger.ERROR) + return result + + # noinspection PyUnusedLocal + def cb_response(self, r, *args, **kwargs): + self.response = dict(status_code=r.status_code) + return r + + def _update_library_json(self, host=None, show_name=None): + """ Handle updating Kodi host via HTTP JSON-RPC + + Update the video library for a specific tv show if passed, otherwise update the whole library if option enabled. + + Args: + show_name: Name of a TV show to target for a library update + + Return: + True or False + """ + + if not host: + self._log(u'No host specified, aborting update', logger.WARNING) + return False + + # if we're doing per-show + if show_name: + self._log(u'JSON library update. Host: %s Show: %s' % (host, show_name), logger.DEBUG) + + # try fetching tvshowid using show_name with a fallback to getting show list + show_name = urllib.unquote_plus(show_name) + commands = [dict(method='VideoLibrary.GetTVShows', + params={'filter': {'field': 'title', 'operator': 'is', 'value': '%s' % show_name}, + 'properties': ['title']}), + dict(method='VideoLibrary.GetTVShows')] + + shows = None + for command in commands: + response = self._send_to_kodi_json(host, command) + shows = response.get('tvshows') + if shows: + break + + if not shows: + self._log(u'No items in GetTVShows response', logger.DEBUG) + return False + + tvshowid = -1 + path = '' + for show in shows: + if show_name == show.get('title') or show_name == show.get('label'): + tvshowid = show.get('tvshowid', -1) + path = show.get('file', '') + break + del shows + + # we didn't find the show (exact match), thus revert to just doing a full update if enabled + if -1 == tvshowid: + self._log(u'Doesn\'t have "%s" in it\'s known shows, full library update required' % show_name, + logger.DEBUG) + return False + + # lookup tv-show path if we don't already know it + if not len(path): + command = dict(method='VideoLibrary.GetTVShowDetails', + params={'tvshowid': tvshowid, 'properties': ['file']}) + response = self._send_to_kodi_json(host, command) + path = 'tvshowdetails' in response and response['tvshowdetails'].get('file', '') or '' + + if not len(path): + self._log(u'No valid path found for %s with ID: %s on %s' % (show_name, tvshowid, host), logger.WARNING) + return False + + self._log(u'Updating %s on %s at %s' % (show_name, host, path), logger.DEBUG) + command = dict(method='VideoLibrary.Scan', params={'directory': '%s' % json.dumps(path)[1:-1]}) + response_scan = self._send_to_kodi_json(host, command) + if not response_scan.get('OK'): + self._log(u'Update of show directory failed for %s on %s at %s response: %s' % + (show_name, host, path, response_scan), logger.ERROR) + return False + # do a full update if requested else: - 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: %s' % host, logger.ERROR) + self._log(u'Full library update on host: %s' % host, logger.DEBUG) + response_scan = self._send_to_kodi_json(host, dict(method='VideoLibrary.Scan')) + if not response_scan.get('OK'): + self._log(u'Failed full library update on: %s response: %s' % (host, response_scan), logger.ERROR) return False + return True + def _maybe_log_failed_detection(self, host): + + self._maybe_log(u'Failed to detect version for %s, check configuration.' % host) + + def _maybe_log(self, msg, log_level=None): + + if msg and (sickbeard.KODI_ALWAYS_ON or self.test_mode): + self._log(msg + (not sickbeard.KODI_ALWAYS_ON and self.test_mode and ' (Test mode always logs)' or ''), + log_level) + + @staticmethod + def _log(msg, log_level=logger.WARNING): + + logger.log(u'Kodi: %s' % msg, log_level) + + ############################################################################## + # Public functions which will call the JSON or Legacy HTTP API methods + ############################################################################## + def notify_snatch(self, ep_name): if sickbeard.KODI_NOTIFY_ONSNATCH: self._notify_kodi(ep_name, common.notifyStrings[common.NOTIFY_SNATCH]) @@ -193,34 +448,61 @@ class KODINotifier: 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) + self.test_mode, self.username, self.password = True, username, password + return self._notify_kodi('Testing SickGear Kodi notifier', 'Test Notification', kodi_hosts=host) def update_library(self, showName=None): + """ Wrapper for the update library functions + + Call either the JSON-RPC over HTTP or the legacy HTTP API methods depending on the Kodi API version. + + Uses a list of comma delimited hosts where only one is updated, the first to respond with success. This is a + workaround for SQL backend users because updating multiple clients causes duplicate entries. + + Future plan is to revisit how host/ip/username/pw/options are stored so that this may become more flexible. + + Args: + showName: Name of a TV show to target for a library update + + Returns: + True or False + """ if sickbeard.USE_KODI and sickbeard.KODI_UPDATE_LIBRARY: if not sickbeard.KODI_HOST: - logger.log(u'KODI: No host specified, check your settings', logger.DEBUG) + self._log(u'No Kodi hosts specified, check your settings', logger.DEBUG) return False - # either update each host, or only attempt to update first only + # either update each host, or only attempt to update until one successful result result = 0 - 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 %s , stopped sending library update commands' - % host, logger.DEBUG) - return True + only_first = dict(show='', first='', first_note='') + showName and only_first.update(show=' for show;"%s"' % showName) + sickbeard.KODI_UPDATE_ONLYFIRST and only_first.update(dict( + first=' first', first_note=' in line with the "Only update first host"%s' % ' setting')) + + for cur_host in [x.strip() for x in sickbeard.KODI_HOST.split(',')]: + + response = self._send_to_kodi_json(cur_host, dict(method='Profiles.GetCurrentProfile')) + if self.response and 401 == self.response.get('status_code'): + self._log(u'Failed to authenticate with %s' % cur_host, logger.DEBUG) + continue + if not response: + self._maybe_log_failed_detection(cur_host) + continue + + if self._send_update_library(cur_host, showName): + only_first.update(dict(profile=response.get('label') or 'Master', host=cur_host)) + self._log('Success: profile;' + + u'"%(profile)s" at%(first)s host;%(host)s updated%(show)s%(first_note)s' % only_first) else: - if sickbeard.KODI_ALWAYS_ON: - result = result + 1 + self._maybe_log_failed_detection(cur_host) + result += 1 + + if sickbeard.KODI_UPDATE_ONLYFIRST: + return True # 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 + return 0 == result -notifier = KODINotifier +notifier = KodiNotifier diff --git a/sickbeard/notifiers/libnotify.py b/sickbeard/notifiers/libnotify.py index 90a45ad1..3ac4dc48 100644 --- a/sickbeard/notifiers/libnotify.py +++ b/sickbeard/notifiers/libnotify.py @@ -70,15 +70,15 @@ class LibnotifyNotifier: logger.log(u"Unable to import pynotify. libnotify notifications won't work.", logger.ERROR) return False try: - import gobject + from gi.repository import GObject except ImportError: - logger.log(u"Unable to import gobject. We can't catch a GError in display.", logger.ERROR) + logger.log(u"Unable to import GObject from gi.repository. We can't catch a GError in display.", logger.ERROR) return False if not pynotify.init('SickGear'): logger.log(u"Initialization of pynotify failed. libnotify notifications won't work.", logger.ERROR) return False self.pynotify = pynotify - self.gobject = gobject + self.gobject = GObject return True def notify_snatch(self, ep_name): @@ -92,7 +92,7 @@ class LibnotifyNotifier: def notify_subtitle_download(self, ep_name, lang): if sickbeard.LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD: self._notify(common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD], ep_name + ": " + lang) - + def notify_git_update(self, new_version = "??"): if sickbeard.USE_LIBNOTIFY: update_text=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 6d7b6280..ff41eada 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -1,3 +1,4 @@ +# coding=utf-8 # Author: Nic Wolfe # URL: http://code.google.com/p/sickbeard/ # @@ -791,20 +792,15 @@ class Home(MainHandler): def testKODI(self, host=None, username=None, password=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - host = config.clean_hosts(host) + hosts = config.clean_hosts(host) + if not hosts: + return 'Fail: At least one invalid host' + if None is not password and set('*') == set(password): password = sickbeard.KODI_PASSWORD - finalResult = '' - for curHost in [x.strip() for x in host.split(',')]: - curResult = notifiers.kodi_notifier.test_notify(urllib.unquote_plus(curHost), username, password) - if len(curResult.split(':')) > 2 and 'OK' in curResult.split(':')[2]: - finalResult += 'Test Kodi notice sent successfully to ' + urllib.unquote_plus(curHost) - else: - finalResult += 'Test Kodi notice failed to ' + urllib.unquote_plus(curHost) - finalResult += '
\n' - - return finalResult + total_success, cur_message = notifiers.kodi_notifier.test_notify(hosts, username, password) + return (cur_message, u'Success. All Kodi hosts tested.')[total_success] def testPMC(self, host=None, username=None, password=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store')