mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-03 01:43:37 +00:00
Merge pull request #648 from JackDandy/feature/ChangeKodiNotifier
Change overhaul Kodi notifier and tidy up config/notification/KodiNot…
This commit is contained in:
commit
5e848c157f
7 changed files with 467 additions and 182 deletions
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
<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"> </span>
|
||||
<span class="component-desc">host running Kodi (eg. 192.168.1.100:8080)</span>
|
||||
</label>
|
||||
<label>
|
||||
<span class="component-title"> </span>
|
||||
<span class="component-desc">(multiple host strings must be separated by commas)</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" />
|
||||
<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>
|
||||
<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"> </span>
|
||||
<span class="component-desc">username for your KODI server (blank for none)</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" />
|
||||
<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>
|
||||
<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"> </span>
|
||||
<span class="component-desc">password for your KODI server (blank for none)</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" />
|
||||
<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>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
# 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, '<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
|
||||
|
||||
##############################################################################
|
||||
# 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;<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)
|
||||
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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue