diff --git a/CHANGES.md b/CHANGES.md
index bb3b232..183d3d2 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -22,6 +22,19 @@
+### 0.21.6 (2020-01-21 22:30:00 UTC)
+
+* Fix Kodi service addon + bump to 1.0.7 (select "Check for updates" on menu of "SickGear Add-on repository")
+* Change Kodi Add-on/"What's new" list order to be latest version info at top
+* Add output to SG log when a new Kodi Add-on version is available for upgrade
+* Fix a rare post processing issue that created `dictionary changed size` error
+* Fix ensure PySocks is available for Requests/urllib3
+* Fix fanart image update issue
+* Change add examples to config/general/advanced/"Proxy host" that show scheme and authentication usage
+* Change add warning that Kodi Add-on requires IP to setting config/general/"Allow IP use for connections"
+* Change About page version string
+
+
### 0.21.5 (2020-01-15 02:25:00 UTC)
* Update Fuzzywuzzy 0.17.0 (778162c) to 0.17.0 (0cfb2c8)
diff --git a/gui/slick/interfaces/default/config.tmpl b/gui/slick/interfaces/default/config.tmpl
index 451ccf7..ed83147 100644
--- a/gui/slick/interfaces/default/config.tmpl
+++ b/gui/slick/interfaces/default/config.tmpl
@@ -22,7 +22,7 @@
Version:
- BRANCH: #echo $sg_str('BRANCH') or 'UNKNOWN'# / COMMIT: #echo ($sg_str('CUR_COMMIT_HASH')[0:7] or 'UNKNOWN') + ('', ' @ ')[bool($version)]#$version
+ BRANCH: #echo $sg_str('BRANCH') or 'UNKNOWN'# @ py#echo '.'.join(['%s' % x for x in sys.version_info[0:3]])# / COMMIT: #echo ($sg_str('CUR_COMMIT_HASH')[0:7] or 'UNKNOWN') + ('', ' @ ')[bool($version)]#$version This is BETA software
#if not $sg_var('VERSION_NOTIFY') and not $sg_var('EXT_UPDATES'):
You don't have version checking turned on, see "Check software updates" in Config > General.
diff --git a/gui/slick/interfaces/default/config_general.tmpl b/gui/slick/interfaces/default/config_general.tmpl
index 7d06559..ef984f6 100644
--- a/gui/slick/interfaces/default/config_general.tmpl
+++ b/gui/slick/interfaces/default/config_general.tmpl
@@ -619,8 +619,9 @@
@@ -744,7 +745,8 @@
blank to disable
-
proxy address for connecting to providers (use 'PAC:Url' for PAC support)
+
proxy address for connecting to providers (use 'PAC:Url' for PAC support)
+ e.g. socks5://host:port, socks5://user:pass@host:port, socks4a://user:pass@host:port
diff --git a/lib/socks/__init__.py b/lib/socks/__init__.py
index e69de29..811f07b 100644
--- a/lib/socks/__init__.py
+++ b/lib/socks/__init__.py
@@ -0,0 +1,20 @@
+# coding=utf-8
+#
+# This file is part of SickGear.
+#
+# SickGear is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# SickGear is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with SickGear. If not, see .
+
+# --------------------------------------------------------------------------
+# Note: Ensures PySocks is imported to be used by libs Requests/urllib3
+from .socks import *
diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py
index 0b1dad0..ba6eb18 100755
--- a/sickbeard/__init__.py
+++ b/sickbeard/__init__.py
@@ -565,6 +565,8 @@ CACHE_IMAGE_URL_LIST = classes.ImageUrlList()
__INITIALIZED__ = False
__INIT_STAGE__ = 0
+MEMCACHE = {}
+
def get_backlog_cycle_time():
cycletime = RECENTSEARCH_FREQUENCY * 2 + 7
@@ -1366,7 +1368,7 @@ def init_stage_1(console_logging):
def init_stage_2():
# Misc
- global __INITIALIZED__, RECENTSEARCH_STARTUP
+ global __INITIALIZED__, MEMCACHE, RECENTSEARCH_STARTUP
# Schedulers
# global traktCheckerScheduler
global recentSearchScheduler, backlogSearchScheduler, showUpdateScheduler, \
diff --git a/sickbeard/clients/kodi/service.sickgear.watchedstate.updater/addon.xml b/sickbeard/clients/kodi/service.sickgear.watchedstate.updater/addon.xml
index c6945ec..04c7c0c 100644
--- a/sickbeard/clients/kodi/service.sickgear.watchedstate.updater/addon.xml
+++ b/sickbeard/clients/kodi/service.sickgear.watchedstate.updater/addon.xml
@@ -1,5 +1,5 @@
-
+
@@ -24,14 +24,20 @@
icon.png
- [B]1.0.0[/B] (2017-10-04)
-- Initial release
-[B]1.0.2[/B] (2017-11-15)
-- Devel release for an SG API change
+ [B]1.0.7[/B] (2020-01-21)
+- Public release
+[B]1.0.6[/B] (2020-01-18)
+- Public test release
+[B]1.0.5[/B] (2020-01-16)
+- Fix service code for py3
+[B]1.0.4[/B] (2019-08-13)
+- Tidy up service code
[B]1.0.3[/B] (2018-02-28)
- Add episodeid to payload
-[B]1.0.4[/B] (2019-08-13)
-- Tidy up Service code
+[B]1.0.2[/B] (2017-11-15)
+- Devel release for an SG API change
+[B]1.0.0[/B] (2017-10-04)
+- Initial release
diff --git a/sickbeard/clients/kodi/service.sickgear.watchedstate.updater/changelog.txt b/sickbeard/clients/kodi/service.sickgear.watchedstate.updater/changelog.txt
index e5a1368..3a1e2e6 100644
--- a/sickbeard/clients/kodi/service.sickgear.watchedstate.updater/changelog.txt
+++ b/sickbeard/clients/kodi/service.sickgear.watchedstate.updater/changelog.txt
@@ -1,8 +1,14 @@
-[B]1.0.0[/B] (2017-10-04)
-- Initial release
-[B]1.0.2[/B] (2017-11-15)
-- Devel release for an SG API change
-[B]1.0.3[/B] (2018-02-28)
-- Add episodeid to payload
+[B]1.0.7[/B] (2020-01-21)
+- Public release
+[B]1.0.6[/B] (2020-01-18)
+- Public test release
+[B]1.0.5[/B] (2020-01-16)
+- Fix service code for py3
[B]1.0.4[/B] (2019-08-13)
- Service code cleanup
+[B]1.0.3[/B] (2018-02-28)
+- Add episodeid to payload
+[B]1.0.2[/B] (2017-11-15)
+- Devel release for an SG API change
+[B]1.0.0[/B] (2017-10-04)
+- Initial release
diff --git a/sickbeard/clients/kodi/service.sickgear.watchedstate.updater/service.py b/sickbeard/clients/kodi/service.sickgear.watchedstate.updater/service.py
index bdfeabf..2ac21a6 100644
--- a/sickbeard/clients/kodi/service.sickgear.watchedstate.updater/service.py
+++ b/sickbeard/clients/kodi/service.sickgear.watchedstate.updater/service.py
@@ -36,14 +36,19 @@ import xbmcgui
# noinspection PyUnresolvedReferences
import xbmcvfs
+ADDON_VERSION = '1.0.7'
+
PY2 = 2 == sys.version_info[0]
if PY2:
# noinspection PyCompatibility,PyUnresolvedReferences
- from urllib2 import Request, urlopen, URLError, urlencode
+ from urllib2 import Request, urlopen, URLError
+ # noinspection PyUnresolvedReferences
+ from urllib import urlencode
# noinspection PyCompatibility,PyUnresolvedReferences
string_types = (basestring,)
+ binary_type = str
def iterkeys(d, **kw):
# noinspection PyCompatibility
@@ -57,8 +62,12 @@ if PY2:
# noinspection PyCompatibility
return d.iteritems(**kw)
- def iterlists(d, **kw):
- return d.iterlists(**kw)
+ # noinspection PyUnusedLocal
+ def decode_bytes(d, **kw):
+ if not isinstance(d, binary_type):
+ return bytes(d)
+ return d
+
else:
# noinspection PyCompatibility,PyUnresolvedReferences
from urllib.error import URLError
@@ -68,6 +77,7 @@ else:
from urllib.request import Request, urlopen
string_types = (str,)
+ binary_type = bytes
def iterkeys(d, **kw):
return iter(d.keys(**kw))
@@ -78,8 +88,19 @@ else:
def iteritems(d, **kw):
return iter(d.items(**kw))
- def iterlists(d, **kw):
- return iter(d.lists(**kw))
+ def decode_bytes(d, encoding='utf-8', errors='replace'):
+ if not isinstance(d, binary_type):
+ # noinspection PyArgumentList
+ return bytes(d, encoding=encoding, errors=errors)
+ return d
+
+
+def decode_str(s, encoding='utf-8', errors=None):
+ if isinstance(s, binary_type):
+ if None is errors:
+ return s.decode(encoding)
+ return s.decode(encoding, errors)
+ return s
class SickGearWatchedStateUpdater(object):
@@ -128,13 +149,20 @@ class SickGearWatchedStateUpdater(object):
self.log('Started')
self.notify('Started in background')
+ cache_pkg = 'special://home/addons/packages/service.sickgear.watchedstate.updater-%s.zip' % ADDON_VERSION
+ if xbmcvfs.exists(cache_pkg):
+ try:
+ xbmcvfs.delete(cache_pkg)
+ except (BaseException, Exception):
+ pass
+
self.kodi_events = xbmc.Monitor()
sock_buffer, depth, methods, method = '', 0, {'VideoLibrary.OnUpdate': self.video_library_on_update}, None
# socks listener parsing Kodi json output into action to perform
while not self.kodi_events.abortRequested():
- chunk = self.sock_kodi.recv(1)
+ chunk = decode_str(self.sock_kodi.recv(1))
sock_buffer += chunk
if chunk in '{}':
if '{' == chunk:
@@ -229,7 +257,7 @@ class SickGearWatchedStateUpdater(object):
def video_library_on_update(self, json_msg):
"""
- Actions to perform for: Kodi Notifications / VideoLibrary/ VideoLibrary.OnUpdate
+ Actions to perform for: Kodi Notifications / VideoLibrary / VideoLibrary.OnUpdate
invoked in Kodi when: A video item has been updated
source: http://kodi.wiki/view/JSON-RPC_API/v8#VideoLibrary.OnUpdate
@@ -270,9 +298,9 @@ class SickGearWatchedStateUpdater(object):
payload_json = self.payload_prep(dict(media_id=media_id, path_file=path_file, played=play_count, label=profile))
if payload_json:
- payload = urlencode(dict(payload=payload_json))
+ payload = urlencode(dict(payload=payload_json, version=ADDON_VERSION))
try:
- rq = Request(url, data=payload)
+ rq = Request(url, data=decode_bytes(payload))
r = urlopen(rq)
response = json.load(r)
r.close()
@@ -282,7 +310,7 @@ class SickGearWatchedStateUpdater(object):
msg = 'Success, watched state updated'
else:
msg = 'Success, %s/%s watched stated updated' % (
- len([v for v in itervalues(response) if v]), len(itervalues(response)))
+ len([None for v in itervalues(response) if v]), len([None for _ in itervalues(response)]))
self.log(msg)
self.notify(msg, error=False)
else:
@@ -362,18 +390,19 @@ class SickGearWatchedStateUpdater(object):
# setting esallinterfaces: allow remote control by programs on other systems
settings = [dict(esenabled=True), dict(esallinterfaces=True)]
for setting in settings:
+ name = next(iterkeys(setting))
if not self.kodi_request(dict(
method='Settings.SetSettingValue',
- params=dict(setting='services.%s' % iterkeys(setting)[0], value=itervalues(setting)[0])
+ params=dict(setting='services.%s' % name, value=next(itervalues(setting)))
)).get('result', {}):
settings[setting] = self.kodi_request(dict(
method='Settings.GetSettingValue',
- params=dict(setting='services.%s' % iterkeys(setting)[0])
+ params=dict(setting='services.%s' % name)
)).get('result', {}).get('value')
except (BaseException, Exception):
return
- setting_states = [itervalues(setting)[0] for setting in settings]
+ setting_states = [next(itervalues(setting)) for setting in settings]
if not all(setting_states):
if not (any(setting_states)):
msg = 'Please enable *all* Kodi settings to allow remote control by programs...'
diff --git a/sickbeard/image_cache.py b/sickbeard/image_cache.py
index e47edea..bb311e6 100644
--- a/sickbeard/image_cache.py
+++ b/sickbeard/image_cache.py
@@ -213,7 +213,7 @@ class ImageCache(object):
:return: true if a cached fanart exists for the given tvid prodid
:rtype: bool
"""
- return self.has_file(self.fanart_path(tvid, prodid).replace('fanart.jpg', '*.001.*.fanart.jpg'))
+ return self.has_file(self.fanart_path(tvid, prodid).replace('fanart.jpg', '001.*.fanart.jpg'))
def has_poster_thumbnail(self, tvid, prodid):
# type: (int, int) -> bool
diff --git a/sickbeard/processTV.py b/sickbeard/processTV.py
index 67a0361..22101e5 100644
--- a/sickbeard/processTV.py
+++ b/sickbeard/processTV.py
@@ -43,7 +43,7 @@ from .history import reset_status
from .name_parser.parser import InvalidNameException, InvalidShowException, NameParser
from _23 import filter_list, filter_iter, list_values, map_iter
-from six import iterkeys, string_types, PY2, text_type
+from six import iteritems, iterkeys, string_types, PY2, text_type
from sg_helpers import long_path
import lib.rarfile.rarfile as rarfile
@@ -566,9 +566,7 @@ class ProcessTVShow(object):
init_history_cnt = len(archive_history)
- for archive in archive_history:
- if not ek.ek(os.path.isfile, archive):
- del archive_history[archive]
+ archive_history = {k_arc: v for k_arc, v in iteritems(archive_history) if ek.ek(os.path.isfile, k_arc)}
unused_files = list(set([ek.ek(os.path.join, path, x) for x in archives]) - set(iterkeys(archive_history)))
archives = [ek.ek(os.path.basename, x) for x in unused_files]
diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py
index 01f439d..9475c9f 100644
--- a/sickbeard/webserve.py
+++ b/sickbeard/webserve.py
@@ -583,11 +583,30 @@ class RepoHandler(BaseStaticFileHandler):
return re.findall(r'(?si)addon\sid="([^"]+)[^>]+version="([^"]+)',
self.get_watchedstate_updater_addon_xml())[0]
- @staticmethod
- def get_watchedstate_updater_addon_xml():
+ def get_watchedstate_updater_addon_xml(self):
+ mem_key = 'kodi_xml'
+ if SGDatetime.now().totimestamp(default=0) < sickbeard.MEMCACHE.get(mem_key, {}).get('last_update', 0):
+ return sickbeard.MEMCACHE.get(mem_key).get('data')
+
with io.open(ek.ek(os.path.join, sickbeard.PROG_DIR, 'sickbeard', 'clients',
'kodi', 'service.sickgear.watchedstate.updater', 'addon.xml'), 'r') as fh:
- return fh.read().strip()
+ xml = fh.read().strip() % dict(ADDON_VERSION=self.get_addon_version())
+
+ sickbeard.MEMCACHE[mem_key] = dict(last_update=30 + SGDatetime.now().totimestamp(default=0), data=xml)
+ return xml
+
+ @staticmethod
+ def get_addon_version():
+ mem_key = 'kodi_ver'
+ if SGDatetime.now().totimestamp(default=0) < sickbeard.MEMCACHE.get(mem_key, {}).get('last_update', 0):
+ return sickbeard.MEMCACHE.get(mem_key).get('data')
+
+ with io.open(ek.ek(os.path.join, sickbeard.PROG_DIR, 'sickbeard', 'clients',
+ 'kodi', 'service.sickgear.watchedstate.updater', 'service.py'), 'r') as fh:
+ version = re.findall(r'ADDON_VERSION\s*?=\s*?\'([^\']+)', fh.read())[0]
+
+ sickbeard.MEMCACHE[mem_key] = dict(last_update=30 + SGDatetime.now().totimestamp(default=0), data=version)
+ return version
def render_kodi_repo_addon_xml(self):
t = PageTemplate(web_handler=self, file='repo_kodi_addon.tmpl')
@@ -633,8 +652,7 @@ class RepoHandler(BaseStaticFileHandler):
bfr.close()
return zip_data
- @staticmethod
- def kodi_service_sickgear_watchedstate_updater_zip():
+ def kodi_service_sickgear_watchedstate_updater_zip(self):
bfr = io.BytesIO()
basepath = ek.ek(os.path.join, sickbeard.PROG_DIR, 'sickbeard', 'clients', 'kodi')
@@ -650,8 +668,12 @@ class RepoHandler(BaseStaticFileHandler):
for f in helpers.scantree(zip_path):
if f.is_file(follow_symlinks=False) and f.name[-4:] not in '.xcf':
try:
- with io.open(f.path, 'rb') as fh:
- infile = fh.read()
+ infile = None
+ if 'service.sickgear.watchedstate.updater' in f.path and f.path.endswith('addon.xml'):
+ infile = self.get_watchedstate_updater_addon_xml()
+ if not infile:
+ with io.open(f.path, 'rb') as fh:
+ infile = fh.read()
with zipfile.ZipFile(bfr, 'a') as zh:
zh.writestr(ek.ek(os.path.relpath, f.path, basepath), infile, zipfile.ZIP_DEFLATED)
@@ -680,7 +702,7 @@ class NoXSRFHandler(RouteHandler):
self.route_method(route, limit_route=False, xsrf_filter=False)
@staticmethod
- def update_watched_state_kodi(payload=None, as_json=True):
+ def update_watched_state_kodi(payload=None, as_json=True, **kwargs):
data = {}
try:
data = json.loads(payload)
@@ -713,6 +735,12 @@ class NoXSRFHandler(RouteHandler):
logger.log('Folder mappings used, the first of %s is [%s] in Kodi is [%s] in SickGear' %
(mapped, mapping[0], mapping[1]))
+ req_version = tuple([int(x) for x in kwargs.get('version', '0.0.0').split('.')])
+ this_version = RepoHandler.get_addon_version()
+ if not kwargs or (req_version < tuple([int(x) for x in this_version.split('.')])):
+ logger.log('Kodi Add-on update available. To upgrade to version %s; '
+ 'select "Check for updates" on menu of "SickGear Add-on repository"' % this_version)
+
return MainHandler.update_watched_state(data, as_json)