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

View file

@ -211,19 +211,19 @@
</div>
<div class="field-pair">
<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">
<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>
</label>
</div>
<div class="field-pair">
<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">
<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>
</label>
</div>
@ -238,36 +238,31 @@
</div>
<div class="field-pair">
<label for="kodi_host">
<span class="component-title">Kodi IP:Port</span>
<span class="component-title">Host(s) running Kodi</span>
<span class="component-desc">
<input type="text" name="kodi_host" id="kodi_host" value="$sickbeard.KODI_HOST" class="form-control input-sm input350" />
</label>
<label>
<span class="component-title">&nbsp;</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>
<div class="clear-left"><p>IP:Port [, IP:Port] (e.g. 192.168.0.1:8080, 192.168.1.2:8080)</p></div>
</span>
</label>
</div>
<div class="field-pair">
<label for="kodi_username">
<span class="component-title">Kodi username</span>
<span class="component-title">Kodi web server username</span>
<span class="component-desc">
<input type="text" name="kodi_username" id="kodi_username" value="$sickbeard.KODI_USERNAME" class="form-control input-sm input250" />
</label>
<label>
<span class="component-title">&nbsp;</span>
<span class="component-desc">username for your KODI server (blank for none)</span>
<p>(blank for none)</p>
<div class="clear-left"><p>in Kodi System/Settings/Services/Web server</p></div>
</span>
</label>
</div>
<div class="field-pair">
<label for="kodi_password">
<span class="component-title">Kodi password</span>
<span class="component-title">Kodi web server password</span>
<span class="component-desc">
<input type="password" name="kodi_password" id="kodi_password" value="#echo '*' * len($sickbeard.KODI_PASSWORD)#" class="form-control input-sm input250" />
</label>
<label>
<span class="component-title">&nbsp;</span>
<span class="component-desc">password for your KODI server (blank for none)</span>
<p>(blank for none)</p>
<div class="clear-left"><p>in Kodi System/Settings/Services/Web server</p></div>
</span>
</label>
</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
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:
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

View file

@ -16,8 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
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()

View file

@ -1,7 +1,8 @@
# Author: Nic Wolfe <nic@wolfeden.ca>
# 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 <http://www.gnu.org/licenses/>.
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
def _get_kodi_version(self, host):
""" Return Kodi JSON-RPC API version (odd # = dev, even # = stable)
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
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 host:
host = sickbeard.KODI_HOST
if not username:
username = sickbeard.KODI_USERNAME
if not password:
password = sickbeard.KODI_PASSWORD
if not kodi_hosts:
kodi_hosts = sickbeard.KODI_HOST
# suppress notifications if the notifier is disabled but the notify options are checked
if not sickbeard.USE_KODI and not force:
return False
if not sickbeard.USE_KODI and not self.test_mode:
self._log(u'Notification not enabled, skipping this notification', logger.DEBUG)
return False, None
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'])
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
def _send_to_kodi(self, command, host=None, username=None, password=None):
# fill in omitted parameters
if not username:
username = sickbeard.KODI_USERNAME
if not password:
password = sickbeard.KODI_PASSWORD
if not host:
logger.log(u'KODI: No host specified, check your settings', logger.ERROR)
return False
data = json.dumps(command)
logger.log(u'KODI: JSON command: %s' % data, logger.DEBUG)
url = 'http://%s/jsonrpc' % host
headers = {'Content-type': 'application/json'}
# if we have a password, use authentication
if password:
base64string = base64.encodestring('%s:%s' % (username, password))[:-1]
authheader = 'Basic %s' % base64string
headers['Authorization'] = authheader
logger.log(u'KODI: Contacting (with auth header) via url: %s' % fixStupidEncodings(url), logger.DEBUG)
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:
logger.log(u'KODI: Contacting via url: %s' % fixStupidEncodings(url), logger.DEBUG)
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})
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)
response_notify = __method_send(cur_host, command)
if response_notify:
message += ['%s: %s' % ((response_notify, 'OK')['OK' in response_notify], cur_host)]
return total_success, '<br />\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
if response.status_code == 401:
logger.log(u'KODI: Invalid login credentials', logger.ERROR)
return False
##############################################################################
# Legacy HTTP API (pre Kodi 12) methods
##############################################################################
# parse the json result
try:
result = response.json()
logger.log(u'KODI: JSON response: %s' % result, logger.DEBUG)
return result # need to return response for parsing
except ValueError as e:
logger.log(u'KODI: Unable to decode JSON response: %s' % response.text, logger.WARNING)
return False
def _send_to_kodi(self, host, command):
""" Handle communication to Kodi servers via HTTP API
def _update_library(self, host=None, showName=None):
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.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)
args = {}
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)
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)
return response or False
def _update_library(self, host=None, show_name=None):
""" Handle updating Kodi host via HTTP API
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
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;<xml>;footer;</xml>;' +
'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
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)
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)
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
# 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)
# 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

View file

@ -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):

View file

@ -1,3 +1,4 @@
# coding=utf-8
# Author: Nic Wolfe <nic@wolfeden.ca>
# 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 += '<br />\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')