Browse Source

Add to config/General, "Package updates" and list packages, check packages by default on Windows, others must enable.

Change simplify section config/General/Updates.
Add check for package updates to menu item action "Check for Updates".
Add known failures are cleared for a fresh check when "Check for Updates" is used.
Change add pycryptodome for py7z to recommended.txt
Change add prebuilt AMD64 python-Levenshtein to recommended.txt.
Change add cryptography to recommended.txt as is now deprecated from 2.7
Change add pip and setuptools to piper to control installed versions under py2.
Change move lib path registration to earlier in startup because needed by piper.
Change improve performance by using pkg_resources calls instead of cmdline pip.
Change add restart trigger during loading, use changed pid of server as trigger.
Change don't load shows at startup, or save at shutdown when doing an update restart.
Change minimise redundant threads at startup when an update restart is pending.
Change handle case where pip may output what appears a failure, but isn't.
Change add capture of requirements parse errors to assist when adding new specs.
Change auto-install Cheetah dependency on first time installations (tested on Win).
Change Cheetah to fallback to setup.py type install instead of pure binary, this addresses install issues when reverting a version, and no doubt other things.
Changed cmdline_runner migrated to sg_helpers.py
tags/release_0.25.1
JackDandy 5 years ago
parent
commit
c2b6ec234e
  1. 8
      CHANGES.md
  2. 4
      gui/slick/interfaces/default/config.tmpl
  3. 104
      gui/slick/interfaces/default/config_general.tmpl
  4. 15
      gui/slick/js/loadingStartup.js
  5. 41
      lib/encodingKludge.py
  6. 69
      lib/sg_helpers.py
  7. 24
      recommended.txt
  8. 114
      sickbeard/__init__.py
  9. 40
      sickbeard/config.py
  10. 27
      sickbeard/helpers.py
  11. 2
      sickbeard/notifiers/synoindex.py
  12. 2
      sickbeard/notifiers/synologynotifier.py
  13. 345
      sickbeard/piper.py
  14. 4
      sickbeard/postProcessor.py
  15. 2
      sickbeard/show_updater.py
  16. 108
      sickbeard/version_checker.py
  17. 32
      sickbeard/webserve.py
  18. 99
      sickgear.py

8
CHANGES.md

@ -1,5 +1,13 @@
### 0.24.0 (202x-xx-xx xx:xx:00 UTC) ### 0.24.0 (202x-xx-xx xx:xx:00 UTC)
* Add to config/General, "Package updates" and list packages, check packages by default on Windows, others must enable
* Change simplify section config/General/Updates
* Add check for package updates to menu item action "Check for Updates"
* Add known failures are cleared for a fresh check when "Check for Updates" is used
* Change auto-install Cheetah dependency on first time installations (tested on Win)
* Change add cryptography to recommended.txt
* Change add pycryptodome to recommended.txt
* Change add prebuilt AMD64 python-Levenshtein to recommended.txt
* Change initialise Manage/Media Process folder and method from Config/Media Process when no previous values are stored * Change initialise Manage/Media Process folder and method from Config/Media Process when no previous values are stored
* Change remember Manage/Media Process folder and method when button 'Process' is used * Change remember Manage/Media Process folder and method when button 'Process' is used
* Change abbreviate long titles under menu tab * Change abbreviate long titles under menu tab

4
gui/slick/interfaces/default/config.tmpl

@ -24,8 +24,8 @@
<td class="infoTableCell"> <td class="infoTableCell">
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<br /> 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<br />
<em class="red-text">This is BETA software</em><br /> <em class="red-text">This is BETA software</em><br />
#if not $sg_var('VERSION_NOTIFY') and not $sg_var('EXT_UPDATES'): #if not $sg_var('UPDATE_NOTIFY') and not $sg_var('EXT_UPDATES'):
You don't have version checking turned on, see "Check software updates" in Config > General. No checks are run for outdated software, see "Software updates" in Config > General.
#end if #end if
</td> </td>
</tr> </tr>

104
gui/slick/interfaces/default/config_general.tmpl

