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')