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)