@ -1,13 +1,15 @@
#import sickbeard #from sys import version_info
#import datetime #import datetime
#import locale #import locale
#import operator #import operator
#import sickbeard
#from sickbeard import config, metadata #from sickbeard import config, metadata
#from sickbeard.metadata.generic import GenericMetadata #from sickbeard.metadata.generic import GenericMetadata
#from sickbeard.common import * #from sickbeard.common import *
#from sickbeard.db import db_supports_backup #from sickbeard.db import db_supports_backup
#from sickbeard.helpers import anon_url, maybe_plural #from sickbeard.helpers import anon_url, maybe_plural
#from sickbeard.logger import reverseNames as file_logging_presets #from sickbeard.logger import reverseNames as file_logging_presets
#from sickbeard.piper import check_pip_env
#from sickbeard.sgdatetime import * #from sickbeard.sgdatetime import *
<% def sg_var(varname, default=False): return getattr(sickbeard, varname, default) %>#slurp# <% def sg_var(varname, default=False): return getattr(sickbeard, varname, default) %>#slurp#
<% def sg_str(varname, default=''): return getattr(sickbeard, varname, default) %>#slurp# <% def sg_str(varname, default=''): return getattr(sickbeard, varname, default) %>#slurp#
@ -168,50 +170,94 @@
<div class="component-group-desc"> <div class="component-group-desc">
<h3>Updates</h3> <h3>Updates</h3>
<p>Options for software updates.</p> <p>Options for software, package and alternative shownames/numbers.</p>
</div> </div>
<fieldset class="component-group-list"> <fieldset class="component-group-list">
#if not $sg_var('EXT_UPDATES') #if not $sg_var('EXT_UPDATES')
<div class="field-pair"> <div class="field-pair">
<label for="version_notify"> <span class="component-title">Software updates</span>
<span class="component-title">Check software updates</span>
<span class="component-desc"> <span class="component-desc">
<input type="checkbox" name="version_notify" id="version_notify"#echo ('', $checked)[$sg_var('VERSION_NOTIFY', True)]#> <label class="nextline-block">
<p>and display notifications when updates are available. <input type="checkbox" name="update_notify"#echo ('', $checked)[$sg_var('UPDATE_NOTIFY', True)]#>
Checks are run on startup and at the interval set below*</p> <p>display message for outdated software, run checks at startup and at interval<sup>1</sup></p>
</span>
</label> </label>
</div>
<div class="field-pair"> <label class="nextline-block">
<label for="auto_update"> <input type="checkbox" name="update_auto"#echo ('', $checked)[$sg_var('UPDATE_AUTO')]#>
<span class="component-title">Automatically update</span> <p>update software automatically at startup and at interval<sup>1</sup></p>
<span class="component-desc">
<input type="checkbox" name="auto_update" id="auto_update"#echo ('', $checked)[$sg_var('AUTO_UPDATE')]#>
<p>fetch and install software updates.
Updates are run on startup and in the background at the interval set below<sup>1</sup></p>
</span>
</label> </label>
</div>
<div class="field-pair"> <label class="nextline-block" style="padding:6px 0">
<label> <input type="text" name="update_interval" value="$sg_var('UPDATE_INTERVAL', 12)" class="form-control input-sm input75">
<span class="component-title">Check the server every<sup>1</sup></span> <p>hours interval<sup>1</sup> (default:$sg_var('DEFAULT_UPDATE_INTERVAL', 'n/a'))</p>
<span class="component-desc"> </label>
<input type="text" name="update_interval" id="update_interval" value="$sg_var('UPDATE_INTERVAL', 12)" class="form-control input-sm input75">
<p>hours for software updates (default:12)</p> <label class="nextline-block">
</span> <input type="checkbox" name="notify_on_update"#echo ('', $checked)[$sg_var('NOTIFY_ON_UPDATE')]#>
<p>send a message to all enabled notifiers when SickGear is updated</p>
</label> </label>
</span>
</div> </div>
<div class="field-pair"> <div class="field-pair">
<label for="notify_on_update"> <div class="field-pair">
<span class="component-title">Notify on software update</span> <label for="update-libs" class="nextline-block">
<span class="component-title">Package updates</span>
<span class="component-desc"> <span class="component-desc">
<input type="checkbox" name="notify_on_update" id="notify_on_update"#echo ('', $checked)[$sg_var('NOTIFY_ON_UPDATE')]#> <input class="view-if" type="checkbox" name="update_packages_notify" id="update-libs"#echo ('', $checked)[$sg_var('UPDATE_PACKAGES_NOTIFY')]#>
<p>send a message to all enabled notifiers when SickGear has been updated</p> <p>display message for outdated packages, run checks at startup and at interval<sup>2</sup></p>
</span> </span>
</label> </label>
<div class="show-if-update-libs">
<span class="component-title"></span>
<span class="component-desc">
<label class="nextline-block">
<input type="checkbox" name="update_packages_auto"#echo ('', $checked)[$sg_var('UPDATE_PACKAGES_AUTO')]#>
<p>update packages automatically at startup and at interval<sup>2</sup></p>
</label>
<label class="nextline-block" style="padding-top:6px">
<input type="text" name="update_packages_interval" value="$sg_var('UPDATE_PACKAGES_INTERVAL', 24)" class="form-control input-sm input75">
<p>hours interval<sup>2</sup> (default:$sg_var('DEFAULT_UPDATE_PACKAGES_INTERVAL', 'n/a'))</p>
</label>
#try
#set $installed, $extra_info, $known_failed = $check_pip_env()
#except
#set $installed, $extra_info, $known_failed = ([], None, [])
#end try
<style>
.extras{padding-top:10px}
.extras p{margin-bottom:3px}
.extras th{padding:0 2px}
.extras td{white-space:nowrap}
.extras .center{text-align:center}
.extras .package-col{width:135px;padding-left:0}
.extras .info-col{min-width:135px;padding-left:30px}
</style>
<div class="extras">
<table cellspacing="0" border="0" cellpadding="0">
<thead>
<tr class="grey-text">
<th class="package-col">package (py #echo '.'.join('%s' % n for n in sys.version_info[0:3])#)</th>
<th class="center">version in use</th>
<th class="info-col">extra info</th>
</tr>
</thead>
<tbody>
#for $item in $installed
<tr><td class="package-col">$item[0].lower()</td><td class="center">#echo $item[1] or '-'#</td><td class="info-col">#echo $sg_var('UPDATES_TODO', {}).get($item[0]) and 'update ready' or $extra_info.get($item[0], 'filled optional')#</td></tr>
#end for
#set $update_link = '<a href="%s/home/check-update">check update (clear failed)</a>' % $sbRoot
#for $item in $known_failed
<tr class="grey-text"><td>$item.lower()</td><td class="center">failed</td><td class="info-col">$update_link</td></tr>
#set $update_link = ''
#end for
</tbody></table>
<p class="extras">the above info may help with packages that don't update internally<br>manual installation tip: <code>python -m pip install -U --user package</code></p>
</div>
</span>
</div>
</div> </div>
#else #else
<div class="field-pair"> <div class="field-pair">

15
gui/slick/js/loadingStartup.js

@ -4,6 +4,8 @@ var dev = !1,
logInfo = dev && console.info.bind(window.console) || function (){}, logInfo = dev && console.info.bind(window.console) || function (){},
logErr = dev && console.error.bind(window.console) || function (){}; logErr = dev && console.error.bind(window.console) || function (){};
var p_id = 0;
$(function () { $(function () {
ajaxConsumer.checkLoadNotifications(); ajaxConsumer.checkLoadNotifications();
}); });
@ -53,7 +55,7 @@ var ajaxConsumer = function () {
function putMsg(msg) { function putMsg(msg) {
var loading = '.loading-step', lastStep$ = $(loading).filter(':last'); 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$); lastStep$.clone().insertAfter(lastStep$);
var result$ = lastStep$.find('.result'); var result$ = lastStep$.find('.result');
@ -64,7 +66,7 @@ function putMsg(msg) {
result$.addClass('hide'); result$.addClass('hide');
} }
lastStep$ = $(loading).filter(':last'); lastStep$ = $(loading).filter(':last');
lastStep$.attr('data-message', msg); lastStep$.attr('data-message', escape(msg));
lastStep$.find('.desc').text(msg + ': '); lastStep$.find('.desc').text(msg + ': ');
lastStep$.find('.count').text(''); lastStep$.find('.count').text('');
lastStep$.find('.spinner').removeClass('hide'); lastStep$.find('.spinner').removeClass('hide');
@ -75,11 +77,18 @@ function putMsg(msg) {
function uiUpdateComplete(data) { function uiUpdateComplete(data) {
$.each(data, function (i, msg) { $.each(data, function (i, msg) {
var loading = '.loading-step'; 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) { if (i >= $(loading).length) {
putMsg(msg.msg); putMsg(msg.msg);
} }
if (-1 !== msg.progress) { 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('.spinner, .result').addClass('hide');
loading$.find('.count').text(msg.progress); loading$.find('.count').text(msg.progress);
} }

41
lib/encodingKludge.py

@ -19,27 +19,60 @@
import os import os
import logging import logging
import locale 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 = logging.getLogger('encodingKludge')
logger.addHandler(logging.NullHandler()) logger.addHandler(logging.NullHandler())
# noinspection PyUnreachableCode
if False:
# noinspection PyUnresolvedReferences
from typing import AnyStr
SYS_ENCODING = None SYS_ENCODING = None
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: try:
locale.setlocale(locale.LC_ALL, '') locale.setlocale(locale.LC_ALL, '')
except (locale.Error, IOError): except (locale.Error, IOError):
pass pass
try: try:
SYS_ENCODING = locale.getpreferredencoding() sys_encoding = locale.getpreferredencoding()
except (locale.Error, IOError): except (locale.Error, IOError):
pass pass
# For OSes that are poorly configured I'll just randomly force UTF-8 # 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'): if not sys_encoding or sys_encoding in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
SYS_ENCODING = 'UTF-8' 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 # 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 # encodings. It tries to just use unicode, but if that fails then it tries forcing it to utf-8. Any functions

69
lib/sg_helpers.py

@ -2,6 +2,7 @@
# --------------- # ---------------
# functions are placed here to remove cyclic import issues from placement in helpers # functions are placed here to remove cyclic import issues from placement in helpers
# #
import ast
import codecs import codecs
import datetime import datetime
import getpass import getpass
@ -14,6 +15,7 @@ import shutil
import socket import socket
import stat import stat
import subprocess import subprocess
import sys
import tempfile import tempfile
import threading import threading
import time import time
@ -29,7 +31,8 @@ from send2trash import send2trash
import encodingKludge as ek import encodingKludge as ek
import requests 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 from six import integer_types, iteritems, iterkeys, itervalues, PY2, string_types, text_type
import zipfile import zipfile
@ -49,6 +52,7 @@ if False:
# global tmdb_info cache # global tmdb_info cache
_TMDB_INFO_CACHE = {'date': datetime.datetime(2000, 1, 1), 'data': None} _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 # Mapping error status codes to official W3C names
http_error_code = { http_error_code = {
@ -1419,3 +1423,66 @@ def scantree(path, # type: AnyStr
yield subentry yield subentry
if no_filter: if no_filter:
yield entry 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

24
recommended.txt

@ -1,5 +1,21 @@
lxml>=4.4.2 cryptography; '3.7' <= python_version
regex>=2019.11.1 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 >= 0.12.0
scandir>=1.10.0; python_version < '3.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)
py7zr >= 0.10.1; python_version > '3.0' 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

114
sickbeard/__init__.py

@ -30,7 +30,6 @@ import socket
import webbrowser import webbrowser
# apparently py2exe won't build these unless they're imported somewhere # apparently py2exe won't build these unless they're imported somewhere
import ast
import os.path import os.path
import sys import sys
import threading import threading
@ -96,7 +95,8 @@ events = None # type: Events
recent_search_scheduler = None recent_search_scheduler = None
backlog_search_scheduler = None backlog_search_scheduler = None
show_update_scheduler = None show_update_scheduler = None
version_check_scheduler = None update_software_scheduler = None
update_packages_scheduler = None
show_queue_scheduler = None show_queue_scheduler = None
search_queue_scheduler = None search_queue_scheduler = None
proper_finder_scheduler = None proper_finder_scheduler = None
@ -128,9 +128,21 @@ metadata_provider_dict = {}
MODULE_UPDATE_STRING = None MODULE_UPDATE_STRING = None
NEWEST_VERSION_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 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 CUR_COMMIT_HASH = None
EXT_UPDATES = False EXT_UPDATES = False
BRANCH = '' BRANCH = ''
@ -246,14 +258,12 @@ NEWZNAB_DATA = ''
DEFAULT_MEDIAPROCESS_INTERVAL = 10 DEFAULT_MEDIAPROCESS_INTERVAL = 10
DEFAULT_BACKLOG_PERIOD = 21 DEFAULT_BACKLOG_PERIOD = 21
DEFAULT_RECENTSEARCH_INTERVAL = 40 DEFAULT_RECENTSEARCH_INTERVAL = 40
DEFAULT_UPDATE_INTERVAL = 1
DEFAULT_WATCHEDSTATE_INTERVAL = 10 DEFAULT_WATCHEDSTATE_INTERVAL = 10
MEDIAPROCESS_INTERVAL = DEFAULT_MEDIAPROCESS_INTERVAL MEDIAPROCESS_INTERVAL = DEFAULT_MEDIAPROCESS_INTERVAL
BACKLOG_PERIOD = DEFAULT_BACKLOG_PERIOD BACKLOG_PERIOD = DEFAULT_BACKLOG_PERIOD
BACKLOG_LIMITED_PERIOD = 7 BACKLOG_LIMITED_PERIOD = 7
RECENTSEARCH_INTERVAL = DEFAULT_RECENTSEARCH_INTERVAL RECENTSEARCH_INTERVAL = DEFAULT_RECENTSEARCH_INTERVAL
UPDATE_INTERVAL = DEFAULT_UPDATE_INTERVAL
RECENTSEARCH_STARTUP = False RECENTSEARCH_STARTUP = False
BACKLOG_NOFULL = False BACKLOG_NOFULL = False
@ -262,7 +272,6 @@ MIN_MEDIAPROCESS_INTERVAL = 1
MIN_RECENTSEARCH_INTERVAL = 10 MIN_RECENTSEARCH_INTERVAL = 10
MIN_BACKLOG_PERIOD = 7 MIN_BACKLOG_PERIOD = 7
MAX_BACKLOG_PERIOD = 42 MAX_BACKLOG_PERIOD = 42
MIN_UPDATE_INTERVAL = 1
MIN_WATCHEDSTATE_INTERVAL = 10 MIN_WATCHEDSTATE_INTERVAL = 10
MAX_WATCHEDSTATE_INTERVAL = 60 MAX_WATCHEDSTATE_INTERVAL = 60
@ -554,6 +563,8 @@ BACKUP_DB_ONEDAY = False # type: bool
BACKUP_DB_MAX_COUNT = 14 # type: int BACKUP_DB_MAX_COUNT = 14 # type: int
BACKUP_DB_DEFAULT_COUNT = 14 # type: int BACKUP_DB_DEFAULT_COUNT = 14 # type: int
UPDATES_TODO = {}
EXTRA_SCRIPTS = [] EXTRA_SCRIPTS = []
SG_EXTRA_SCRIPTS = [] SG_EXTRA_SCRIPTS = []
@ -660,7 +671,8 @@ def init_stage_1(console_logging):
# Gen Config/Misc # Gen Config/Misc
global LAUNCH_BROWSER, UPDATE_SHOWS_ON_START, SHOW_UPDATE_HOUR, \ 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, \ 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 # Gen Config/Interface
global THEME_NAME, DEFAULT_HOME, FANART_LIMIT, SHOWLIST_TAGVIEW, SHOW_TAGS, \ 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, \ 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 global ANIME_TREAT_AS_HDTV, USE_ANIDB, ANIDB_USERNAME, ANIDB_PASSWORD, ANIDB_USE_MYLIST
# db backup settings # db backup settings
global BACKUP_DB_PATH, BACKUP_DB_ONEDAY, BACKUP_DB_MAX_COUNT, BACKUP_DB_DEFAULT_COUNT 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', for stanza in ('General', 'Blackhole', 'SABnzbd', 'NZBGet', 'Emby', 'Kodi', 'XBMC', 'PLEX',
'Growl', 'Prowl', 'Slack', 'Discord', 'Boxcar2', 'NMJ', 'NMJv2', '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') DEFAULT_HOME = check_setting_str(CFG, 'GUI', 'default_home', 'episodes')
FANART_LIMIT = check_setting_int(CFG, 'GUI', 'fanart_limit', 3) FANART_LIMIT = check_setting_int(CFG, 'GUI', 'fanart_limit', 3)
FANART_PANEL = check_setting_str(CFG, 'GUI', 'fanart_panel', 'highlight2') FANART_PANEL = check_setting_str(CFG, 'GUI', 'fanart_panel', 'highlight2')
FANART_RATINGS = check_setting_str(CFG, 'GUI', 'fanart_ratings', None) FANART_RATINGS = sg_helpers.ast_eval(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 = {}
USE_IMDB_INFO = bool(check_setting_int(CFG, 'GUI', 'use_imdb_info', 1)) 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]) 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)) 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) STATUS_DEFAULT = check_setting_int(CFG, 'General', 'status_default', SKIPPED)
WANTED_BEGIN_DEFAULT = check_setting_int(CFG, 'General', 'wanted_begin_default', 0) WANTED_BEGIN_DEFAULT = check_setting_int(CFG, 'General', 'wanted_begin_default', 0)
WANTED_LATEST_DEFAULT = check_setting_int(CFG, 'General', 'wanted_latest_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)) 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)) FLATTEN_FOLDERS_DEFAULT = bool(check_setting_int(CFG, 'General', 'flatten_folders_default', 0))
TVINFO_DEFAULT = check_setting_int(CFG, 'General', 'indexer_default', 0) TVINFO_DEFAULT = check_setting_int(CFG, 'General', 'indexer_default', 0)
if TVINFO_DEFAULT and not TVInfoAPI(TVINFO_DEFAULT).config['active']: 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)) SCENE_DEFAULT = bool(check_setting_int(CFG, 'General', 'scene_default', 0))
PROVIDER_ORDER = check_setting_str(CFG, 'General', 'provider_order', '').split() 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_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') 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_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) 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)) 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)) 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' % ( 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), (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()]) [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) BROWSELIST_MRU = sg_helpers.ast_eval(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 = {}
BACKUP_DB_PATH = check_setting_str(CFG, 'Backup', 'backup_db_path', '') 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_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_MAX_COUNT = minimax(check_setting_int(CFG, 'Backup', 'backup_db_max_count', BACKUP_DB_DEFAULT_COUNT),
BACKUP_DB_DEFAULT_COUNT, 0, 90) 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.db = db
sg_helpers.DOMAIN_FAILURES.load_from_db() sg_helpers.DOMAIN_FAILURES.load_from_db()
@ -1472,12 +1492,12 @@ def init_stage_2():
# Schedulers # Schedulers
# global trakt_checker_scheduler # global trakt_checker_scheduler
global recent_search_scheduler, backlog_search_scheduler, show_update_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, \ proper_finder_scheduler, media_process_scheduler, subtitles_finder_scheduler, \
background_mapping_task, \ background_mapping_task, \
watched_state_queue_scheduler, emby_watched_state_scheduler, plex_watched_state_scheduler watched_state_queue_scheduler, emby_watched_state_scheduler, plex_watched_state_scheduler
# Gen Config/Misc # Gen Config/Misc
global SHOW_UPDATE_HOUR, UPDATE_INTERVAL global SHOW_UPDATE_HOUR, UPDATE_INTERVAL, UPDATE_PACKAGES_INTERVAL
# Search Settings/Episode # Search Settings/Episode
global RECENTSEARCH_INTERVAL global RECENTSEARCH_INTERVAL
# Subtitles # Subtitles
@ -1525,10 +1545,17 @@ def init_stage_2():
# initialize schedulers # initialize schedulers
# updaters # updaters
update_now = datetime.timedelta(minutes=0) update_now = datetime.timedelta(minutes=0)
version_check_scheduler = scheduler.Scheduler( update_software_scheduler = scheduler.Scheduler(
version_checker.CheckVersion(), version_checker.SoftwareUpdater(),
cycleTime=datetime.timedelta(hours=UPDATE_INTERVAL), 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) silent=False)
show_queue_scheduler = scheduler.Scheduler( show_queue_scheduler = scheduler.Scheduler(
@ -1614,7 +1641,7 @@ def init_stage_2():
threadName='FINDSUBTITLES', threadName='FINDSUBTITLES',
silent=not USE_SUBTITLES) 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( watched_state_queue_scheduler = scheduler.Scheduler(
watchedstate_queue.WatchedStateQueue(), watchedstate_queue.WatchedStateQueue(),
@ -1650,10 +1677,12 @@ def init_stage_2():
def enabled_schedulers(is_init=False): def enabled_schedulers(is_init=False):
# ([], [trakt_checker_scheduler])[USE_TRAKT] + \ # ([], [trakt_checker_scheduler])[USE_TRAKT] + \
return ([], [events])[is_init] \ return ([], [events])[is_init] \
+ [recent_search_scheduler, backlog_search_scheduler, show_update_scheduler, + ([], [recent_search_scheduler, backlog_search_scheduler, show_update_scheduler,
version_check_scheduler, show_queue_scheduler, search_queue_scheduler, proper_finder_scheduler, update_software_scheduler, update_packages_scheduler,
show_queue_scheduler, search_queue_scheduler, proper_finder_scheduler,
media_process_scheduler, subtitles_finder_scheduler, media_process_scheduler, subtitles_finder_scheduler,
emby_watched_state_scheduler, plex_watched_state_scheduler, watched_state_queue_scheduler]\ emby_watched_state_scheduler, plex_watched_state_scheduler, watched_state_queue_scheduler]
)[not MEMCACHE.get('update_restart')] \
+ ([events], [])[is_init] + ([events], [])[is_init]
@ -1754,6 +1783,7 @@ def halt():
def save_all(): def save_all():
if not MEMCACHE.get('update_restart'):
global showList global showList
# write all shows # write all shows
@ -1816,7 +1846,6 @@ def save_config():
new_config['General']['recentsearch_interval'] = int(RECENTSEARCH_INTERVAL) new_config['General']['recentsearch_interval'] = int(RECENTSEARCH_INTERVAL)
new_config['General']['backlog_period'] = int(BACKLOG_PERIOD) new_config['General']['backlog_period'] = int(BACKLOG_PERIOD)
new_config['General']['backlog_limited_period'] = int(BACKLOG_LIMITED_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']['download_propers'] = int(DOWNLOAD_PROPERS)
new_config['General']['propers_webdl_onegrp'] = int(PROPERS_WEBDL_ONEGRP) new_config['General']['propers_webdl_onegrp'] = int(PROPERS_WEBDL_ONEGRP)
new_config['General']['allow_high_priority'] = int(ALLOW_HIGH_PRIORITY) 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_order'] = ' '.join(PROVIDER_ORDER)
new_config['General']['provider_homes'] = '%s' % dict([(pid, v) for pid, v in list_items(PROVIDER_HOMES) if pid in [ 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]]]) 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']['update_notify'] = int(UPDATE_NOTIFY)
new_config['General']['auto_update'] = int(AUTO_UPDATE) 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']['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_strip_year'] = int(NAMING_STRIP_YEAR)
new_config['General']['naming_pattern'] = NAMING_PATTERN new_config['General']['naming_pattern'] = NAMING_PATTERN
new_config['General']['naming_custom_abd'] = int(NAMING_CUSTOM_ABD) 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']['require_words'] = helpers.generate_word_str(REQUIRE_WORDS, REQUIRE_WORDS_REGEX)
new_config['General']['calendar_unprotected'] = int(CALENDAR_UNPROTECTED) new_config['General']['calendar_unprotected'] = int(CALENDAR_UNPROTECTED)
new_config['Updates'] = {}
new_config['Updates']['updates_todo'] = '%s' % (UPDATES_TODO or {})
new_config['Backup'] = {} new_config['Backup'] = {}
if BACKUP_DB_PATH: if BACKUP_DB_PATH:
new_config['Backup']['backup_db_path'] = 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']['default_home'] = DEFAULT_HOME
new_config['GUI']['fanart_limit'] = FANART_LIMIT new_config['GUI']['fanart_limit'] = FANART_LIMIT
new_config['GUI']['fanart_panel'] = FANART_PANEL 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']['use_imdb_info'] = int(USE_IMDB_INFO)
new_config['GUI']['imdb_accounts'] = IMDB_ACCOUNTS new_config['GUI']['imdb_accounts'] = IMDB_ACCOUNTS
new_config['GUI']['fuzzy_dating'] = int(FUZZY_DATING) 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']['show_tag_default'] = SHOW_TAG_DEFAULT
new_config['GUI']['history_layout'] = HISTORY_LAYOUT new_config['GUI']['history_layout'] = HISTORY_LAYOUT
new_config['GUI']['browselist_hidden'] = '|~|'.join(BROWSELIST_HIDDEN) 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'] = {}
new_config['Subtitles']['use_subtitles'] = int(USE_SUBTITLES) new_config['Subtitles']['use_subtitles'] = int(USE_SUBTITLES)

40
sickbeard/config.py

@ -177,25 +177,49 @@ def schedule_backlog(iv):
sickbeard.backlog_search_scheduler.action.cycleTime = sickbeard.BACKLOG_PERIOD 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) sickbeard.UPDATE_INTERVAL = to_int(iv, default=sickbeard.DEFAULT_UPDATE_INTERVAL)
if sickbeard.UPDATE_INTERVAL < sickbeard.MIN_UPDATE_INTERVAL: if sickbeard.UPDATE_INTERVAL < sickbeard.MIN_UPDATE_INTERVAL:
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): def schedule_update_software_notify(update_notify):
old_setting = sickbeard.VERSION_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 sickbeard.NEWEST_VERSION_STRING = None
if not old_setting and version_notify: if not old_setting and update_notify:
sickbeard.version_check_scheduler.action.run() 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): def schedule_download_propers(download_propers):

27
sickbeard/helpers.py

@ -30,7 +30,6 @@ import shutil
import socket import socket
import time import time
import uuid import uuid
import subprocess
import sys import sys
try: try:
@ -1793,32 +1792,6 @@ def xhtml_escape(text, br=True):
return escape.xhtml_escape(text) 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): def parse_imdb_id(string):
# type: (AnyStr) -> Optional[AnyStr] # type: (AnyStr) -> Optional[AnyStr]
""" Parse an IMDB ID from a string """ Parse an IMDB ID from a string

2
sickbeard/notifiers/synoindex.py

@ -22,6 +22,7 @@ from .generic import BaseNotifier
# noinspection PyPep8Naming # noinspection PyPep8Naming
import encodingKludge as ek import encodingKludge as ek
from exceptions_helper import ex from exceptions_helper import ex
from sg_helpers import cmdline_runner
# noinspection PyPep8Naming # noinspection PyPep8Naming
@ -37,7 +38,6 @@ class SynoIndexNotifier(BaseNotifier):
self._log_debug(u'Executing command ' + str(synoindex_cmd)) 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])) self._log_debug(u'Absolute path to command: ' + ek.ek(os.path.abspath, synoindex_cmd[0]))
try: try:
from sickbeard.helpers import cmdline_runner
output, err, exit_status = cmdline_runner(synoindex_cmd) output, err, exit_status = cmdline_runner(synoindex_cmd)
self._log_debug(u'Script result: %s' % output) self._log_debug(u'Script result: %s' % output)
except (BaseException, Exception) as e: except (BaseException, Exception) as e:

