diff --git a/gui/slick/js/loadingStartup.js b/gui/slick/js/loadingStartup.js
index 9233053..1538973 100644
--- a/gui/slick/js/loadingStartup.js
+++ b/gui/slick/js/loadingStartup.js
@@ -4,6 +4,8 @@ var dev = !1,
logInfo = dev && console.info.bind(window.console) || function (){},
logErr = dev && console.error.bind(window.console) || function (){};
+var p_id = 0;
+
$(function () {
ajaxConsumer.checkLoadNotifications();
});
@@ -53,7 +55,7 @@ var ajaxConsumer = function () {
function putMsg(msg) {
var loading = '.loading-step', lastStep$ = $(loading).filter(':last');
- if (msg !== lastStep$.attr('data-message')) {
+ if (msg !== unescape(lastStep$.attr('data-message'))) {
lastStep$.clone().insertAfter(lastStep$);
var result$ = lastStep$.find('.result');
@@ -64,7 +66,7 @@ function putMsg(msg) {
result$.addClass('hide');
}
lastStep$ = $(loading).filter(':last');
- lastStep$.attr('data-message', msg);
+ lastStep$.attr('data-message', escape(msg));
lastStep$.find('.desc').text(msg + ': ');
lastStep$.find('.count').text('');
lastStep$.find('.spinner').removeClass('hide');
@@ -75,11 +77,18 @@ function putMsg(msg) {
function uiUpdateComplete(data) {
$.each(data, function (i, msg) {
var loading = '.loading-step';
+ if (msg.msg === 'Process-id') {
+ if (p_id !== msg.progress) {
+ p_id = msg.progress;
+ $('.loading-step:not(:first)').remove();
+ }
+ return
+ }
if (i >= $(loading).length) {
putMsg(msg.msg);
}
if (-1 !== msg.progress) {
- var loading$ = $(loading + '[data-message="' + msg.msg + '"]');
+ var loading$ = $(loading + '[data-message="' + escape(msg.msg) + '"]');
loading$.find('.spinner, .result').addClass('hide');
loading$.find('.count').text(msg.progress);
}
diff --git a/lib/encodingKludge.py b/lib/encodingKludge.py
index 77949b2..2b1cdf9 100644
--- a/lib/encodingKludge.py
+++ b/lib/encodingKludge.py
@@ -19,28 +19,61 @@
import os
import logging
import locale
+import sys
-from six import iteritems, PY2, text_type, string_types
+from six import iteritems, moves, PY2, text_type, string_types
logger = logging.getLogger('encodingKludge')
logger.addHandler(logging.NullHandler())
+# noinspection PyUnreachableCode
+if False:
+ # noinspection PyUnresolvedReferences
+ from typing import AnyStr
SYS_ENCODING = None
-try:
- locale.setlocale(locale.LC_ALL, '')
-except (locale.Error, IOError):
- pass
-try:
- SYS_ENCODING = locale.getpreferredencoding()
-except (locale.Error, IOError):
- pass
-
-# For OSes that are poorly configured I'll just randomly force UTF-8
-if not SYS_ENCODING or SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
- SYS_ENCODING = 'UTF-8'
+EXIT_BAD_ENCODING = None
+def set_sys_encoding():
+ # type: (...) -> (bool, AnyStr)
+ """Set system encoding
+
+ :return: The encoding that is set
+ """
+ sys_encoding = None
+ should_exit = False
+ try:
+ locale.setlocale(locale.LC_ALL, '')
+ except (locale.Error, IOError):
+ pass
+ try:
+ sys_encoding = locale.getpreferredencoding()
+ except (locale.Error, IOError):
+ pass
+
+ # For OSes that are poorly configured I'll just randomly force UTF-8
+ if not sys_encoding or sys_encoding in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
+ sys_encoding = 'UTF-8'
+
+ if not hasattr(sys, 'setdefaultencoding'):
+ moves.reload_module(sys)
+
+ if PY2:
+ try:
+ # On non-unicode builds this raises an AttributeError,
+ # if encoding type is not valid it throws a LookupError
+ # noinspection PyUnresolvedReferences
+ sys.setdefaultencoding(sys_encoding)
+ except (BaseException, Exception):
+ should_exit = True
+
+ return should_exit, sys_encoding
+
+
+if None is EXIT_BAD_ENCODING:
+ EXIT_BAD_ENCODING, SYS_ENCODING = set_sys_encoding()
+
# This module tries to deal with the apparently random behavior of python when dealing with unicode <-> utf-8
# encodings. It tries to just use unicode, but if that fails then it tries forcing it to utf-8. Any functions
# which return something should always return unicode.
diff --git a/lib/sg_helpers.py b/lib/sg_helpers.py
index 721666a..d2fa2b6 100644
--- a/lib/sg_helpers.py
+++ b/lib/sg_helpers.py
@@ -2,6 +2,7 @@
# ---------------
# functions are placed here to remove cyclic import issues from placement in helpers
#
+import ast
import codecs
import datetime
import getpass
@@ -14,6 +15,7 @@ import shutil
import socket
import stat
import subprocess
+import sys
import tempfile
import threading
import time
@@ -29,7 +31,8 @@ from send2trash import send2trash
import encodingKludge as ek
import requests
-from _23 import decode_bytes, filter_list, html_unescape, list_range, scandir, urlparse, urlsplit, urlunparse
+from _23 import decode_bytes, filter_list, html_unescape, list_range, \
+ ordered_dict, Popen, scandir, urlparse, urlsplit, urlunparse
from six import integer_types, iteritems, iterkeys, itervalues, PY2, string_types, text_type
import zipfile
@@ -49,6 +52,7 @@ if False:
# global tmdb_info cache
_TMDB_INFO_CACHE = {'date': datetime.datetime(2000, 1, 1), 'data': None}
+PROG_DIR = ek.ek(os.path.join, os.path.dirname(os.path.normpath(os.path.abspath(__file__))), '..')
# Mapping error status codes to official W3C names
http_error_code = {
@@ -1419,3 +1423,66 @@ def scantree(path, # type: AnyStr
yield subentry
if no_filter:
yield entry
+
+
+def cmdline_runner(cmd, shell=False, suppress_stderr=False):
+ # type: (Union[AnyStr, List[AnyStr]], bool, bool) -> Tuple[AnyStr, Optional[AnyStr], int]
+ """ Execute a child program in a new process.
+
+ Can raise an exception to be caught in callee
+
+ :param cmd: A string, or a sequence of program arguments
+ :param shell: If true, the command will be executed through the shell.
+ :param suppress_stderr: Suppress stderr output if True
+ """
+ # noinspection PyUnresolvedReferences
+ kw = dict(cwd=PROG_DIR, shell=shell, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+ stderr=(open(os.devnull, 'w') if PY2 else subprocess.DEVNULL, subprocess.STDOUT)[not suppress_stderr])
+
+ if not PY2:
+ kw.update(dict(encoding=ek.SYS_ENCODING, text=True, bufsize=0))
+
+ if 'win32' == sys.platform:
+ kw['creationflags'] = 0x08000000 # CREATE_NO_WINDOW (needed for py2exe)
+
+ with Popen(cmd, **kw) as p:
+ out, err = p.communicate()
+ if out:
+ out = out.strip()
+
+ return out, err, p.returncode
+
+
+def ast_eval(value, default=None):
+ # type: (AnyStr, Any) -> Any
+ """Convert string typed value into actual Python type and value
+
+ :param value: string value to convert
+ :param default: value to return if cannot convert
+ :return: converted type and value or default
+ """
+ if not isinstance(value, string_types):
+ return default
+
+ if 'OrderedDict()' == value:
+ value = ordered_dict()
+
+ elif 'OrderedDict([(' == value[0:14]:
+ try:
+ list_of_tuples = ast.literal_eval(value[12:-1])
+ value = ordered_dict()
+ for cur_tuple in list_of_tuples:
+ value[cur_tuple[0]] = cur_tuple[1]
+ except (BaseException, Exception):
+ value = default
+
+ elif '{' == value[0:1] and '}' == value[-1]: # this way avoids index out of range with (value = '' and [-1])
+ try:
+ value = ast.literal_eval(value)
+ except (BaseException, Exception):
+ value = default
+
+ else:
+ value = default
+
+ return value
diff --git a/recommended.txt b/recommended.txt
index 25f9c1d..f2f1b98 100644
--- a/recommended.txt
+++ b/recommended.txt
@@ -1,5 +1,21 @@
-lxml>=4.4.2
-regex>=2019.11.1
-python-Levenshtein>=0.12.0
-scandir>=1.10.0; python_version < '3.0'
-py7zr >= 0.10.1; python_version > '3.0'
+cryptography; '3.7' <= python_version
+cryptography <= 3.2.1; '3.0' > python_version
+lxml >= 4.6.1
+pip >= 20.0.0; '3.7' <= python_version
+pip <= 19.3.1; '3.0' > python_version
+py7zr >= 0.10.1; '3.7' <= python_version
+pycryptodome; '3.7' <= python_version
+python-Levenshtein >= 0.12.0
+python-Levenshtein@https://raw.githubusercontent.com/wiki/SickGear/SickGear/packages/python_Levenshtein-0.12.0-cp39-cp39-win_amd64.whl ; '3.9' == python_version and 'Windows' == platform_system and ('AMD64' == platform_machine or 'x86_64' == platform_machine)
+python-Levenshtein@https://raw.githubusercontent.com/wiki/SickGear/SickGear/packages/python_Levenshtein-0.12.0-cp38-cp38-win_amd64.whl ; '3.8' == python_version and 'Windows' == platform_system and ('AMD64' == platform_machine or 'x86_64' == platform_machine)
+python-Levenshtein@https://raw.githubusercontent.com/wiki/SickGear/SickGear/packages/python_Levenshtein-0.12.0-cp37-cp37m-win_amd64.whl ; '3.7' == python_version and 'Windows' == platform_system and ('AMD64' == platform_machine or 'x86_64' == platform_machine)
+python-Levenshtein@https://raw.githubusercontent.com/wiki/SickGear/SickGear/packages/python_Levenshtein-0.12.0-cp27-cp27m-win_amd64.whl ; '2.7' == python_version and 'Windows' == platform_system and ('AMD64' == platform_machine or 'x86_64' == platform_machine)
+python-Levenshtein@https://raw.githubusercontent.com/wiki/SickGear/SickGear/packages/python_Levenshtein-0.12.0-cp39-cp39-win32.whl ; '3.9' == python_version and 'Windows' == platform_system and ('AMD64' != platform_machine and 'x86_64' != platform_machine)
+python-Levenshtein@https://raw.githubusercontent.com/wiki/SickGear/SickGear/packages/python_Levenshtein-0.12.0-cp38-cp38-win32.whl ; '3.8' == python_version and 'Windows' == platform_system and ('AMD64' != platform_machine and 'x86_64' != platform_machine)
+python-Levenshtein@https://raw.githubusercontent.com/wiki/SickGear/SickGear/packages/python_Levenshtein-0.12.0-cp37-cp37m-win32.whl ; '3.7' == python_version and 'Windows' == platform_system and ('AMD64' != platform_machine and 'x86_64' != platform_machine)
+python-Levenshtein@https://raw.githubusercontent.com/wiki/SickGear/SickGear/packages/python_Levenshtein-0.12.0-cp27-cp27m-win32.whl ; '2.7' == python_version and 'Windows' == platform_system and ('AMD64' != platform_machine and 'x86_64' != platform_machine)
+regex >= 2020.11.1; '3.7' <= python_version
+regex <= 2020.10.28; '3.0' > python_version
+scandir >= 1.10.0; '3.0' > python_version
+setuptools >= 50.0.0; '3.7' <= python_version
+setuptools <= 44.1.1; '3.0' > python_version
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 79b4e2d..7ea7442 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1 @@
-Cheetah3>=3.2.5
+Cheetah3>=3.2.5
\ No newline at end of file
diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py
index 6bc9741..6e09c9a 100755
--- a/sickbeard/__init__.py
+++ b/sickbeard/__init__.py
@@ -30,7 +30,6 @@ import socket
import webbrowser
# apparently py2exe won't build these unless they're imported somewhere
-import ast
import os.path
import sys
import threading
@@ -96,7 +95,8 @@ events = None # type: Events
recent_search_scheduler = None
backlog_search_scheduler = None
show_update_scheduler = None
-version_check_scheduler = None
+update_software_scheduler = None
+update_packages_scheduler = None
show_queue_scheduler = None
search_queue_scheduler = None
proper_finder_scheduler = None
@@ -128,9 +128,21 @@ metadata_provider_dict = {}
MODULE_UPDATE_STRING = None
NEWEST_VERSION_STRING = None
-VERSION_NOTIFY = False
-AUTO_UPDATE = False
+
+MIN_UPDATE_INTERVAL = 1
+DEFAULT_UPDATE_INTERVAL = 12
+UPDATE_NOTIFY = False
+UPDATE_AUTO = False
+UPDATE_INTERVAL = DEFAULT_UPDATE_INTERVAL
NOTIFY_ON_UPDATE = False
+
+MIN_UPDATE_PACKAGES_INTERVAL = 1
+MAX_UPDATE_PACKAGES_INTERVAL = 9999
+DEFAULT_UPDATE_PACKAGES_INTERVAL = 24
+UPDATE_PACKAGES_NOTIFY = False
+UPDATE_PACKAGES_AUTO = False
+UPDATE_PACKAGES_INTERVAL = DEFAULT_UPDATE_PACKAGES_INTERVAL
+
CUR_COMMIT_HASH = None
EXT_UPDATES = False
BRANCH = ''
@@ -246,14 +258,12 @@ NEWZNAB_DATA = ''
DEFAULT_MEDIAPROCESS_INTERVAL = 10
DEFAULT_BACKLOG_PERIOD = 21
DEFAULT_RECENTSEARCH_INTERVAL = 40
-DEFAULT_UPDATE_INTERVAL = 1
DEFAULT_WATCHEDSTATE_INTERVAL = 10
MEDIAPROCESS_INTERVAL = DEFAULT_MEDIAPROCESS_INTERVAL
BACKLOG_PERIOD = DEFAULT_BACKLOG_PERIOD
BACKLOG_LIMITED_PERIOD = 7
RECENTSEARCH_INTERVAL = DEFAULT_RECENTSEARCH_INTERVAL
-UPDATE_INTERVAL = DEFAULT_UPDATE_INTERVAL
RECENTSEARCH_STARTUP = False
BACKLOG_NOFULL = False
@@ -262,7 +272,6 @@ MIN_MEDIAPROCESS_INTERVAL = 1
MIN_RECENTSEARCH_INTERVAL = 10
MIN_BACKLOG_PERIOD = 7
MAX_BACKLOG_PERIOD = 42
-MIN_UPDATE_INTERVAL = 1
MIN_WATCHEDSTATE_INTERVAL = 10
MAX_WATCHEDSTATE_INTERVAL = 60
@@ -554,6 +563,8 @@ BACKUP_DB_ONEDAY = False # type: bool
BACKUP_DB_MAX_COUNT = 14 # type: int
BACKUP_DB_DEFAULT_COUNT = 14 # type: int
+UPDATES_TODO = {}
+
EXTRA_SCRIPTS = []
SG_EXTRA_SCRIPTS = []
@@ -660,7 +671,8 @@ def init_stage_1(console_logging):
# Gen Config/Misc
global LAUNCH_BROWSER, UPDATE_SHOWS_ON_START, SHOW_UPDATE_HOUR, \
TRASH_REMOVE_SHOW, TRASH_ROTATE_LOGS, ACTUAL_LOG_DIR, LOG_DIR, TVINFO_TIMEOUT, ROOT_DIRS, \
- VERSION_NOTIFY, AUTO_UPDATE, UPDATE_INTERVAL, NOTIFY_ON_UPDATE
+ UPDATE_NOTIFY, UPDATE_AUTO, UPDATE_INTERVAL, NOTIFY_ON_UPDATE,\
+ UPDATE_PACKAGES_NOTIFY, UPDATE_PACKAGES_AUTO, UPDATE_PACKAGES_INTERVAL
# Gen Config/Interface
global THEME_NAME, DEFAULT_HOME, FANART_LIMIT, SHOWLIST_TAGVIEW, SHOW_TAGS, \
HOME_SEARCH_FOCUS, USE_IMDB_INFO, IMDB_ACCOUNTS, DISPLAY_FREESPACE, SORT_ARTICLE, FUZZY_DATING, TRIM_ZERO, \
@@ -754,6 +766,8 @@ def init_stage_1(console_logging):
global ANIME_TREAT_AS_HDTV, USE_ANIDB, ANIDB_USERNAME, ANIDB_PASSWORD, ANIDB_USE_MYLIST
# db backup settings
global BACKUP_DB_PATH, BACKUP_DB_ONEDAY, BACKUP_DB_MAX_COUNT, BACKUP_DB_DEFAULT_COUNT
+ # pip update states
+ global UPDATES_TODO
for stanza in ('General', 'Blackhole', 'SABnzbd', 'NZBGet', 'Emby', 'Kodi', 'XBMC', 'PLEX',
'Growl', 'Prowl', 'Slack', 'Discord', 'Boxcar2', 'NMJ', 'NMJv2',
@@ -805,11 +819,7 @@ def init_stage_1(console_logging):
DEFAULT_HOME = check_setting_str(CFG, 'GUI', 'default_home', 'episodes')
FANART_LIMIT = check_setting_int(CFG, 'GUI', 'fanart_limit', 3)
FANART_PANEL = check_setting_str(CFG, 'GUI', 'fanart_panel', 'highlight2')
- FANART_RATINGS = check_setting_str(CFG, 'GUI', 'fanart_ratings', None)
- if None is not FANART_RATINGS:
- FANART_RATINGS = ast.literal_eval(FANART_RATINGS or '{}')
- if not isinstance(FANART_RATINGS, dict):
- FANART_RATINGS = {}
+ FANART_RATINGS = sg_helpers.ast_eval(check_setting_str(CFG, 'GUI', 'fanart_ratings', None), {})
USE_IMDB_INFO = bool(check_setting_int(CFG, 'GUI', 'use_imdb_info', 1))
IMDB_ACCOUNTS = CFG.get('GUI', []).get('imdb_accounts', [IMDB_DEFAULT_LIST_ID, IMDB_DEFAULT_LIST_NAME])
HOME_SEARCH_FOCUS = bool(check_setting_int(CFG, 'General', 'home_search_focus', HOME_SEARCH_FOCUS))
@@ -903,9 +913,25 @@ def init_stage_1(console_logging):
STATUS_DEFAULT = check_setting_int(CFG, 'General', 'status_default', SKIPPED)
WANTED_BEGIN_DEFAULT = check_setting_int(CFG, 'General', 'wanted_begin_default', 0)
WANTED_LATEST_DEFAULT = check_setting_int(CFG, 'General', 'wanted_latest_default', 0)
- VERSION_NOTIFY = bool(check_setting_int(CFG, 'General', 'version_notify', 1))
- AUTO_UPDATE = bool(check_setting_int(CFG, 'General', 'auto_update', 0))
+
+ UPDATE_NOTIFY = bool(check_setting_int(CFG, 'General', 'update_notify', None))
+ if None is UPDATE_NOTIFY:
+ UPDATE_NOTIFY = check_setting_int(CFG, 'General', 'version_notify', 1) # deprecated 2020.11.21 no config update
+ UPDATE_AUTO = bool(check_setting_int(CFG, 'General', 'update_auto', None))
+ if None is UPDATE_AUTO:
+ UPDATE_AUTO = check_setting_int(CFG, 'General', 'auto_update', 0) # deprecated 2020.11.21 no config update
+ UPDATE_INTERVAL = max(
+ MIN_UPDATE_INTERVAL,
+ check_setting_int(CFG, 'General', 'update_interval', DEFAULT_UPDATE_INTERVAL))
NOTIFY_ON_UPDATE = bool(check_setting_int(CFG, 'General', 'notify_on_update', 1))
+
+ UPDATE_PACKAGES_NOTIFY = bool(
+ check_setting_int(CFG, 'General', 'update_packages_notify', 'win' == sys.platform[0:3]))
+ UPDATE_PACKAGES_AUTO = bool(check_setting_int(CFG, 'General', 'update_packages_auto', 0))
+ UPDATE_PACKAGES_INTERVAL = max(
+ MIN_UPDATE_PACKAGES_INTERVAL,
+ check_setting_int(CFG, 'General', 'update_packages_interval', DEFAULT_UPDATE_PACKAGES_INTERVAL))
+
FLATTEN_FOLDERS_DEFAULT = bool(check_setting_int(CFG, 'General', 'flatten_folders_default', 0))
TVINFO_DEFAULT = check_setting_int(CFG, 'General', 'indexer_default', 0)
if TVINFO_DEFAULT and not TVInfoAPI(TVINFO_DEFAULT).config['active']:
@@ -915,7 +941,7 @@ def init_stage_1(console_logging):
SCENE_DEFAULT = bool(check_setting_int(CFG, 'General', 'scene_default', 0))
PROVIDER_ORDER = check_setting_str(CFG, 'General', 'provider_order', '').split()
- PROVIDER_HOMES = ast.literal_eval(check_setting_str(CFG, 'General', 'provider_homes', None) or '{}')
+ PROVIDER_HOMES = sg_helpers.ast_eval(check_setting_str(CFG, 'General', 'provider_homes', None), {})
NAMING_PATTERN = check_setting_str(CFG, 'General', 'naming_pattern', 'Season %0S/%SN - S%0SE%0E - %EN')
NAMING_ABD_PATTERN = check_setting_str(CFG, 'General', 'naming_abd_pattern', '%SN - %A.D - %EN')
@@ -968,10 +994,6 @@ def init_stage_1(console_logging):
BACKLOG_PERIOD = minimax(BACKLOG_PERIOD, DEFAULT_BACKLOG_PERIOD, MIN_BACKLOG_PERIOD, MAX_BACKLOG_PERIOD)
BACKLOG_LIMITED_PERIOD = check_setting_int(CFG, 'General', 'backlog_limited_period', 7)
- UPDATE_INTERVAL = check_setting_int(CFG, 'General', 'update_interval', DEFAULT_UPDATE_INTERVAL)
- if UPDATE_INTERVAL < MIN_UPDATE_INTERVAL:
- UPDATE_INTERVAL = MIN_UPDATE_INTERVAL
-
SEARCH_UNAIRED = bool(check_setting_int(CFG, 'General', 'search_unaired', 0))
UNAIRED_RECENT_SEARCH_ONLY = bool(check_setting_int(CFG, 'General', 'unaired_recent_search_only', 1))
@@ -1317,17 +1339,15 @@ def init_stage_1(console_logging):
lambda y: TVidProdid.glue in y and y or '%s%s%s' % (
(TVINFO_TVDB, TVINFO_IMDB)[bool(helpers.parse_imdb_id(y))], TVidProdid.glue, y),
[x.strip() for x in check_setting_str(CFG, 'GUI', 'browselist_hidden', '').split('|~|') if x.strip()])
- BROWSELIST_MRU = check_setting_str(CFG, 'GUI', 'browselist_prefs', None)
- if None is not BROWSELIST_MRU:
- BROWSELIST_MRU = ast.literal_eval(BROWSELIST_MRU or '{}')
- if not isinstance(BROWSELIST_MRU, dict):
- BROWSELIST_MRU = {}
+ BROWSELIST_MRU = sg_helpers.ast_eval(check_setting_str(CFG, 'GUI', 'browselist_prefs', None), {})
BACKUP_DB_PATH = check_setting_str(CFG, 'Backup', 'backup_db_path', '')
BACKUP_DB_ONEDAY = bool(check_setting_int(CFG, 'Backup', 'backup_db_oneday', 0))
BACKUP_DB_MAX_COUNT = minimax(check_setting_int(CFG, 'Backup', 'backup_db_max_count', BACKUP_DB_DEFAULT_COUNT),
BACKUP_DB_DEFAULT_COUNT, 0, 90)
+ UPDATES_TODO = sg_helpers.ast_eval(check_setting_str(CFG, 'Updates', 'updates_todo', None), {})
+
sg_helpers.db = db
sg_helpers.DOMAIN_FAILURES.load_from_db()
@@ -1472,12 +1492,12 @@ def init_stage_2():
# Schedulers
# global trakt_checker_scheduler
global recent_search_scheduler, backlog_search_scheduler, show_update_scheduler, \
- version_check_scheduler, show_queue_scheduler, search_queue_scheduler, \
+ update_software_scheduler, update_packages_scheduler, show_queue_scheduler, search_queue_scheduler, \
proper_finder_scheduler, media_process_scheduler, subtitles_finder_scheduler, \
background_mapping_task, \
watched_state_queue_scheduler, emby_watched_state_scheduler, plex_watched_state_scheduler
# Gen Config/Misc
- global SHOW_UPDATE_HOUR, UPDATE_INTERVAL
+ global SHOW_UPDATE_HOUR, UPDATE_INTERVAL, UPDATE_PACKAGES_INTERVAL
# Search Settings/Episode
global RECENTSEARCH_INTERVAL
# Subtitles
@@ -1525,10 +1545,17 @@ def init_stage_2():
# initialize schedulers
# updaters
update_now = datetime.timedelta(minutes=0)
- version_check_scheduler = scheduler.Scheduler(
- version_checker.CheckVersion(),
+ update_software_scheduler = scheduler.Scheduler(
+ version_checker.SoftwareUpdater(),
cycleTime=datetime.timedelta(hours=UPDATE_INTERVAL),
- threadName='CHECKVERSION',
+ threadName='SOFTWAREUPDATER',
+ silent=False)
+
+ update_packages_scheduler = scheduler.Scheduler(
+ version_checker.PackagesUpdater(),
+ cycleTime=datetime.timedelta(hours=UPDATE_PACKAGES_INTERVAL),
+ # run_delay=datetime.timedelta(minutes=2),
+ threadName='PACKAGESUPDATER',
silent=False)
show_queue_scheduler = scheduler.Scheduler(
@@ -1614,7 +1641,7 @@ def init_stage_2():
threadName='FINDSUBTITLES',
silent=not USE_SUBTITLES)
- background_mapping_task = threading.Thread(name='LOAD-MAPPINGS', target=indexermapper.load_mapped_ids)
+ background_mapping_task = threading.Thread(name='MAPPINGSUPDATER', target=indexermapper.load_mapped_ids)
watched_state_queue_scheduler = scheduler.Scheduler(
watchedstate_queue.WatchedStateQueue(),
@@ -1650,10 +1677,12 @@ def init_stage_2():
def enabled_schedulers(is_init=False):
# ([], [trakt_checker_scheduler])[USE_TRAKT] + \
return ([], [events])[is_init] \
- + [recent_search_scheduler, backlog_search_scheduler, show_update_scheduler,
- version_check_scheduler, show_queue_scheduler, search_queue_scheduler, proper_finder_scheduler,
- media_process_scheduler, subtitles_finder_scheduler,
- emby_watched_state_scheduler, plex_watched_state_scheduler, watched_state_queue_scheduler]\
+ + ([], [recent_search_scheduler, backlog_search_scheduler, show_update_scheduler,
+ update_software_scheduler, update_packages_scheduler,
+ show_queue_scheduler, search_queue_scheduler, proper_finder_scheduler,
+ media_process_scheduler, subtitles_finder_scheduler,
+ emby_watched_state_scheduler, plex_watched_state_scheduler, watched_state_queue_scheduler]
+ )[not MEMCACHE.get('update_restart')] \
+ ([events], [])[is_init]
@@ -1754,12 +1783,13 @@ def halt():
def save_all():
- global showList
+ if not MEMCACHE.get('update_restart'):
+ global showList
- # write all shows
- logger.log(u'Saving all shows to the database')
- for show_obj in showList: # type: tv.TVShow
- show_obj.save_to_db()
+ # write all shows
+ logger.log(u'Saving all shows to the database')
+ for show_obj in showList: # type: tv.TVShow
+ show_obj.save_to_db()
# save config
logger.log(u'Saving config file to disk')
@@ -1816,7 +1846,6 @@ def save_config():
new_config['General']['recentsearch_interval'] = int(RECENTSEARCH_INTERVAL)
new_config['General']['backlog_period'] = int(BACKLOG_PERIOD)
new_config['General']['backlog_limited_period'] = int(BACKLOG_LIMITED_PERIOD)
- new_config['General']['update_interval'] = int(UPDATE_INTERVAL)
new_config['General']['download_propers'] = int(DOWNLOAD_PROPERS)
new_config['General']['propers_webdl_onegrp'] = int(PROPERS_WEBDL_ONEGRP)
new_config['General']['allow_high_priority'] = int(ALLOW_HIGH_PRIORITY)
@@ -1836,9 +1865,13 @@ def save_config():
new_config['General']['provider_order'] = ' '.join(PROVIDER_ORDER)
new_config['General']['provider_homes'] = '%s' % dict([(pid, v) for pid, v in list_items(PROVIDER_HOMES) if pid in [
p.get_id() for p in [x for x in providers.sortedProviderList() if GenericProvider.TORRENT == x.providerType]]])
- new_config['General']['version_notify'] = int(VERSION_NOTIFY)
- new_config['General']['auto_update'] = int(AUTO_UPDATE)
+ new_config['General']['update_notify'] = int(UPDATE_NOTIFY)
+ new_config['General']['update_auto'] = int(UPDATE_AUTO)
+ new_config['General']['update_interval'] = int(UPDATE_INTERVAL)
new_config['General']['notify_on_update'] = int(NOTIFY_ON_UPDATE)
+ new_config['General']['update_packages_notify'] = int(UPDATE_PACKAGES_NOTIFY)
+ new_config['General']['update_packages_auto'] = int(UPDATE_PACKAGES_AUTO)
+ new_config['General']['update_packages_interval'] = int(UPDATE_PACKAGES_INTERVAL)
new_config['General']['naming_strip_year'] = int(NAMING_STRIP_YEAR)
new_config['General']['naming_pattern'] = NAMING_PATTERN
new_config['General']['naming_custom_abd'] = int(NAMING_CUSTOM_ABD)
@@ -1905,6 +1938,9 @@ def save_config():
new_config['General']['require_words'] = helpers.generate_word_str(REQUIRE_WORDS, REQUIRE_WORDS_REGEX)
new_config['General']['calendar_unprotected'] = int(CALENDAR_UNPROTECTED)
+ new_config['Updates'] = {}
+ new_config['Updates']['updates_todo'] = '%s' % (UPDATES_TODO or {})
+
new_config['Backup'] = {}
if BACKUP_DB_PATH:
new_config['Backup']['backup_db_path'] = BACKUP_DB_PATH
@@ -2228,7 +2264,7 @@ def save_config():
new_config['GUI']['default_home'] = DEFAULT_HOME
new_config['GUI']['fanart_limit'] = FANART_LIMIT
new_config['GUI']['fanart_panel'] = FANART_PANEL
- new_config['GUI']['fanart_ratings'] = '%s' % FANART_RATINGS
+ new_config['GUI']['fanart_ratings'] = '%s' % (FANART_RATINGS or {})
new_config['GUI']['use_imdb_info'] = int(USE_IMDB_INFO)
new_config['GUI']['imdb_accounts'] = IMDB_ACCOUNTS
new_config['GUI']['fuzzy_dating'] = int(FUZZY_DATING)
@@ -2267,7 +2303,7 @@ def save_config():
new_config['GUI']['show_tag_default'] = SHOW_TAG_DEFAULT
new_config['GUI']['history_layout'] = HISTORY_LAYOUT
new_config['GUI']['browselist_hidden'] = '|~|'.join(BROWSELIST_HIDDEN)
- new_config['GUI']['browselist_prefs'] = '%s' % BROWSELIST_MRU
+ new_config['GUI']['browselist_prefs'] = '%s' % (BROWSELIST_MRU or {})
new_config['Subtitles'] = {}
new_config['Subtitles']['use_subtitles'] = int(USE_SUBTITLES)
diff --git a/sickbeard/config.py b/sickbeard/config.py
index 962e5cd..3bca7fb 100644
--- a/sickbeard/config.py
+++ b/sickbeard/config.py
@@ -177,25 +177,49 @@ def schedule_backlog(iv):
sickbeard.backlog_search_scheduler.action.cycleTime = sickbeard.BACKLOG_PERIOD
-def schedule_update(iv):
+def schedule_update_software(iv):
sickbeard.UPDATE_INTERVAL = to_int(iv, default=sickbeard.DEFAULT_UPDATE_INTERVAL)
if sickbeard.UPDATE_INTERVAL < sickbeard.MIN_UPDATE_INTERVAL:
sickbeard.UPDATE_INTERVAL = sickbeard.MIN_UPDATE_INTERVAL
- sickbeard.version_check_scheduler.cycleTime = datetime.timedelta(hours=sickbeard.UPDATE_INTERVAL)
+ sickbeard.update_software_scheduler.cycleTime = datetime.timedelta(hours=sickbeard.UPDATE_INTERVAL)
-def schedule_version_notify(version_notify):
- old_setting = sickbeard.VERSION_NOTIFY
+def schedule_update_software_notify(update_notify):
+ old_setting = sickbeard.UPDATE_NOTIFY
- sickbeard.VERSION_NOTIFY = version_notify
+ sickbeard.UPDATE_NOTIFY = update_notify
- if not version_notify:
+ if not update_notify:
sickbeard.NEWEST_VERSION_STRING = None
- if not old_setting and version_notify:
- sickbeard.version_check_scheduler.action.run()
+ if not old_setting and update_notify:
+ sickbeard.update_software_scheduler.action.run()
+
+
+def schedule_update_packages(iv):
+ sickbeard.UPDATE_PACKAGES_INTERVAL = minimax(iv, sickbeard.DEFAULT_UPDATE_PACKAGES_INTERVAL,
+ sickbeard.MIN_UPDATE_PACKAGES_INTERVAL,
+ sickbeard.MAX_UPDATE_PACKAGES_INTERVAL)
+
+ sickbeard.update_packages_scheduler.cycleTime = datetime.timedelta(hours=sickbeard.UPDATE_PACKAGES_INTERVAL)
+
+
+def schedule_update_packages_notify(update_packages_notify):
+ # this adds too much time to the save_config button click, see below
+ # old_setting = sickbeard.UPDATE_PACKAGES_NOTIFY
+
+ sickbeard.UPDATE_PACKAGES_NOTIFY = update_packages_notify
+
+ if not update_packages_notify:
+ sickbeard.NEWEST_VERSION_STRING = None
+
+ # this adds too much time to the save_config button click,
+ # also the call to save_config raises the risk of a race condition
+ # user must instead restart to activate an update on startup
+ # if not old_setting and update_packages_notify:
+ # sickbeard.update_packages_scheduler.action.run()
def schedule_download_propers(download_propers):
diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py
index b2d3db8..cfd585a 100644
--- a/sickbeard/helpers.py
+++ b/sickbeard/helpers.py
@@ -30,7 +30,6 @@ import shutil
import socket
import time
import uuid
-import subprocess
import sys
try:
@@ -1793,32 +1792,6 @@ def xhtml_escape(text, br=True):
return escape.xhtml_escape(text)
-def cmdline_runner(cmd, shell=False):
- # type: (Union[AnyStr, List[AnyStr]], bool) -> Tuple[AnyStr, Optional[AnyStr], int]
- """ Execute a child program in a new process.
-
- Can raise an exception to be caught in callee
-
- :param cmd: A string, or a sequence of program arguments
- :param shell: If true, the command will be executed through the shell.
- """
- kw = dict(cwd=sickbeard.PROG_DIR, shell=shell,
- stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-
- if not PY2:
- kw.update(dict(encoding=sickbeard.SYS_ENCODING, text=True, bufsize=0))
-
- if 'win32' == sys.platform:
- kw['creationflags'] = 0x08000000 # CREATE_NO_WINDOW (needed for py2exe)
-
- with Popen(cmd, **kw) as p:
- out, err = p.communicate()
- if out:
- out = out.strip()
-
- return out, err, p.returncode
-
-
def parse_imdb_id(string):
# type: (AnyStr) -> Optional[AnyStr]
""" Parse an IMDB ID from a string
diff --git a/sickbeard/notifiers/synoindex.py b/sickbeard/notifiers/synoindex.py
index 39be214..857b665 100644
--- a/sickbeard/notifiers/synoindex.py
+++ b/sickbeard/notifiers/synoindex.py
@@ -22,6 +22,7 @@ from .generic import BaseNotifier
# noinspection PyPep8Naming
import encodingKludge as ek
from exceptions_helper import ex
+from sg_helpers import cmdline_runner
# noinspection PyPep8Naming
@@ -37,7 +38,6 @@ class SynoIndexNotifier(BaseNotifier):
self._log_debug(u'Executing command ' + str(synoindex_cmd))
self._log_debug(u'Absolute path to command: ' + ek.ek(os.path.abspath, synoindex_cmd[0]))
try:
- from sickbeard.helpers import cmdline_runner
output, err, exit_status = cmdline_runner(synoindex_cmd)
self._log_debug(u'Script result: %s' % output)
except (BaseException, Exception) as e:
diff --git a/sickbeard/notifiers/synologynotifier.py b/sickbeard/notifiers/synologynotifier.py
index f480577..bc6a6d9 100644
--- a/sickbeard/notifiers/synologynotifier.py
+++ b/sickbeard/notifiers/synologynotifier.py
@@ -21,6 +21,7 @@ from .generic import Notifier
# noinspection PyPep8Naming
import encodingKludge as ek
from exceptions_helper import ex
+from sg_helpers import cmdline_runner
class SynologyNotifier(Notifier):
@@ -31,7 +32,6 @@ class SynologyNotifier(Notifier):
self._log(u'Executing command ' + str(synodsmnotify_cmd))
self._log_debug(u'Absolute path to command: ' + ek.ek(os.path.abspath, synodsmnotify_cmd[0]))
try:
- from sickbeard.helpers import cmdline_runner
output, err, exit_status = cmdline_runner(synodsmnotify_cmd)
self._log_debug(u'Script result: %s' % output)
except (BaseException, Exception) as e:
diff --git a/sickbeard/piper.py b/sickbeard/piper.py
new file mode 100644
index 0000000..2edbc4e
--- /dev/null
+++ b/sickbeard/piper.py
@@ -0,0 +1,345 @@
+import sys
+
+# noinspection PyPep8Naming
+import encodingKludge as ek
+
+if ek.EXIT_BAD_ENCODING:
+ print('Sorry, you MUST add the SickGear folder to the PYTHONPATH environment variable')
+ print('or find another way to force Python to use %s for string encoding.' % ek.SYS_ENCODING)
+ sys.exit(1)
+
+# #################################
+# Sanity check passed, can continue
+# #################################
+import io
+import json
+import os
+import re
+
+from sg_helpers import cmdline_runner, try_int
+
+from _23 import filter_list, ordered_dict
+from six import iteritems, PY2
+
+# noinspection PyUnreachableCode
+if False:
+ from typing import Any, AnyStr, Dict, List, Optional, Tuple, Union
+
+
+def is_pip_ok():
+ # type: (...) -> bool
+ """Check pip availability
+
+ :return: True if pip is ok
+ """
+ pip_ok = '/' != ek.ek(os.path.expanduser, '~')
+ if pip_ok:
+ try:
+ # noinspection PyPackageRequirements,PyProtectedMember
+ import pip
+ except ImportError:
+ try:
+ import ensurepip
+ ensurepip.bootstrap()
+ except (BaseException, Exception):
+ pip_ok = False
+ return pip_ok
+
+
+def run_pip(pip_cmd, suppress_stderr=False):
+ # type: (List[AnyStr], bool) -> Tuple[AnyStr, Optional[AnyStr], int]
+ """Run pip command
+
+ :param pip_cmd:
+ :param suppress_stderr:
+ :return: out, err, returncode
+ """
+ if 'uninstall' == pip_cmd[0]:
+ pip_cmd += ['-y']
+ elif 'install' == pip_cmd[0]:
+ pip_cmd += ['--progress-bar', 'off']
+
+ new_pip_arg = ['--no-python-version-warning']
+ if PY2:
+ # noinspection PyCompatibility, PyPackageRequirements
+ from pip import __version__ as pip_version
+ if pip_version and 20 > int(pip_version.split('.')[0]):
+ new_pip_arg = []
+
+ return cmdline_runner(
+ [sys.executable, '-m', 'pip'] + new_pip_arg + ['--disable-pip-version-check'] + pip_cmd,
+ suppress_stderr=suppress_stderr)
+
+
+def initial_requirements():
+ """Process requirements
+
+ * Upgrades legacy Cheetah version 2 to version 3+
+ """
+ if is_pip_ok():
+ try:
+ # noinspection PyUnresolvedReferences
+ import Cheetah
+ # noinspection PyUnresolvedReferences
+ if 3 > try_int(Cheetah.Version[0]):
+ run_pip(['uninstall', 'cheetah', 'markdown'])
+ raise ValueError
+ except (BaseException, Exception):
+ run_pip(['install', '-U', '--user', '-r', 'requirements.txt'])
+ module = 'Cheetah'
+ try:
+ locals()[module] = __import__(module)
+ sys.modules[module] = __import__(module)
+ except (BaseException, Exception) as e:
+ pass
+
+
+def extras_failed_filepath(data_dir):
+ return ek.ek(os.path.join, data_dir, '.pip_req_spec_failed.txt')
+
+
+def load_ignorables(data_dir):
+ # type: (AnyStr) -> List[AnyStr]
+
+ data = []
+
+ filepath = extras_failed_filepath(data_dir)
+ if ek.ek(os.path.isfile, filepath):
+ try:
+ with io.open(filepath, 'r', encoding='UTF8') as fp:
+ data = fp.readlines()
+ except (BaseException, Exception):
+ pass
+
+ return data
+
+
+def save_ignorables(data_dir, data):
+ # type: (AnyStr, List[AnyStr]) -> None
+
+ try:
+ with io.open(extras_failed_filepath(data_dir), 'w', encoding='UTF8') as fp:
+ fp.writelines(data)
+ fp.flush()
+ os.fsync(fp.fileno())
+ except (BaseException, Exception):
+ pass
+
+
+def check_pip_outdated(reset_fails=False):
+ # type: (bool) -> Dict[Any]
+ """Check outdated or missing Python performance packages"""
+ _, work_todo, _, _ = _check_pip_env(pip_outdated=True, reset_fails=reset_fails)
+ return work_todo
+
+
+def check_pip_installed():
+ # type: (...) -> Tuple[List[tuple], List[AnyStr]]
+ """Return working installed Python performance packages"""
+ input_reco, _, installed, _ = _check_pip_env()
+ return installed, input_reco
+
+
+def check_pip_env():
+ # type: (...) -> Tuple[List[tuple], Dict[AnyStr, AnyStr], List[AnyStr]]
+ """Return working installed Python performance packages, extra info, and failed packages, for ui"""
+
+ _, _, installed, failed_names = _check_pip_env()
+
+ py2_last = 'final py2 release'
+ boost = 'performance boost'
+ extra_info = dict({'Cheetah3': 'filled requirement', 'lxml': boost, 'python-Levenshtein': boost})
+ extra_info.update((dict(cryptography=py2_last, pip='stable py2 release', regex=py2_last,
+ scandir=boost, setuptools=py2_last),
+ dict(regex=boost))[not PY2])
+ return installed, extra_info, failed_names
+
+
+def _check_pip_env(pip_outdated=False, reset_fails=False):
+ # type: (bool, bool) -> Tuple[List[AnyStr], Dict[Dict[AnyStr, Union[bool, AnyStr]]], List[tuple], List[AnyStr]]
+ """Checking Python requirements and recommendations for installed, outdated, and missing performance packages
+
+ :param pip_outdated: do a Pip list outdated if True
+ :param reset_fails: reset known failures if True
+ :return combined required + recommended names,
+ dictionary of work names:version info,
+ combined required + recommended names with either installed version or '' if not installed,
+ failed item names
+ """
+ if not is_pip_ok():
+ return [], dict(), [], []
+
+ input_reco = []
+ from sickbeard import logger, PROG_DIR, DATA_DIR
+ for cur_reco_file in ['requirements.txt', 'recommended.txt']:
+ try:
+ with io.open(ek.ek(os.path.join, PROG_DIR, cur_reco_file)) as fh:
+ input_reco += ['%s\n' % line.strip() for line in fh] # must ensure EOL marker
+ except (BaseException, Exception):
+ pass
+
+ environment = {}
+ # noinspection PyUnresolvedReferences
+ import six.moves
+ import pkg_resources
+ six.moves.reload_module(pkg_resources)
+ for cur_distinfo in pkg_resources.working_set:
+ environment[cur_distinfo.project_name] = cur_distinfo.parsed_version
+
+ save_failed = False
+ known_failed = load_ignorables(DATA_DIR)
+ if reset_fails and known_failed:
+ known_failed = []
+ save_failed = True
+ failed_names = []
+ output_reco = {}
+ names_reco = []
+ specifiers = {}
+ requirement_update = set()
+ from pkg_resources import parse_requirements
+ for cur_line in input_reco:
+ try:
+ requirement = next(parse_requirements(cur_line)) # https://packaging.pypa.io/en/latest/requirements.html
+ except ValueError as e:
+ logger.error('Error [%s] with line: %s' % (e, cur_line)) # name@url ; whitespace/newline must follow url
+ continue
+ project_name = getattr(requirement, 'project_name', None)
+ if cur_line in known_failed and project_name not in environment:
+ failed_names += [project_name]
+ else:
+ marker = getattr(requirement, 'marker', None)
+ if marker and not marker.evaluate():
+ continue
+ if project_name:
+ # explicitly output the most recent line where project names repeat, i.e. max(line number)
+ # therefore, position items with greater specificity _after_ items with a broad spec in requirements.txt
+ output_reco[project_name] = cur_line
+ if project_name not in names_reco:
+ names_reco += [project_name]
+ if project_name in environment:
+ if environment[project_name] in requirement.specifier:
+ specifiers[project_name] = requirement.specifier # requirement is met in the env
+ if cur_line in known_failed:
+ known_failed.remove(cur_line) # manually installed item that previously failed
+ save_failed = True
+ else:
+ requirement_update.add(project_name) # e.g. when '!=' matches an installed project to uninstall
+ if save_failed:
+ save_ignorables(DATA_DIR, known_failed)
+
+ to_install = set(names_reco).difference(set(environment))
+ fresh_install = len(to_install) == len(names_reco)
+ installed = [(cur_name, getattr(environment.get(cur_name), 'public', '')) for cur_name in names_reco]
+
+ to_update = set()
+ names_outdated = dict()
+ if pip_outdated and not fresh_install:
+ output, err, exit_status = run_pip(['list', '--outdated', '--format', 'json'], suppress_stderr=True)
+ try:
+ names_outdated = dict({cur_item.get('name'): {k: cur_item.get(k) for k in ('version', 'latest_version')}
+ for cur_item in json.loads(output)})
+ to_update = set(filter_list(
+ lambda name: name in specifiers and names_outdated[name]['latest_version'] in specifiers[name],
+ set(names_reco).intersection(set(names_outdated))))
+ except (BaseException, Exception):
+ pass
+
+ updates_todo = ordered_dict()
+ todo = to_install.union(to_update, requirement_update)
+ for cur_name in [cur_n for cur_n in names_reco if cur_n in todo]:
+ updates_todo[cur_name] = dict({
+ _tuple[0]: _tuple[1] for _tuple in
+ (cur_name in names_outdated and [('info', names_outdated[cur_name])] or [])
+ + (cur_name in requirement_update and [('requirement', True)] or [])
+ + [('require_spec', output_reco.get(cur_name, '%s>0.0.0\n' % cur_name))]})
+
+ return input_reco, updates_todo, installed, failed_names
+
+
+def pip_update(loading_msg, updates_todo, data_dir):
+ # type: (AnyStr, Dict[Any], AnyStr) -> bool
+ result = False
+ if not is_pip_ok():
+ return result
+
+ from sickbeard import logger
+ failed_lines = []
+ input_reco = None
+
+ piper_path = ek.ek(os.path.join, data_dir, '.pip_req_spec_temp.txt')
+ for cur_project_name, cur_data in iteritems(updates_todo):
+ msg = 'Installing package "%s"' % cur_project_name
+ if cur_data.get('info'):
+ info = dict(name=cur_project_name, ver=cur_data.get('info').get('version'))
+ if not cur_data.get('requirement'):
+ msg = 'Updating package "%(name)s" version %(ver)s to {}'.format(
+ cur_data.get('info').get('latest_version')) % info
+ else:
+ msg = 'Checking package "%(name)s" version %(ver)s with "{}"'.format(
+ re.sub(r',\b', ', ', cur_data.get('require_spec').strip())) % info
+ loading_msg.set_msg_progress(msg, 'Installing...')
+
+ try:
+ with io.open(piper_path, 'w', encoding='utf-8') as fp:
+ fp.writelines(cur_data.get('require_spec'))
+ fp.flush()
+ os.fsync(fp.fileno())
+ except (BaseException, Exception):
+ loading_msg.set_msg_progress(msg, 'Failed to save install data')
+ continue
+
+ # exclude Cheetah3 to prevent `No matching distro found` and fallback to its legacy setup.py installer
+ output, err, exit_status = run_pip(['install', '-U']
+ + ([], ['--only-binary=:all:'])[cur_project_name not in ('Cheetah3', )]
+ + ['--user', '-r', piper_path])
+ pip_version = None
+ try:
+ # ensure '-' in a project name is not escaped in order to convert the '-' into a `[_-]` regex
+ find_name = re.escape(cur_project_name.replace(r'-', r'44894489')).replace(r'44894489', r'[_-]')
+ parsed_name = re.findall(r'(?sim).*(%s[^\s]+)\.whl.*' % find_name, output) or \
+ re.findall(r'(?sim).*Successfully installed.*?(%s[^\s]+)' % find_name, output)
+ if not parsed_name:
+ parsed_name = re.findall(r'(?sim)up-to-date[^\s]+\s*(%s).*?\s\(([^)]+)\)$' % find_name, output)
+ parsed_name = ['' if not parsed_name else '-'.join(parsed_name[0])]
+ pip_version = re.findall(r'%s-([\d.]+).*?' % find_name, ek.ek(os.path.basename, parsed_name[0]), re.I)[0]
+ except (BaseException, Exception):
+ pass
+
+ # pip may output `...WinError 5 Access denied...` yet still install what appears a failure
+ # therefore, for any apparent failure, recheck the environment to figure if the failure is actually true
+ installed, input_reco = check_pip_installed()
+ if 0 == exit_status or (cur_project_name, pip_version) in installed:
+ result = True
+ installed_version = pip_version or cur_data.get('info', {}).get('latest_version') or 'n/a'
+ msg_result = 'Installed version: %s' % installed_version
+ logger.log('Installed %s version: %s' % (cur_project_name, installed_version))
+ else:
+ failed_lines += [cur_data.get('require_spec')]
+ msg_result = 'Failed to install'
+ log_error = ''
+ for cur_line in output.splitlines()[::-1]:
+ if 'error' in cur_line.lower():
+ msg_result = re.sub(r'(?i)(\berror:\s*|\s*\(from.*)', '', cur_line)
+ log_error = ': %s' % msg_result
+ break
+ logger.debug('Failed to install %s%s' % (cur_project_name, log_error))
+ loading_msg.set_msg_progress(msg, msg_result)
+
+ if failed_lines:
+ # for example, python-Levenshtein failed due to no matching PyPI distro. A recheck at the next PY or SG upgrade
+ # was considered, but an is_env_changed() helper doesn't exist which makes that idea outside this feature scope.
+ # Therefore, prevent a re-attempt and present the missing pkg to the ui for the user to optionally handle.
+ failed_lines += [cur_line for cur_line in load_ignorables(data_dir) if cur_line not in failed_lines]
+ if None is input_reco:
+ _, input_reco = check_pip_installed() # known items in file content order
+ sorted_failed = [cur_line for cur_line in input_reco if cur_line in failed_lines]
+ save_ignorables(data_dir, sorted_failed)
+
+ return result
+
+
+if '__main__' == __name__:
+ print('This module is supposed to be used as import in other scripts and not run standalone.')
+ sys.exit(1)
+
+initial_requirements()
diff --git a/sickbeard/postProcessor.py b/sickbeard/postProcessor.py
index 78292d4..29033b3 100644
--- a/sickbeard/postProcessor.py
+++ b/sickbeard/postProcessor.py
@@ -38,7 +38,7 @@ from .name_parser.parser import InvalidNameException, InvalidShowException, Name
from _23 import decode_str
from six import iteritems, PY2, string_types
-from sg_helpers import long_path
+from sg_helpers import long_path, cmdline_runner
# noinspection PyUnreachableCode
if False:
@@ -849,7 +849,7 @@ class PostProcessor(object):
try:
# run the command and capture output
- output, err, exit_status = helpers.cmdline_runner(script_cmd)
+ output, err, exit_status = cmdline_runner(script_cmd)
self._log('Script result: %s' % output, logger.DEBUG)
except OSError as e:
diff --git a/sickbeard/show_updater.py b/sickbeard/show_updater.py
index fb8197b..3a4a701 100644
--- a/sickbeard/show_updater.py
+++ b/sickbeard/show_updater.py
@@ -136,7 +136,7 @@ class ShowUpdater(object):
import threading
try:
sickbeard.background_mapping_task = threading.Thread(
- name='LOAD-MAPPINGS', target=sickbeard.indexermapper.load_mapped_ids, kwargs={'update': True})
+ name='MAPPINGSUPDATER', target=sickbeard.indexermapper.load_mapped_ids, kwargs={'update': True})
sickbeard.background_mapping_task.start()
except (BaseException, Exception):
logger.log('missing mapped ids update error', logger.ERROR)
diff --git a/sickbeard/version_checker.py b/sickbeard/version_checker.py
index 14cf298..8554a7d 100644
--- a/sickbeard/version_checker.py
+++ b/sickbeard/version_checker.py
@@ -31,13 +31,68 @@ from exceptions_helper import ex
import sickbeard
from . import logger, notifiers, ui
-from .helpers import cmdline_runner
+from .piper import check_pip_outdated
+from sg_helpers import cmdline_runner
# noinspection PyUnresolvedReferences
from six.moves import urllib
+from _23 import list_keys
-class CheckVersion(object):
+class PackagesUpdater(object):
+
+ def __init__(self):
+ self.install_type = 'Python package updates'
+
+ def run(self, force=False):
+ if not sickbeard.EXT_UPDATES \
+ and self.check_for_new_version(force) \
+ and sickbeard.UPDATE_PACKAGES_AUTO:
+ msg = 'Automatic %s enabled, restarting to update...' % self.install_type
+ logger.log(msg)
+ ui.notifications.message(msg)
+ sickbeard.restart()
+
+ def check_for_new_version(self, force=False):
+ """
+ Checks for available Python package installs/updates
+
+ :param force: ignore the UPDATE_PACKAGES_NOTIFY setting
+
+ :returns: True when package install/updates are available
+ """
+ if not sickbeard.UPDATE_PACKAGES_NOTIFY and not sickbeard.UPDATE_PACKAGES_AUTO and not force:
+ logger.log('Checking for %s is not enabled' % self.install_type)
+ return False
+
+ logger.log('Checking for %s%s' % (self.install_type, ('', ' (from menu)')[force]))
+ sickbeard.UPDATES_TODO = check_pip_outdated(force)
+ if not sickbeard.UPDATES_TODO:
+ msg = 'No %s needed' % self.install_type
+ logger.log(msg)
+
+ if force:
+ ui.notifications.message(msg)
+ return False
+
+ logger.log('Update(s) for %s found %s' % (self.install_type, list_keys(sickbeard.UPDATES_TODO)))
+
+ # save updates_todo to config to be loaded after restart
+ sickbeard.save_config()
+
+ msg = '%s available —
Update Now ' % (
+ self.install_type, '%s/home/restart/?pid=%s' % (sickbeard.WEB_ROOT, sickbeard.PID))
+ if None is sickbeard.NEWEST_VERSION_STRING:
+ sickbeard.NEWEST_VERSION_STRING = ''
+ if msg not in sickbeard.NEWEST_VERSION_STRING:
+ if sickbeard.NEWEST_VERSION_STRING:
+ sickbeard.NEWEST_VERSION_STRING += '
Also, '
+ sickbeard.NEWEST_VERSION_STRING += msg
+
+ return True
+
+
+class SoftwareUpdater(object):
"""
Version check class meant to run as a thread object with the sg scheduler.
"""
@@ -56,14 +111,14 @@ class CheckVersion(object):
# set current branch version
sickbeard.BRANCH = self.get_branch()
- if self.check_for_new_version(force):
- if sickbeard.AUTO_UPDATE:
- logger.log(u'New update found for SickGear, starting auto-updater...')
- ui.notifications.message('New update found for SickGear, starting auto-updater')
- if sickbeard.version_check_scheduler.action.update():
- logger.log(u'Update was successful!')
- ui.notifications.message('Update was successful')
- sickbeard.events.put(sickbeard.events.SystemEvent.RESTART)
+ if not sickbeard.EXT_UPDATES \
+ and self.check_for_new_version(force) \
+ and sickbeard.UPDATE_AUTO \
+ and sickbeard.update_software_scheduler.action.update():
+ msg = 'Automatic software updates enabled, restarting with updated...'
+ logger.log(msg)
+ ui.notifications.message(msg)
+ sickbeard.restart()
@staticmethod
def find_install_type():
@@ -81,29 +136,29 @@ class CheckVersion(object):
def check_for_new_version(self, force=False):
"""
- Checks the internet for a newer version.
+ Checks for a new software release
- returns: bool, True for new version or False for no new version.
+ :param force: ignore the UPDATE_NOTIFY setting
- force: if true the VERSION_NOTIFY setting will be ignored and a check will be forced
+ :returns: True when a new software version is available
"""
- if not sickbeard.VERSION_NOTIFY and not sickbeard.AUTO_UPDATE and not force:
- logger.log(u'Version checking is disabled, not checking for the newest version')
+ if not sickbeard.UPDATE_NOTIFY and not sickbeard.UPDATE_AUTO and not force:
+ logger.log('Checking for software updates is not enabled')
return False
- if not sickbeard.AUTO_UPDATE:
- logger.log(u'Checking if %s needs an update' % self.install_type)
+ logger.log('Checking for "%s" software update%s' % (self.install_type, ('', ' (from menu)')[force]))
if not self.updater.need_update():
sickbeard.NEWEST_VERSION_STRING = None
- if not sickbeard.AUTO_UPDATE:
- logger.log(u'No update needed')
+ msg = 'No "%s" software update needed' % self.install_type
+ logger.log(msg)
if force:
- ui.notifications.message('No update needed')
+ ui.notifications.message(msg)
return False
self.updater.set_newest_text()
+
return True
def update(self):
@@ -147,9 +202,11 @@ class GitUpdateManager(UpdateManager):
self.github_repo_user = self.get_github_repo_user()
self.github_repo = self.get_github_repo()
- self.branch = sickbeard.BRANCH
- if '' == sickbeard.BRANCH:
- self.branch = self._find_installed_branch()
+ self.branch = self._find_installed_branch()
+ if '' == self.branch:
+ self.branch = sickbeard.BRANCH
+ if self.branch and self.branch != sickbeard.BRANCH:
+ sickbeard.BRANCH = self.branch
self._cur_commit_hash = None
self._newest_commit_hash = None
@@ -242,8 +299,11 @@ class GitUpdateManager(UpdateManager):
logger.log(u'Failed: %s returned: %s' % (cmd, output), logger.ERROR)
elif 128 == exit_status or 'fatal:' in output or err:
- logger.log(u'Fatal: %s returned: %s' % (cmd, output), logger.ERROR)
+ level = logger.DEBUG
exit_status = 128
+ if 'develop' in output.lower() or 'master' in output.lower():
+ level = logger.ERROR
+ logger.log(u'Fatal: %s returned: %s' % (cmd, output), level)
else:
logger.log(u'Treat as error for now, command: %s returned: %s' % (cmd, output), logger.ERROR)
@@ -402,7 +462,7 @@ class GitUpdateManager(UpdateManager):
% (sickbeard.GIT_REMOTE, self._cur_pr_number, self.branch))
else:
- output, err, exit_status = self._run_git(self._git_path, 'fetch %s' % sickbeard.GIT_REMOTE)
+ self._run_git(self._git_path, 'fetch %s' % sickbeard.GIT_REMOTE)
output, err, exit_status = self._run_git(self._git_path, 'checkout -f -B "%s" "%s/%s"'
% (self.branch, sickbeard.GIT_REMOTE, self.branch))
diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py
index 59161a8..4676c33 100644
--- a/sickbeard/webserve.py
+++ b/sickbeard/webserve.py
@@ -1823,8 +1823,11 @@ class Home(MainHandler):
def check_update(self):
# force a check to see if there is a new version
- if sickbeard.version_check_scheduler.action.check_for_new_version(force=True):
- logger.log(u'Forcing version check')
+ if sickbeard.update_software_scheduler.action.check_for_new_version(force=True):
+ logger.log(u'Forced version check found results')
+
+ if sickbeard.update_packages_scheduler.action.check_for_new_version(force=True):
+ logger.log(u'Forced package version check found results')
self.redirect('/home/')
@@ -1898,7 +1901,7 @@ class Home(MainHandler):
if str(pid) != str(sickbeard.PID):
return self.redirect('/home/')
- if sickbeard.version_check_scheduler.action.update():
+ if sickbeard.update_software_scheduler.action.update():
return self.restart(pid)
return self._generic_message('Update Failed',
@@ -1912,7 +1915,7 @@ class Home(MainHandler):
def pull_request_checkout(self, branch):
pull_request = branch
branch = branch.split(':')[1]
- fetched = sickbeard.version_check_scheduler.action.fetch(pull_request)
+ fetched = sickbeard.update_software_scheduler.action.fetch(pull_request)
if fetched:
sickbeard.BRANCH = branch
ui.notifications.message('Checking out branch: ', branch)
@@ -6993,8 +6996,9 @@ class ConfigGeneral(Config):
log_dir=None, web_log=None,
indexer_default=None, indexer_timeout=None,
show_dirs_with_dots=None,
- version_notify=None, auto_update=None, update_interval=None, notify_on_update=None,
- update_frequency=None,
+ update_notify=None, update_auto=None, update_interval=None, notify_on_update=None,
+ update_packages_notify=None, update_packages_auto=None, update_packages_interval=None,
+ update_frequency=None, # deprecated 2020.11.07
theme_name=None, default_home=None, fanart_limit=None, showlist_tagview=None, show_tags=None,
home_search_focus=None, use_imdb_info=None, display_freespace=None, sort_article=None,
fuzzy_dating=None, trim_zero=None, date_preset=None, time_preset=None,
@@ -7008,7 +7012,7 @@ class ConfigGeneral(Config):
git_path=None, cpu_preset=None, anon_redirect=None, encryption_version=None,
proxy_setting=None, proxy_indexers=None, file_logging_preset=None, backup_db_oneday=None):
- # prevent deprecated var issues from existing ui, delete in future, added 2020.11.07
+ # 2020.11.07 prevent deprecated var issues from existing ui, delete in future, added
if None is update_interval and None is not update_frequency:
update_interval = update_frequency
@@ -7037,11 +7041,15 @@ class ConfigGeneral(Config):
sickbeard.SHOW_DIRS_WITH_DOTS = config.checkbox_to_value(show_dirs_with_dots)
# Updates
- config.schedule_version_notify(config.checkbox_to_value(version_notify))
- sickbeard.AUTO_UPDATE = config.checkbox_to_value(auto_update)
- config.schedule_update(update_interval)
+ config.schedule_update_software_notify(config.checkbox_to_value(update_notify))
+ sickbeard.UPDATE_AUTO = config.checkbox_to_value(update_auto)
+ config.schedule_update_software(update_interval)
sickbeard.NOTIFY_ON_UPDATE = config.checkbox_to_value(notify_on_update)
+ config.schedule_update_packages_notify(config.checkbox_to_value(update_packages_notify))
+ sickbeard.UPDATE_PACKAGES_AUTO = config.checkbox_to_value(update_packages_auto)
+ config.schedule_update_packages(update_packages_interval)
+
# Interface
sickbeard.THEME_NAME = theme_name
sickbeard.DEFAULT_HOME = default_home
@@ -7149,7 +7157,7 @@ class ConfigGeneral(Config):
return json.dumps({'result': 'success', 'pulls': []})
else:
try:
- pulls = sickbeard.version_check_scheduler.action.list_remote_pulls()
+ pulls = sickbeard.update_software_scheduler.action.list_remote_pulls()
return json.dumps({'result': 'success', 'pulls': pulls})
except (BaseException, Exception) as e:
logger.log(u'exception msg: ' + ex(e), logger.DEBUG)
@@ -7158,7 +7166,7 @@ class ConfigGeneral(Config):
@staticmethod
def fetch_branches():
try:
- branches = sickbeard.version_check_scheduler.action.list_remote_branches()
+ branches = sickbeard.update_software_scheduler.action.list_remote_branches()
return json.dumps({'result': 'success', 'branches': branches, 'current': sickbeard.BRANCH or 'master'})
except (BaseException, Exception) as e:
logger.log(u'exception msg: ' + ex(e), logger.DEBUG)
diff --git a/sickgear.py b/sickgear.py
index d466257..66c322b 100755
--- a/sickgear.py
+++ b/sickgear.py
@@ -23,7 +23,6 @@ import codecs
import datetime
import errno
import getopt
-import locale
import os
import signal
import sys
@@ -47,6 +46,9 @@ if not any(list(map(lambda v: v[0] <= sys.version_info[:3] <= v[1], versions)))
lambda r: '%s - %s' % tuple(map(lambda v: str(v).replace(',', '.')[1:-1], r)), versions)))
sys.exit(1)
+sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), 'lib')))
+is_win = 'win' == sys.platform[0:3]
+
try:
try:
py_cache_path = os.path.normpath(os.path.join(os.path.dirname(__file__), '__pycache__'))
@@ -59,31 +61,34 @@ try:
except (BaseException, Exception):
pass
import _cleaner
+ from sickbeard import piper
except (BaseException, Exception):
pass
try:
import Cheetah
-
- if Cheetah.Version[0] < '2':
- raise ValueError
-except ValueError:
- print('Sorry, requires Python module Cheetah 2.1.0 or newer.')
- sys.exit(1)
except (BaseException, Exception):
print('The Python module Cheetah is required')
+ if is_win:
+ print('(1) However, this first run may have just installed it, so try to simply rerun sickgear.py again')
+ print('(2) If this output is a rerun of (1) then open a command line prompt and manually install using...')
+ else:
+ print('Manually install using...')
+ print('cd
')
+ print('python -m pip install --user -r requirements.txt')
+ print('python sickgear.py')
sys.exit(1)
# Compatibility fixes for Windows
-if 'win32' == sys.platform:
+if is_win:
codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None)
# We only need this for compiling an EXE
from multiprocessing import freeze_support
-sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), 'lib')))
-
from configobj import ConfigObj
+# noinspection PyPep8Naming
+from encodingKludge import EXIT_BAD_ENCODING, SYS_ENCODING
from exceptions_helper import ex
import sickbeard
from sickbeard import db, logger, name_cache, network_timezones
@@ -91,12 +96,12 @@ from sickbeard.event_queue import Events
from sickbeard.tv import TVShow
from sickbeard.webserveInit import WebServer
-from six import integer_types, moves, PY2
+from six import integer_types
throwaway = datetime.datetime.strptime('20110101', '%Y%m%d')
rollback_loaded = None
-for signal_type in [signal.SIGTERM, signal.SIGINT] + ([] if 'win32' != sys.platform else [signal.SIGBREAK]):
+for signal_type in [signal.SIGTERM, signal.SIGINT] + ([] if not is_win else [signal.SIGBREAK]):
signal.signal(signal_type, lambda signum, void: sickbeard.sig_handler(signum=signum, _=void))
@@ -129,6 +134,7 @@ class SickGear(object):
"""
print help message for commandline options
"""
+ global is_win
help_msg = ['']
help_msg += ['Usage: %s \n' % sickbeard.MY_FULLNAME]
help_msg += ['Options:\n']
@@ -142,7 +148,7 @@ class SickGear(object):
]:
help_msg += [help_tmpl % ln]
- if 'win32' == sys.platform:
+ if is_win:
for ln in [
('-d', '--daemon', 'Running as daemon is not supported on Windows'),
('', '', 'On Windows, --daemon is substituted with: --quiet --nolaunch')
@@ -186,40 +192,18 @@ class SickGear(object):
pass
def start(self):
+ global is_win
# do some preliminary stuff
sickbeard.MY_FULLNAME = os.path.normpath(os.path.abspath(__file__))
sickbeard.MY_NAME = os.path.basename(sickbeard.MY_FULLNAME)
sickbeard.PROG_DIR = os.path.dirname(sickbeard.MY_FULLNAME)
sickbeard.DATA_DIR = sickbeard.PROG_DIR
sickbeard.MY_ARGS = sys.argv[1:]
- sickbeard.SYS_ENCODING = None
-
- try:
- locale.setlocale(locale.LC_ALL, '')
- except (locale.Error, IOError):
- pass
- try:
- sickbeard.SYS_ENCODING = locale.getpreferredencoding()
- except (locale.Error, IOError):
- pass
-
- # For OSes that are poorly configured I'll just randomly force UTF-8
- if not sickbeard.SYS_ENCODING or sickbeard.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
- sickbeard.SYS_ENCODING = 'UTF-8'
-
- if not hasattr(sys, 'setdefaultencoding'):
- moves.reload_module(sys)
-
- if PY2:
- try:
- # On non-unicode builds this raises an AttributeError,
- # if encoding type is not valid it throws a LookupError
- # noinspection PyUnresolvedReferences
- sys.setdefaultencoding(sickbeard.SYS_ENCODING)
- except (BaseException, Exception):
- print('Sorry, you MUST add the SickGear folder to the PYTHONPATH environment variable')
- print('or find another way to force Python to use %s for string encoding.' % sickbeard.SYS_ENCODING)
- sys.exit(1)
+ if EXIT_BAD_ENCODING:
+ print('Sorry, you MUST add the SickGear folder to the PYTHONPATH environment variable')
+ print('or find another way to force Python to use %s for string encoding.' % SYS_ENCODING)
+ sys.exit(1)
+ sickbeard.SYS_ENCODING = SYS_ENCODING
# Need console logging for sickgear.py and SickBeard-console.exe
self.console_logging = (not hasattr(sys, 'frozen')) or (0 < sickbeard.MY_NAME.lower().find('-console'))
@@ -230,7 +214,7 @@ class SickGear(object):
try:
opts, args = getopt.getopt(sys.argv[1:], 'hfqdsp::',
['help', 'forceupdate', 'quiet', 'nolaunch', 'daemon', 'systemd', 'pidfile=',
- 'port=', 'datadir=', 'config=', 'noresize'])
+ 'port=', 'datadir=', 'config=', 'noresize', 'update-restart'])
except getopt.GetoptError:
sys.exit(self.help_message())
@@ -267,11 +251,11 @@ class SickGear(object):
self.console_logging = False
self.no_launch = True
- if 'win32' == sys.platform:
+ if is_win:
self.run_as_daemon = False
# Run as a systemd service
- if o in ('-s', '--systemd') and 'win32' != sys.platform:
+ if o in ('-s', '--systemd') and not is_win:
self.run_as_systemd = True
self.run_as_daemon = False
self.console_logging = False
@@ -429,6 +413,9 @@ class SickGear(object):
if sickbeard.LAUNCH_BROWSER and not self.no_launch:
sickbeard.launch_browser(self.start_port)
+ # send pid of sg instance to ui
+ sickbeard.classes.loading_msg.set_msg_progress('Process-id', sickbeard.PID)
+
# check all db versions
for d, min_v, max_v, base_v, mo in [
('failed.db', sickbeard.failed_db.MIN_DB_VERSION, sickbeard.failed_db.MAX_DB_VERSION,
@@ -515,19 +502,39 @@ class SickGear(object):
else:
logger.log_error_and_exit(u'Restore FAILED!')
- # Build from the DB to start with
- sickbeard.classes.loading_msg.message = 'Loading shows from db'
- self.load_shows_from_db()
- if not db.DBConnection().has_flag('ignore_require_cleaned'):
- from sickbeard.show_updater import clean_ignore_require_words
- sickbeard.classes.loading_msg.message = 'Cleaning ignore/require words lists'
- clean_ignore_require_words()
- db.DBConnection().set_flag('ignore_require_cleaned')
-
- # Fire up all our threads
+ update_arg = '--update-restart'
+ if update_arg not in sickbeard.MY_ARGS and sickbeard.UPDATES_TODO:
+ sickbeard.MEMCACHE['update_restart'] = piper.pip_update(
+ sickbeard.classes.loading_msg, sickbeard.UPDATES_TODO, sickbeard.DATA_DIR)
+ sickbeard.UPDATES_TODO = dict()
+ sickbeard.save_config()
+
+ if not sickbeard.MEMCACHE.get('update_restart'):
+ # Build from the DB to start with
+ sickbeard.classes.loading_msg.message = 'Loading shows from db'
+ self.load_shows_from_db()
+ if not db.DBConnection().has_flag('ignore_require_cleaned'):
+ from sickbeard.show_updater import clean_ignore_require_words
+ sickbeard.classes.loading_msg.message = 'Cleaning ignore/require words lists'
+ clean_ignore_require_words()
+ db.DBConnection().set_flag('ignore_require_cleaned')
+
+ # Fire up threads
sickbeard.classes.loading_msg.message = 'Starting threads'
sickbeard.start()
+ if sickbeard.MEMCACHE.get('update_restart'):
+ sickbeard.MY_ARGS.append(update_arg)
+ sickbeard.classes.loading_msg.message = 'Restarting SickGear after update'
+ time.sleep(3)
+ sickbeard.restart(soft=False)
+ # restart wait loop
+ while True:
+ time.sleep(1)
+
+ if update_arg in sickbeard.MY_ARGS:
+ sickbeard.MY_ARGS.remove(update_arg)
+
# Build internal name cache
sickbeard.classes.loading_msg.message = 'Build name cache'
name_cache.buildNameCache()
@@ -538,7 +545,7 @@ class SickGear(object):
# load all ids from xem
sickbeard.classes.loading_msg.message = 'Loading xem data'
- startup_background_tasks = threading.Thread(name='FETCH-XEMDATA', target=sickbeard.scene_exceptions.get_xem_ids)
+ startup_background_tasks = threading.Thread(name='XEMUPDATER', target=sickbeard.scene_exceptions.get_xem_ids)
startup_background_tasks.start()
sickbeard.classes.loading_msg.message = 'Checking history'
@@ -687,11 +694,9 @@ class SickGear(object):
if sickbeard.events.SystemEvent.RESTART == ev_type:
- install_type = sickbeard.version_check_scheduler.action.install_type
-
popen_list = []
- if install_type in ('git', 'source'):
+ if sickbeard.update_software_scheduler.action.install_type in ('git', 'source'):
popen_list = [sys.executable, sickbeard.MY_FULLNAME]
if popen_list: