Merge pull request #648 from JackDandy/feature/ChangeKodiNotifier

Change overhaul Kodi notifier and tidy up config/notification/KodiNot…
This commit is contained in:
JackDandy 2016-02-19 17:41:45 +00:00
commit 5e848c157f
7 changed files with 467 additions and 182 deletions

View file

@ -30,6 +30,8 @@
* Allow episode status "Skipped" to be changed to "Downloaded" * Allow episode status "Skipped" to be changed to "Downloaded"
* Allow found "Skipped" episode files to be set "Unknown" quality * Allow found "Skipped" episode files to be set "Unknown" quality
* Add CPU throttling preset "Disabled" to config/General/Advanced Settings * 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) ### 0.11.6 (2016-02-18 23:10:00 UTC)

View file

@ -211,19 +211,19 @@
</div> </div>
<div class="field-pair"> <div class="field-pair">
<label for="kodi_update_library"> <label for="kodi_update_library">
<span class="component-title">Update library</span> <span class="component-title">Update shows known to Kodi</span>
<span class="component-desc"> <span class="component-desc">
<input type="checkbox" name="kodi_update_library" id="kodi_update_library" #if $sickbeard.KODI_UPDATE_LIBRARY then 'checked="checked"' else ''# /> <input type="checkbox" name="kodi_update_library" id="kodi_update_library" #if $sickbeard.KODI_UPDATE_LIBRARY then 'checked="checked"' else ''# />
<p>update Kodi library when a download finishes ?</p> <p>with changes under the path of processed shows ?</p>
</span> </span>
</label> </label>
</div> </div>
<div class="field-pair"> <div class="field-pair">
<label for="kodi_update_full"> <label for="kodi_update_full">
<span class="component-title">Full library update</span> <span class="component-title">Perform full library update</span>
<span class="component-desc"> <span class="component-desc">
<input type="checkbox" name="kodi_update_full" id="kodi_update_full" #if $sickbeard.KODI_UPDATE_FULL then 'checked="checked"' else ''# /> <input type="checkbox" name="kodi_update_full" id="kodi_update_full" #if $sickbeard.KODI_UPDATE_FULL then 'checked="checked"' else ''# />
<p>perform a full library update if update per-show fails ?</p> <p>if "Update shows" fails (e.g. to have Kodi find newly added shows)</p>
</span> </span>
</label> </label>
</div> </div>
@ -238,36 +238,31 @@
</div> </div>
<div class="field-pair"> <div class="field-pair">
<label for="kodi_host"> <label for="kodi_host">
<span class="component-title">Kodi IP:Port</span> <span class="component-title">Host(s) running Kodi</span>
<input type="text" name="kodi_host" id="kodi_host" value="$sickbeard.KODI_HOST" class="form-control input-sm input350" /> <span class="component-desc">
</label> <input type="text" name="kodi_host" id="kodi_host" value="$sickbeard.KODI_HOST" class="form-control input-sm input350" />
<label> <div class="clear-left"><p>IP:Port [, IP:Port] (e.g. 192.168.0.1:8080, 192.168.1.2:8080)</p></div>
<span class="component-title">&nbsp;</span> </span>
<span class="component-desc">host running Kodi (eg. 192.168.1.100:8080)</span>
</label>
<label>
<span class="component-title">&nbsp;</span>
<span class="component-desc">(multiple host strings must be separated by commas)</span>
</label> </label>
</div> </div>
<div class="field-pair"> <div class="field-pair">
<label for="kodi_username"> <label for="kodi_username">
<span class="component-title">Kodi username</span> <span class="component-title">Kodi web server username</span>
<input type="text" name="kodi_username" id="kodi_username" value="$sickbeard.KODI_USERNAME" class="form-control input-sm input250" /> <span class="component-desc">
</label> <input type="text" name="kodi_username" id="kodi_username" value="$sickbeard.KODI_USERNAME" class="form-control input-sm input250" />
<label> <p>(blank for none)</p>
<span class="component-title">&nbsp;</span> <div class="clear-left"><p>in Kodi System/Settings/Services/Web server</p></div>
<span class="component-desc">username for your KODI server (blank for none)</span> </span>
</label> </label>
</div> </div>
<div class="field-pair"> <div class="field-pair">
<label for="kodi_password"> <label for="kodi_password">
<span class="component-title">Kodi password</span> <span class="component-title">Kodi web server password</span>
<input type="password" name="kodi_password" id="kodi_password" value="#echo '*' * len($sickbeard.KODI_PASSWORD)#" class="form-control input-sm input250" /> <span class="component-desc">
</label> <input type="password" name="kodi_password" id="kodi_password" value="#echo '*' * len($sickbeard.KODI_PASSWORD)#" class="form-control input-sm input250" />
<label> <p>(blank for none)</p>
<span class="component-title">&nbsp;</span> <div class="clear-left"><p>in Kodi System/Settings/Services/Web server</p></div>
<span class="component-desc">password for your KODI server (blank for none)</span> </span>
</label> </label>
</div> </div>
<div class="testNotification" id="testKODI-result">Click below to test.</div> <div class="testNotification" id="testKODI-result">Click below to test.</div>