2
sickbeard/notifiers/synologynotifier.py

@ -21,6 +21,7 @@ from .generic import Notifier
# noinspection PyPep8Naming # noinspection PyPep8Naming
import encodingKludge as ek import encodingKludge as ek
from exceptions_helper import ex from exceptions_helper import ex
from sg_helpers import cmdline_runner
class SynologyNotifier(Notifier): class SynologyNotifier(Notifier):
@ -31,7 +32,6 @@ class SynologyNotifier(Notifier):
self._log(u'Executing command ' + str(synodsmnotify_cmd)) self._log(u'Executing command ' + str(synodsmnotify_cmd))
self._log_debug(u'Absolute path to command: ' + ek.ek(os.path.abspath, synodsmnotify_cmd[0])) self._log_debug(u'Absolute path to command: ' + ek.ek(os.path.abspath, synodsmnotify_cmd[0]))
try: try:
from sickbeard.helpers import cmdline_runner
output, err, exit_status = cmdline_runner(synodsmnotify_cmd) output, err, exit_status = cmdline_runner(synodsmnotify_cmd)
self._log_debug(u'Script result: %s' % output) self._log_debug(u'Script result: %s' % output)
except (BaseException, Exception) as e: except (BaseException, Exception) as e:

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

4
sickbeard/postProcessor.py

@ -38,7 +38,7 @@ from .name_parser.parser import InvalidNameException, InvalidShowException, Name
from _23 import decode_str from _23 import decode_str
from six import iteritems, PY2, string_types from six import iteritems, PY2, string_types
from sg_helpers import long_path from sg_helpers import long_path, cmdline_runner
# noinspection PyUnreachableCode # noinspection PyUnreachableCode
if False: if False:
@ -849,7 +849,7 @@ class PostProcessor(object):
try: try:
# run the command and capture output # 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) self._log('Script result: %s' % output, logger.DEBUG)
except OSError as e: except OSError as e:

2
sickbeard/show_updater.py

@ -136,7 +136,7 @@ class ShowUpdater(object):
import threading import threading
try: try:
sickbeard.background_mapping_task = threading.Thread( 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() sickbeard.background_mapping_task.start()
except (BaseException, Exception): except (BaseException, Exception):
logger.log('missing mapped ids update error', logger.ERROR) logger.log('missing mapped ids update error', logger.ERROR)

108
sickbeard/version_checker.py

@ -31,13 +31,68 @@ from exceptions_helper import ex
import sickbeard import sickbeard
from . import logger, notifiers, ui from . import logger, notifiers, ui
from .helpers import cmdline_runner from .piper import check_pip_outdated
from sg_helpers import cmdline_runner
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
from six.moves import urllib 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 &mdash; <a href="%s">Update Now</a>' % (
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 += '<br>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. 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 # set current branch version
sickbeard.BRANCH = self.get_branch() sickbeard.BRANCH = self.get_branch()
if self.check_for_new_version(force): if not sickbeard.EXT_UPDATES \
if sickbeard.AUTO_UPDATE: and self.check_for_new_version(force) \
logger.log(u'New update found for SickGear, starting auto-updater...') and sickbeard.UPDATE_AUTO \
ui.notifications.message('New update found for SickGear, starting auto-updater') and sickbeard.update_software_scheduler.action.update():
if sickbeard.version_check_scheduler.action.update(): msg = 'Automatic software updates enabled, restarting with updated...'
logger.log(u'Update was successful!') logger.log(msg)
ui.notifications.message('Update was successful') ui.notifications.message(msg)
sickbeard.events.put(sickbeard.events.SystemEvent.RESTART) sickbeard.restart()
@staticmethod @staticmethod
def find_install_type(): def find_install_type():
@ -81,29 +136,29 @@ class CheckVersion(object):
def check_for_new_version(self, force=False): 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: if not sickbeard.UPDATE_NOTIFY and not sickbeard.UPDATE_AUTO and not force:
logger.log(u'Version checking is disabled, not checking for the newest version') logger.log('Checking for software updates is not enabled')
return False return False
if not sickbeard.AUTO_UPDATE: logger.log('Checking for "%s" software update%s' % (self.install_type, ('', ' (from menu)')[force]))
logger.log(u'Checking if %s needs an update' % self.install_type)
if not self.updater.need_update(): if not self.updater.need_update():
sickbeard.NEWEST_VERSION_STRING = None sickbeard.NEWEST_VERSION_STRING = None
if not sickbeard.AUTO_UPDATE: msg = 'No "%s" software update needed' % self.install_type
logger.log(u'No update needed') logger.log(msg)
if force: if force:
ui.notifications.message('No update needed') ui.notifications.message(msg)
return False return False
self.updater.set_newest_text() self.updater.set_newest_text()
return True return True
def update(self): def update(self):
@ -147,9 +202,11 @@ class GitUpdateManager(UpdateManager):
self.github_repo_user = self.get_github_repo_user() self.github_repo_user = self.get_github_repo_user()
self.github_repo = self.get_github_repo() 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._cur_commit_hash = None
self._newest_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) logger.log(u'Failed: %s returned: %s' % (cmd, output), logger.ERROR)
elif 128 == exit_status or 'fatal:' in output or err: 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 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: else:
logger.log(u'Treat as error for now, command: %s returned: %s' % (cmd, output), logger.ERROR) 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)) % (sickbeard.GIT_REMOTE, self._cur_pr_number, self.branch))
else: 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"' output, err, exit_status = self._run_git(self._git_path, 'checkout -f -B "%s" "%s/%s"'
% (self.branch, sickbeard.GIT_REMOTE, self.branch)) % (self.branch, sickbeard.GIT_REMOTE, self.branch))

32
sickbeard/webserve.py

@ -1823,8 +1823,11 @@ class Home(MainHandler):
def check_update(self): def check_update(self):
# force a check to see if there is a new version # force a check to see if there is a new version
if sickbeard.version_check_scheduler.action.check_for_new_version(force=True): if sickbeard.update_software_scheduler.action.check_for_new_version(force=True):
logger.log(u'Forcing version check') 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/') self.redirect('/home/')
@ -1898,7 +1901,7 @@ class Home(MainHandler):
if str(pid) != str(sickbeard.PID): if str(pid) != str(sickbeard.PID):
return self.redirect('/home/') return self.redirect('/home/')
if sickbeard.version_check_scheduler.action.update(): if sickbeard.update_software_scheduler.action.update():
return self.restart(pid) return self.restart(pid)
return self._generic_message('Update Failed', return self._generic_message('Update Failed',
@ -1912,7 +1915,7 @@ class Home(MainHandler):
def pull_request_checkout(self, branch): def pull_request_checkout(self, branch):
pull_request = branch pull_request = branch
branch = branch.split(':')[1] branch = branch.split(':')[1]
fetched = sickbeard.version_check_scheduler.action.fetch(pull_request) fetched = sickbeard.update_software_scheduler.action.fetch(pull_request)
if fetched: if fetched:
sickbeard.BRANCH = branch sickbeard.BRANCH = branch
ui.notifications.message('Checking out branch: ', branch) ui.notifications.message('Checking out branch: ', branch)
@ -6993,8 +6996,9 @@ class ConfigGeneral(Config):
log_dir=None, web_log=None, log_dir=None, web_log=None,
indexer_default=None, indexer_timeout=None, indexer_default=None, indexer_timeout=None,
show_dirs_with_dots=None, show_dirs_with_dots=None,
version_notify=None, auto_update=None, update_interval=None, notify_on_update=None, update_notify=None, update_auto=None, update_interval=None, notify_on_update=None,
update_frequency=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, 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, 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, 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, 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): 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: if None is update_interval and None is not update_frequency:
update_interval = 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) sickbeard.SHOW_DIRS_WITH_DOTS = config.checkbox_to_value(show_dirs_with_dots)
# Updates # Updates
config.schedule_version_notify(config.checkbox_to_value(version_notify)) config.schedule_update_software_notify(config.checkbox_to_value(update_notify))
sickbeard.AUTO_UPDATE = config.checkbox_to_value(auto_update) sickbeard.UPDATE_AUTO = config.checkbox_to_value(update_auto)
config.schedule_update(update_interval) config.schedule_update_software(update_interval)
sickbeard.NOTIFY_ON_UPDATE = config.checkbox_to_value(notify_on_update) 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 # Interface
sickbeard.THEME_NAME = theme_name sickbeard.THEME_NAME = theme_name
sickbeard.DEFAULT_HOME = default_home sickbeard.DEFAULT_HOME = default_home
@ -7149,7 +7157,7 @@ class ConfigGeneral(Config):
return json.dumps({'result': 'success', 'pulls': []}) return json.dumps({'result': 'success', 'pulls': []})
else: else:
try: 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}) return json.dumps({'result': 'success', 'pulls': pulls})
except (BaseException, Exception) as e: except (BaseException, Exception) as e:
logger.log(u'exception msg: ' + ex(e), logger.DEBUG) logger.log(u'exception msg: ' + ex(e), logger.DEBUG)
@ -7158,7 +7166,7 @@ class ConfigGeneral(Config):
@staticmethod @staticmethod
def fetch_branches(): def fetch_branches():
try: 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'}) return json.dumps({'result': 'success', 'branches': branches, 'current': sickbeard.BRANCH or 'master'})
except (BaseException, Exception) as e: except (BaseException, Exception) as e:
logger.log(u'exception msg: ' + ex(e), logger.DEBUG) logger.log(u'exception msg: ' + ex(e), logger.DEBUG)