View file

@ -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 # 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: 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: else:
resp = session.get(url, timeout=timeout, **kwargs) 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 return
except requests.exceptions.HTTPError as e: 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 return
except requests.exceptions.ConnectionError as e: 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 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' % (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 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' % (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 return
except Exception as e: except Exception as e:
url = hasattr(e, 'request') and e.request.url or url
if e.message: 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: 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 return
if json: 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 return resp.content

View file

@ -16,8 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>. # along with SickGear. If not, see <http://www.gnu.org/licenses/>.
import sickbeard
import xbmc import xbmc
import kodi import kodi
import plex import plex
@ -41,11 +39,9 @@ import tweet
from lib import libtrakt from lib import libtrakt
import emailnotify import emailnotify
from sickbeard.common import *
# home theater / nas # home theater / nas
xbmc_notifier = xbmc.XBMCNotifier() xbmc_notifier = xbmc.XBMCNotifier()
kodi_notifier = kodi.KODINotifier() kodi_notifier = kodi.KodiNotifier()
plex_notifier = plex.PLEXNotifier() plex_notifier = plex.PLEXNotifier()
nmj_notifier = nmj.NMJNotifier() nmj_notifier = nmj.NMJNotifier()
nmjv2_notifier = nmjv2.NMJv2Notifier() nmjv2_notifier = nmjv2.NMJv2Notifier()

View file

@ -1,7 +1,8 @@
# Author: Nic Wolfe <nic@wolfeden.ca> # coding=utf-8
# URL: http://code.google.com/p/sickbeard/
# #
# This file is part of SickGear. # This file is part of SickGear.
# Author: SickGear
# Thanks to: Nic Wolfe
# #
# SickGear is free software: you can redistribute it and/or modify # SickGear is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # 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 # You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>. # along with SickGear. If not, see <http://www.gnu.org/licenses/>.
import base64 import time
import requests import urllib
import sickbeard import sickbeard
from sickbeard import logger, common import sickbeard.helpers
from sickbeard.exceptions import ex 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: try:
import json import json
@ -29,151 +38,397 @@ except ImportError:
from lib import simplejson as json from lib import simplejson as json
class KODINotifier: class KodiNotifier:
sg_logo_url = 'https://raw.githubusercontent.com/SickGear/SickGear/master/gui/slick/images/ico/apple-touch-icon' \ def __init__(self):
'-precomposed.png' 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 def _get_kodi_version(self, host):
if not host: """ Return Kodi JSON-RPC API version (odd # = dev, even # = stable)
host = sickbeard.KODI_HOST
if not username:
username = sickbeard.KODI_USERNAME
if not password:
password = sickbeard.KODI_PASSWORD
# suppress notifications if the notifier is disabled but the notify options are checked Communicate with Kodi hosts using JSON-RPC to determine whether to use the legacy API or the JSON-RPC API.
if not sickbeard.USE_KODI and not force:
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 return False
result = '' if response.get('version'):
for curHost in [x.strip() for x in host.split(',')]: version = response.get('version')
logger.log(u'KODI: Sending Kodi notification to "%s" - %s' % (curHost, message), logger.MESSAGE) return isinstance(version, dict) and version.get('major') or version
command = {'jsonrpc': '2.0', 'method': 'GUI.ShowNotification',
'params': {'title': title, 'message': message, 'image': self.sg_logo_url}, 'id': 1} # fallback to legacy HTTPAPI method
notifyResult = self._send_to_kodi(command, curHost, username, password) test_command = {'command': 'Help'}
if notifyResult: if self._send_to_kodi(host, test_command):
result += '%s:%s' % (curHost, notifyResult['result']) # 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: else:
if sickbeard.KODI_ALWAYS_ON or force: if 4 >= api_version:
result += '%s:False' % curHost self._log(u'Detected %sversion <= 11, using HTTP API'
return result % 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 return total_success, '<br />\n'.join(message)
if not username:
username = sickbeard.KODI_USERNAME def _send_update_library(self, host, show_name=None):
if not password: """ Internal wrapper for the update library function
password = sickbeard.KODI_PASSWORD
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: 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 return False
data = json.dumps(command) args = {}
logger.log(u'KODI: JSON command: %s' % data, logger.DEBUG) 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'} url = 'http://%s/%sCmds/%sHttp' % (host, self.prefix or 'kodi', self.prefix or 'kodi')
# if we have a password, use authentication response = sickbeard.helpers.getURL(url=url, params=command, hooks=dict(response=self.cb_response), **args)
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)
try: return response or False
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
if response.status_code == 401: def _update_library(self, host=None, show_name=None):
logger.log(u'KODI: Invalid login credentials', logger.ERROR) """ Handle updating Kodi host via HTTP API
return False
# parse the json result Update the video library for a specific tv show if passed, otherwise update the whole library if option enabled.
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): Args:
show_name: Name of a TV show to target for a library update
Return:
True or False
"""
if not host: 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 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 we're doing per-show
if showName: if show_name:
logger.log(u'KODI: Updating library for show %s' % showName, logger.DEBUG) self._log(u'Updating library via HTTP method for show %s' % show_name, logger.DEBUG)
# get tvshowid by showName path_sql = 'SELECT path.strPath FROM path, tvshow, tvshowlinkpath WHERE ' \
showsCommand = {'jsonrpc': '2.0', 'method': 'VideoLibrary.GetTVShows', 'id': 1} 'tvshow.c00 = "%s"' % show_name \
showsResponse = self._send_to_kodi(showsCommand, host) + ' AND tvshowlinkpath.idShow = tvshow.idShow AND tvshowlinkpath.idPath = path.idPath'
try: # set xml response format, if this fails then don't bother with the rest
shows = showsResponse['result']['tvshows'] if not self._send_to_kodi(
except: host, {'command': 'SetResponseFormat(webheader;false;webfooter;false;header;<xml>;footer;</xml>;' +
logger.log(u'KODI: No TV shows in Kodi TV show list', logger.DEBUG) 'opentag;<tag>;closetag;</tag>;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 return False
try: try:
tvshowid = next((show['tvshowid'] for show in shows if show['label'] == showName)) et = etree.fromstring(urllib.quote(response, ':\\/<>'))
except StopIteration: except SyntaxError as e:
logger.log(u'XBMC: Exact show name not matched in XBMC TV show list', logger.DEBUG) self._log(u'Unable to parse XML in response: %s' % ex(e), logger.ERROR)
return False return False
# lookup tv-show path paths = et.findall('.//field')
pathCommand = {'jsonrpc': '2.0', 'method': 'VideoLibrary.GetTVShowDetails', if not paths:
'params': {'tvshowid': tvshowid, 'properties': ['file']}, 'id': 1} self._log(u'No valid path found for %s on %s' % (show_name, host), logger.DEBUG)
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)
return False return False
logger.log(u'KODI: Updating %s on %s at %s' % (showName, host, path), logger.DEBUG) for path in paths:
updateCommand = {'jsonrpc': '2.0', 'method': 'VideoLibrary.Scan', # we do not need it double-encoded, gawd this is dumb
'params': {'directory': json.dumps(path)}, 'id': 1} un_enc_path = urllib.unquote(path.text).decode(sickbeard.SYS_ENCODING)
request = self._send_to_kodi(updateCommand, host) self._log(u'Updating %s on %s at %s' % (show_name, host, un_enc_path), logger.DEBUG)
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
# catch if there was an error in the returned request if not self._send_to_kodi(
for r in request: host, {'command': 'ExecBuiltIn', 'parameter': 'Kodi.updatelibrary(video, %s)' % un_enc_path}):
if 'error' in r: self._log(u'Update of show directory failed for %s on %s at %s'
logger.log(u'KODI: Error while attempting to update show directory for %s on %s at %s' % (show_name, host, un_enc_path), logger.ERROR)
% (showName, host, path), logger.ERROR)
return False 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 # do a full update if requested
else: else:
logger.log(u'KODI: Performing full library update on host: %s' % host, logger.DEBUG) self._log(u'Full library update on host: %s' % host, logger.DEBUG)
updateCommand = {'jsonrpc': '2.0', 'method': 'VideoLibrary.Scan', 'id': 1} response_scan = self._send_to_kodi_json(host, dict(method='VideoLibrary.Scan'))
request = self._send_to_kodi(updateCommand, host, sickbeard.KODI_USERNAME, sickbeard.KODI_PASSWORD) if not response_scan.get('OK'):
self._log(u'Failed full library update on: %s response: %s' % (host, response_scan), logger.ERROR)
if not request:
logger.log(u'KODI: Full library update failed on host: %s' % host, logger.ERROR)
return False return False
return True 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): def notify_snatch(self, ep_name):
if sickbeard.KODI_NOTIFY_ONSNATCH: if sickbeard.KODI_NOTIFY_ONSNATCH:
self._notify_kodi(ep_name, common.notifyStrings[common.NOTIFY_SNATCH]) 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) self._notify_kodi('%s %s' % (update_text, new_version), title)
def test_notify(self, host, username, password): def test_notify(self, host, username, password):
return self._notify_kodi( self.test_mode, self.username, self.password = True, username, password
'Testing Kodi notifications from SickGear', 'Test', host, username, password, force=True) return self._notify_kodi('Testing SickGear Kodi notifier', 'Test Notification', kodi_hosts=host)
def update_library(self, showName=None): 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 sickbeard.USE_KODI and sickbeard.KODI_UPDATE_LIBRARY:
if not sickbeard.KODI_HOST: 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 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 result = 0
for host in [x.strip() for x in sickbeard.KODI_HOST.split(',')]: only_first = dict(show='', first='', first_note='')
if self._update_library(host, showName): showName and only_first.update(show=' for show;"%s"' % showName)
if sickbeard.KODI_UPDATE_ONLYFIRST: sickbeard.KODI_UPDATE_ONLYFIRST and only_first.update(dict(
logger.log( first=' first', first_note=' in line with the "Only update first host"%s' % ' setting'))
u'KODI: Update first host successful on host %s , stopped sending library update commands'
% host, logger.DEBUG) for cur_host in [x.strip() for x in sickbeard.KODI_HOST.split(',')]:
return True
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: else:
if sickbeard.KODI_ALWAYS_ON: self._maybe_log_failed_detection(cur_host)
result = result + 1 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 # needed for the 'update kodi' submenu command as it only cares of the final result vs the individual ones
if result == 0: return 0 == result
return True
else:
return False
notifier = KODINotifier notifier = KodiNotifier

View file

@ -70,15 +70,15 @@ class LibnotifyNotifier:
logger.log(u"Unable to import pynotify. libnotify notifications won't work.", logger.ERROR) logger.log(u"Unable to import pynotify. libnotify notifications won't work.", logger.ERROR)
return False return False
try: try:
import gobject from gi.repository import GObject
except ImportError: 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 return False
if not pynotify.init('SickGear'): if not pynotify.init('SickGear'):
logger.log(u"Initialization of pynotify failed. libnotify notifications won't work.", logger.ERROR) logger.log(u"Initialization of pynotify failed. libnotify notifications won't work.", logger.ERROR)
return False return False
self.pynotify = pynotify self.pynotify = pynotify
self.gobject = gobject self.gobject = GObject
return True return True
def notify_snatch(self, ep_name): def notify_snatch(self, ep_name):

View file

@ -1,3 +1,4 @@
# coding=utf-8
# Author: Nic Wolfe <nic@wolfeden.ca> # Author: Nic Wolfe <nic@wolfeden.ca>
# URL: http://code.google.com/p/sickbeard/ # URL: http://code.google.com/p/sickbeard/
# #
@ -791,20 +792,15 @@ class Home(MainHandler):
def testKODI(self, host=None, username=None, password=None): def testKODI(self, host=None, username=None, password=None):
self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') 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): if None is not password and set('*') == set(password):
password = sickbeard.KODI_PASSWORD password = sickbeard.KODI_PASSWORD
finalResult = '' total_success, cur_message = notifiers.kodi_notifier.test_notify(hosts, username, password)
for curHost in [x.strip() for x in host.split(',')]: return (cur_message, u'Success. All Kodi hosts tested.')[total_success]
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 += '<br />\n'
return finalResult
def testPMC(self, host=None, username=None, password=None): def testPMC(self, host=None, username=None, password=None):
self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') self.set_header('Cache-Control', 'max-age=0,no-cache,no-store')