99
sickgear.py

@ -23,7 +23,6 @@ import codecs
import datetime import datetime
import errno import errno
import getopt import getopt
import locale
import os import os
import signal import signal
import sys 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))) lambda r: '%s - %s' % tuple(map(lambda v: str(v).replace(',', '.')[1:-1], r)), versions)))
sys.exit(1) 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:
try: try:
py_cache_path = os.path.normpath(os.path.join(os.path.dirname(__file__), '__pycache__')) py_cache_path = os.path.normpath(os.path.join(os.path.dirname(__file__), '__pycache__'))
@ -59,31 +61,34 @@ try:
except (BaseException, Exception): except (BaseException, Exception):
pass pass
import _cleaner import _cleaner
from sickbeard import piper
except (BaseException, Exception): except (BaseException, Exception):
pass pass
try: try:
import Cheetah 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): except (BaseException, Exception):
print('The Python module Cheetah is required') 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 <sickgear_installed_folder>')
print('python -m pip install --user -r requirements.txt')
print('python sickgear.py')
sys.exit(1) sys.exit(1)
# Compatibility fixes for Windows # Compatibility fixes for Windows
if 'win32' == sys.platform: if is_win:
codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None) codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None)
# We only need this for compiling an EXE # We only need this for compiling an EXE
from multiprocessing import freeze_support from multiprocessing import freeze_support
sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), 'lib')))
from configobj import ConfigObj from configobj import ConfigObj
# noinspection PyPep8Naming
from encodingKludge import EXIT_BAD_ENCODING, SYS_ENCODING
from exceptions_helper import ex from exceptions_helper import ex
import sickbeard import sickbeard
from sickbeard import db, logger, name_cache, network_timezones 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.tv import TVShow
from sickbeard.webserveInit import WebServer 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') throwaway = datetime.datetime.strptime('20110101', '%Y%m%d')
rollback_loaded = None 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)) 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 print help message for commandline options
""" """
global is_win
help_msg = [''] help_msg = ['']
help_msg += ['Usage: %s <option> <another option>\n' % sickbeard.MY_FULLNAME] help_msg += ['Usage: %s <option> <another option>\n' % sickbeard.MY_FULLNAME]
help_msg += ['Options:\n'] help_msg += ['Options:\n']
@ -142,7 +148,7 @@ class SickGear(object):
]: ]:
help_msg += [help_tmpl % ln] help_msg += [help_tmpl % ln]
if 'win32' == sys.platform: if is_win:
for ln in [ for ln in [
('-d', '--daemon', 'Running as daemon is not supported on Windows'), ('-d', '--daemon', 'Running as daemon is not supported on Windows'),
('', '', 'On Windows, --daemon is substituted with: --quiet --nolaunch') ('', '', 'On Windows, --daemon is substituted with: --quiet --nolaunch')
@ -186,40 +192,18 @@ class SickGear(object):
pass pass
def start(self): def start(self):
global is_win
# do some preliminary stuff # do some preliminary stuff
sickbeard.MY_FULLNAME = os.path.normpath(os.path.abspath(__file__)) sickbeard.MY_FULLNAME = os.path.normpath(os.path.abspath(__file__))
sickbeard.MY_NAME = os.path.basename(sickbeard.MY_FULLNAME) sickbeard.MY_NAME = os.path.basename(sickbeard.MY_FULLNAME)
sickbeard.PROG_DIR = os.path.dirname(sickbeard.MY_FULLNAME) sickbeard.PROG_DIR = os.path.dirname(sickbeard.MY_FULLNAME)
sickbeard.DATA_DIR = sickbeard.PROG_DIR sickbeard.DATA_DIR = sickbeard.PROG_DIR
sickbeard.MY_ARGS = sys.argv[1:] sickbeard.MY_ARGS = sys.argv[1:]
sickbeard.SYS_ENCODING = None if EXIT_BAD_ENCODING:
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('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) print('or find another way to force Python to use %s for string encoding.' % SYS_ENCODING)
sys.exit(1) sys.exit(1)
sickbeard.SYS_ENCODING = SYS_ENCODING
# Need console logging for sickgear.py and SickBeard-console.exe # 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')) self.console_logging = (not hasattr(sys, 'frozen')) or (0 < sickbeard.MY_NAME.lower().find('-console'))
@ -230,7 +214,7 @@ class SickGear(object):
try: try:
opts, args = getopt.getopt(sys.argv[1:], 'hfqdsp::', opts, args = getopt.getopt(sys.argv[1:], 'hfqdsp::',
['help', 'forceupdate', 'quiet', 'nolaunch', 'daemon', 'systemd', 'pidfile=', ['help', 'forceupdate', 'quiet', 'nolaunch', 'daemon', 'systemd', 'pidfile=',
'port=', 'datadir=', 'config=', 'noresize']) 'port=', 'datadir=', 'config=', 'noresize', 'update-restart'])
except getopt.GetoptError: except getopt.GetoptError:
sys.exit(self.help_message()) sys.exit(self.help_message())
@ -267,11 +251,11 @@ class SickGear(object):
self.console_logging = False self.console_logging = False
self.no_launch = True self.no_launch = True
if 'win32' == sys.platform: if is_win:
self.run_as_daemon = False self.run_as_daemon = False
# Run as a systemd service # 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_systemd = True
self.run_as_daemon = False self.run_as_daemon = False
self.console_logging = False self.console_logging = False
@ -429,6 +413,9 @@ class SickGear(object):
if sickbeard.LAUNCH_BROWSER and not self.no_launch: if sickbeard.LAUNCH_BROWSER and not self.no_launch:
sickbeard.launch_browser(self.start_port) 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 # check all db versions
for d, min_v, max_v, base_v, mo in [ for d, min_v, max_v, base_v, mo in [
('failed.db', sickbeard.failed_db.MIN_DB_VERSION, sickbeard.failed_db.MAX_DB_VERSION, ('failed.db', sickbeard.failed_db.MIN_DB_VERSION, sickbeard.failed_db.MAX_DB_VERSION,
@ -515,6 +502,14 @@ class SickGear(object):
else: else:
logger.log_error_and_exit(u'Restore FAILED!') logger.log_error_and_exit(u'Restore FAILED!')
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 # Build from the DB to start with
sickbeard.classes.loading_msg.message = 'Loading shows from db' sickbeard.classes.loading_msg.message = 'Loading shows from db'
self.load_shows_from_db() self.load_shows_from_db()
@ -524,10 +519,22 @@ class SickGear(object):
clean_ignore_require_words() clean_ignore_require_words()
db.DBConnection().set_flag('ignore_require_cleaned') db.DBConnection().set_flag('ignore_require_cleaned')
# Fire up all our threads # Fire up threads
sickbeard.classes.loading_msg.message = 'Starting threads' sickbeard.classes.loading_msg.message = 'Starting threads'
sickbeard.start() 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 # Build internal name cache
sickbeard.classes.loading_msg.message = 'Build name cache' sickbeard.classes.loading_msg.message = 'Build name cache'
name_cache.buildNameCache() name_cache.buildNameCache()
@ -538,7 +545,7 @@ class SickGear(object):
# load all ids from xem # load all ids from xem
sickbeard.classes.loading_msg.message = 'Loading xem data' 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() startup_background_tasks.start()
sickbeard.classes.loading_msg.message = 'Checking history' sickbeard.classes.loading_msg.message = 'Checking history'
@ -687,11 +694,9 @@ class SickGear(object):
if sickbeard.events.SystemEvent.RESTART == ev_type: if sickbeard.events.SystemEvent.RESTART == ev_type:
install_type = sickbeard.version_check_scheduler.action.install_type
popen_list = [] 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] popen_list = [sys.executable, sickbeard.MY_FULLNAME]
if popen_list: if popen_list:

Loading…
Cancel
Save