Browse Source

Black code-style everything

pull/1502/head
Safihre 5 years ago
parent
commit
9bcbcaefdf
  1. 22
      .github/workflows/black.yml
  2. 674
      SABnzbd.py
  3. 70
      po/main/SABnzbd.pot
  4. 1245
      sabnzbd/api.py
  5. 11
      sabnzbd/articlecache.py
  6. 147
      sabnzbd/assembler.py
  7. 132
      sabnzbd/bpsmeter.py
  8. 169
      sabnzbd/directunpacker.py
  9. 224
      sabnzbd/downloader.py
  10. 14
      sabnzbd/encoding.py
  11. 1870
      sabnzbd/interface.py
  12. 1127
      sabnzbd/newsunpack.py
  13. 104
      sabnzbd/newswrapper.py
  14. 172
      sabnzbd/nzbqueue.py
  15. 624
      sabnzbd/nzbstuff.py
  16. 229
      sabnzbd/osxmenu.py
  17. 131
      sabnzbd/panic.py
  18. 92
      sabnzbd/powersup.py
  19. 153
      sabnzbd/rating.py
  20. 90
      sabnzbd/sabtray.py
  21. 32
      sabnzbd/sabtraylinux.py
  22. 183
      sabnzbd/scheduler.py
  23. 1935
      sabnzbd/skintext.py
  24. 573
      sabnzbd/sorting.py
  25. 178
      sabnzbd/urlgrabber.py
  26. 38
      sabnzbd/zconfig.py

22
.github/workflows/black.yml

@ -10,29 +10,11 @@ jobs:
uses: lgeiger/black-action@v1.0.1
with:
args: >
SABnzbd.py
sabnzbd
scripts
tools
tests
sabnzbd/utils
sabnzbd/__init__.py
sabnzbd/cfg.py
sabnzbd/config.py
sabnzbd/emailer.py
sabnzbd/constants.py
sabnzbd/decorators.py
sabnzbd/decoder.py
sabnzbd/database.py
sabnzbd/getipaddress.py
sabnzbd/filesystem.py
sabnzbd/dirscanner.py
sabnzbd/postproc.py
sabnzbd/misc.py
sabnzbd/lang.py
sabnzbd/nzbparser.py
sabnzbd/notifier.py
sabnzbd/rss.py
sabnzbd/par2file.py
sabnzbd/version.py
--line-length=120
--target-version=py35
--check

674
SABnzbd.py

File diff suppressed because it is too large

70
po/main/SABnzbd.pot

@ -12,7 +12,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=ASCII\n"
"Content-Transfer-Encoding: 7bit\n"
"POT-Creation-Date: 2020-06-07 09:41+0200\n"
"POT-Creation-Date: 2020-06-11 10:12+0200\n"
"Generated-By: pygettext.py 1.5\n"
@ -24,11 +24,11 @@ msgstr ""
msgid "Cannot find web template: %s, trying standard template"
msgstr ""
#: SABnzbd.py [Error message]
#: SABnzbd.py
msgid "SABYenc disabled: no correct version found! (Found v%s, expecting v%s)"
msgstr ""
#: SABnzbd.py [Error message]
#: SABnzbd.py
msgid "SABYenc module... NOT found! Expecting v%s - https://sabnzbd.org/sabyenc"
msgstr ""
@ -36,15 +36,15 @@ msgstr ""
msgid "par2 binary... NOT found!"
msgstr ""
#: SABnzbd.py [Error message]
#: SABnzbd.py
msgid "MultiPar binary... NOT found!"
msgstr ""
#: SABnzbd.py [Error message]
#: SABnzbd.py
msgid "Verification and repair will not be possible."
msgstr ""
#: SABnzbd.py [Warning message]
#: SABnzbd.py
msgid "Your UNRAR version is %s, we recommend version %s or higher.<br />"
msgstr ""
@ -72,7 +72,7 @@ msgstr ""
msgid "HTTP and HTTPS ports cannot be the same"
msgstr ""
#: SABnzbd.py [Warning message]
#: SABnzbd.py
msgid "SABnzbd was started with encoding %s, this should be UTF-8. Expect problems with Unicoded file and directory names in downloads."
msgstr ""
@ -236,11 +236,11 @@ msgstr ""
msgid "Fatal error in Assembler"
msgstr ""
#: sabnzbd/assembler.py [Warning message]
#: sabnzbd/assembler.py
msgid "WARNING: Paused job \"%s\" because of encrypted RAR file (if supplied, all passwords were tried)"
msgstr ""
#: sabnzbd/assembler.py [Warning message]
#: sabnzbd/assembler.py
msgid "WARNING: Aborted job \"%s\" because of encrypted RAR file (if supplied, all passwords were tried)"
msgstr ""
@ -248,7 +248,7 @@ msgstr ""
msgid "Aborted, encryption detected"
msgstr ""
#: sabnzbd/assembler.py [Warning message]
#: sabnzbd/assembler.py
msgid "WARNING: In \"%s\" unwanted extension in RAR file. Unwanted file is %s "
msgstr ""
@ -260,11 +260,11 @@ msgstr ""
msgid "Aborted, unwanted extension detected"
msgstr ""
#: sabnzbd/assembler.py [Warning message]
#: sabnzbd/assembler.py
msgid "WARNING: Paused job \"%s\" because of rating (%s)"
msgstr ""
#: sabnzbd/assembler.py [Warning message]
#: sabnzbd/assembler.py
msgid "WARNING: Aborted job \"%s\" because of rating (%s)"
msgstr ""
@ -272,7 +272,7 @@ msgstr ""
msgid "Aborted, rating filter matched (%s)"
msgstr ""
#: sabnzbd/assembler.py [Warning message]
#: sabnzbd/assembler.py
msgid "Job \"%s\" is probably encrypted due to RAR with same name inside this RAR"
msgstr ""
@ -376,10 +376,6 @@ msgstr ""
msgid "UUencode detected, only yEnc encoding is supported [%s]"
msgstr ""
#: sabnzbd/decoder.py
msgid "Badly formed yEnc article in %s"
msgstr ""
#: sabnzbd/decoder.py [Warning message]
msgid "Unknown Error while decoding %s"
msgstr ""
@ -396,11 +392,11 @@ msgstr ""
msgid "Unpacked %s files/folders in %s"
msgstr ""
#: sabnzbd/directunpacker.py [Warning message]
#: sabnzbd/directunpacker.py
msgid "Direct Unpack was automatically enabled."
msgstr ""
#: sabnzbd/directunpacker.py [Warning message] # sabnzbd/skintext.py
#: sabnzbd/directunpacker.py # sabnzbd/skintext.py
msgid "Jobs will start unpacking during the downloading to reduce post-processing time. Only works for jobs that do not need repair."
msgstr ""
@ -444,7 +440,7 @@ msgstr ""
msgid "Server %s will be ignored for %s minutes"
msgstr ""
#: sabnzbd/downloader.py [Error message]
#: sabnzbd/downloader.py
msgid "Failed to initialize %s@%s with reason: %s"
msgstr ""
@ -460,7 +456,7 @@ msgstr ""
msgid "Failed login for server %s"
msgstr ""
#: sabnzbd/downloader.py [Error message]
#: sabnzbd/downloader.py
msgid "Connecting %s@%s failed, message=%s"
msgstr ""
@ -646,15 +642,15 @@ msgstr ""
msgid "Category folder cannot be a subfolder of the Temporary Download Folder."
msgstr ""
#: sabnzbd/interface.py
msgid "Back"
msgstr ""
#: sabnzbd/interface.py # sabnzbd/skintext.py
msgid "ERROR:"
msgstr ""
#: sabnzbd/interface.py
msgid "Back"
msgstr ""
#: sabnzbd/interface.py
msgid "Incorrect value for %s: %s"
msgstr ""
@ -1014,7 +1010,7 @@ msgstr ""
msgid "Incompatible queuefile found, cannot proceed"
msgstr ""
#: sabnzbd/nzbqueue.py [Error message]
#: sabnzbd/nzbqueue.py
msgid "Error loading %s, corrupt file detected"
msgstr ""
@ -1291,11 +1287,11 @@ msgid "Program did not start!"
msgstr ""
#: sabnzbd/panic.py
msgid "Unable to bind to port %s on %s. Some other software uses the port or SABnzbd is already running."
msgid "Fatal error"
msgstr ""
#: sabnzbd/panic.py
msgid "Fatal error"
msgid "Unable to bind to port %s on %s. Some other software uses the port or SABnzbd is already running."
msgstr ""
#: sabnzbd/panic.py [Warning message]
@ -2210,7 +2206,7 @@ msgstr ""
msgid "Purge Failed NZBs"
msgstr ""
#: sabnzbd/skintext.py [Button to delete all failed jobs in History, including files]
#: sabnzbd/skintext.py
msgid "Purge Failed NZBs & Delete Files"
msgstr ""
@ -2254,7 +2250,7 @@ msgstr ""
msgid "Force Disconnect"
msgstr ""
#: sabnzbd/skintext.py [Status page button text]
#: sabnzbd/skintext.py
msgid "Disconnect all active connections to usenet servers. Connections will be reopened after a few seconds if there are items in the queue."
msgstr ""
@ -3322,7 +3318,7 @@ msgstr ""
msgid "Optional"
msgstr ""
#: sabnzbd/skintext.py [Explain server optional tickbox]
#: sabnzbd/skintext.py
msgid "For unreliable servers, will be ignored longer in case of failures"
msgstr ""
@ -3606,7 +3602,7 @@ msgstr ""
msgid "Emergency retry"
msgstr ""
#: sabnzbd/skintext.py [Pushover settings]
#: sabnzbd/skintext.py
msgid "How often (in seconds) the same notification will be sent"
msgstr ""
@ -3614,7 +3610,7 @@ msgstr ""
msgid "Emergency expire"
msgstr ""
#: sabnzbd/skintext.py [Pushover settings]
#: sabnzbd/skintext.py
msgid "How many seconds your notification will continue to be retried"
msgstr ""
@ -4110,7 +4106,7 @@ msgstr ""
msgid "Update Available!"
msgstr ""
#: sabnzbd/skintext.py [Don't translate LocalStorage]
#: sabnzbd/skintext.py
msgid "LocalStorage (cookies) are disabled in your browser, interface settings will be lost after you close the browser!"
msgstr ""
@ -4386,11 +4382,11 @@ msgstr ""
msgid "Closing any browser windows/tabs will NOT close SABnzbd."
msgstr ""
#: sabnzbd/skintext.py [Wizard tip]
#: sabnzbd/skintext.py
msgid "It is recommended you right click and bookmark this location and use this bookmark to access SABnzbd when it is running in the background."
msgstr ""
#: sabnzbd/skintext.py [Will be appended with a wiki-link, adjust word order accordingly]
#: sabnzbd/skintext.py
msgid "Further help can be found on our"
msgstr ""
@ -4426,7 +4422,7 @@ msgstr ""
msgid "Error getting TV info (%s)"
msgstr ""
#: sabnzbd/sorting.py [Error message]
#: sabnzbd/sorting.py [Error message] # sabnzbd/sorting.py
msgid "Failed to rename: %s to %s"
msgstr ""

1245
sabnzbd/api.py

File diff suppressed because it is too large

11
sabnzbd/articlecache.py

@ -39,8 +39,8 @@ class ArticleCache:
self.__cache_limit_org = 0
self.__cache_limit = 0
self.__cache_size = 0
self.__article_list = [] # List of buffered articles
self.__article_table = {} # Dict of buffered articles
self.__article_list = [] # List of buffered articles
self.__article_table = {} # Dict of buffered articles
# Limit for the decoder is based on the total available cache
# so it can be larger on memory-rich systems
@ -50,7 +50,7 @@ class ArticleCache:
# For 64 bit we allow up to 4GB, in case somebody wants that
self.__cache_upper_limit = GIGI
if sabnzbd.DARWIN or sabnzbd.WIN64 or (struct.calcsize("P") * 8) == 64:
self.__cache_upper_limit = 4*GIGI
self.__cache_upper_limit = 4 * GIGI
ArticleCache.do = self
@ -67,7 +67,7 @@ class ArticleCache:
# The decoder-limit should not be larger than 1/3th of the whole cache
# Calculated in number of articles, assuming 1 article = 1MB max
decoder_cache_limit = int(min(self.__cache_limit/3/MEBI, LIMIT_DECODE_QUEUE))
decoder_cache_limit = int(min(self.__cache_limit / 3 / MEBI, LIMIT_DECODE_QUEUE))
# The cache should also not be too small
self.decoder_cache_article_limit = max(decoder_cache_limit, MIN_DECODE_QUEUE)
@ -128,8 +128,7 @@ class ArticleCache:
self.__article_list.remove(article)
self.free_reserved_space(len(data))
elif article.art_id:
data = sabnzbd.load_data(article.art_id, nzo.workpath, remove=True,
do_pickle=False, silent=True)
data = sabnzbd.load_data(article.art_id, nzo.workpath, remove=True, do_pickle=False, silent=True)
nzo.remove_saved_article(article)
return data

147
sabnzbd/assembler.py

@ -29,8 +29,7 @@ import hashlib
import sabnzbd
from sabnzbd.misc import get_all_passwords
from sabnzbd.filesystem import set_permissions, clip_path, has_win_device, \
diskspace, get_filename, get_ext
from sabnzbd.filesystem import set_permissions, clip_path, has_win_device, diskspace, get_filename, get_ext
from sabnzbd.constants import Status, GIGI, MAX_ASSEMBLER_QUEUE
import sabnzbd.cfg as cfg
from sabnzbd.articlecache import ArticleCache
@ -70,10 +69,13 @@ class Assembler(Thread):
if nzf:
# Check if enough disk space is free after each file is done
# If not enough space left, pause downloader and send email
if file_done and diskspace(force=True)['download_dir'][1] < (cfg.download_free.get_float() + nzf.bytes) / GIGI:
if (
file_done
and diskspace(force=True)["download_dir"][1] < (cfg.download_free.get_float() + nzf.bytes) / GIGI
):
# Only warn and email once
if not sabnzbd.downloader.Downloader.do.paused:
logging.warning(T('Too little diskspace forcing PAUSE'))
logging.warning(T("Too little diskspace forcing PAUSE"))
# Pause downloader, but don't save, since the disk is almost full!
sabnzbd.downloader.Downloader.do.pause()
sabnzbd.emailer.diskfull_mail()
@ -84,7 +86,7 @@ class Assembler(Thread):
filepath = nzf.prepare_filepath()
if filepath:
logging.debug('Decoding part of %s', filepath)
logging.debug("Decoding part of %s", filepath)
try:
self.assemble(nzf, file_done)
except IOError as err:
@ -92,16 +94,16 @@ class Assembler(Thread):
if not nzo.deleted and not nzo.is_gone() and not nzo.pp_active:
# 28 == disk full => pause downloader
if err.errno == 28:
logging.error(T('Disk full! Forcing Pause'))
logging.error(T("Disk full! Forcing Pause"))
else:
logging.error(T('Disk error on creating file %s'), clip_path(filepath))
logging.error(T("Disk error on creating file %s"), clip_path(filepath))
# Log traceback
logging.info('Traceback: ', exc_info=True)
logging.info("Traceback: ", exc_info=True)
# Pause without saving
sabnzbd.downloader.Downloader.do.pause()
continue
except:
logging.error(T('Fatal error in Assembler'), exc_info=True)
logging.error(T("Fatal error in Assembler"), exc_info=True)
break
# Continue after partly written data
@ -109,7 +111,7 @@ class Assembler(Thread):
continue
# Clean-up admin data
logging.info('Decoding finished %s', filepath)
logging.info("Decoding finished %s", filepath)
nzf.remove_admin()
# Do rar-related processing
@ -118,23 +120,43 @@ class Assembler(Thread):
rar_encrypted, unwanted_file = check_encrypted_and_unwanted_files(nzo, filepath)
if rar_encrypted:
if cfg.pause_on_pwrar() == 1:
logging.warning(remove_warning_label(T('WARNING: Paused job "%s" because of encrypted RAR file (if supplied, all passwords were tried)')), nzo.final_name)
logging.warning(
remove_warning_label(
T(
'WARNING: Paused job "%s" because of encrypted RAR file (if supplied, all passwords were tried)'
)
),
nzo.final_name,
)
nzo.pause()
else:
logging.warning(remove_warning_label(T('WARNING: Aborted job "%s" because of encrypted RAR file (if supplied, all passwords were tried)')), nzo.final_name)
nzo.fail_msg = T('Aborted, encryption detected')
logging.warning(
remove_warning_label(
T(
'WARNING: Aborted job "%s" because of encrypted RAR file (if supplied, all passwords were tried)'
)
),
nzo.final_name,
)
nzo.fail_msg = T("Aborted, encryption detected")
sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo)
if unwanted_file:
logging.warning(remove_warning_label(T('WARNING: In "%s" unwanted extension in RAR file. Unwanted file is %s ')), nzo.final_name, unwanted_file)
logging.debug(T('Unwanted extension is in rar file %s'), filepath)
logging.warning(
remove_warning_label(
T('WARNING: In "%s" unwanted extension in RAR file. Unwanted file is %s ')
),
nzo.final_name,
unwanted_file,
)
logging.debug(T("Unwanted extension is in rar file %s"), filepath)
if cfg.action_on_unwanted_extensions() == 1 and nzo.unwanted_ext == 0:
logging.debug('Unwanted extension ... pausing')
logging.debug("Unwanted extension ... pausing")
nzo.unwanted_ext = 1
nzo.pause()
if cfg.action_on_unwanted_extensions() == 2:
logging.debug('Unwanted extension ... aborting')
nzo.fail_msg = T('Aborted, unwanted extension detected')
logging.debug("Unwanted extension ... aborting")
nzo.fail_msg = T("Aborted, unwanted extension detected")
sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo)
# Add to direct unpack
@ -146,11 +168,19 @@ class Assembler(Thread):
filter_output, reason = nzo_filtered_by_rating(nzo)
if filter_output == 1:
logging.warning(remove_warning_label(T('WARNING: Paused job "%s" because of rating (%s)')), nzo.final_name, reason)
logging.warning(
remove_warning_label(T('WARNING: Paused job "%s" because of rating (%s)')),
nzo.final_name,
reason,
)
nzo.pause()
elif filter_output == 2:
logging.warning(remove_warning_label(T('WARNING: Aborted job "%s" because of rating (%s)')), nzo.final_name, reason)
nzo.fail_msg = T('Aborted, rating filter matched (%s)') % reason
logging.warning(
remove_warning_label(T('WARNING: Aborted job "%s" because of rating (%s)')),
nzo.final_name,
reason,
)
nzo.fail_msg = T("Aborted, rating filter matched (%s)") % reason
sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo)
else:
@ -166,7 +196,7 @@ class Assembler(Thread):
if not nzf.md5:
nzf.md5 = hashlib.md5()
with open(nzf.filepath, 'ab') as fout:
with open(nzf.filepath, "ab") as fout:
for article in nzf.decodetable:
# Break if deleted during writing
if nzf.nzo.status is Status.DELETED:
@ -214,21 +244,31 @@ def file_has_articles(nzf):
return has
RE_SUBS = re.compile(r'\W+sub|subs|subpack|subtitle|subtitles(?![a-z])', re.I)
SAFE_EXTS = ('.mkv', '.mp4', '.avi', '.wmv', '.mpg', '.webm')
RE_SUBS = re.compile(r"\W+sub|subs|subpack|subtitle|subtitles(?![a-z])", re.I)
SAFE_EXTS = (".mkv", ".mp4", ".avi", ".wmv", ".mpg", ".webm")
def is_cloaked(nzo, path, names):
""" Return True if this is likely to be a cloaked encrypted post """
fname = os.path.splitext(get_filename(path.lower()))[0]
for name in names:
name = get_filename(name.lower())
name, ext = os.path.splitext(name)
if ext == '.rar' and fname.startswith(name) and (len(fname) - len(name)) < 8 and len(names) < 3 and not RE_SUBS.search(fname):
if (
ext == ".rar"
and fname.startswith(name)
and (len(fname) - len(name)) < 8
and len(names) < 3
and not RE_SUBS.search(fname)
):
# Only warn once
if nzo.encrypted == 0:
logging.warning(T('Job "%s" is probably encrypted due to RAR with same name inside this RAR'), nzo.final_name)
logging.warning(
T('Job "%s" is probably encrypted due to RAR with same name inside this RAR'), nzo.final_name
)
nzo.encrypted = 1
return True
elif 'password' in name and ext not in SAFE_EXTS:
elif "password" in name and ext not in SAFE_EXTS:
# Only warn once
if nzo.encrypted == 0:
logging.warning(T('Job "%s" is probably encrypted: "password" in filename "%s"'), nzo.final_name, name)
@ -242,7 +282,9 @@ def check_encrypted_and_unwanted_files(nzo, filepath):
encrypted = False
unwanted = None
if (cfg.unwanted_extensions() and cfg.action_on_unwanted_extensions()) or (nzo.encrypted == 0 and cfg.pause_on_pwrar()):
if (cfg.unwanted_extensions() and cfg.action_on_unwanted_extensions()) or (
nzo.encrypted == 0 and cfg.pause_on_pwrar()
):
# These checks should not break the assembler
try:
# Rarfile freezes on Windows special names, so don't try those!
@ -256,7 +298,11 @@ def check_encrypted_and_unwanted_files(nzo, filepath):
zf = rarfile.RarFile(filepath, single_file_check=True)
# Check for encryption
if nzo.encrypted == 0 and cfg.pause_on_pwrar() and (zf.needs_password() or is_cloaked(nzo, filepath, zf.namelist())):
if (
nzo.encrypted == 0
and cfg.pause_on_pwrar()
and (zf.needs_password() or is_cloaked(nzo, filepath, zf.namelist()))
):
# Load all passwords
passwords = get_all_passwords(nzo)
@ -290,7 +336,7 @@ def check_encrypted_and_unwanted_files(nzo, filepath):
break
except Exception as e:
# Did we start from the right volume?
if 'need to start extraction from a previous volume' in str(e):
if "need to start extraction from a previous volume" in str(e):
return encrypted, unwanted
# This one failed
pass
@ -312,15 +358,15 @@ def check_encrypted_and_unwanted_files(nzo, filepath):
# Check for unwanted extensions
if cfg.unwanted_extensions() and cfg.action_on_unwanted_extensions():
for somefile in zf.namelist():
logging.debug('File contains: %s', somefile)
if get_ext(somefile).replace('.', '').lower() in cfg.unwanted_extensions():
logging.debug('Unwanted file %s', somefile)
logging.debug("File contains: %s", somefile)
if get_ext(somefile).replace(".", "").lower() in cfg.unwanted_extensions():
logging.debug("Unwanted file %s", somefile)
unwanted = somefile
zf.close()
del zf
except:
logging.info('Error during inspection of RAR-file %s', filepath)
logging.debug('Traceback: ', exc_info=True)
logging.info("Error during inspection of RAR-file %s", filepath)
logging.debug("Traceback: ", exc_info=True)
return encrypted, unwanted
@ -343,32 +389,39 @@ def rating_filtered(rating, filename, abort):
def check_keyword(keyword):
clean_keyword = keyword.strip().lower()
return (len(clean_keyword) > 0) and (clean_keyword in filename)
audio = cfg.rating_filter_abort_audio() if abort else cfg.rating_filter_pause_audio()
video = cfg.rating_filter_abort_video() if abort else cfg.rating_filter_pause_video()
spam = cfg.rating_filter_abort_spam() if abort else cfg.rating_filter_pause_spam()
spam_confirm = cfg.rating_filter_abort_spam_confirm() if abort else cfg.rating_filter_pause_spam_confirm()
encrypted = cfg.rating_filter_abort_encrypted() if abort else cfg.rating_filter_pause_encrypted()
encrypted_confirm = cfg.rating_filter_abort_encrypted_confirm() if abort else cfg.rating_filter_pause_encrypted_confirm()
encrypted_confirm = (
cfg.rating_filter_abort_encrypted_confirm() if abort else cfg.rating_filter_pause_encrypted_confirm()
)
downvoted = cfg.rating_filter_abort_downvoted() if abort else cfg.rating_filter_pause_downvoted()
keywords = cfg.rating_filter_abort_keywords() if abort else cfg.rating_filter_pause_keywords()
if (video > 0) and (rating.avg_video > 0) and (rating.avg_video <= video):
return T('video')
return T("video")
if (audio > 0) and (rating.avg_audio > 0) and (rating.avg_audio <= audio):
return T('audio')
if (spam and ((rating.avg_spam_cnt > 0) or rating.avg_encrypted_confirm)) or (spam_confirm and rating.avg_spam_confirm):
return T('spam')
if (encrypted and ((rating.avg_encrypted_cnt > 0) or rating.avg_encrypted_confirm)) or (encrypted_confirm and rating.avg_encrypted_confirm):
return T('passworded')
return T("audio")
if (spam and ((rating.avg_spam_cnt > 0) or rating.avg_encrypted_confirm)) or (
spam_confirm and rating.avg_spam_confirm
):
return T("spam")
if (encrypted and ((rating.avg_encrypted_cnt > 0) or rating.avg_encrypted_confirm)) or (
encrypted_confirm and rating.avg_encrypted_confirm
):
return T("passworded")
if downvoted and (rating.avg_vote_up < rating.avg_vote_down):
return T('downvoted')
if any(check_keyword(k) for k in keywords.split(',')):
return T('keywords')
return T("downvoted")
if any(check_keyword(k) for k in keywords.split(",")):
return T("keywords")
return None
def remove_warning_label(msg):
""" Standardize errors by removing obsolete
"WARNING:" part in all languages """
if ':' in msg:
return msg.split(':')[1].strip()
if ":" in msg:
return msg.split(":")[1].strip()
return msg

132
sabnzbd/bpsmeter.py

@ -62,6 +62,8 @@ def this_month(t):
_DAYS = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
def last_month_day(tm):
""" Return last day of this month """
year, month = tm[:2]
@ -105,31 +107,40 @@ class BPSMeter:
self.timeline_total = {}
self.day_label = time.strftime("%Y-%m-%d")
self.end_of_day = tomorrow(t) # Time that current day will end
self.end_of_week = next_week(t) # Time that current day will end
self.end_of_day = tomorrow(t) # Time that current day will end
self.end_of_week = next_week(t) # Time that current day will end
self.end_of_month = next_month(t) # Time that current month will end
self.q_day = 1 # Day of quota reset
self.q_period = 'm' # Daily/Weekly/Monthly quota = d/w/m
self.quota = self.left = 0.0 # Quota and remaining quota
self.have_quota = False # Flag for quota active
self.q_time = 0 # Next reset time for quota
self.q_hour = 0 # Quota reset hour
self.q_minute = 0 # Quota reset minute
self.quota_enabled = True # Scheduled quota enable/disable
self.q_day = 1 # Day of quota reset
self.q_period = "m" # Daily/Weekly/Monthly quota = d/w/m
self.quota = self.left = 0.0 # Quota and remaining quota
self.have_quota = False # Flag for quota active
self.q_time = 0 # Next reset time for quota
self.q_hour = 0 # Quota reset hour
self.q_minute = 0 # Quota reset minute
self.quota_enabled = True # Scheduled quota enable/disable
BPSMeter.do = self
def save(self):
""" Save admin to disk """
data = (self.last_update, self.grand_total,
self.day_total, self.week_total, self.month_total,
self.end_of_day, self.end_of_week, self.end_of_month,
self.quota, self.left, self.q_time, self.timeline_total
)
data = (
self.last_update,
self.grand_total,
self.day_total,
self.week_total,
self.month_total,
self.end_of_day,
self.end_of_week,
self.end_of_month,
self.quota,
self.left,
self.q_time,
self.timeline_total,
)
sabnzbd.save_admin(data, BYTES_FILE_NAME)
def defaults(self):
""" Get the latest data from the database and assign to a fake server """
logging.debug('Setting default BPS meter values')
logging.debug("Setting default BPS meter values")
history_db = sabnzbd.database.HistoryDB()
grand, month, week = history_db.get_history_size()
history_db.close()
@ -138,11 +149,11 @@ class BPSMeter:
self.week_total = {}
self.day_total = {}
if grand:
self.grand_total['x'] = grand
self.grand_total["x"] = grand
if month:
self.month_total['x'] = month
self.month_total["x"] = month
if week:
self.week_total['x'] = week
self.week_total["x"] = week
self.quota = self.left = cfg.quota_size.get_float()
def read(self):
@ -152,10 +163,20 @@ class BPSMeter:
self.have_quota = bool(cfg.quota_size())
data = sabnzbd.load_admin(BYTES_FILE_NAME)
try:
self.last_update, self.grand_total, \
self.day_total, self.week_total, self.month_total, \
self.end_of_day, self.end_of_week, self.end_of_month, \
self.quota, self.left, self.q_time, self.timeline_total = data
(
self.last_update,
self.grand_total,
self.day_total,
self.week_total,
self.month_total,
self.end_of_day,
self.end_of_week,
self.end_of_month,
self.quota,
self.left,
self.q_time,
self.timeline_total,
) = data
if abs(quota - self.quota) > 0.5:
self.change_quota()
res = self.reset_quota()
@ -210,7 +231,7 @@ class BPSMeter:
if server not in self.timeline_total:
self.timeline_total[server] = {}
if self.day_label not in self.timeline_total[server]:
self.timeline_total[server][self.day_label]= 0
self.timeline_total[server][self.day_label] = 0
self.timeline_total[server][self.day_label] += amount
# Quota check
@ -219,7 +240,7 @@ class BPSMeter:
if self.left <= 0.0:
if sabnzbd.downloader.Downloader.do and not sabnzbd.downloader.Downloader.do.paused:
sabnzbd.downloader.Downloader.do.pause()
logging.warning(T('Quota spent, pausing downloading'))
logging.warning(T("Quota spent, pausing downloading"))
# Speedometer
try:
@ -261,22 +282,26 @@ class BPSMeter:
# Always trim the list to the max-length
if len(self.bps_list) > self.bps_list_max:
self.bps_list = self.bps_list[len(self.bps_list) - self.bps_list_max:]
self.bps_list = self.bps_list[len(self.bps_list) - self.bps_list_max :]
def get_sums(self):
""" return tuple of grand, month, week, day totals """
return (sum([v for v in self.grand_total.values()]),
sum([v for v in self.month_total.values()]),
sum([v for v in self.week_total.values()]),
sum([v for v in self.day_total.values()]))
return (
sum([v for v in self.grand_total.values()]),
sum([v for v in self.month_total.values()]),
sum([v for v in self.week_total.values()]),
sum([v for v in self.day_total.values()]),
)
def amounts(self, server):
""" Return grand, month, week, day totals for specified server """
return self.grand_total.get(server, 0), \
self.month_total.get(server, 0), \
self.week_total.get(server, 0), \
self.day_total.get(server, 0), \
self.timeline_total.get(server, {})
return (
self.grand_total.get(server, 0),
self.month_total.get(server, 0),
self.week_total.get(server, 0),
self.day_total.get(server, 0),
self.timeline_total.get(server, {}),
)
def clear_server(self, server):
""" Clean counters for specified server """
@ -330,9 +355,9 @@ class BPSMeter:
"""
if force or (self.have_quota and time.time() > (self.q_time - 50)):
self.quota = self.left = cfg.quota_size.get_float()
logging.info('Quota was reset to %s', self.quota)
logging.info("Quota was reset to %s", self.quota)
if cfg.quota_resume():
logging.info('Auto-resume due to quota reset')
logging.info("Auto-resume due to quota reset")
if sabnzbd.downloader.Downloader.do:
sabnzbd.downloader.Downloader.do.resume()
self.next_reset()
@ -344,20 +369,24 @@ class BPSMeter:
""" Determine next reset time """
t = t or time.time()
tm = time.localtime(t)
if self.q_period == 'd':
if self.q_period == "d":
nx = (tm[0], tm[1], tm[2], self.q_hour, self.q_minute, 0, 0, 0, tm[8])
if (tm.tm_hour * 60 + tm.tm_min) >= (self.q_hour * 60 + self.q_minute):
# If today's moment has passed, it will happen tomorrow
t = time.mktime(nx) + 24 * 3600
tm = time.localtime(t)
elif self.q_period == 'w':
if self.q_day < tm.tm_wday + 1 or (self.q_day == tm.tm_wday + 1 and (tm.tm_hour * 60 + tm.tm_min) >= (self.q_hour * 60 + self.q_minute)):
elif self.q_period == "w":
if self.q_day < tm.tm_wday + 1 or (
self.q_day == tm.tm_wday + 1 and (tm.tm_hour * 60 + tm.tm_min) >= (self.q_hour * 60 + self.q_minute)
):
tm = time.localtime(next_week(t))
dif = abs(self.q_day - tm.tm_wday - 1)
t = time.mktime(tm) + dif * 24 * 3600
tm = time.localtime(t)
elif self.q_period == 'm':
if self.q_day < tm.tm_mday or (self.q_day == tm.tm_mday and (tm.tm_hour * 60 + tm.tm_min) >= (self.q_hour * 60 + self.q_minute)):
elif self.q_period == "m":
if self.q_day < tm.tm_mday or (
self.q_day == tm.tm_mday and (tm.tm_hour * 60 + tm.tm_min) >= (self.q_hour * 60 + self.q_minute)
):
tm = time.localtime(next_month(t))
day = min(last_month_day(tm), self.q_day)
tm = (tm[0], tm[1], day, self.q_hour, self.q_minute, 0, 0, 0, tm[8])
@ -365,7 +394,7 @@ class BPSMeter:
return
tm = (tm[0], tm[1], tm[2], self.q_hour, self.q_minute, 0, 0, 0, tm[8])
self.q_time = time.mktime(tm)
logging.debug('Will reset quota at %s', tm)
logging.debug("Will reset quota at %s", tm)
def change_quota(self, allow_resume=True):
""" Update quota, potentially pausing downloader """
@ -373,11 +402,11 @@ class BPSMeter:
# Never set, use last period's size
per = cfg.quota_period()
sums = self.get_sums()
if per == 'd':
if per == "d":
self.left = sums[3]
elif per == 'w':
elif per == "w":
self.left = sums[2]
elif per == 'm':
elif per == "m":
self.left = sums[1]
self.have_quota = bool(cfg.quota_size())
@ -399,8 +428,9 @@ class BPSMeter:
# Pattern = <day#> <hh:mm>
# The <day> and <hh:mm> part can both be optional
__re_day = re.compile(r'^\s*(\d+)[^:]*')
__re_hm = re.compile(r'(\d+):(\d+)\s*$')
__re_day = re.compile(r"^\s*(\d+)[^:]*")
__re_hm = re.compile(r"(\d+):(\d+)\s*$")
def get_quota(self):
""" If quota active, return check-function, hour, minute """
if self.have_quota:
@ -415,10 +445,10 @@ class BPSMeter:
if m:
self.q_hour = int(m.group(1))
self.q_minute = int(m.group(2))
if self.q_period == 'w':
if self.q_period == "w":
self.q_day = max(1, self.q_day)
self.q_day = min(7, self.q_day)
elif self.q_period == 'm':
elif self.q_period == "m":
self.q_day = max(1, self.q_day)
self.q_day = min(31, self.q_day)
else:
@ -447,7 +477,7 @@ class BPSMeter:
def quota_handler():
""" To be called from scheduler """
logging.debug('Checking quota')
logging.debug("Checking quota")
BPSMeter.do.reset_quota()

169
sabnzbd/directunpacker.py

@ -44,11 +44,10 @@ START_STOP_LOCK = threading.RLock()
ACTIVE_UNPACKERS = []
RAR_NR = re.compile(r'(.*?)(\.part(\d*).rar|\.r(\d*))$', re.IGNORECASE)
RAR_NR = re.compile(r"(.*?)(\.part(\d*).rar|\.r(\d*))$", re.IGNORECASE)
class DirectUnpacker(threading.Thread):
def __init__(self, nzo):
threading.Thread.__init__(self)
@ -94,7 +93,13 @@ class DirectUnpacker(threading.Thread):
self.rarfile_nzf = None
def check_requirements(self):
if not cfg.direct_unpack() or self.killed or not self.nzo.unpack or self.nzo.bad_articles or sabnzbd.newsunpack.RAR_PROBLEM:
if (
not cfg.direct_unpack()
or self.killed
or not self.nzo.unpack
or self.nzo.bad_articles
or sabnzbd.newsunpack.RAR_PROBLEM
):
return False
return True
@ -137,12 +142,12 @@ class DirectUnpacker(threading.Thread):
# Are we doing this set?
if self.cur_setname and self.cur_setname == nzf.setname:
logging.debug('DirectUnpack queued %s for %s', nzf.filename, self.cur_setname)
logging.debug("DirectUnpack queued %s for %s", nzf.filename, self.cur_setname)
# Is this the first one of the first set?
if not self.active_instance and not self.is_alive() and self.have_next_volume():
# Too many runners already?
if len(ACTIVE_UNPACKERS) >= cfg.direct_unpack_threads():
logging.info('Too many DirectUnpackers currently to start %s', self.cur_setname)
logging.info("Too many DirectUnpackers currently to start %s", self.cur_setname)
return
# Start the unrar command and the loop
@ -158,8 +163,8 @@ class DirectUnpacker(threading.Thread):
def run(self):
# Input and output
linebuf = ''
last_volume_linebuf = ''
linebuf = ""
last_volume_linebuf = ""
unrar_log = []
rarfiles = []
extracted = []
@ -179,17 +184,30 @@ class DirectUnpacker(threading.Thread):
linebuf += char
# Error? Let PP-handle it
if linebuf.endswith(('ERROR: ', 'Cannot create', 'in the encrypted file', 'CRC failed', 'checksum failed',
'You need to start extraction from a previous volume', 'password is incorrect',
'Incorrect password', 'Write error', 'checksum error', 'Cannot open',
'start extraction from a previous volume', 'Unexpected end of archive')):
logging.info('Error in DirectUnpack of %s: %s', self.cur_setname, linebuf.strip())
if linebuf.endswith(
(
"ERROR: ",
"Cannot create",
"in the encrypted file",
"CRC failed",
"checksum failed",
"You need to start extraction from a previous volume",
"password is incorrect",
"Incorrect password",
"Write error",
"checksum error",
"Cannot open",
"start extraction from a previous volume",
"Unexpected end of archive",
)
):
logging.info("Error in DirectUnpack of %s: %s", self.cur_setname, linebuf.strip())
self.abort()
if linebuf.endswith('\n'):
if linebuf.endswith("\n"):
# List files we used
if linebuf.startswith('Extracting from'):
filename = (re.search(EXTRACTFROM_RE, linebuf.strip()).group(1))
if linebuf.startswith("Extracting from"):
filename = re.search(EXTRACTFROM_RE, linebuf.strip()).group(1)
if filename not in rarfiles:
rarfiles.append(filename)
@ -203,27 +221,30 @@ class DirectUnpacker(threading.Thread):
extracted.append(real_path(self.unpack_dir_info[0], unpacked_file))
# Did we reach the end?
if linebuf.endswith('All OK'):
if linebuf.endswith("All OK"):
# Stop timer and finish
self.unpack_time += time.time() - start_time
ACTIVE_UNPACKERS.remove(self)
# Add to success
rarfile_path = os.path.join(self.nzo.downpath, self.rarfile_nzf.filename)
self.success_sets[self.cur_setname] = (rar_volumelist(rarfile_path, self.nzo.password, rarfiles), extracted)
logging.info('DirectUnpack completed for %s', self.cur_setname)
self.nzo.set_action_line(T('Direct Unpack'), T('Completed'))
self.success_sets[self.cur_setname] = (
rar_volumelist(rarfile_path, self.nzo.password, rarfiles),
extracted,
)
logging.info("DirectUnpack completed for %s", self.cur_setname)
self.nzo.set_action_line(T("Direct Unpack"), T("Completed"))
# List success in history-info
msg = T('Unpacked %s files/folders in %s') % (len(extracted), format_time_string(self.unpack_time))
msg = '%s - %s' % (T('Direct Unpack'), msg)
self.nzo.set_unpack_info('Unpack', msg, self.cur_setname)
msg = T("Unpacked %s files/folders in %s") % (len(extracted), format_time_string(self.unpack_time))
msg = "%s - %s" % (T("Direct Unpack"), msg)
self.nzo.set_unpack_info("Unpack", msg, self.cur_setname)
# Write current log and clear
unrar_log.append(linebuf.strip())
linebuf = ''
last_volume_linebuf = ''
logging.debug('DirectUnpack Unrar output %s', '\n'.join(unrar_log))
linebuf = ""
last_volume_linebuf = ""
logging.debug("DirectUnpack Unrar output %s", "\n".join(unrar_log))
unrar_log = []
rarfiles = []
extracted = []
@ -247,7 +268,7 @@ class DirectUnpacker(threading.Thread):
self.killed = True
break
if linebuf.endswith('[C]ontinue, [Q]uit '):
if linebuf.endswith("[C]ontinue, [Q]uit "):
# Stop timer
self.unpack_time += time.time() - start_time
@ -259,7 +280,7 @@ class DirectUnpacker(threading.Thread):
# If unrar stopped or is killed somehow, writing will cause a crash
try:
# Give unrar some time to do it's thing
self.active_instance.stdin.write(b'C\n')
self.active_instance.stdin.write(b"C\n")
start_time = time.time()
time.sleep(0.1)
except IOError:
@ -270,14 +291,14 @@ class DirectUnpacker(threading.Thread):
if not last_volume_linebuf or last_volume_linebuf != linebuf:
# Next volume
self.cur_volume += 1
self.nzo.set_action_line(T('Direct Unpack'), self.get_formatted_stats())
logging.info('DirectUnpacked volume %s for %s', self.cur_volume, self.cur_setname)
self.nzo.set_action_line(T("Direct Unpack"), self.get_formatted_stats())
logging.info("DirectUnpacked volume %s for %s", self.cur_volume, self.cur_setname)
# If lines did not change and we don't have the next volume, this download is missing files!
# In rare occasions we can get stuck forever with repeating lines
if last_volume_linebuf == linebuf:
if not self.have_next_volume() or self.duplicate_lines > 10:
logging.info('DirectUnpack failed due to missing files %s', self.cur_setname)
logging.info("DirectUnpack failed due to missing files %s", self.cur_setname)
self.abort()
else:
logging.debug('Duplicate output line detected: "%s"', last_volume_linebuf)
@ -287,13 +308,13 @@ class DirectUnpacker(threading.Thread):
last_volume_linebuf = linebuf
# Show the log
if linebuf.endswith('\n'):
if linebuf.endswith("\n"):
unrar_log.append(linebuf.strip())
linebuf = ''
linebuf = ""
# Add last line
unrar_log.append(linebuf.strip())
logging.debug('DirectUnpack Unrar output %s', '\n'.join(unrar_log))
logging.debug("DirectUnpack Unrar output %s", "\n".join(unrar_log))
# Make more space
self.reset_active()
@ -309,7 +330,7 @@ class DirectUnpacker(threading.Thread):
Make sure that files are 100% written to disk by checking md5sum
"""
for nzf_search in reversed(self.nzo.finished_files):
if nzf_search.setname == self.cur_setname and nzf_search.vol == (self.cur_volume+1) and nzf_search.md5sum:
if nzf_search.setname == self.cur_setname and nzf_search.vol == (self.cur_volume + 1) and nzf_search.md5sum:
return nzf_search
return False
@ -338,14 +359,14 @@ class DirectUnpacker(threading.Thread):
# Set options
if self.nzo.password:
password_command = '-p%s' % self.nzo.password
password_command = "-p%s" % self.nzo.password
else:
password_command = '-p-'
password_command = "-p-"
if one_folder or cfg.flat_unpack():
action = 'e'
action = "e"
else:
action = 'x'
action = "x"
# The first NZF
self.rarfile_nzf = self.have_next_volume()
@ -360,36 +381,60 @@ class DirectUnpacker(threading.Thread):
if sabnzbd.WIN32:
# For Unrar to support long-path, we need to cricumvent Python's list2cmdline
# See: https://github.com/sabnzbd/sabnzbd/issues/1043
command = ['%s' % sabnzbd.newsunpack.RAR_COMMAND, action, '-vp', '-idp', '-o+', '-ai', password_command,
'%s' % clip_path(rarfile_path), '%s\\' % long_path(extraction_path)]
command = [
"%s" % sabnzbd.newsunpack.RAR_COMMAND,
action,
"-vp",
"-idp",
"-o+",
"-ai",
password_command,
"%s" % clip_path(rarfile_path),
"%s\\" % long_path(extraction_path),
]
else:
# Don't use "-ai" (not needed for non-Windows)
command = ['%s' % sabnzbd.newsunpack.RAR_COMMAND, action, '-vp', '-idp', '-o+', password_command,
'%s' % rarfile_path, '%s/' % extraction_path]
command = [
"%s" % sabnzbd.newsunpack.RAR_COMMAND,
action,
"-vp",
"-idp",
"-o+",
password_command,
"%s" % rarfile_path,
"%s/" % extraction_path,
]
if cfg.ignore_unrar_dates():
command.insert(3, '-tsm-')
command.insert(3, "-tsm-")
# Let's start from the first one!
self.cur_volume = 1
stup, need_shell, command, creationflags = build_command(command, flatten_command=True)
logging.debug('Running unrar for DirectUnpack %s', command)
logging.debug("Running unrar for DirectUnpack %s", command)
# Need to disable buffer to have direct feedback
self.active_instance = Popen(command, shell=False, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
startupinfo=stup, creationflags=creationflags, bufsize=0)
self.active_instance = Popen(
command,
shell=False,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
startupinfo=stup,
creationflags=creationflags,
bufsize=0,
)
# Add to runners
ACTIVE_UNPACKERS.append(self)
# Doing the first
logging.info('DirectUnpacked volume %s for %s', self.cur_volume, self.cur_setname)
logging.info("DirectUnpacked volume %s for %s", self.cur_volume, self.cur_setname)
@synchronized(START_STOP_LOCK)
def abort(self):
""" Abort running instance and delete generated files """
if not self.killed and self.cur_setname:
logging.info('Aborting DirectUnpack for %s', self.cur_setname)
logging.info("Aborting DirectUnpack for %s", self.cur_setname)
self.killed = True
# Save reference to the first rarfile
@ -399,7 +444,7 @@ class DirectUnpacker(threading.Thread):
if self.active_instance:
# First we try to abort gracefully
try:
self.active_instance.stdin.write(b'Q\n')
self.active_instance.stdin.write(b"Q\n")
time.sleep(0.2)
except IOError:
pass
@ -427,14 +472,18 @@ class DirectUnpacker(threading.Thread):
if one_folder:
# RarFile can fail for mysterious reasons
try:
rar_contents = RarFile(os.path.join(self.nzo.downpath, rarfile_nzf.filename), single_file_check=True).filelist()
rar_contents = RarFile(
os.path.join(self.nzo.downpath, rarfile_nzf.filename), single_file_check=True
).filelist()
for rm_file in rar_contents:
# Flat-unpack, so remove foldername from RarFile output
f = os.path.join(extraction_path, os.path.basename(rm_file))
remove_file(f)
except:
# The user will have to remove it themselves
logging.info('Failed to clean Direct Unpack after aborting %s', rarfile_nzf.filename, exc_info=True)
logging.info(
"Failed to clean Direct Unpack after aborting %s", rarfile_nzf.filename, exc_info=True
)
else:
# We can just remove the whole path
remove_all(extraction_path, recursive=True)
@ -449,7 +498,7 @@ class DirectUnpacker(threading.Thread):
if self.cur_setname and self.cur_setname in self.total_volumes:
# This won't work on obfuscated posts
if self.total_volumes[self.cur_setname] >= self.cur_volume and self.cur_volume:
return '%02d/%02d' % (self.cur_volume, self.total_volumes[self.cur_setname])
return "%02d/%02d" % (self.cur_volume, self.total_volumes[self.cur_setname])
return self.cur_volume
@ -465,14 +514,14 @@ def analyze_rar_filename(filename):
return m.group(1), int_conv(m.group(3))
else:
# Detect if first of "rxx" set
if filename.endswith('.rar'):
if filename.endswith(".rar"):
return os.path.splitext(filename)[0], 1
return None, None
def abort_all():
""" Abort all running DirectUnpackers """
logging.info('Aborting all DirectUnpackers')
logging.info("Aborting all DirectUnpackers")
for direct_unpacker in ACTIVE_UNPACKERS:
direct_unpacker.abort()
@ -483,8 +532,14 @@ def test_disk_performance():
"""
if diskspeedmeasure(sabnzbd.cfg.download_dir.get_path()) > 40:
cfg.direct_unpack.set(True)
logging.warning(T('Direct Unpack was automatically enabled.') + ' ' + T('Jobs will start unpacking during the downloading to reduce post-processing time. Only works for jobs that do not need repair.'))
logging.warning(
T("Direct Unpack was automatically enabled.")
+ " "
+ T(
"Jobs will start unpacking during the downloading to reduce post-processing time. Only works for jobs that do not need repair."
)
)
else:
logging.info('Direct Unpack was not enabled, incomplete folder disk speed below 40MB/s')
logging.info("Direct Unpack was not enabled, incomplete folder disk speed below 40MB/s")
cfg.direct_unpack_tested.set(True)
sabnzbd.config.save_config()

224
sabnzbd/downloader.py

@ -41,13 +41,13 @@ from sabnzbd.utils.happyeyeballs import happyeyeballs
# Timeout penalty in minutes for each cause
_PENALTY_UNKNOWN = 3 # Unknown cause
_PENALTY_502 = 5 # Unknown 502
_PENALTY_TIMEOUT = 10 # Server doesn't give an answer (multiple times)
_PENALTY_SHARE = 10 # Account sharing detected
_PENALTY_TOOMANY = 10 # Too many connections
_PENALTY_PERM = 10 # Permanent error, like bad username/password
_PENALTY_SHORT = 1 # Minimal penalty when no_penalties is set
_PENALTY_UNKNOWN = 3 # Unknown cause
_PENALTY_502 = 5 # Unknown 502
_PENALTY_TIMEOUT = 10 # Server doesn't give an answer (multiple times)
_PENALTY_SHARE = 10 # Account sharing detected
_PENALTY_TOOMANY = 10 # Too many connections
_PENALTY_PERM = 10 # Permanent error, like bad username/password
_PENALTY_SHORT = 1 # Minimal penalty when no_penalties is set
_PENALTY_VERYSHORT = 0.1 # Error 400 without cause clues
@ -55,9 +55,24 @@ TIMER_LOCK = RLock()
class Server:
def __init__(self, server_id, displayname, host, port, timeout, threads, priority, ssl, ssl_verify, ssl_ciphers,
send_group, username=None, password=None, optional=False, retention=0):
def __init__(
self,
server_id,
displayname,
host,
port,
timeout,
threads,
priority,
ssl,
ssl_verify,
ssl_ciphers,
send_group,
username=None,
password=None,
optional=False,
retention=0,
):
self.id = server_id
self.newid = None
@ -82,12 +97,12 @@ class Server:
self.idle_threads = []
self.active = True
self.bad_cons = 0
self.errormsg = ''
self.warning = ''
self.info = None # Will hold getaddrinfo() list
self.ssl_info = '' # Will hold the type and cipher of SSL connection
self.errormsg = ""
self.warning = ""
self.info = None # Will hold getaddrinfo() list
self.ssl_info = "" # Will hold the type and cipher of SSL connection
self.request = False # True if a getaddrinfo() request is pending
self.have_body = 'free.xsusenet.com' not in host
self.have_body = "free.xsusenet.com" not in host
self.have_stat = True # Assume server has "STAT", until proven otherwise
for i in range(threads):
@ -105,30 +120,30 @@ class Server:
# Check if already a successful ongoing connection
if self.busy_threads and self.busy_threads[0].nntp:
# Re-use that IP
logging.debug('%s: Re-using address %s', self.host, self.busy_threads[0].nntp.host)
logging.debug("%s: Re-using address %s", self.host, self.busy_threads[0].nntp.host)
return self.busy_threads[0].nntp.host
# Determine new IP
if cfg.load_balancing() == 0 and self.info:
# Just return the first one, so all next threads use the same IP
ip = self.info[0][4][0]
logging.debug('%s: Connecting to address %s', self.host, ip)
logging.debug("%s: Connecting to address %s", self.host, ip)
elif cfg.load_balancing() == 1 and self.info and len(self.info) > 1:
# Return a random entry from the possible IPs
rnd = random.randint(0, len(self.info) - 1)
ip = self.info[rnd][4][0]
logging.debug('%s: Connecting to address %s', self.host, ip)
logging.debug("%s: Connecting to address %s", self.host, ip)
elif cfg.load_balancing() == 2 and self.info and len(self.info) > 1:
# RFC6555 / Happy Eyeballs:
ip = happyeyeballs(self.host, port=self.port, ssl=self.ssl)
if ip:
logging.debug('%s: Connecting to address %s', self.host, ip)
logging.debug("%s: Connecting to address %s", self.host, ip)
else:
# nothing returned, so there was a connection problem
ip = self.host
logging.debug('%s: No successful IP connection was possible', self.host)
logging.debug("%s: No successful IP connection was possible", self.host)
else:
ip = self.host
return ip
@ -152,6 +167,7 @@ class Server:
class Downloader(Thread):
""" Singleton Downloader Thread """
do = None
def __init__(self, paused=False):
@ -187,7 +203,7 @@ class Downloader(Thread):
self.write_fds = {}
self.servers = []
self.server_dict = {} # For faster lookups, but is not updated later!
self.server_dict = {} # For faster lookups, but is not updated later!
self.server_nr = 0
self._timers = {}
@ -235,8 +251,23 @@ class Downloader(Thread):
break
if create and enabled and host and port and threads:
server = Server(newserver, displayname, host, port, timeout, threads, priority, ssl, ssl_verify,
ssl_ciphers, send_group, username, password, optional, retention)
server = Server(
newserver,
displayname,
host,
port,
timeout,
threads,
priority,
ssl,
ssl_verify,
ssl_ciphers,
send_group,
username,
password,
optional,
retention,
)
self.servers.append(server)
self.server_dict[newserver] = server
@ -255,7 +286,7 @@ class Downloader(Thread):
# Do not notify when SABnzbd is still starting
if self.paused and sabnzbd.WEB_DIR:
logging.info("Resuming")
notifier.send_notification("SABnzbd", T('Resuming'), 'download')
notifier.send_notification("SABnzbd", T("Resuming"), "download")
self.paused = False
@NzbQueueLocker
@ -264,7 +295,7 @@ class Downloader(Thread):
if not self.paused:
self.paused = True
logging.info("Pausing")
notifier.send_notification("SABnzbd", T('Paused'), 'download')
notifier.send_notification("SABnzbd", T("Paused"), "download")
if self.is_paused():
BPSMeter.do.reset()
if cfg.autodisconnect():
@ -289,13 +320,13 @@ class Downloader(Thread):
"""
if value:
mx = cfg.bandwidth_max.get_int()
if '%' in str(value) or (0 < from_units(value) < 101):
limit = value.strip(' %')
if "%" in str(value) or (0 < from_units(value) < 101):
limit = value.strip(" %")
self.bandwidth_perc = from_units(limit)
if mx:
self.bandwidth_limit = mx * self.bandwidth_perc / 100
else:
logging.warning(T('You must set a maximum bandwidth before you can set a bandwidth limit'))
logging.warning(T("You must set a maximum bandwidth before you can set a bandwidth limit"))
else:
self.bandwidth_limit = from_units(value)
if mx:
@ -347,11 +378,11 @@ class Downloader(Thread):
# Was it resolving problem?
if server.info is False:
# Warn about resolving issues
errormsg = T('Cannot connect to server %s [%s]') % (server.host, T('Server name does not resolve'))
errormsg = T("Cannot connect to server %s [%s]") % (server.host, T("Server name does not resolve"))
if server.errormsg != errormsg:
server.errormsg = errormsg
logging.warning(errormsg)
logging.warning(T('Server %s will be ignored for %s minutes'), server.host, _PENALTY_TIMEOUT)
logging.warning(T("Server %s will be ignored for %s minutes"), server.host, _PENALTY_TIMEOUT)
# Not fully the same as the code below for optional servers
server.bad_cons = 0
@ -363,7 +394,7 @@ class Downloader(Thread):
if server.optional and server.active and (server.bad_cons / server.threads) > 3:
server.bad_cons = 0
server.active = False
logging.warning(T('Server %s will be ignored for %s minutes'), server.host, _PENALTY_TIMEOUT)
logging.warning(T("Server %s will be ignored for %s minutes"), server.host, _PENALTY_TIMEOUT)
self.plan_server(server, _PENALTY_TIMEOUT)
# Remove all connections to server
@ -389,23 +420,27 @@ class Downloader(Thread):
# See if we need to delay because the queues are full
logged = False
while not self.shutdown and (sabnzbd.decoder.Decoder.do.queue_full() or sabnzbd.assembler.Assembler.do.queue_full()):
while not self.shutdown and (
sabnzbd.decoder.Decoder.do.queue_full() or sabnzbd.assembler.Assembler.do.queue_full()
):
if not logged:
# Only log once, to not waste any CPU-cycles
logging.debug("Delaying - Decoder queue: %s - Assembler queue: %s",
sabnzbd.decoder.Decoder.do.decoder_queue.qsize(),
sabnzbd.assembler.Assembler.do.queue.qsize())
logging.debug(
"Delaying - Decoder queue: %s - Assembler queue: %s",
sabnzbd.decoder.Decoder.do.decoder_queue.qsize(),
sabnzbd.assembler.Assembler.do.queue.qsize(),
)
logged = True
time.sleep(0.05)
def run(self):
# First check IPv6 connectivity
sabnzbd.EXTERNAL_IPV6 = sabnzbd.test_ipv6()
logging.debug('External IPv6 test result: %s', sabnzbd.EXTERNAL_IPV6)
logging.debug("External IPv6 test result: %s", sabnzbd.EXTERNAL_IPV6)
# Then we check SSL certificate checking
sabnzbd.CERTIFICATE_VALIDATION = sabnzbd.test_cert_checking()
logging.debug('SSL verification test: %s', sabnzbd.CERTIFICATE_VALIDATION)
logging.debug("SSL verification test: %s", sabnzbd.CERTIFICATE_VALIDATION)
# Kick BPS-Meter to check quota
BPSMeter.do.update()
@ -462,7 +497,7 @@ class Downloader(Thread):
if server.retention and article.nzf.nzo.avg_stamp < time.time() - server.retention:
# Let's get rid of all the articles for this server at once
logging.info('Job %s too old for %s, moving on', article.nzf.nzo.final_name, server.host)
logging.info("Job %s too old for %s, moving on", article.nzf.nzo.final_name, server.host)
while article:
self.decode(article, None)
article = article.nzf.nzo.get_article(server, self.servers)
@ -480,7 +515,12 @@ class Downloader(Thread):
logging.info("%s@%s: Initiating connection", nw.thrdnum, server.host)
nw.init_connect(self.write_fds)
except:
logging.error(T('Failed to initialize %s@%s with reason: %s'), nw.thrdnum, server.host, sys.exc_info()[1])
logging.error(
T("Failed to initialize %s@%s with reason: %s"),
nw.thrdnum,
server.host,
sys.exc_info()[1],
)
self.__reset_nw(nw, "failed to initialize")
# Exit-point
@ -530,7 +570,7 @@ class Downloader(Thread):
# Now let's check if it was stable in the last 10 seconds
self.can_be_slowed = BPSMeter.do.get_stable_speed(timespan=10)
self.can_be_slowed_timer = 0
logging.debug('Downloader-slowdown: %r', self.can_be_slowed)
logging.debug("Downloader-slowdown: %r", self.can_be_slowed)
else:
read, write, error = ([], [], [])
@ -540,8 +580,11 @@ class Downloader(Thread):
time.sleep(1.0)
DOWNLOADER_CV.acquire()
while (sabnzbd.nzbqueue.NzbQueue.do.is_empty() or self.is_paused() or self.postproc) and not \
self.shutdown and not self.__restart:
while (
(sabnzbd.nzbqueue.NzbQueue.do.is_empty() or self.is_paused() or self.postproc)
and not self.shutdown
and not self.__restart
):
DOWNLOADER_CV.wait()
DOWNLOADER_CV.release()
@ -599,7 +642,9 @@ class Downloader(Thread):
try:
nw.finish_connect(nw.status_code)
if sabnzbd.LOG_ALL:
logging.debug("%s@%s last message -> %s", nw.thrdnum, nw.server.host, nntp_to_msg(nw.data))
logging.debug(
"%s@%s last message -> %s", nw.thrdnum, nw.server.host, nntp_to_msg(nw.data)
)
nw.clear_data()
except NNTPPermanentError as error:
# Handle login problems
@ -607,45 +652,45 @@ class Downloader(Thread):
penalty = 0
msg = error.response
ecode = int_conv(msg[:3])
display_msg = ' [%s]' % msg
logging.debug('Server login problem: %s, %s', ecode, msg)
display_msg = " [%s]" % msg
logging.debug("Server login problem: %s, %s", ecode, msg)
if ecode in (502, 400, 481, 482) and clues_too_many(msg):
# Too many connections: remove this thread and reduce thread-setting for server
# Plan to go back to the full number after a penalty timeout
if server.active:
errormsg = T('Too many connections to server %s') % display_msg
errormsg = T("Too many connections to server %s") % display_msg
if server.errormsg != errormsg:
server.errormsg = errormsg
logging.warning(T('Too many connections to server %s'), server.host)
logging.warning(T("Too many connections to server %s"), server.host)
self.__reset_nw(nw, None, warn=False, destroy=True, send_quit=True)
self.plan_server(server, _PENALTY_TOOMANY)
server.threads -= 1
elif ecode in (502, 481, 482) and clues_too_many_ip(msg):
# Account sharing?
if server.active:
errormsg = T('Probable account sharing') + display_msg
errormsg = T("Probable account sharing") + display_msg
if server.errormsg != errormsg:
server.errormsg = errormsg
name = ' (%s)' % server.host
logging.warning(T('Probable account sharing') + name)
name = " (%s)" % server.host
logging.warning(T("Probable account sharing") + name)
penalty = _PENALTY_SHARE
block = True
elif ecode in (452, 481, 482, 381) or (ecode == 502 and clues_login(msg)):
# Cannot login, block this server
if server.active:
errormsg = T('Failed login for server %s') % display_msg
errormsg = T("Failed login for server %s") % display_msg
if server.errormsg != errormsg:
server.errormsg = errormsg
logging.error(T('Failed login for server %s'), server.host)
logging.error(T("Failed login for server %s"), server.host)
penalty = _PENALTY_PERM
block = True
elif ecode in (502, 482):
# Cannot connect (other reasons), block this server
if server.active:
errormsg = T('Cannot connect to server %s [%s]') % ('', display_msg)
errormsg = T("Cannot connect to server %s [%s]") % ("", display_msg)
if server.errormsg != errormsg:
server.errormsg = errormsg
logging.warning(T('Cannot connect to server %s [%s]'), server.host, msg)
logging.warning(T("Cannot connect to server %s [%s]"), server.host, msg)
if clues_pay(msg):
penalty = _PENALTY_PERM
else:
@ -654,16 +699,16 @@ class Downloader(Thread):
elif ecode == 400:
# Temp connection problem?
if server.active:
logging.debug('Unspecified error 400 from server %s', server.host)
logging.debug("Unspecified error 400 from server %s", server.host)
penalty = _PENALTY_VERYSHORT
block = True
else:
# Unknown error, just keep trying
if server.active:
errormsg = T('Cannot connect to server %s [%s]') % ('', display_msg)
errormsg = T("Cannot connect to server %s [%s]") % ("", display_msg)
if server.errormsg != errormsg:
server.errormsg = errormsg
logging.warning(T('Cannot connect to server %s [%s]'), server.host, msg)
logging.warning(T("Cannot connect to server %s [%s]"), server.host, msg)
penalty = _PENALTY_UNKNOWN
block = True
if block or (penalty and server.optional):
@ -675,8 +720,12 @@ class Downloader(Thread):
self.__reset_nw(nw, None, warn=False, send_quit=True)
continue
except:
logging.error(T('Connecting %s@%s failed, message=%s'),
nw.thrdnum, nw.server.host, nntp_to_msg(nw.data))
logging.error(
T("Connecting %s@%s failed, message=%s"),
nw.thrdnum,
nw.server.host,
nntp_to_msg(nw.data),
)
# No reset-warning needed, above logging is sufficient
self.__reset_nw(nw, None, warn=False)
@ -686,7 +735,7 @@ class Downloader(Thread):
elif nw.status_code == 223:
done = True
logging.debug('Article <%s> is present', article.article)
logging.debug("Article <%s> is present", article.article)
elif nw.status_code == 211:
done = False
@ -697,27 +746,32 @@ class Downloader(Thread):
elif nw.status_code in (411, 423, 430):
done = True
logging.debug('Thread %s@%s: Article %s missing (error=%s)',
nw.thrdnum, nw.server.host, article.article, nw.status_code)
logging.debug(
"Thread %s@%s: Article %s missing (error=%s)",
nw.thrdnum,
nw.server.host,
article.article,
nw.status_code,
)
nw.clear_data()
elif nw.status_code == 500:
if nzo.precheck:
# Assume "STAT" command is not supported
server.have_stat = False
logging.debug('Server %s does not support STAT', server.host)
logging.debug("Server %s does not support STAT", server.host)
else:
# Assume "BODY" command is not supported
server.have_body = False
logging.debug('Server %s does not support BODY', server.host)
logging.debug("Server %s does not support BODY", server.host)
nw.clear_data()
self.__request_article(nw)
if done:
server.bad_cons = 0 # Successful data, clear "bad" counter
server.errormsg = server.warning = ''
server.errormsg = server.warning = ""
if sabnzbd.LOG_ALL:
logging.debug('Thread %s@%s: %s done', nw.thrdnum, server.host, article.article)
logging.debug("Thread %s@%s: %s done", nw.thrdnum, server.host, article.article)
self.decode(article, nw.data)
nw.soft_reset()
@ -749,9 +803,9 @@ class Downloader(Thread):
if warn and reset_msg:
server.warning = reset_msg
logging.info('Thread %s@%s: %s', nw.thrdnum, server.host, reset_msg)
logging.info("Thread %s@%s: %s", nw.thrdnum, server.host, reset_msg)
elif reset_msg:
logging.debug('Thread %s@%s: %s', nw.thrdnum, server.host, reset_msg)
logging.debug("Thread %s@%s: %s", nw.thrdnum, server.host, reset_msg)
if nw in server.busy_threads:
server.busy_threads.remove(nw)
@ -777,7 +831,7 @@ class Downloader(Thread):
nw.hard_reset(wait, send_quit=send_quit)
# Empty SSL info, it might change on next connect
server.ssl_info = ''
server.ssl_info = ""
def __request_article(self, nw):
try:
@ -785,25 +839,25 @@ class Downloader(Thread):
if nw.server.send_group and nzo.group != nw.group:
group = nzo.group
if sabnzbd.LOG_ALL:
logging.debug('Thread %s@%s: GROUP <%s>', nw.thrdnum, nw.server.host, group)
logging.debug("Thread %s@%s: GROUP <%s>", nw.thrdnum, nw.server.host, group)
nw.send_group(group)
else:
if sabnzbd.LOG_ALL:
logging.debug('Thread %s@%s: BODY %s', nw.thrdnum, nw.server.host, nw.article.article)
logging.debug("Thread %s@%s: BODY %s", nw.thrdnum, nw.server.host, nw.article.article)
nw.body(nzo.precheck)
fileno = nw.nntp.sock.fileno()
if fileno not in self.read_fds:
self.read_fds[fileno] = nw
except socket.error as err:
logging.info('Looks like server closed connection: %s', err)
logging.info("Looks like server closed connection: %s", err)
self.__reset_nw(nw, "server broke off connection", send_quit=False)
except:
logging.error(T('Suspect error in downloader'))
logging.error(T("Suspect error in downloader"))
logging.info("Traceback: ", exc_info=True)
self.__reset_nw(nw, "server broke off connection", send_quit=False)
#------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Timed restart of servers admin.
# For each server all planned events are kept in a list.
# When the first timer of a server fires, all other existing timers
@ -817,7 +871,7 @@ class Downloader(Thread):
# Overwrite in case of no_penalties
interval = _PENALTY_SHORT
logging.debug('Set planned server resume %s in %s mins', server.host, interval)
logging.debug("Set planned server resume %s in %s mins", server.host, interval)
if server.id not in self._timers:
self._timers[server.id] = []
stamp = time.time() + 60.0 * interval
@ -828,7 +882,7 @@ class Downloader(Thread):
@synchronized(TIMER_LOCK)
def trigger_server(self, server_id, timestamp):
""" Called by scheduler, start server if timer still valid """
logging.debug('Trigger planned server resume for server-id %s', server_id)
logging.debug("Trigger planned server resume for server-id %s", server_id)
if server_id in self._timers:
if timestamp in self._timers[server_id]:
del self._timers[server_id]
@ -845,7 +899,7 @@ class Downloader(Thread):
# Activate server if it was inactive
for server in self.servers:
if server.id == server_id and not server.active:
logging.debug('Unblock server %s', server.host)
logging.debug("Unblock server %s", server.host)
self.init_server(server_id, server_id)
break
@ -862,7 +916,7 @@ class Downloader(Thread):
kicked = []
for server_id in self._timers.keys():
if not [stamp for stamp in self._timers[server_id] if stamp >= now]:
logging.debug('Forcing re-evaluation of server-id %s', server_id)
logging.debug("Forcing re-evaluation of server-id %s", server_id)
del self._timers[server_id]
self.init_server(server_id, server_id)
kicked.append(server_id)
@ -870,7 +924,7 @@ class Downloader(Thread):
for server in self.servers:
if server.id not in self._timers:
if server.id not in kicked and not server.active:
logging.debug('Forcing activation of server %s', server.host)
logging.debug("Forcing activation of server %s", server.host)
self.init_server(server.id, server.id)
def update_server(self, oldserver, newserver):
@ -886,7 +940,7 @@ class Downloader(Thread):
def stop(self):
self.shutdown = True
notifier.send_notification("SABnzbd", T('Shutting down'), 'startup')
notifier.send_notification("SABnzbd", T("Shutting down"), "startup")
def stop():
@ -905,7 +959,7 @@ def stop():
def clues_login(text):
""" Check for any "failed login" clues in the response code """
text = text.lower()
for clue in ('username', 'password', 'invalid', 'authen', 'access denied'):
for clue in ("username", "password", "invalid", "authen", "access denied"):
if clue in text:
return True
return False
@ -914,9 +968,9 @@ def clues_login(text):
def clues_too_many(text):
""" Check for any "too many connections" clues in the response code """
text = text.lower()
for clue in ('exceed', 'connections', 'too many', 'threads', 'limit'):
for clue in ("exceed", "connections", "too many", "threads", "limit"):
# Not 'download limit exceeded' error
if (clue in text) and ('download' not in text) and ('byte' not in text):
if (clue in text) and ("download" not in text) and ("byte" not in text):
return True
return False
@ -924,7 +978,7 @@ def clues_too_many(text):
def clues_too_many_ip(text):
""" Check for any "account sharing" clues in the response code """
text = text.lower()
for clue in ('simultaneous ip', 'multiple ip'):
for clue in ("simultaneous ip", "multiple ip"):
if clue in text:
return True
return False
@ -933,7 +987,7 @@ def clues_too_many_ip(text):
def clues_pay(text):
""" Check for messages about payments """
text = text.lower()
for clue in ('credits', 'paym', 'expired', 'exceeded'):
for clue in ("credits", "paym", "expired", "exceeded"):
if clue in text:
return True
return False

14
sabnzbd/encoding.py

@ -31,14 +31,14 @@ def utob(str_in):
""" Shorthand for converting UTF-8 to bytes """
if isinstance(str_in, bytes):
return str_in
return str_in.encode('utf-8')
return str_in.encode("utf-8")
def ubtou(str_in):
""" Shorthand for converting unicode bytes to UTF-8 """
if not isinstance(str_in, bytes):
return str_in
return str_in.decode('utf-8')
return str_in.decode("utf-8")
def platform_btou(str_in):
@ -50,7 +50,7 @@ def platform_btou(str_in):
try:
return ubtou(str_in)
except UnicodeDecodeError:
return str_in.decode(CODEPAGE, errors='replace').replace('?', '!')
return str_in.decode(CODEPAGE, errors="replace").replace("?", "!")
else:
return str_in
@ -63,7 +63,7 @@ def correct_unknown_encoding(str_or_bytes_in):
"""
# If already string, back to bytes
if not isinstance(str_or_bytes_in, bytes):
str_or_bytes_in = str_or_bytes_in.encode('utf-8', 'surrogateescape')
str_or_bytes_in = str_or_bytes_in.encode("utf-8", "surrogateescape")
# Try simple bytes-to-string
try:
@ -71,14 +71,12 @@ def correct_unknown_encoding(str_or_bytes_in):
except UnicodeDecodeError:
try:
# Try using 8-bit ASCII, if came from Windows
return str_or_bytes_in.decode('ISO-8859-1')
return str_or_bytes_in.decode("ISO-8859-1")
except ValueError:
# Last resort we use the slow chardet package
return str_or_bytes_in.decode(chardet.detect(str_or_bytes_in)['encoding'])
return str_or_bytes_in.decode(chardet.detect(str_or_bytes_in)["encoding"])
def xml_name(p):
""" Prepare name for use in HTML/XML contect """
return escape(str(p))

1870
sabnzbd/interface.py

File diff suppressed because it is too large

1127
sabnzbd/newsunpack.py

File diff suppressed because it is too large

104
sabnzbd/newswrapper.py

@ -43,7 +43,7 @@ socket.setdefaulttimeout(DEF_TIMEOUT)
def _retrieve_info(server):
""" Async attempt to run getaddrinfo() for specified server """
logging.debug('Retrieving server address information for %s', server.host)
logging.debug("Retrieving server address information for %s", server.host)
info = GetServerParms(server.host, server.port)
if not info:
server.bad_cons += server.threads
@ -67,12 +67,12 @@ def GetServerParms(host, port):
except:
port = 119
opt = sabnzbd.cfg.ipv6_servers()
''' ... with the following meaning for 'opt':
""" ... with the following meaning for 'opt':
Control the use of IPv6 Usenet server addresses. Meaning:
0 = don't use
1 = use when available and reachable (DEFAULT)
2 = force usage (when SABnzbd's detection fails)
'''
"""
try:
# Standard IPV4 or IPV6
ips = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
@ -82,13 +82,14 @@ def GetServerParms(host, port):
return ips
else:
# IPv6 unreachable or not allowed by user, so only return IPv4 address(es):
return [ip for ip in ips if ':' not in ip[4][0]]
return [ip for ip in ips if ":" not in ip[4][0]]
except:
if opt == 2 or (opt == 1 and sabnzbd.EXTERNAL_IPV6) or (opt == 1 and sabnzbd.cfg.load_balancing() == 2):
try:
# Try IPV6 explicitly
return socket.getaddrinfo(host, port, socket.AF_INET6,
socket.SOCK_STREAM, socket.IPPROTO_IP, socket.AI_CANONNAME)
return socket.getaddrinfo(
host, port, socket.AF_INET6, socket.SOCK_STREAM, socket.IPPROTO_IP, socket.AI_CANONNAME
)
except:
# Nothing found!
pass
@ -101,8 +102,9 @@ def con(sock, host, port, sslenabled, write_fds, nntp):
sock.setblocking(0)
if sslenabled:
# Log SSL/TLS info
logging.info("%s@%s: Connected using %s (%s)",
nntp.nw.thrdnum, nntp.nw.server.host, sock.version(), sock.cipher()[0])
logging.info(
"%s@%s: Connected using %s (%s)", nntp.nw.thrdnum, nntp.nw.server.host, sock.version(), sock.cipher()[0]
)
nntp.nw.server.ssl_info = "%s (%s)" % (sock.version(), sock.cipher()[0])
# Now it's safe to add the socket to the list of active sockets.
@ -132,7 +134,7 @@ def con(sock, host, port, sslenabled, write_fds, nntp):
class NNTP:
# Pre-define attributes to save memory
__slots__ = ('host', 'port', 'nw', 'blocking', 'error_msg', 'sock')
__slots__ = ("host", "port", "nw", "blocking", "error_msg", "sock")
def __init__(self, host, port, info, sslenabled, nw, block=False, write_fds=None):
self.host = host
@ -192,8 +194,13 @@ class NNTP:
self.sock.connect((self.host, self.port))
if sslenabled:
# Log SSL/TLS info
logging.info("%s@%s: Connected using %s (%s)",
self.nw.thrdnum, self.nw.server.host, self.sock.version(), self.sock.cipher()[0])
logging.info(
"%s@%s: Connected using %s (%s)",
self.nw.thrdnum,
self.nw.server.host,
self.sock.version(),
self.sock.cipher()[0],
)
self.nw.server.ssl_info = "%s (%s)" % (self.sock.version(), self.sock.cipher()[0])
except (ssl.SSLError, ssl.CertificateError) as e:
@ -216,23 +223,25 @@ class NNTP:
def error(self, error):
raw_error_str = str(error)
if 'SSL23_GET_SERVER_HELLO' in str(error) or 'SSL3_GET_RECORD' in raw_error_str:
error = T('This server does not allow SSL on this port')
if "SSL23_GET_SERVER_HELLO" in str(error) or "SSL3_GET_RECORD" in raw_error_str:
error = T("This server does not allow SSL on this port")
# Catch certificate errors
if type(error) == ssl.CertificateError or 'CERTIFICATE_VERIFY_FAILED' in raw_error_str:
if type(error) == ssl.CertificateError or "CERTIFICATE_VERIFY_FAILED" in raw_error_str:
# Log the raw message for debug purposes
logging.info('Certificate error for host %s: %s', self.nw.server.host, raw_error_str)
logging.info("Certificate error for host %s: %s", self.nw.server.host, raw_error_str)
# Try to see if we should catch this message and provide better text
if 'hostname' in raw_error_str:
raw_error_str = T('Certificate hostname mismatch: the server hostname is not listed in the certificate. This is a server issue.')
elif 'certificate verify failed' in raw_error_str:
raw_error_str = T('Certificate not valid. This is most probably a server issue.')
if "hostname" in raw_error_str:
raw_error_str = T(
"Certificate hostname mismatch: the server hostname is not listed in the certificate. This is a server issue."
)
elif "certificate verify failed" in raw_error_str:
raw_error_str = T("Certificate not valid. This is most probably a server issue.")
# Reformat error
error = T('Server %s uses an untrusted certificate [%s]') % (self.nw.server.host, raw_error_str)
error = '%s - %s: %s' % (error, T('Wiki'), 'https://sabnzbd.org/certificate-errors')
error = T("Server %s uses an untrusted certificate [%s]") % (self.nw.server.host, raw_error_str)
error = "%s - %s: %s" % (error, T("Wiki"), "https://sabnzbd.org/certificate-errors")
# Prevent throwing a lot of errors or when testing server
if error not in self.nw.server.warning and not self.blocking:
@ -254,8 +263,24 @@ class NNTP:
class NewsWrapper:
# Pre-define attributes to save memory
__slots__ = ('server', 'thrdnum', 'blocking', 'timeout', 'article', 'data', 'last_line', 'nntp',
'recv', 'connected', 'user_sent', 'pass_sent', 'group', 'user_ok', 'pass_ok', 'force_login')
__slots__ = (
"server",
"thrdnum",
"blocking",
"timeout",
"article",
"data",
"last_line",
"nntp",
"recv",
"connected",
"user_sent",
"pass_sent",
"group",
"user_ok",
"pass_ok",
"force_login",
)
def __init__(self, server, thrdnum, block=False):
self.server = server
@ -265,7 +290,7 @@ class NewsWrapper:
self.timeout = None
self.article = None
self.data = []
self.last_line = ''
self.last_line = ""
self.nntp = None
self.recv = None
@ -296,8 +321,9 @@ class NewsWrapper:
self.server.info = GetServerParms(self.server.host, self.server.port)
# Construct NNTP object and shorthands
self.nntp = NNTP(self.server.hostip, self.server.port, self.server.info, self.server.ssl,
self, self.blocking, write_fds)
self.nntp = NNTP(
self.server.hostip, self.server.port, self.server.info, self.server.ssl, self, self.blocking, write_fds
)
self.recv = self.nntp.sock.recv
self.timeout = time.time() + self.server.timeout
@ -312,7 +338,7 @@ class NewsWrapper:
if code == 501 and self.user_sent:
# Change to a sensible text
code = 481
self.data[0] = "%d %s" % (code, T('Authentication failed, check username/password.'))
self.data[0] = "%d %s" % (code, T("Authentication failed, check username/password."))
self.user_ok = True
self.pass_sent = True
@ -327,7 +353,7 @@ class NewsWrapper:
if code in (400, 502):
raise NNTPPermanentError(nntp_to_msg(self.data))
elif not self.user_sent:
command = utob('authinfo user %s\r\n' % self.server.username)
command = utob("authinfo user %s\r\n" % self.server.username)
self.nntp.sock.sendall(command)
self.data = []
self.user_sent = True
@ -342,7 +368,7 @@ class NewsWrapper:
self.connected = True
if self.user_ok and not self.pass_sent:
command = utob('authinfo pass %s\r\n' % self.server.password)
command = utob("authinfo pass %s\r\n" % self.server.password)
self.nntp.sock.sendall(command)
self.data = []
self.pass_sent = True
@ -359,19 +385,19 @@ class NewsWrapper:
self.timeout = time.time() + self.server.timeout
if precheck:
if self.server.have_stat:
command = utob('STAT <%s>\r\n' % (self.article.article))
command = utob("STAT <%s>\r\n" % (self.article.article))
else:
command = utob('HEAD <%s>\r\n' % (self.article.article))
command = utob("HEAD <%s>\r\n" % (self.article.article))
elif self.server.have_body:
command = utob('BODY <%s>\r\n' % (self.article.article))
command = utob("BODY <%s>\r\n" % (self.article.article))
else:
command = utob('ARTICLE <%s>\r\n' % (self.article.article))
command = utob("ARTICLE <%s>\r\n" % (self.article.article))
self.nntp.sock.sendall(command)
self.data = []
def send_group(self, group):
self.timeout = time.time() + self.server.timeout
command = utob('GROUP %s\r\n' % (group))
command = utob("GROUP %s\r\n" % (group))
self.nntp.sock.sendall(command)
self.data = []
@ -403,13 +429,13 @@ class NewsWrapper:
# Official end-of-article is ".\r\n" but sometimes it can get lost between 2 chunks
chunk_len = len(chunk)
if chunk[-5:] == b'\r\n.\r\n':
if chunk[-5:] == b"\r\n.\r\n":
return (chunk_len, True, False)
elif chunk_len < 5 and len(self.data) > 1:
# We need to make sure the end is not split over 2 chunks
# This is faster than join()
combine_chunk = self.data[-2][-5:] + chunk
if combine_chunk[-5:] == b'\r\n.\r\n':
if combine_chunk[-5:] == b"\r\n.\r\n":
return (chunk_len, True, False)
# Still in middle of data, so continue!
@ -422,13 +448,13 @@ class NewsWrapper:
def clear_data(self):
self.data = []
self.last_line = ''
self.last_line = ""
def hard_reset(self, wait=True, send_quit=True):
if self.nntp:
try:
if send_quit:
self.nntp.sock.sendall(b'QUIT\r\n')
self.nntp.sock.sendall(b"QUIT\r\n")
time.sleep(0.1)
self.nntp.sock.close()
except:
@ -449,7 +475,7 @@ class NewsWrapper:
if self.nntp:
try:
if quit:
self.nntp.sock.sendall(b'QUIT\r\n')
self.nntp.sock.sendall(b"QUIT\r\n")
time.sleep(0.1)
self.nntp.sock.close()
except:

172
sabnzbd/nzbqueue.py

@ -32,10 +32,23 @@ from sabnzbd.filesystem import get_admin_path, remove_all, globber_full, remove_
from sabnzbd.panic import panic_queue
import sabnzbd.database as database
from sabnzbd.decorators import NzbQueueLocker
from sabnzbd.constants import QUEUE_FILE_NAME, QUEUE_VERSION, FUTURE_Q_FOLDER, \
JOB_ADMIN, LOW_PRIORITY, NORMAL_PRIORITY, HIGH_PRIORITY, TOP_PRIORITY, \
REPAIR_PRIORITY, STOP_PRIORITY, VERIFIED_FILE, \
Status, IGNORED_FOLDERS, QNFO, DIRECT_WRITE_TRIGGER
from sabnzbd.constants import (
QUEUE_FILE_NAME,
QUEUE_VERSION,
FUTURE_Q_FOLDER,
JOB_ADMIN,
LOW_PRIORITY,
NORMAL_PRIORITY,
HIGH_PRIORITY,
TOP_PRIORITY,
REPAIR_PRIORITY,
STOP_PRIORITY,
VERIFIED_FILE,
Status,
IGNORED_FOLDERS,
QNFO,
DIRECT_WRITE_TRIGGER,
)
import sabnzbd.cfg as cfg
import sabnzbd.downloader
@ -47,6 +60,7 @@ from sabnzbd.dirscanner import process_single_nzb
class NzbQueue:
""" Singleton NzbQueue """
do = None
def __init__(self):
@ -71,14 +85,16 @@ class NzbQueue:
queue_vers, nzo_ids, _ = data
if not queue_vers == QUEUE_VERSION:
nzo_ids = []
logging.error(T('Incompatible queuefile found, cannot proceed'))
logging.error(T("Incompatible queuefile found, cannot proceed"))
if not repair:
panic_queue(os.path.join(cfg.admin_dir.get_path(), QUEUE_FILE_NAME))
exit_sab(2)
except:
nzo_ids = []
logging.error(T('Error loading %s, corrupt file detected'),
os.path.join(cfg.admin_dir.get_path(), QUEUE_FILE_NAME))
logging.error(
T("Error loading %s, corrupt file detected"),
os.path.join(cfg.admin_dir.get_path(), QUEUE_FILE_NAME),
)
# First handle jobs in the queue file
folders = []
@ -103,7 +119,7 @@ class NzbQueue:
for item in globber_full(os.path.join(cfg.admin_dir.get_path(), FUTURE_Q_FOLDER)):
path, nzo_id = os.path.split(item)
if nzo_id not in self.__nzo_table:
if nzo_id.startswith('SABnzbd_nzo'):
if nzo_id.startswith("SABnzbd_nzo"):
nzo = sabnzbd.load_data(nzo_id, path, remove=True)
if nzo:
self.add(nzo, save=True)
@ -130,20 +146,25 @@ class NzbQueue:
# Retryable folders from History
items = sabnzbd.api.build_history(output=True)[0]
# Anything waiting or active or retryable is a known item
registered.extend([os.path.basename(item['path'])
for item in items if item['retry'] or item['loaded'] or item['status'] == Status.QUEUED])
registered.extend(
[
os.path.basename(item["path"])
for item in items
if item["retry"] or item["loaded"] or item["status"] == Status.QUEUED
]
)
# Repair unregistered folders
for folder in globber_full(cfg.download_dir.get_path()):
name = os.path.basename(folder)
if os.path.isdir(folder) and name not in registered and name not in IGNORED_FOLDERS:
if action:
logging.info('Repairing job %s', folder)
logging.info("Repairing job %s", folder)
self.repair_job(folder)
result.append(os.path.basename(folder))
else:
if action:
logging.info('Skipping repair for job %s', folder)
logging.info("Skipping repair for job %s", folder)
return result
def repair_job(self, folder, new_nzb=None, password=None):
@ -154,40 +175,49 @@ class NzbQueue:
name = os.path.basename(folder)
path = os.path.join(folder, JOB_ADMIN)
if hasattr(new_nzb, 'filename'):
if hasattr(new_nzb, "filename"):
filename = new_nzb.filename
else:
filename = ''
filename = ""
if not filename:
# Was this file already post-processed?
verified = sabnzbd.load_data(VERIFIED_FILE, path, remove=False)
if not verified or not all(verified[x] for x in verified):
filename = globber_full(path, '*.gz')
filename = globber_full(path, "*.gz")
if len(filename) > 0:
logging.debug('Repair job %s by re-parsing stored NZB', name)
nzo_id = sabnzbd.add_nzbfile(filename[0], pp=None, script=None, cat=None, priority=None, nzbname=name,
reuse=True, password=password)[1]
logging.debug("Repair job %s by re-parsing stored NZB", name)
nzo_id = sabnzbd.add_nzbfile(
filename[0],
pp=None,
script=None,
cat=None,
priority=None,
nzbname=name,
reuse=True,
password=password,
)[1]
else:
logging.debug('Repair job %s without stored NZB', name)
nzo = NzbObject(name, pp=None, script=None, nzb='', cat=None, priority=None, nzbname=name, reuse=True)
logging.debug("Repair job %s without stored NZB", name)
nzo = NzbObject(name, pp=None, script=None, nzb="", cat=None, priority=None, nzbname=name, reuse=True)
nzo.password = password
self.add(nzo)
nzo_id = nzo.nzo_id
else:
remove_all(path, '*.gz')
logging.debug('Repair job %s with new NZB (%s)', name, filename)
nzo_id = sabnzbd.add_nzbfile(new_nzb, pp=None, script=None, cat=None, priority=None, nzbname=name,
reuse=True, password=password)[1]
remove_all(path, "*.gz")
logging.debug("Repair job %s with new NZB (%s)", name, filename)
nzo_id = sabnzbd.add_nzbfile(
new_nzb, pp=None, script=None, cat=None, priority=None, nzbname=name, reuse=True, password=password
)[1]
return nzo_id
@NzbQueueLocker
def send_back(self, nzo):
""" Send back job to queue after successful pre-check """
try:
nzb_path = globber_full(nzo.workpath, '*.gz')[0]
nzb_path = globber_full(nzo.workpath, "*.gz")[0]
except:
logging.debug('Failed to find NZB file after pre-check (%s)', nzo.nzo_id)
logging.debug("Failed to find NZB file after pre-check (%s)", nzo.nzo_id)
return
# Need to remove it first, otherwise it might still be downloading
@ -221,14 +251,25 @@ class NzbQueue:
def generate_future(self, msg, pp=None, script=None, cat=None, url=None, priority=NORMAL_PRIORITY, nzbname=None):
""" Create and return a placeholder nzo object """
logging.debug('Creating placeholder NZO')
future_nzo = NzbObject(msg, pp, script, None, futuretype=True, cat=cat, url=url, priority=priority, nzbname=nzbname, status=Status.GRABBING)
logging.debug("Creating placeholder NZO")
future_nzo = NzbObject(
msg,
pp,
script,
None,
futuretype=True,
cat=cat,
url=url,
priority=priority,
nzbname=nzbname,
status=Status.GRABBING,
)
self.add(future_nzo)
return future_nzo
def change_opts(self, nzo_ids, pp):
result = 0
for nzo_id in [item.strip() for item in nzo_ids.split(',')]:
for nzo_id in [item.strip() for item in nzo_ids.split(",")]:
if nzo_id in self.__nzo_table:
self.__nzo_table[nzo_id].set_pp(pp)
result += 1
@ -236,20 +277,20 @@ class NzbQueue:
def change_script(self, nzo_ids, script):
result = 0
for nzo_id in [item.strip() for item in nzo_ids.split(',')]:
for nzo_id in [item.strip() for item in nzo_ids.split(",")]:
if nzo_id in self.__nzo_table:
self.__nzo_table[nzo_id].script = script
logging.info('Set script=%s for job %s', script, self.__nzo_table[nzo_id].final_name)
logging.info("Set script=%s for job %s", script, self.__nzo_table[nzo_id].final_name)
result += 1
return result
def change_cat(self, nzo_ids, cat, explicit_priority=None):
result = 0
for nzo_id in [item.strip() for item in nzo_ids.split(',')]:
for nzo_id in [item.strip() for item in nzo_ids.split(",")]:
if nzo_id in self.__nzo_table:
nzo = self.__nzo_table[nzo_id]
nzo.cat, pp, nzo.script, prio = cat_to_opts(cat)
logging.info('Set cat=%s for job %s', cat, nzo.final_name)
logging.info("Set cat=%s for job %s", cat, nzo.final_name)
nzo.set_pp(pp)
if explicit_priority is None:
self.set_priority(nzo_id, prio)
@ -261,7 +302,7 @@ class NzbQueue:
def change_name(self, nzo_id, name, password=None):
if nzo_id in self.__nzo_table:
nzo = self.__nzo_table[nzo_id]
logging.info('Renaming %s to %s', nzo.final_name, name)
logging.info("Renaming %s to %s", nzo.final_name, name)
# Abort any ongoing unpacking if the name changed (dirs change)
nzo.abort_direct_unpacker()
if not nzo.futuretype:
@ -283,12 +324,12 @@ class NzbQueue:
@NzbQueueLocker
def add(self, nzo, save=True, quiet=False):
if not nzo.nzo_id:
nzo.nzo_id = sabnzbd.get_new_id('nzo', nzo.workpath, self.__nzo_table)
nzo.nzo_id = sabnzbd.get_new_id("nzo", nzo.workpath, self.__nzo_table)
# If no files are to be downloaded anymore, send to postproc
if not nzo.files and not nzo.futuretype:
self.end_job(nzo)
return ''
return ""
# Reset try_lists
nzo.reset_try_list()
@ -330,7 +371,7 @@ class NzbQueue:
self.save(nzo)
if not (quiet or nzo.status == Status.FETCHING):
notifier.send_notification(T('NZB added to queue'), nzo.filename, 'download', nzo.cat)
notifier.send_notification(T("NZB added to queue"), nzo.filename, "download", nzo.cat)
if not quiet and cfg.auto_sort():
self.sort_by_avg_age()
@ -344,7 +385,7 @@ class NzbQueue:
"""
if nzo_id in self.__nzo_table:
nzo = self.__nzo_table.pop(nzo_id)
logging.info('[%s] Removing job %s', caller_name(), nzo.final_name)
logging.info("[%s] Removing job %s", caller_name(), nzo.final_name)
# Set statuses
nzo.deleted = True
@ -410,10 +451,10 @@ class NzbQueue:
elif force_delete:
# Force-remove all trace
nzo.bytes -= nzf.bytes
nzo.bytes_tried -= (nzf.bytes - nzf.bytes_left)
nzo.bytes_tried -= nzf.bytes - nzf.bytes_left
del nzo.files_table[nzf_id]
nzo.finished_files.remove(nzf)
logging.info('Removed NZFs %s from job %s', removed, nzo.final_name)
logging.info("Removed NZFs %s from job %s", removed, nzo.final_name)
return removed
def pause_multiple_nzo(self, nzo_ids):
@ -492,7 +533,13 @@ class NzbQueue:
item_id_pos2 = i
if (item_id_pos1 > -1) and (item_id_pos2 > -1):
item = self.__nzo_list[item_id_pos1]
logging.info('Switching job [%s] %s => [%s] %s', item_id_pos1, item.final_name, item_id_pos2, self.__nzo_list[item_id_pos2].final_name)
logging.info(
"Switching job [%s] %s => [%s] %s",
item_id_pos1,
item.final_name,
item_id_pos2,
self.__nzo_list[item_id_pos2].final_name,
)
del self.__nzo_list[item_id_pos1]
self.__nzo_list.insert(item_id_pos2, item)
return item_id_pos2, nzo1.priority
@ -538,17 +585,17 @@ class NzbQueue:
def sort_queue(self, field, reverse=None):
if isinstance(reverse, str):
if reverse.lower() == 'desc':
if reverse.lower() == "desc":
reverse = True
else:
reverse = False
if reverse is None:
reverse = False
if field.lower() == 'name':
if field.lower() == "name":
self.sort_by_name(reverse)
elif field.lower() == 'size' or field.lower() == 'bytes':
elif field.lower() == "size" or field.lower() == "bytes":
self.sort_by_size(reverse)
elif field.lower() == 'avg_age':
elif field.lower() == "avg_age":
self.sort_by_avg_age(reverse)
else:
logging.debug("Sort: %s not recognized", field)
@ -578,8 +625,11 @@ class NzbQueue:
return nzo_id_pos1
nzo.set_priority(priority)
if sabnzbd.scheduler.analyse(False, priority) and \
nzo.status in (Status.CHECKING, Status.DOWNLOADING, Status.QUEUED):
if sabnzbd.scheduler.analyse(False, priority) and nzo.status in (
Status.CHECKING,
Status.DOWNLOADING,
Status.QUEUED,
):
nzo.status = Status.PAUSED
elif nzo.status == Status.PAUSED:
nzo.status = Status.QUEUED
@ -620,7 +670,9 @@ class NzbQueue:
self.__nzo_list.append(nzo)
pos = 0
logging.info('Set priority=%s for job %s => position=%s ', priority, self.__nzo_table[nzo_id].final_name, pos)
logging.info(
"Set priority=%s for job %s => position=%s ", priority, self.__nzo_table[nzo_id].final_name, pos
)
return pos
except:
@ -630,7 +682,7 @@ class NzbQueue:
def set_priority(self, nzo_ids, priority):
try:
n = -1
for nzo_id in [item.strip() for item in nzo_ids.split(',')]:
for nzo_id in [item.strip() for item in nzo_ids.split(",")]:
n = self.__set_priority(nzo_id, priority)
return n
except:
@ -667,7 +719,11 @@ class NzbQueue:
# Not when queue paused and not a forced item
if nzo.status not in (Status.PAUSED, Status.GRABBING) or nzo.priority == TOP_PRIORITY:
# Check if past propagation delay, or forced
if not propagtion_delay or nzo.priority == TOP_PRIORITY or (nzo.avg_stamp + propagtion_delay) < time.time():
if (
not propagtion_delay
or nzo.priority == TOP_PRIORITY
or (nzo.avg_stamp + propagtion_delay) < time.time()
):
if not nzo.server_in_try_list(server):
article = nzo.get_article(server, servers)
if article:
@ -690,7 +746,7 @@ class NzbQueue:
articles_left, file_done, post_done = nzo.remove_article(article, success)
if nzo.is_gone():
logging.debug('Discarding article for file %s, no longer in queue', nzf.filename)
logging.debug("Discarding article for file %s, no longer in queue", nzf.filename)
else:
# Write data if file is done or at trigger time
if file_done or (articles_left and (articles_left % DIRECT_WRITE_TRIGGER) == 0):
@ -699,12 +755,12 @@ class NzbQueue:
# The type is only set if sabyenc could decode the article
if nzf.filename and nzf.type:
Assembler.do.process((nzo, nzf, file_done))
elif nzf.filename.lower().endswith('.par2'):
elif nzf.filename.lower().endswith(".par2"):
# Broken par2 file, try to get another one
nzo.promote_par2(nzf)
else:
if file_has_articles(nzf):
logging.warning(T('%s -> Unknown encoding'), nzf.filename)
logging.warning(T("%s -> Unknown encoding"), nzf.filename)
# Save bookkeeping in case of crash
if file_done and (nzo.next_save is None or time.time() > nzo.next_save):
@ -721,7 +777,7 @@ class NzbQueue:
def end_job(self, nzo):
""" Send NZO to the post-processing queue """
logging.info('[%s] Ending job %s', caller_name(), nzo.final_name)
logging.info("[%s] Ending job %s", caller_name(), nzo.final_name)
# Notify assembler to call postprocessor
if not nzo.deleted:
@ -808,7 +864,7 @@ class NzbQueue:
empty = []
for nzo in self.__nzo_list:
if not nzo.futuretype and not nzo.files and nzo.status not in (Status.PAUSED, Status.GRABBING):
logging.info('Found idle job %s', nzo.final_name)
logging.info("Found idle job %s", nzo.final_name)
empty.append(nzo)
# Stall prevention by checking if all servers are in the trylist
@ -818,11 +874,11 @@ class NzbQueue:
for nzf in nzo.files:
if len(nzf.try_list) == sabnzbd.downloader.Downloader.do.server_nr:
# We do not want to reset all article trylists, they are good
logging.info('Resetting bad trylist for file %s in job %s', nzf.filename, nzo.final_name)
logging.info("Resetting bad trylist for file %s in job %s", nzf.filename, nzo.final_name)
nzf.reset_try_list()
# Reset main trylist, minimal performance impact
logging.info('Resetting bad trylist for job %s', nzo.final_name)
logging.info("Resetting bad trylist for job %s", nzo.final_name)
nzo.reset_try_list()
for nzo in empty:
@ -859,7 +915,7 @@ class NzbQueue:
nzo = self.__nzo_table[nzo_id]
if nzo.futuretype:
url = nzo.url
if nzo.futuretype and url.lower().startswith('http'):
if nzo.futuretype and url.lower().startswith("http"):
lst.append((url, nzo))
return lst

624
sabnzbd/nzbstuff.py

File diff suppressed because it is too large

229
sabnzbd/osxmenu.py

@ -49,9 +49,9 @@ import sabnzbd.dirscanner as dirscanner
from sabnzbd.bpsmeter import BPSMeter
status_icons = {
'idle': 'icons/sabnzbd_osx_idle.tiff',
'pause': 'icons/sabnzbd_osx_pause.tiff',
'clicked': 'icons/sabnzbd_osx_clicked.tiff'
"idle": "icons/sabnzbd_osx_idle.tiff",
"pause": "icons/sabnzbd_osx_pause.tiff",
"clicked": "icons/sabnzbd_osx_clicked.tiff",
}
start_time = NSDate.date()
debug = 0
@ -71,7 +71,9 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] awake")
self.buildMenu()
# Timer for updating menu
self.timer = NSTimer.alloc().initWithFireDate_interval_target_selector_userInfo_repeats_(start_time, 3.0, self, 'updateAction:', None, True)
self.timer = NSTimer.alloc().initWithFireDate_interval_target_selector_userInfo_repeats_(
start_time, 3.0, self, "updateAction:", None, True
)
NSRunLoop.currentRunLoop().addTimer_forMode_(self.timer, NSDefaultRunLoopMode)
NSRunLoop.currentRunLoop().addTimer_forMode_(self.timer, NSEventTrackingRunLoopMode)
# NSRunLoop.currentRunLoop().addTimer_forMode_(self.timer, NSModalPanelRunLoopMode)
@ -86,15 +88,15 @@ class SABnzbdDelegate(NSObject):
icon_path = status_icons[icon]
if hasattr(sys, "frozen"):
# Path is modified for the binary
icon_path = os.path.join(os.path.dirname(sys.executable), '..', 'Resources', status_icons[icon])
icon_path = os.path.join(os.path.dirname(sys.executable), "..", "Resources", status_icons[icon])
self.icons[icon] = NSImage.alloc().initByReferencingFile_(icon_path)
if sabnzbd.DARWIN_VERSION > 9:
# Support for Yosemite Dark Mode
self.icons[icon].setTemplate_(YES)
self.status_item.setImage_(self.icons['idle'])
self.status_item.setAlternateImage_(self.icons['clicked'])
self.status_item.setImage_(self.icons["idle"])
self.status_item.setAlternateImage_(self.icons["clicked"])
self.status_item.setHighlightMode_(1)
self.status_item.setToolTip_('SABnzbd')
self.status_item.setToolTip_("SABnzbd")
self.status_item.setEnabled_(YES)
if debug == 1:
@ -125,7 +127,7 @@ class SABnzbdDelegate(NSObject):
self.menu = NSMenu.alloc().init()
try:
menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Dummy", '', '')
menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Dummy", "", "")
menu_item.setHidden_(YES)
self.isLeopard = 1
except:
@ -135,7 +137,9 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 3 construction")
# Warnings Item
self.warnings_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Warnings'), 'openBrowserAction:', '')
self.warnings_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
T("Warnings"), "openBrowserAction:", ""
)
if self.isLeopard:
self.warnings_menu_item.setHidden_(YES)
else:
@ -147,7 +151,9 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 4 warning added")
# State Item
self.state_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Idle'), 'openBrowserAction:', '')
self.state_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
T("Idle"), "openBrowserAction:", ""
)
self.state_menu_item.setRepresentedObject_("")
self.menu.addItem_(self.state_menu_item)
@ -155,7 +161,7 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 5 state added")
# Config Item
menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Configuration'), 'openBrowserAction:', '')
menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T("Configuration"), "openBrowserAction:", "")
menu_item.setRepresentedObject_("config/general/")
menu_item.setAlternate_(YES)
menu_item.setKeyEquivalentModifierMask_(NSAlternateKeyMask)
@ -165,7 +171,9 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 6 config added")
# Queue Item
self.queue_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Queue'), 'openBrowserAction:', '')
self.queue_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
T("Queue"), "openBrowserAction:", ""
)
self.queue_menu_item.setRepresentedObject_("")
self.menu.addItem_(self.queue_menu_item)
@ -173,7 +181,9 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 7 queue added")
# Purge Queue Item
self.purgequeue_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Purge Queue'), 'purgeAction:', '')
self.purgequeue_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
T("Purge Queue"), "purgeAction:", ""
)
self.purgequeue_menu_item.setRepresentedObject_("queue")
self.purgequeue_menu_item.setAlternate_(YES)
self.purgequeue_menu_item.setKeyEquivalentModifierMask_(NSAlternateKeyMask)
@ -183,7 +193,9 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 8 purge queue added")
# History Item
self.history_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('History'), 'openBrowserAction:', '')
self.history_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
T("History"), "openBrowserAction:", ""
)
self.history_menu_item.setRepresentedObject_("")
self.menu.addItem_(self.history_menu_item)
@ -191,7 +203,9 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 9 history added")
# Purge History Item
self.purgehistory_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Purge History'), 'purgeAction:', '')
self.purgehistory_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
T("Purge History"), "purgeAction:", ""
)
self.purgehistory_menu_item.setRepresentedObject_("history")
self.purgehistory_menu_item.setAlternate_(YES)
self.purgehistory_menu_item.setKeyEquivalentModifierMask_(NSAlternateKeyMask)
@ -204,16 +218,27 @@ class SABnzbdDelegate(NSObject):
self.menu.addItem_(self.separator_menu_item)
# Limit Speed Item & Submenu
self.speed_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Limit Speed'), '', '')
self.speed_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T("Limit Speed"), "", "")
self.menu_speed = NSMenu.alloc().init()
speeds = {10: '10%', 20: '20%', 30: '30%', 40: '40%', 50: '50%',
60: '60%', 70: '70%', 80: '80%', 90: '90%', 100: '100%'
}
speeds = {
10: "10%",
20: "20%",
30: "30%",
40: "40%",
50: "50%",
60: "60%",
70: "70%",
80: "80%",
90: "90%",
100: "100%",
}
for speed in sorted(speeds.keys()):
menu_speed_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_('%s' % (speeds[speed]), 'speedlimitAction:', '')
menu_speed_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
"%s" % (speeds[speed]), "speedlimitAction:", ""
)
menu_speed_item.setRepresentedObject_("%s" % speed)
self.menu_speed.addItem_(menu_speed_item)
@ -224,13 +249,15 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 11 limit speed added")
# Pause Item & Submenu
self.pause_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Pause'), 'pauseAction:', '')
self.pause_menu_item.setRepresentedObject_('0')
self.pause_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T("Pause"), "pauseAction:", "")
self.pause_menu_item.setRepresentedObject_("0")
self.menu_pause = NSMenu.alloc().init()
for i in range(6):
menu_pause_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("%s %s" % ((i + 1) * 10, T('min.')), 'pauseAction:', '')
menu_pause_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
"%s %s" % ((i + 1) * 10, T("min.")), "pauseAction:", ""
)
menu_pause_item.setRepresentedObject_("%s" % ((i + 1) * 10))
self.menu_pause.addItem_(menu_pause_item)
@ -241,7 +268,7 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 12 pause added")
# Resume Item
self.resume_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Resume'), 'resumeAction:', '')
self.resume_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T("Resume"), "resumeAction:", "")
if self.isLeopard:
self.resume_menu_item.setHidden_(YES)
else:
@ -252,7 +279,9 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 13 resume added")
# Watched folder Item
self.watched_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Scan watched folder'), 'watchedFolderAction:', '')
self.watched_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
T("Scan watched folder"), "watchedFolderAction:", ""
)
if self.isLeopard:
self.watched_menu_item.setHidden_(YES)
else:
@ -260,7 +289,9 @@ class SABnzbdDelegate(NSObject):
self.menu.addItem_(self.watched_menu_item)
# All RSS feeds
self.rss_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Read all RSS feeds'), 'rssAction:', '')
self.rss_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
T("Read all RSS feeds"), "rssAction:", ""
)
if self.isLeopard:
self.rss_menu_item.setHidden_(YES)
else:
@ -274,12 +305,16 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 14 watched folder added")
# Complete Folder Item
self.completefolder_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Complete Folder') + '\t\t\t', 'openFolderAction:', '')
self.completefolder_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
T("Complete Folder") + "\t\t\t", "openFolderAction:", ""
)
self.completefolder_menu_item.setRepresentedObject_(sabnzbd.cfg.complete_dir.get_path())
self.menu.addItem_(self.completefolder_menu_item)
# Incomplete Folder Item
self.incompletefolder_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Incomplete Folder') + '\t\t', 'openFolderAction:', '')
self.incompletefolder_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
T("Incomplete Folder") + "\t\t", "openFolderAction:", ""
)
self.incompletefolder_menu_item.setRepresentedObject_(sabnzbd.cfg.download_dir.get_path())
self.menu.addItem_(self.incompletefolder_menu_item)
@ -289,14 +324,15 @@ class SABnzbdDelegate(NSObject):
self.menu.addItem_(NSMenuItem.separatorItem())
# Set diagnostic menu
self.diagnostic_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Troubleshoot'), '', '')
self.diagnostic_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T("Troubleshoot"), "", "")
self.menu_diagnostic = NSMenu.alloc().init()
diag_items = ((T('Restart'), 'restartAction:'),
(T('Restart') + ' - 127.0.0.1:8080', 'restartSafeHost:'),
(T('Restart without login'), 'restartNoLogin:')
)
diag_items = (
(T("Restart"), "restartAction:"),
(T("Restart") + " - 127.0.0.1:8080", "restartSafeHost:"),
(T("Restart without login"), "restartNoLogin:"),
)
for item in diag_items:
menu_diag_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(item[0], item[1], '')
menu_diag_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(item[0], item[1], "")
menu_diag_item.setRepresentedObject_(item[0])
self.menu_diagnostic.addItem_(menu_diag_item)
@ -307,7 +343,7 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 16 Diagnostic added")
# Quit Item
menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Quit'), 'terminate:', '')
menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T("Quit"), "terminate:", "")
self.menu.addItem_(menu_item)
if debug == 1:
@ -361,7 +397,9 @@ class SABnzbdDelegate(NSObject):
if len(pnfo_list):
menu_queue_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Queue First 10 Items'), '', '')
menu_queue_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
T("Queue First 10 Items"), "", ""
)
self.menu_queue.addItem_(menu_queue_item)
self.menu_queue.addItem_(NSMenuItem.separatorItem())
@ -373,13 +411,17 @@ class SABnzbdDelegate(NSObject):
timeleft = self.calc_timeleft_(bytesleftprogess, BPSMeter.do.bps)
job = "%s\t(%d/%d MB) %s" % (pnfo.filename, bytesleft, bytes_total, timeleft)
menu_queue_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(job, '', '')
menu_queue_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(job, "", "")
self.menu_queue.addItem_(menu_queue_item)
self.info = "%d nzb(s)\t( %d / %d MB )" % (qnfo.q_size_list, (qnfo.bytes_left / MEBI), (qnfo.bytes / MEBI))
self.info = "%d nzb(s)\t( %d / %d MB )" % (
qnfo.q_size_list,
(qnfo.bytes_left / MEBI),
(qnfo.bytes / MEBI),
)
else:
menu_queue_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Empty'), '', '')
menu_queue_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T("Empty"), "", "")
self.menu_queue.addItem_(menu_queue_item)
self.queue_menu_item.setSubmenu_(self.menu_queue)
@ -395,33 +437,40 @@ class SABnzbdDelegate(NSObject):
items, fetched_items, _total_items = self.history_db.fetch_history(0, 10, None)
self.menu_history = NSMenu.alloc().init()
self.failedAttributes = {NSForegroundColorAttributeName: NSColor.redColor(), NSFontAttributeName: NSFont.menuFontOfSize_(14.0)}
self.failedAttributes = {
NSForegroundColorAttributeName: NSColor.redColor(),
NSFontAttributeName: NSFont.menuFontOfSize_(14.0),
}
menu_history_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('History Last 10 Items'), '', '')
menu_history_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
T("History Last 10 Items"), "", ""
)
self.menu_history.addItem_(menu_history_item)
self.menu_history.addItem_(NSMenuItem.separatorItem())
if fetched_items:
for history in items:
# logging.info("[osx] history : %s" % (history))
job = "%s" % (history['name'])
job = "%s" % (history["name"])
path = ""
if os.path.isdir(history['storage']) or os.path.isfile(history['storage']):
if os.path.isfile(history['storage']):
path = os.path.dirname(history['storage'])
if os.path.isdir(history["storage"]) or os.path.isfile(history["storage"]):
if os.path.isfile(history["storage"]):
path = os.path.dirname(history["storage"])
else:
path = history['storage']
path = history["storage"]
if path:
menu_history_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(job, 'openFolderAction:', '')
menu_history_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
job, "openFolderAction:", ""
)
else:
menu_history_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(job, '', '')
if history['status'] != Status.COMPLETED:
menu_history_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(job, "", "")
if history["status"] != Status.COMPLETED:
jobfailed = NSAttributedString.alloc().initWithString_attributes_(job, self.failedAttributes)
menu_history_item.setAttributedTitle_(jobfailed)
menu_history_item.setRepresentedObject_("%s" % path)
self.menu_history.addItem_(menu_history_item)
else:
menu_history_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Empty'), '', '')
menu_history_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T("Empty"), "", "")
self.menu_history.addItem_(menu_history_item)
self.history_menu_item.setSubmenu_(self.menu_history)
@ -434,10 +483,12 @@ class SABnzbdDelegate(NSObject):
if warnings:
warningsAttributes = {
NSForegroundColorAttributeName: NSColor.redColor(),
NSFontAttributeName: NSFont.menuFontOfSize_(14.0)
NSFontAttributeName: NSFont.menuFontOfSize_(14.0),
}
warningsTitle = NSAttributedString.alloc().initWithString_attributes_("%s : %s" % (T('Warnings'), warnings), warningsAttributes)
warningsTitle = NSAttributedString.alloc().initWithString_attributes_(
"%s : %s" % (T("Warnings"), warnings), warningsAttributes
)
self.warnings_menu_item.setAttributedTitle_(warningsTitle)
if self.isLeopard:
@ -445,7 +496,7 @@ class SABnzbdDelegate(NSObject):
else:
self.warnings_menu_item.setEnabled_(YES)
else:
self.warnings_menu_item.setTitle_("%s : 0" % (T('Warnings')))
self.warnings_menu_item.setTitle_("%s : 0" % (T("Warnings")))
if self.isLeopard:
self.warnings_menu_item.setHidden_(YES)
else:
@ -458,7 +509,7 @@ class SABnzbdDelegate(NSObject):
paused, bytes_left, bpsnow, time_left = fast_queue()
if paused:
self.state = T('Paused')
self.state = T("Paused")
if sabnzbd.scheduler.pause_int() != "0":
self.setMenuTitle_("\n\n%s\n" % (sabnzbd.scheduler.pause_int()))
else:
@ -467,8 +518,8 @@ class SABnzbdDelegate(NSObject):
self.state = ""
speed = to_units(bpsnow)
# "10.1 MB/s" doesn't fit, remove space char
if 'M' in speed and len(speed) > 5:
speed = speed.replace(' ', '')
if "M" in speed and len(speed) > 5:
speed = speed.replace(" ", "")
time_left = (bpsnow > 10 and time_left) or "------"
statusbarText = "\n\n%s\n%sB/s\n" % (time_left, speed)
@ -481,7 +532,7 @@ class SABnzbdDelegate(NSObject):
self.setMenuTitle_(statusbarText)
else:
self.state = T('Idle')
self.state = T("Idle")
self.setMenuTitle_("")
if self.state != "" and self.info != "":
@ -497,9 +548,9 @@ class SABnzbdDelegate(NSObject):
def iconUpdate(self):
try:
if sabnzbd.downloader.Downloader.do.paused:
self.status_item.setImage_(self.icons['pause'])
self.status_item.setImage_(self.icons["pause"])
else:
self.status_item.setImage_(self.icons['idle'])
self.status_item.setImage_(self.icons["idle"])
except:
logging.info("[osx] iconUpdate Exception %s" % (sys.exc_info()[0]))
@ -542,7 +593,7 @@ class SABnzbdDelegate(NSObject):
if sabnzbd.NEW_VERSION and self.version_notify:
# logging.info("[osx] New Version : %s" % (sabnzbd.NEW_VERSION))
new_release, _new_rel_url = sabnzbd.NEW_VERSION
notifier.send_notification("SABnzbd", "%s : %s" % (T('New release available'), new_release), 'other')
notifier.send_notification("SABnzbd", "%s : %s" % (T("New release available"), new_release), "other")
self.version_notify = 0
except:
logging.info("[osx] versionUpdate Exception %s" % (sys.exc_info()[0]))
@ -574,7 +625,7 @@ class SABnzbdDelegate(NSObject):
def serverUpdate(self):
try:
if not config.get_servers():
self.state_menu_item.setTitle_(T('Go to wizard'))
self.state_menu_item.setTitle_(T("Go to wizard"))
hide = YES
alternate = NO
value = 0
@ -622,8 +673,12 @@ class SABnzbdDelegate(NSObject):
def diskspaceUpdate(self):
try:
self.completefolder_menu_item.setTitle_("%s%.2f GB" % (T('Complete Folder') + '\t\t\t', diskspace()['complete_dir'][1]))
self.incompletefolder_menu_item.setTitle_("%s%.2f GB" % (T('Incomplete Folder') + '\t\t', diskspace()['download_dir'][1]))
self.completefolder_menu_item.setTitle_(
"%s%.2f GB" % (T("Complete Folder") + "\t\t\t", diskspace()["complete_dir"][1])
)
self.incompletefolder_menu_item.setTitle_(
"%s%.2f GB" % (T("Incomplete Folder") + "\t\t", diskspace()["download_dir"][1])
)
except:
logging.info("[osx] diskspaceUpdate Exception %s" % (sys.exc_info()[0]))
@ -648,7 +703,7 @@ class SABnzbdDelegate(NSObject):
NSBaselineOffsetAttributeName: 5.0,
NSFontAttributeName: NSFont.menuFontOfSize_(9.0),
NSParagraphStyleAttributeName: style
#,NSForegroundColorAttributeName: titleColor
# ,NSForegroundColorAttributeName: titleColor
}
title = NSAttributedString.alloc().initWithString_attributes_(text, titleAttributes)
@ -663,12 +718,12 @@ class SABnzbdDelegate(NSObject):
minutes, seconds = divmod(totalseconds, 60)
hours, minutes = divmod(minutes, 60)
if minutes < 10:
minutes = '0%s' % minutes
minutes = "0%s" % minutes
if seconds < 10:
seconds = '0%s' % seconds
return '%s:%s:%s' % (hours, minutes, seconds)
seconds = "0%s" % seconds
return "%s:%s:%s" % (hours, minutes, seconds)
except:
return '0:00:00'
return "0:00:00"
def openBrowserAction_(self, sender):
if sender.representedObject:
@ -681,7 +736,7 @@ class SABnzbdDelegate(NSObject):
# logging.info("[osx] speed limit to %s" % (sender.representedObject()))
speed = int(sender.representedObject())
if speed != self.speed:
sabnzbd.downloader.Downloader.do.limit_speed('%s%%' % speed)
sabnzbd.downloader.Downloader.do.limit_speed("%s%%" % speed)
self.speedlimitUpdate()
def purgeAction_(self, sender):
@ -717,38 +772,38 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] %@", folder2open)
os.system('open "%s"' % folder2open)
# def aboutAction_(self, sender):
# app = NSApplication.sharedApplication()
# app.orderFrontStandardAboutPanel_(nil)
# def aboutAction_(self, sender):
# app = NSApplication.sharedApplication()
# app.orderFrontStandardAboutPanel_(nil)
def restartAction_(self, sender):
self.setMenuTitle_("\n\n%s\n" % (T('Stopping...')))
logging.info('Restart requested by tray')
self.setMenuTitle_("\n\n%s\n" % (T("Stopping...")))
logging.info("Restart requested by tray")
sabnzbd.trigger_restart()
self.setMenuTitle_("\n\n%s\n" % (T('Stopping...')))
self.setMenuTitle_("\n\n%s\n" % (T("Stopping...")))
def restartSafeHost_(self, sender):
sabnzbd.cfg.cherryhost.set('127.0.0.1')
sabnzbd.cfg.cherryport.set('8080')
sabnzbd.cfg.cherryhost.set("127.0.0.1")
sabnzbd.cfg.cherryport.set("8080")
sabnzbd.cfg.enable_https.set(False)
sabnzbd.config.save_config()
self.setMenuTitle_("\n\n%s\n" % (T('Stopping...')))
self.setMenuTitle_("\n\n%s\n" % (T("Stopping...")))
sabnzbd.trigger_restart()
self.setMenuTitle_("\n\n%s\n" % (T('Stopping...')))
self.setMenuTitle_("\n\n%s\n" % (T("Stopping...")))
def restartNoLogin_(self, sender):
sabnzbd.cfg.username.set('')
sabnzbd.cfg.password.set('')
sabnzbd.cfg.username.set("")
sabnzbd.cfg.password.set("")
sabnzbd.config.save_config()
self.setMenuTitle_("\n\n%s\n" % (T('Stopping...')))
self.setMenuTitle_("\n\n%s\n" % (T("Stopping...")))
sabnzbd.trigger_restart()
self.setMenuTitle_("\n\n%s\n" % (T('Stopping...')))
self.setMenuTitle_("\n\n%s\n" % (T("Stopping...")))
def application_openFiles_(self, nsapp, filenames):
# logging.info('[osx] file open')
# logging.info('[osx] file : %s' % (filenames))
for name in filenames:
logging.info('[osx] receiving from OSX : %s', name)
logging.info("[osx] receiving from OSX : %s", name)
if os.path.exists(name):
fn = get_filename(name)
# logging.info('[osx] filename : %s' % (fn))
@ -762,8 +817,8 @@ class SABnzbdDelegate(NSObject):
# logging.info('opening done')
def applicationShouldTerminate_(self, sender):
logging.info('[osx] application terminating')
self.setMenuTitle_("\n\n%s\n" % (T('Stopping...')))
logging.info("[osx] application terminating")
self.setMenuTitle_("\n\n%s\n" % (T("Stopping...")))
self.status_item.setHighlightMode_(NO)
self.osx_icon = False
sabnzbd.shutdown_program()

131
sabnzbd/panic.py

@ -42,10 +42,13 @@ PANIC_HOST = 8
def MSG_BAD_NEWS():
return r'''
return (
r"""
<html>
<head>
<title>''' + T('Problem with') + ''' %s %s</title>
<title>"""
+ T("Problem with")
+ """ %s %s</title>
</head>
<body>
<h1><font color="#0000FF"> %s %s</font></h1>
@ -57,83 +60,100 @@ def MSG_BAD_NEWS():
<br>%s<br>
</body>
</html>
'''
"""
)
def MSG_BAD_PORT():
return T(r'''
return (
T(
r"""
SABnzbd needs a free tcp/ip port for its internal web server.<br>
Port %s on %s was tried , but it is not available.<br>
Some other software uses the port or SABnzbd is already running.<br>
<br>
Please restart SABnzbd with a different port number.''') + \
'''<br>
Please restart SABnzbd with a different port number."""
)
+ """<br>
<br>
%s<br>
&nbsp;&nbsp;&nbsp;&nbsp;%s --server %s:%s<br>
<br>''' + \
T(r'If you get this error message again, please try a different number.<br>')
<br>"""
+ T(r"If you get this error message again, please try a different number.<br>")
)
def MSG_BAD_HOST():
return T(r'''
return (
T(
r"""
SABnzbd needs a valid host address for its internal web server.<br>
You have specified an invalid address.<br>
Safe values are <b>localhost</b> and <b>0.0.0.0</b><br>
<br>
Please restart SABnzbd with a proper host address.''') + \
'''<br>
Please restart SABnzbd with a proper host address."""
)
+ """<br>
<br>
%s<br>
&nbsp;&nbsp;&nbsp;&nbsp;%s --server %s:%s<br>
<br>
'''
"""
)
def MSG_BAD_QUEUE():
return T(r'''
return (
T(
r"""
SABnzbd detected saved data from an other SABnzbd version<br>
but cannot re-use the data of the other program.<br><br>
You may want to finish your queue first with the other program.<br><br>
After that, start this program with the "--clean" option.<br>
This will erase the current queue and history!<br>
SABnzbd read the file "%s".''') + \
'''<br>
SABnzbd read the file "%s"."""
)
+ """<br>
<br>
%s<br>
&nbsp;&nbsp;&nbsp;&nbsp;%s --clean<br>
<br>
'''
"""
)
def MSG_BAD_TEMPL():
return T(r'''
return T(
r"""
SABnzbd cannot find its web interface files in %s.<br>
Please install the program again.<br>
<br>
''')
"""
)
def MSG_OTHER():
return T('SABnzbd detected a fatal error:') + '<br>%s<br><br>%s<br>'
return T("SABnzbd detected a fatal error:") + "<br>%s<br><br>%s<br>"
def MSG_SQLITE():
return T(r'''
return T(
r"""
SABnzbd detected that the file sqlite3.dll is missing.<br><br>
Some poorly designed virus-scanners remove this file.<br>
Please check your virus-scanner, try to re-install SABnzbd and complain to your virus-scanner vendor.<br>
<br>
''')
"""
)
def panic_message(panic_code, a=None, b=None):
""" Create the panic message from templates """
if sabnzbd.WIN32:
os_str = T('Press Startkey+R and type the line (example):')
os_str = T("Press Startkey+R and type the line (example):")
prog_path = '"%s"' % sabnzbd.MY_FULLNAME
else:
os_str = T('Open a Terminal window and type the line (example):')
os_str = T("Open a Terminal window and type the line (example):")
prog_path = sabnzbd.MY_FULLNAME
if panic_code == PANIC_PORT:
@ -147,27 +167,40 @@ def panic_message(panic_code, a=None, b=None):
elif panic_code == PANIC_SQLITE:
msg = MSG_SQLITE()
elif panic_code == PANIC_HOST:
msg = MSG_BAD_HOST() % (os_str, prog_path, 'localhost', b)
msg = MSG_BAD_HOST() % (os_str, prog_path, "localhost", b)
else:
msg = MSG_OTHER() % (a, b)
msg = MSG_BAD_NEWS() % (sabnzbd.MY_NAME, sabnzbd.__version__, sabnzbd.MY_NAME, sabnzbd.__version__,
msg, T('Program did not start!'))
msg = MSG_BAD_NEWS() % (
sabnzbd.MY_NAME,
sabnzbd.__version__,
sabnzbd.MY_NAME,
sabnzbd.__version__,
msg,
T("Program did not start!"),
)
if sabnzbd.WIN_SERVICE:
sabnzbd.WIN_SERVICE.ErrLogger('Panic exit', msg)
sabnzbd.WIN_SERVICE.ErrLogger("Panic exit", msg)
if (not cfg.autobrowser()) or sabnzbd.DAEMON:
return
msgfile, url = tempfile.mkstemp(suffix='.html')
msgfile, url = tempfile.mkstemp(suffix=".html")
os.write(msgfile, utob(msg))
os.close(msgfile)
return url
def panic_port(host, port):
show_error_dialog("\n%s:\n %s" % (T('Fatal error'), T('Unable to bind to port %s on %s. Some other software uses the port or SABnzbd is already running.') % (port, host)))
show_error_dialog(
"\n%s:\n %s"
% (
T("Fatal error"),
T("Unable to bind to port %s on %s. Some other software uses the port or SABnzbd is already running.")
% (port, host),
)
)
launch_a_browser(panic_message(PANIC_PORT, host, port))
@ -184,7 +217,7 @@ def panic_tmpl(name):
def panic(reason, remedy=""):
show_error_dialog("\n%s:\n %s\n%s" % (T('Fatal error'), reason, remedy))
show_error_dialog("\n%s:\n %s\n%s" % (T("Fatal error"), reason, remedy))
launch_a_browser(panic_message(PANIC_OTHER, reason, remedy))
@ -193,26 +226,26 @@ def launch_a_browser(url, force=False):
if not force and not cfg.autobrowser() or sabnzbd.DAEMON:
return
if '::1' in url and '[::1]' not in url:
if "::1" in url and "[::1]" not in url:
# Get around idiosyncrasy in Python runtime
url = url.replace('::1', '[::1]')
url = url.replace("::1", "[::1]")
if cfg.enable_https() and not cfg.https_port.get_int():
# Must use https, because http is not available
url = url.replace('http:', 'https:')
url = url.replace("http:", "https:")
if 'localhost' in url and not cfg.ipv6_hosting():
url = url.replace('localhost', '127.0.0.1')
if "localhost" in url and not cfg.ipv6_hosting():
url = url.replace("localhost", "127.0.0.1")
logging.info("Launching browser with %s", url)
try:
if url and not url.startswith('http'):
url = 'file:///%s' % url
if url and not url.startswith("http"):
url = "file:///%s" % url
if webbrowser:
webbrowser.open(url, 2, 1)
else:
logging.info('Not showing panic message in webbrowser, no support found')
logging.info("Not showing panic message in webbrowser, no support found")
except:
logging.warning(T('Cannot launch the browser, probably not found'))
logging.warning(T("Cannot launch the browser, probably not found"))
logging.info("Traceback: ", exc_info=True)
@ -221,15 +254,15 @@ def show_error_dialog(msg):
Windows-only, otherwise only print to console
"""
if sabnzbd.WIN32:
ctypes.windll.user32.MessageBoxW(0, msg, T('Fatal error'), 0)
ctypes.windll.user32.MessageBoxW(0, msg, T("Fatal error"), 0)
print(msg)
def error_page_401(status, message, traceback, version):
""" Custom handler for 401 error """
title = T('Access denied')
body = T('Error %s: You need to provide a valid username and password.') % status
return r'''
title = T("Access denied")
body = T("Error %s: You need to provide a valid username and password.") % status
return r"""
<html>
<head>
<title>%s</title>
@ -239,12 +272,16 @@ def error_page_401(status, message, traceback, version):
<font color="#0000FF">%s</font>
</body>
</html>
''' % (title, body)
""" % (
title,
body,
)
def error_page_404(status, message, traceback, version):
""" Custom handler for 404 error, redirect to main page """
return r'''
return (
r"""
<html>
<head>
<script type="text/javascript">
@ -255,4 +292,6 @@ def error_page_404(status, message, traceback, version):
</head>
<body><br/></body>
</html>
''' % cfg.url_base()
"""
% cfg.url_base()
)

92
sabnzbd/powersup.py

@ -51,7 +51,7 @@ def win_hibernate():
win_power_privileges()
win32api.SetSystemPowerState(False, True)
except:
logging.error(T('Failed to hibernate system'))
logging.error(T("Failed to hibernate system"))
logging.info("Traceback: ", exc_info=True)
@ -61,7 +61,7 @@ def win_standby():
win_power_privileges()
win32api.SetSystemPowerState(True, True)
except:
logging.error(T('Failed to standby system'))
logging.error(T("Failed to standby system"))
logging.info("Traceback: ", exc_info=True)
@ -78,12 +78,13 @@ def win_shutdown():
# Power management for OSX
##############################################################################
def osx_shutdown():
""" Shutdown OSX system, never returns """
try:
subprocess.call(['osascript', '-e', 'tell app "System Events" to shut down'])
subprocess.call(["osascript", "-e", 'tell app "System Events" to shut down'])
except:
logging.error(T('Error while shutting down system'))
logging.error(T("Error while shutting down system"))
logging.info("Traceback: ", exc_info=True)
os._exit(0)
@ -91,10 +92,10 @@ def osx_shutdown():
def osx_standby():
""" Make OSX system sleep, returns after wakeup """
try:
subprocess.call(['osascript', '-e', 'tell app "System Events" to sleep'])
subprocess.call(["osascript", "-e", 'tell app "System Events" to sleep'])
time.sleep(10)
except:
logging.error(T('Failed to standby system'))
logging.error(T("Failed to standby system"))
logging.info("Traceback: ", exc_info=True)
@ -119,20 +120,21 @@ def osx_hibernate():
try:
import dbus
HAVE_DBUS = True
except ImportError:
HAVE_DBUS = False
_IS_NOT_INTERACTIVE = False
_LOGIND_SUCCESSFUL_RESULT = 'yes'
_LOGIND_SUCCESSFUL_RESULT = "yes"
def _get_sessionproxy():
""" Return (proxy-object, interface), (None, None) if not available """
name = 'org.freedesktop.PowerManagement'
path = '/org/freedesktop/PowerManagement'
interface = 'org.freedesktop.PowerManagement'
name = "org.freedesktop.PowerManagement"
path = "/org/freedesktop/PowerManagement"
interface = "org.freedesktop.PowerManagement"
try:
bus = dbus.SessionBus()
return bus.get_object(name, path), interface
@ -142,31 +144,31 @@ def _get_sessionproxy():
def _get_systemproxy(method):
""" Return (proxy-object, interface, pinterface), (None, None, None) if not available """
if method == 'ConsoleKit':
name = 'org.freedesktop.ConsoleKit'
path = '/org/freedesktop/ConsoleKit/Manager'
interface = 'org.freedesktop.ConsoleKit.Manager'
if method == "ConsoleKit":
name = "org.freedesktop.ConsoleKit"
path = "/org/freedesktop/ConsoleKit/Manager"
interface = "org.freedesktop.ConsoleKit.Manager"
pinterface = None
elif method == 'DeviceKit':
name = 'org.freedesktop.DeviceKit.Power'
path = '/org/freedesktop/DeviceKit/Power'
interface = 'org.freedesktop.DeviceKit.Power'
pinterface = 'org.freedesktop.DBus.Properties'
elif method == 'UPower':
name = 'org.freedesktop.UPower'
path = '/org/freedesktop/UPower'
interface = 'org.freedesktop.UPower'
pinterface = 'org.freedesktop.DBus.Properties'
elif method == 'Logind':
name = 'org.freedesktop.login1'
path = '/org/freedesktop/login1'
interface = 'org.freedesktop.login1.Manager'
elif method == "DeviceKit":
name = "org.freedesktop.DeviceKit.Power"
path = "/org/freedesktop/DeviceKit/Power"
interface = "org.freedesktop.DeviceKit.Power"
pinterface = "org.freedesktop.DBus.Properties"
elif method == "UPower":
name = "org.freedesktop.UPower"
path = "/org/freedesktop/UPower"
interface = "org.freedesktop.UPower"
pinterface = "org.freedesktop.DBus.Properties"
elif method == "Logind":
name = "org.freedesktop.login1"
path = "/org/freedesktop/login1"
interface = "org.freedesktop.login1.Manager"
pinterface = None
try:
bus = dbus.SystemBus()
return bus.get_object(name, path), interface, pinterface
except dbus.exceptions.DBusException as msg:
logging.info('DBus not reachable (%s)', msg)
logging.info("DBus not reachable (%s)", msg)
return None, None, None
@ -182,19 +184,19 @@ def linux_shutdown():
if proxy.CanShutdown():
proxy.Shutdown(dbus_interface=interface)
else:
proxy, interface, pinterface = _get_systemproxy('Logind')
proxy, interface, pinterface = _get_systemproxy("Logind")
if proxy:
if proxy.CanPowerOff(dbus_interface=interface) == _LOGIND_SUCCESSFUL_RESULT:
proxy.PowerOff(_IS_NOT_INTERACTIVE, dbus_interface=interface)
else:
proxy, interface, _pinterface = _get_systemproxy('ConsoleKit')
proxy, interface, _pinterface = _get_systemproxy("ConsoleKit")
if proxy:
if proxy.CanStop(dbus_interface=interface):
proxy.Stop(dbus_interface=interface)
else:
logging.info('DBus does not support Stop (shutdown)')
logging.info("DBus does not support Stop (shutdown)")
except dbus.exceptions.DBusException as msg:
logging.error('Received a DBus exception %s', msg)
logging.error("Received a DBus exception %s", msg)
os._exit(0)
@ -209,22 +211,22 @@ def linux_hibernate():
if proxy.CanHibernate():
proxy.Hibernate(dbus_interface=interface)
else:
proxy, interface, pinterface = _get_systemproxy('Logind')
proxy, interface, pinterface = _get_systemproxy("Logind")
if proxy:
if proxy.CanHibernate(dbus_interface=interface) == _LOGIND_SUCCESSFUL_RESULT:
proxy.Hibernate(_IS_NOT_INTERACTIVE, dbus_interface=interface)
else:
proxy, interface, pinterface = _get_systemproxy('UPower')
proxy, interface, pinterface = _get_systemproxy("UPower")
if not proxy:
proxy, interface, pinterface = _get_systemproxy('DeviceKit')
proxy, interface, pinterface = _get_systemproxy("DeviceKit")
if proxy:
if proxy.Get(interface, 'can-hibernate', dbus_interface=pinterface):
if proxy.Get(interface, "can-hibernate", dbus_interface=pinterface):
proxy.Hibernate(dbus_interface=interface)
else:
logging.info('DBus does not support Hibernate')
logging.info("DBus does not support Hibernate")
time.sleep(10)
except dbus.exceptions.DBusException as msg:
logging.error('Received a DBus exception %s', msg)
logging.error("Received a DBus exception %s", msg)
def linux_standby():
@ -238,19 +240,19 @@ def linux_standby():
if proxy.CanSuspend():
proxy.Suspend(dbus_interface=interface)
else:
proxy, interface, pinterface = _get_systemproxy('Logind')
proxy, interface, pinterface = _get_systemproxy("Logind")
if proxy:
if proxy.CanSuspend(dbus_interface=interface) == _LOGIND_SUCCESSFUL_RESULT:
proxy.Suspend(_IS_NOT_INTERACTIVE, dbus_interface=interface)
else:
proxy, interface, pinterface = _get_systemproxy('UPower')
proxy, interface, pinterface = _get_systemproxy("UPower")
if not proxy:
proxy, interface, pinterface = _get_systemproxy('DeviceKit')
proxy, interface, pinterface = _get_systemproxy("DeviceKit")
if proxy:
if proxy.Get(interface, 'can-suspend', dbus_interface=pinterface):
if proxy.Get(interface, "can-suspend", dbus_interface=pinterface):
proxy.Suspend(dbus_interface=interface)
else:
logging.info('DBus does not support Suspend (standby)')
logging.info("DBus does not support Suspend (standby)")
time.sleep(10)
except dbus.exceptions.DBusException as msg:
logging.error('Received a DBus exception %s', msg)
logging.error("Received a DBus exception %s", msg)

153
sabnzbd/rating.py

@ -36,11 +36,14 @@ class OrderedSetQueue(queue.Queue):
def _init(self, maxsize):
self.maxsize = maxsize
self.queue = collections.OrderedDict()
def _put(self, item):
self.queue[item] = None
def _get(self):
return self.queue.popitem()[0]
_RATING_URL = "/releaseRatings/releaseRatings.php"
RATING_LOCK = RLock()
@ -60,7 +63,6 @@ def _reset_warn():
class NzbRating:
def __init__(self):
self.avg_video = 0
self.avg_video_cnt = 0
@ -77,7 +79,6 @@ class NzbRating:
class NzbRatingV2(NzbRating):
def __init__(self):
super(NzbRatingV2, self).__init__()
self.avg_spam_cnt = 0
@ -116,8 +117,9 @@ class Rating(Thread):
self.shutdown = False
self.queue = OrderedSetQueue()
try:
self.version, self.ratings, self.nzo_indexer_map = sabnzbd.load_admin("Rating.sab",
silent=not cfg.rating_enable())
self.version, self.ratings, self.nzo_indexer_map = sabnzbd.load_admin(
"Rating.sab", silent=not cfg.rating_enable()
)
if self.version == 1:
ratings = {}
for k, v in self.ratings.items():
@ -152,7 +154,7 @@ class Rating(Thread):
self.queue.put(indexer_id)
except:
pass
logging.debug('Stopping ratings')
logging.debug("Stopping ratings")
@synchronized(RATING_LOCK)
def save(self):
@ -163,32 +165,44 @@ class Rating(Thread):
@synchronized(RATING_LOCK)
def add_rating(self, indexer_id, nzo_id, fields):
if indexer_id and nzo_id:
logging.debug('Add rating (%s, %s: %s, %s, %s, %s)', indexer_id, nzo_id, fields['video'], fields['audio'], fields['voteup'], fields['votedown'])
logging.debug(
"Add rating (%s, %s: %s, %s, %s, %s)",
indexer_id,
nzo_id,
fields["video"],
fields["audio"],
fields["voteup"],
fields["votedown"],
)
try:
rating = self.ratings.get(indexer_id, NzbRatingV2())
if fields['video'] and fields['videocnt']:
rating.avg_video = int(float(fields['video']))
rating.avg_video_cnt = int(float(fields['videocnt']))
if fields['audio'] and fields['audiocnt']:
rating.avg_audio = int(float(fields['audio']))
rating.avg_audio_cnt = int(float(fields['audiocnt']))
if fields['voteup']:
rating.avg_vote_up = int(float(fields['voteup']))
if fields['votedown']:
rating.avg_vote_down = int(float(fields['votedown']))
if fields['spam']:
rating.avg_spam_cnt = int(float(fields['spam']))
if fields['confirmed-spam']:
rating.avg_spam_confirm = (fields['confirmed-spam'].lower() == 'yes')
if fields['passworded']:
rating.avg_encrypted_cnt = int(float(fields['passworded']))
if fields['confirmed-passworded']:
rating.avg_encrypted_confirm = (fields['confirmed-passworded'].lower() == 'yes')
if fields["video"] and fields["videocnt"]:
rating.avg_video = int(float(fields["video"]))
rating.avg_video_cnt = int(float(fields["videocnt"]))
if fields["audio"] and fields["audiocnt"]:
rating.avg_audio = int(float(fields["audio"]))
rating.avg_audio_cnt = int(float(fields["audiocnt"]))
if fields["voteup"]:
rating.avg_vote_up = int(float(fields["voteup"]))
if fields["votedown"]:
rating.avg_vote_down = int(float(fields["votedown"]))
if fields["spam"]:
rating.avg_spam_cnt = int(float(fields["spam"]))
if fields["confirmed-spam"]:
rating.avg_spam_confirm = fields["confirmed-spam"].lower() == "yes"
if fields["passworded"]:
rating.avg_encrypted_cnt = int(float(fields["passworded"]))
if fields["confirmed-passworded"]:
rating.avg_encrypted_confirm = fields["confirmed-passworded"].lower() == "yes"
# Indexers can supply a full URL or just a host
if fields['host']:
rating.host = fields['host'][0] if fields['host'] and isinstance(fields['host'], list) else fields['host']
if fields['url']:
rating.host = fields['url'][0] if fields['url'] and isinstance(fields['url'], list) else fields['url']
if fields["host"]:
rating.host = (
fields["host"][0] if fields["host"] and isinstance(fields["host"], list) else fields["host"]
)
if fields["url"]:
rating.host = (
fields["url"][0] if fields["url"] and isinstance(fields["url"], list) else fields["url"]
)
self.ratings[indexer_id] = rating
self.nzo_indexer_map[nzo_id] = indexer_id
except:
@ -196,22 +210,26 @@ class Rating(Thread):
@synchronized(RATING_LOCK)
def update_user_rating(self, nzo_id, video, audio, vote, flag, flag_detail=None):
logging.debug('Updating user rating (%s: %s, %s, %s, %s)', nzo_id, video, audio, vote, flag)
logging.debug("Updating user rating (%s: %s, %s, %s, %s)", nzo_id, video, audio, vote, flag)
if nzo_id not in self.nzo_indexer_map:
logging.warning(T('Indexer id (%s) not found for ratings file'), nzo_id)
logging.warning(T("Indexer id (%s) not found for ratings file"), nzo_id)
return
indexer_id = self.nzo_indexer_map[nzo_id]
rating = self.ratings[indexer_id]
if video:
rating.user_video = int(video)
rating.avg_video = int((rating.avg_video_cnt * rating.avg_video + rating.user_video) / (rating.avg_video_cnt + 1))
rating.avg_video = int(
(rating.avg_video_cnt * rating.avg_video + rating.user_video) / (rating.avg_video_cnt + 1)
)
rating.changed = rating.changed | Rating.CHANGED_USER_VIDEO
if audio:
rating.user_audio = int(audio)
rating.avg_audio = int((rating.avg_audio_cnt * rating.avg_audio + rating.user_audio) / (rating.avg_audio_cnt + 1))
rating.avg_audio = int(
(rating.avg_audio_cnt * rating.avg_audio + rating.user_audio) / (rating.avg_audio_cnt + 1)
)
rating.changed = rating.changed | Rating.CHANGED_USER_AUDIO
if flag:
rating.user_flag = {'val': int(flag), 'detail': flag_detail}
rating.user_flag = {"val": int(flag), "detail": flag_detail}
rating.changed = rating.changed | Rating.CHANGED_USER_FLAG
if vote:
rating.changed = rating.changed | Rating.CHANGED_USER_VOTE
@ -233,10 +251,10 @@ class Rating(Thread):
def update_auto_flag(self, nzo_id, flag, flag_detail=None):
if not flag or not cfg.rating_enable() or (nzo_id not in self.nzo_indexer_map):
return
logging.debug('Updating auto flag (%s: %s)', nzo_id, flag)
logging.debug("Updating auto flag (%s: %s)", nzo_id, flag)
indexer_id = self.nzo_indexer_map[nzo_id]
rating = self.ratings[indexer_id]
rating.auto_flag = {'val': int(flag), 'detail': flag_detail}
rating.auto_flag = {"val": int(flag), "detail": flag_detail}
rating.changed = rating.changed | Rating.CHANGED_AUTO_FLAG
self.queue.put(indexer_id)
@ -252,73 +270,86 @@ class Rating(Thread):
def _flag_request(self, val, flag_detail, auto):
if val == Rating.FLAG_SPAM:
return {'m': 'rs', 'auto': auto}
return {"m": "rs", "auto": auto}
if val == Rating.FLAG_ENCRYPTED:
return {'m': 'rp', 'auto': auto}
return {"m": "rp", "auto": auto}
if val == Rating.FLAG_EXPIRED:
expired_host = flag_detail if flag_detail and len(flag_detail) > 0 else 'Other'
return {'m': 'rpr', 'pr': expired_host, 'auto': auto}
expired_host = flag_detail if flag_detail and len(flag_detail) > 0 else "Other"
return {"m": "rpr", "pr": expired_host, "auto": auto}
if (val == Rating.FLAG_OTHER) and flag_detail and len(flag_detail) > 0:
return {'m': 'o', 'r': flag_detail}
return {"m": "o", "r": flag_detail}
if (val == Rating.FLAG_COMMENT) and flag_detail and len(flag_detail) > 0:
return {'m': 'rc', 'r': flag_detail}
return {"m": "rc", "r": flag_detail}
def _send_rating(self, indexer_id):
logging.debug('Updating indexer rating (%s)', indexer_id)
logging.debug("Updating indexer rating (%s)", indexer_id)
api_key = cfg.rating_api_key()
rating_host = cfg.rating_host()
rating_url = _RATING_URL
requests = []
_headers = {'User-agent': 'SABnzbd+/%s' % sabnzbd.version.__version__, 'Content-type': 'application/x-www-form-urlencoded'}
rating = self._get_rating_by_indexer(indexer_id) # Requesting info here ensures always have latest information even on retry
if hasattr(rating, 'host') and rating.host:
_headers = {
"User-agent": "SABnzbd+/%s" % sabnzbd.version.__version__,
"Content-type": "application/x-www-form-urlencoded",
}
rating = self._get_rating_by_indexer(
indexer_id
) # Requesting info here ensures always have latest information even on retry
if hasattr(rating, "host") and rating.host:
host_parsed = urllib.parse.urlparse(rating.host)
rating_host = host_parsed.netloc
# Is it an URL or just a HOST?
if host_parsed.path and host_parsed.path != '/':
rating_url = host_parsed.path + '?' + host_parsed.query if host_parsed.query else host_parsed.path
if host_parsed.path and host_parsed.path != "/":
rating_url = host_parsed.path + "?" + host_parsed.query if host_parsed.query else host_parsed.path
if not rating_host:
_warn('%s: %s' % (T('Cannot send, missing required data'), T('Server address')))
_warn("%s: %s" % (T("Cannot send, missing required data"), T("Server address")))
return True
if not api_key:
_warn('%s [%s]: %s - %s' % (T('Cannot send, missing required data'), rating_host, T('API Key'), T('This key provides identity to indexer. Check your profile on the indexer\'s website.')))
_warn(
"%s [%s]: %s - %s"
% (
T("Cannot send, missing required data"),
rating_host,
T("API Key"),
T("This key provides identity to indexer. Check your profile on the indexer's website."),
)
)
return True
if rating.changed & Rating.CHANGED_USER_VIDEO:
requests.append({'m': 'r', 'r': 'videoQuality', 'rn': rating.user_video})
requests.append({"m": "r", "r": "videoQuality", "rn": rating.user_video})
if rating.changed & Rating.CHANGED_USER_AUDIO:
requests.append({'m': 'r', 'r': 'audioQuality', 'rn': rating.user_audio})
requests.append({"m": "r", "r": "audioQuality", "rn": rating.user_audio})
if rating.changed & Rating.CHANGED_USER_VOTE:
up_down = 'up' if rating.user_vote == Rating.VOTE_UP else 'down'
requests.append({'m': 'v', 'v': up_down, 'r': 'overall'})
up_down = "up" if rating.user_vote == Rating.VOTE_UP else "down"
requests.append({"m": "v", "v": up_down, "r": "overall"})
if rating.changed & Rating.CHANGED_USER_FLAG:
requests.append(self._flag_request(rating.user_flag.get('val'), rating.user_flag.get('detail'), 0))
requests.append(self._flag_request(rating.user_flag.get("val"), rating.user_flag.get("detail"), 0))
if rating.changed & Rating.CHANGED_AUTO_FLAG:
requests.append(self._flag_request(rating.auto_flag.get('val'), rating.auto_flag.get('detail'), 1))
requests.append(self._flag_request(rating.auto_flag.get("val"), rating.auto_flag.get("detail"), 1))
try:
conn = http.client.HTTPSConnection(rating_host)
for request in [r for r in requests if r is not None]:
if api_key:
request['apikey'] = api_key
request['i'] = indexer_id
conn.request('POST', rating_url, urllib.parse.urlencode(request), headers=_headers)
request["apikey"] = api_key
request["i"] = indexer_id
conn.request("POST", rating_url, urllib.parse.urlencode(request), headers=_headers)
response = conn.getresponse()
response.read()
if response.status == http.client.UNAUTHORIZED:
_warn('Ratings server unauthorized user')
_warn("Ratings server unauthorized user")
return False
elif response.status != http.client.OK:
_warn('Ratings server failed to process request (%s, %s)' % (response.status, response.reason))
_warn("Ratings server failed to process request (%s, %s)" % (response.status, response.reason))
return False
self.ratings[indexer_id].changed = self.ratings[indexer_id].changed & ~rating.changed
_reset_warn()
return True
except:
_warn('Problem accessing ratings server: %s' % rating_host)
_warn("Problem accessing ratings server: %s" % rating_host)
return False

90
sabnzbd/sabtray.py

@ -37,48 +37,60 @@ from sabnzbd.utils.systrayiconthread import SysTrayIconThread
class SABTrayThread(SysTrayIconThread):
sabicons = {
'default': 'icons/sabnzbd16_32.ico',
'green': 'icons/sabnzbd16_32green.ico',
'pause': 'icons/sabnzbd16_32paused.ico'
"default": "icons/sabnzbd16_32.ico",
"green": "icons/sabnzbd16_32green.ico",
"pause": "icons/sabnzbd16_32paused.ico",
}
def __init__(self):
# Wait for translated texts to be loaded
while not sabnzbd.WEBUI_READY:
sleep(0.2)
logging.debug('language file not loaded, waiting')
logging.debug("language file not loaded, waiting")
self.sabpaused = False
self.counter = 0
self.set_texts()
menu_options = (
(T('Show interface'), None, self.browse),
(T('Open complete folder'), None, self.opencomplete),
('SEPARATOR', None, None),
(T('Pause') + '/' + T('Resume'), None, self.pauseresume),
(T('Pause for'), None, ((T('Pause for 5 minutes'), None, self.pausefor5min),
(T('Pause for 15 minutes'), None, self.pausefor15min),
(T('Pause for 30 minutes'), None, self.pausefor30min),
(T('Pause for 1 hour'), None, self.pausefor1hour),
(T('Pause for 3 hours'), None, self.pausefor3hour),
(T('Pause for 6 hours'), None, self.pausefor6hour))),
('SEPARATOR', None, None),
(T('Read all RSS feeds'), None, self.rss),
('SEPARATOR', None, None),
(T('Troubleshoot'), None, ((T('Restart'), None, self.restart_sab),
(T('Restart without login'), None, self.nologin),
(T('Restart') + ' - 127.0.0.1:8080', None, self.defhost))),
(T('Shutdown'), None, self.shutdown),
(T("Show interface"), None, self.browse),
(T("Open complete folder"), None, self.opencomplete),
("SEPARATOR", None, None),
(T("Pause") + "/" + T("Resume"), None, self.pauseresume),
(
T("Pause for"),
None,
(
(T("Pause for 5 minutes"), None, self.pausefor5min),
(T("Pause for 15 minutes"), None, self.pausefor15min),
(T("Pause for 30 minutes"), None, self.pausefor30min),
(T("Pause for 1 hour"), None, self.pausefor1hour),
(T("Pause for 3 hours"), None, self.pausefor3hour),
(T("Pause for 6 hours"), None, self.pausefor6hour),
),
),
("SEPARATOR", None, None),
(T("Read all RSS feeds"), None, self.rss),
("SEPARATOR", None, None),
(
T("Troubleshoot"),
None,
(
(T("Restart"), None, self.restart_sab),
(T("Restart without login"), None, self.nologin),
(T("Restart") + " - 127.0.0.1:8080", None, self.defhost),
),
),
(T("Shutdown"), None, self.shutdown),
)
SysTrayIconThread.__init__(self, self.sabicons['default'], "SABnzbd", menu_options, None, 0, "SabTrayIcon")
SysTrayIconThread.__init__(self, self.sabicons["default"], "SABnzbd", menu_options, None, 0, "SabTrayIcon")
def set_texts(self):
""" Cache texts for performance, doUpdates is called often """
self.txt_idle = T('Idle')
self.txt_paused = T('Paused')
self.txt_remaining = T('Remaining')
self.txt_idle = T("Idle")
self.txt_paused = T("Paused")
self.txt_remaining = T("Remaining")
# called every few ms by SysTrayIconThread
def doUpdates(self):
@ -94,13 +106,13 @@ class SABTrayThread(SysTrayIconThread):
self.hover_text = "%s - %s: %sB" % (self.txt_paused, self.txt_remaining, mb_left)
else:
self.hover_text = self.txt_paused
self.icon = self.sabicons['pause']
self.icon = self.sabicons["pause"]
elif bytes_left > 0:
self.hover_text = "%sB/s - %s: %sB (%s)" % (speed, self.txt_remaining, mb_left, time_left)
self.icon = self.sabicons['green']
self.icon = self.sabicons["green"]
else:
self.hover_text = self.txt_idle
self.icon = self.sabicons['default']
self.icon = self.sabicons["default"]
self.refresh_icon()
self.counter = 0
@ -148,36 +160,36 @@ class SABTrayThread(SysTrayIconThread):
self.pausefor(60)
def pausefor3hour(self, icon):
self.pausefor(3*60)
self.pausefor(3 * 60)
def pausefor6hour(self, icon):
self.pausefor(6*60)
self.pausefor(6 * 60)
def restart_sab(self, icon):
self.hover_text = T('Restart')
logging.info('Restart requested by tray')
self.hover_text = T("Restart")
logging.info("Restart requested by tray")
sabnzbd.trigger_restart()
def rss(self, icon):
self.hover_text = T('Read all RSS feeds')
self.hover_text = T("Read all RSS feeds")
scheduler.force_rss()
def nologin(self, icon):
sabnzbd.cfg.username.set('')
sabnzbd.cfg.password.set('')
sabnzbd.cfg.username.set("")
sabnzbd.cfg.password.set("")
sabnzbd.config.save_config()
self.hover_text = T('Restart')
self.hover_text = T("Restart")
sabnzbd.trigger_restart()
def defhost(self, icon):
sabnzbd.cfg.cherryhost.set('127.0.0.1')
sabnzbd.cfg.cherryhost.set("127.0.0.1")
sabnzbd.cfg.enable_https.set(False)
sabnzbd.config.save_config()
self.hover_text = T('Restart')
self.hover_text = T("Restart")
sabnzbd.trigger_restart()
def shutdown(self, icon):
self.hover_text = T('Shutdown')
self.hover_text = T("Shutdown")
sabnzbd.shutdown_program()
def pause(self):

32
sabnzbd/sabtraylinux.py

@ -22,10 +22,12 @@ sabnzbd.sabtraylinux - System tray icon for Linux, inspired from the Windows one
import gi
from gi.repository import Gtk, GLib
import logging
try:
gi.require_version('XApp', '1.0')
gi.require_version("XApp", "1.0")
from gi.repository import XApp
if not hasattr(XApp, 'StatusIcon'):
if not hasattr(XApp, "StatusIcon"):
raise ImportError
HAVE_XAPP = True
logging.debug("XApp found: %s" % XApp)
@ -49,9 +51,9 @@ from sabnzbd.utils.upload import add_local
class StatusIcon(Thread):
sabicons = {
'default': abspath('icons/logo-arrow.svg'),
'green': abspath('icons/logo-arrow_green.svg'),
'pause': abspath('icons/logo-arrow_gray.svg')
"default": abspath("icons/logo-arrow.svg"),
"green": abspath("icons/logo-arrow_green.svg"),
"pause": abspath("icons/logo-arrow_gray.svg"),
}
updatefreq = 1000 # ms
@ -64,7 +66,7 @@ class StatusIcon(Thread):
# Wait for translated texts to be loaded
while not sabnzbd.WEBUI_READY:
sleep(0.2)
logging.debug('language file not loaded, waiting')
logging.debug("language file not loaded, waiting")
self.sabpaused = False
if HAVE_XAPP:
@ -73,7 +75,7 @@ class StatusIcon(Thread):
self.statusicon = Gtk.StatusIcon()
self.statusicon.set_name("SABnzbd")
self.statusicon.set_visible(True)
self.icon = self.sabicons['default']
self.icon = self.sabicons["default"]
self.refresh_icon()
self.tooltip = "SABnzbd"
self.refresh_tooltip()
@ -102,14 +104,14 @@ class StatusIcon(Thread):
speed = to_units(bpsnow)
if self.sabpaused:
self.tooltip = T('Paused')
self.icon = self.sabicons['pause']
self.tooltip = T("Paused")
self.icon = self.sabicons["pause"]
elif bytes_left > 0:
self.tooltip = "%sB/s %s: %sB (%s)" % (speed, T('Remaining'), mb_left, time_left)
self.icon = self.sabicons['green']
self.tooltip = "%sB/s %s: %sB (%s)" % (speed, T("Remaining"), mb_left, time_left)
self.icon = self.sabicons["green"]
else:
self.tooltip = T('Idle')
self.icon = self.sabicons['default']
self.tooltip = T("Idle")
self.icon = self.sabicons["default"]
self.refresh_icon()
self.refresh_tooltip()
@ -185,11 +187,11 @@ class StatusIcon(Thread):
self.pause()
def restart(self, icon):
self.hover_text = T('Restart')
self.hover_text = T("Restart")
sabnzbd.trigger_restart()
def shutdown(self, icon):
self.hover_text = T('Shutdown')
self.hover_text = T("Shutdown")
sabnzbd.shutdown_program()
def pause(self):

183
sabnzbd/scheduler.py

@ -86,7 +86,7 @@ def init():
m = int(m)
h = int(h)
except:
logging.warning(T('Bad schedule %s at %s:%s'), action_name, m, h)
logging.warning(T("Bad schedule %s at %s:%s"), action_name, m, h)
continue
if d.isdigit():
@ -94,84 +94,82 @@ def init():
else:
d = list(range(1, 8))
if action_name == 'resume':
if action_name == "resume":
action = scheduled_resume
arguments = []
elif action_name == 'pause':
elif action_name == "pause":
action = sabnzbd.downloader.Downloader.do.pause
arguments = []
elif action_name == 'pause_all':
elif action_name == "pause_all":
action = sabnzbd.pause_all
arguments = []
elif action_name == 'shutdown':
elif action_name == "shutdown":
action = sabnzbd.shutdown_program
arguments = []
elif action_name == 'restart':
elif action_name == "restart":
action = sabnzbd.restart_program
arguments = []
elif action_name == 'pause_post':
elif action_name == "pause_post":
action = pp_pause
elif action_name == 'resume_post':
elif action_name == "resume_post":
action = pp_resume
elif action_name == 'speedlimit' and arguments != []:
elif action_name == "speedlimit" and arguments != []:
action = sabnzbd.downloader.Downloader.do.limit_speed
elif action_name == 'enable_server' and arguments != []:
elif action_name == "enable_server" and arguments != []:
action = sabnzbd.enable_server
elif action_name == 'disable_server' and arguments != []:
elif action_name == "disable_server" and arguments != []:
action = sabnzbd.disable_server
elif action_name == 'scan_folder':
elif action_name == "scan_folder":
action = sabnzbd.dirscanner.dirscan
elif action_name == 'rss_scan':
elif action_name == "rss_scan":
action = rss.run_method
rss_planned = True
elif action_name == 'remove_failed':
elif action_name == "remove_failed":
action = sabnzbd.api.history_remove_failed
elif action_name == 'remove_completed':
elif action_name == "remove_completed":
action = sabnzbd.api.history_remove_completed
elif action_name == 'enable_quota':
elif action_name == "enable_quota":
action = sabnzbd.bpsmeter.BPSMeter.do.set_status
arguments = [True]
elif action_name == 'disable_quota':
elif action_name == "disable_quota":
action = sabnzbd.bpsmeter.BPSMeter.do.set_status
arguments = [False]
elif action_name == 'pause_all_low':
elif action_name == "pause_all_low":
action = sabnzbd.nzbqueue.NzbQueue.do.pause_on_prio
arguments = [LOW_PRIORITY]
elif action_name == 'pause_all_normal':
elif action_name == "pause_all_normal":
action = sabnzbd.nzbqueue.NzbQueue.do.pause_on_prio
arguments = [NORMAL_PRIORITY]
elif action_name == 'pause_all_high':
elif action_name == "pause_all_high":
action = sabnzbd.nzbqueue.NzbQueue.do.pause_on_prio
arguments = [HIGH_PRIORITY]
elif action_name == 'resume_all_low':
elif action_name == "resume_all_low":
action = sabnzbd.nzbqueue.NzbQueue.do.resume_on_prio
arguments = [LOW_PRIORITY]
elif action_name == 'resume_all_normal':
elif action_name == "resume_all_normal":
action = sabnzbd.nzbqueue.NzbQueue.do.resume_on_prio
arguments = [NORMAL_PRIORITY]
elif action_name == 'resume_all_high':
elif action_name == "resume_all_high":
action = sabnzbd.nzbqueue.NzbQueue.do.resume_on_prio
arguments = [HIGH_PRIORITY]
elif action_name == 'pause_cat':
elif action_name == "pause_cat":
action = sabnzbd.nzbqueue.NzbQueue.do.pause_on_cat
arguments = [argument_list]
elif action_name == 'resume_cat':
elif action_name == "resume_cat":
action = sabnzbd.nzbqueue.NzbQueue.do.resume_on_cat
arguments = [argument_list]
else:
logging.warning(T('Unknown action: %s'), action_name)
logging.warning(T("Unknown action: %s"), action_name)
continue
if enabled == '1':
if enabled == "1":
logging.debug("Scheduling %s(%s) on days %s at %02d:%02d", action_name, arguments, d, h, m)
__SCHED.add_daytime_task(action, action_name, d, None, (h, m),
kronos.method.sequential, arguments, None)
__SCHED.add_daytime_task(action, action_name, d, None, (h, m), kronos.method.sequential, arguments, None)
else:
logging.debug("Skipping %s(%s) on days %s at %02d:%02d", action_name, arguments, d, h, m)
# Set Guardian interval to 30 seconds
__SCHED.add_interval_task(sched_guardian, "Guardian", 15, 30,
kronos.method.sequential, None, None)
__SCHED.add_interval_task(sched_guardian, "Guardian", 15, 30, kronos.method.sequential, None, None)
# Set RSS check interval
if not rss_planned:
@ -179,34 +177,53 @@ def init():
delay = random.randint(0, interval - 1)
logging.debug("Scheduling RSS interval task every %s min (delay=%s)", interval, delay)
sabnzbd.rss.next_run(time.time() + delay * 60)
__SCHED.add_interval_task(rss.run_method, "RSS", delay * 60, interval * 60,
kronos.method.sequential, None, None)
__SCHED.add_single_task(rss.run_method, 'RSS', 15, kronos.method.sequential, None, None)
__SCHED.add_interval_task(
rss.run_method, "RSS", delay * 60, interval * 60, kronos.method.sequential, None, None
)
__SCHED.add_single_task(rss.run_method, "RSS", 15, kronos.method.sequential, None, None)
if cfg.version_check():
# Check for new release, once per week on random time
m = random.randint(0, 59)
h = random.randint(0, 23)
d = (random.randint(1, 7), )
d = (random.randint(1, 7),)
logging.debug("Scheduling VersionCheck on day %s at %s:%s", d[0], h, m)
__SCHED.add_daytime_task(sabnzbd.misc.check_latest_version, 'VerCheck', d, None, (h, m),
kronos.method.sequential, [], None)
__SCHED.add_daytime_task(
sabnzbd.misc.check_latest_version, "VerCheck", d, None, (h, m), kronos.method.sequential, [], None
)
action, hour, minute = sabnzbd.bpsmeter.BPSMeter.do.get_quota()
if action:
logging.info('Setting schedule for quota check daily at %s:%s', hour, minute)
__SCHED.add_daytime_task(action, 'quota_reset', list(range(1, 8)), None, (hour, minute),
kronos.method.sequential, [], None)
logging.info("Setting schedule for quota check daily at %s:%s", hour, minute)
__SCHED.add_daytime_task(
action, "quota_reset", list(range(1, 8)), None, (hour, minute), kronos.method.sequential, [], None
)
if sabnzbd.misc.int_conv(cfg.history_retention()) > 0:
logging.info('Setting schedule for midnight auto history-purge')
__SCHED.add_daytime_task(sabnzbd.database.midnight_history_purge, 'midnight_history_purge', list(range(1, 8)), None, (0, 0),
kronos.method.sequential, [], None)
logging.info('Setting schedule for midnight BPS reset')
__SCHED.add_daytime_task(sabnzbd.bpsmeter.midnight_action, 'midnight_bps', list(range(1, 8)), None, (0, 0),
kronos.method.sequential, [], None)
logging.info("Setting schedule for midnight auto history-purge")
__SCHED.add_daytime_task(
sabnzbd.database.midnight_history_purge,
"midnight_history_purge",
list(range(1, 8)),
None,
(0, 0),
kronos.method.sequential,
[],
None,
)
logging.info("Setting schedule for midnight BPS reset")
__SCHED.add_daytime_task(
sabnzbd.bpsmeter.midnight_action,
"midnight_bps",
list(range(1, 8)),
None,
(0, 0),
kronos.method.sequential,
[],
None,
)
# Subscribe to special schedule changes
cfg.rss_rate.callback(schedule_guard)
@ -216,7 +233,7 @@ def start():
""" Start the scheduler """
global __SCHED
if __SCHED:
logging.debug('Starting scheduler')
logging.debug("Starting scheduler")
__SCHED.start()
@ -241,7 +258,7 @@ def stop():
""" Stop the scheduler, destroy instance """
global __SCHED
if __SCHED:
logging.debug('Stopping scheduler')
logging.debug("Stopping scheduler")
try:
__SCHED.stop()
except IndexError:
@ -254,7 +271,7 @@ def abort():
""" Emergency stop, just set the running attribute false """
global __SCHED
if __SCHED:
logging.debug('Terminating scheduler')
logging.debug("Terminating scheduler")
__SCHED.running = False
@ -284,8 +301,8 @@ def sort_schedules(all_events, now=None):
except:
continue # Bad schedule, ignore
action = action.strip()
if dd == '*':
dd = '1234567'
if dd == "*":
dd = "1234567"
if not dd.isdigit():
continue # Bad schedule, ignore
for d in dd:
@ -319,10 +336,10 @@ def analyse(was_paused=False, priority=None):
for ev in sort_schedules(all_events=True):
if priority is None:
logging.debug('Schedule check result = %s', ev)
logging.debug("Schedule check result = %s", ev)
# Skip if disabled
if ev[4] == '0':
if ev[4] == "0":
continue
action = ev[1]
@ -330,48 +347,48 @@ def analyse(was_paused=False, priority=None):
value = ev[2]
except:
value = None
if action == 'pause':
if action == "pause":
paused = True
elif action == 'pause_all':
elif action == "pause_all":
paused_all = True
PP_PAUSE_EVENT = True
elif action == 'resume':
elif action == "resume":
paused = False
paused_all = False
elif action == 'pause_post':
elif action == "pause_post":
pause_post = True
PP_PAUSE_EVENT = True
elif action == 'resume_post':
elif action == "resume_post":
pause_post = False
PP_PAUSE_EVENT = True
elif action == 'speedlimit' and value is not None:
elif action == "speedlimit" and value is not None:
speedlimit = ev[2]
elif action == 'pause_all_low':
elif action == "pause_all_low":
pause_low = True
elif action == 'pause_all_normal':
elif action == "pause_all_normal":
pause_normal = True
elif action == 'pause_all_high':
elif action == "pause_all_high":
pause_high = True
elif action == 'resume_all_low':
elif action == "resume_all_low":
pause_low = False
elif action == 'resume_all_normal':
elif action == "resume_all_normal":
pause_normal = False
elif action == 'resume_all_high':
elif action == "resume_all_high":
pause_high = False
elif action == 'enable_quota':
elif action == "enable_quota":
quota = True
elif action == 'disable_quota':
elif action == "disable_quota":
quota = False
elif action == 'enable_server':
elif action == "enable_server":
try:
servers[value] = 1
except:
logging.warning(T('Schedule for non-existing server %s'), value)
elif action == 'disable_server':
logging.warning(T("Schedule for non-existing server %s"), value)
elif action == "disable_server":
try:
servers[value] = 0
except:
logging.warning(T('Schedule for non-existing server %s'), value)
logging.warning(T("Schedule for non-existing server %s"), value)
# Special case, a priority was passed, so evaluate only that and return state
if priority == LOW_PRIORITY:
@ -399,7 +416,7 @@ def analyse(was_paused=False, priority=None):
for serv in servers:
try:
item = config.get_config('servers', serv)
item = config.get_config("servers", serv)
value = servers[serv]
if bool(item.enable()) != bool(value):
item.enable.set(value)
@ -410,7 +427,7 @@ def analyse(was_paused=False, priority=None):
# Support for single shot pause (=delayed resume)
__PAUSE_END = None # Moment when pause will end
__PAUSE_END = None # Moment when pause will end
def scheduled_resume():
@ -427,10 +444,10 @@ def __oneshot_resume(when):
global __PAUSE_END
if __PAUSE_END is not None and (when > __PAUSE_END - 5) and (when < __PAUSE_END + 55):
__PAUSE_END = None
logging.debug('Resume after pause-interval')
logging.debug("Resume after pause-interval")
sabnzbd.unpause_all()
else:
logging.debug('Ignoring cancelled resume')
logging.debug("Ignoring cancelled resume")
def plan_resume(interval):
@ -438,8 +455,8 @@ def plan_resume(interval):
global __SCHED, __PAUSE_END
if interval > 0:
__PAUSE_END = time.time() + (interval * 60)
logging.debug('Schedule resume at %s', __PAUSE_END)
__SCHED.add_single_task(__oneshot_resume, '', interval * 60, kronos.method.sequential, [__PAUSE_END], None)
logging.debug("Schedule resume at %s", __PAUSE_END)
__SCHED.add_single_task(__oneshot_resume, "", interval * 60, kronos.method.sequential, [__PAUSE_END], None)
sabnzbd.downloader.Downloader.do.pause()
else:
__PAUSE_END = None
@ -454,10 +471,10 @@ def pause_int():
else:
val = __PAUSE_END - time.time()
if val < 0:
sign = '-'
sign = "-"
val = abs(val)
else:
sign = ''
sign = ""
mins = int(val / 60)
sec = int(val - mins * 60)
return "%s%d:%02d" % (sign, mins, sec)
@ -468,18 +485,18 @@ def pause_check():
global __PAUSE_END
if __PAUSE_END is not None and (__PAUSE_END - time.time()) < 0:
__PAUSE_END = None
logging.debug('Force resume, negative timer')
logging.debug("Force resume, negative timer")
sabnzbd.unpause_all()
def plan_server(action, parms, interval):
""" Plan to re-activate server after 'interval' minutes """
__SCHED.add_single_task(action, '', interval * 60, kronos.method.sequential, parms, None)
__SCHED.add_single_task(action, "", interval * 60, kronos.method.sequential, parms, None)
def force_rss():
""" Add a one-time RSS scan, one second from now """
__SCHED.add_single_task(rss.run_method, 'RSS', 1, kronos.method.sequential, None, None)
__SCHED.add_single_task(rss.run_method, "RSS", 1, kronos.method.sequential, None, None)
# Scheduler Guarding system

1935
sabnzbd/skintext.py

File diff suppressed because it is too large

573
sabnzbd/sorting.py

File diff suppressed because it is too large

178
sabnzbd/urlgrabber.py

@ -44,9 +44,22 @@ import sabnzbd.notifier as notifier
from sabnzbd.encoding import ubtou, utob
_BAD_GZ_HOSTS = ('.zip', 'nzbsa.co.za', 'newshost.za.net')
_RARTING_FIELDS = ('x-rating-id', 'x-rating-url', 'x-rating-host', 'x-rating-video', 'x-rating-videocnt', 'x-rating-audio', 'x-rating-audiocnt',
'x-rating-voteup', 'x-rating-votedown', 'x-rating-spam', 'x-rating-confirmed-spam', 'x-rating-passworded', 'x-rating-confirmed-passworded')
_BAD_GZ_HOSTS = (".zip", "nzbsa.co.za", "newshost.za.net")
_RARTING_FIELDS = (
"x-rating-id",
"x-rating-url",
"x-rating-host",
"x-rating-video",
"x-rating-videocnt",
"x-rating-audio",
"x-rating-audiocnt",
"x-rating-voteup",
"x-rating-votedown",
"x-rating-spam",
"x-rating-confirmed-spam",
"x-rating-passworded",
"x-rating-confirmed-passworded",
)
class URLGrabber(Thread):
@ -69,7 +82,7 @@ class URLGrabber(Thread):
# Too many tries? Cancel
if future_nzo.url_tries > cfg.max_url_retries():
self.fail_to_history(future_nzo, url, T('Maximum retries'))
self.fail_to_history(future_nzo, url, T("Maximum retries"))
return
future_nzo.url_wait = time.time() + when
@ -77,12 +90,12 @@ class URLGrabber(Thread):
self.queue.put((url, future_nzo))
def stop(self):
logging.info('URLGrabber shutting down')
logging.info("URLGrabber shutting down")
self.shutdown = True
self.add(None, None)
def run(self):
logging.info('URLGrabber starting up')
logging.info("URLGrabber starting up")
self.shutdown = False
while not self.shutdown:
@ -104,7 +117,7 @@ class URLGrabber(Thread):
time.sleep(1.0)
continue
url = url.replace(' ', '')
url = url.replace(" ", "")
try:
if future_nzo:
@ -114,7 +127,7 @@ class URLGrabber(Thread):
except AttributeError:
deleted = True
if deleted:
logging.debug('Dropping URL %s, job entry missing', url)
logging.debug("Dropping URL %s, job entry missing", url)
continue
filename = None
@ -125,7 +138,7 @@ class URLGrabber(Thread):
retry = True
fetch_request = None
logging.info('Grabbing URL %s', url)
logging.info("Grabbing URL %s", url)
try:
fetch_request = _build_request(url)
except Exception as e:
@ -133,22 +146,22 @@ class URLGrabber(Thread):
error0 = str(sys.exc_info()[0]).lower()
error1 = str(sys.exc_info()[1]).lower()
logging.debug('Error "%s" trying to get the url %s', error1, url)
if 'certificate_verify_failed' in error1 or 'certificateerror' in error0:
msg = T('Server %s uses an untrusted HTTPS certificate') % ''
msg += ' - https://sabnzbd.org/certificate-errors'
if "certificate_verify_failed" in error1 or "certificateerror" in error0:
msg = T("Server %s uses an untrusted HTTPS certificate") % ""
msg += " - https://sabnzbd.org/certificate-errors"
retry = False
elif 'nodename nor servname provided' in error1:
msg = T('Server name does not resolve')
elif "nodename nor servname provided" in error1:
msg = T("Server name does not resolve")
retry = False
elif '401' in error1 or 'unauthorized' in error1:
msg = T('Unauthorized access')
elif "401" in error1 or "unauthorized" in error1:
msg = T("Unauthorized access")
retry = False
elif '404' in error1:
msg = T('File not on server')
elif "404" in error1:
msg = T("File not on server")
retry = False
elif hasattr(e, 'headers') and 'retry-after' in e.headers:
elif hasattr(e, "headers") and "retry-after" in e.headers:
# Catch if the server send retry (e.headers is case-INsensitive)
wait = misc.int_conv(e.headers['retry-after'])
wait = misc.int_conv(e.headers["retry-after"])
if fetch_request:
for hdr in fetch_request.headers:
@ -157,29 +170,29 @@ class URLGrabber(Thread):
value = fetch_request.headers[hdr]
except:
continue
if item in ('content-encoding',) and value == 'gzip':
if item in ("content-encoding",) and value == "gzip":
gzipped = True
if item in ('category_id', 'x-dnzb-category'):
if item in ("category_id", "x-dnzb-category"):
category = value
elif item in ('x-dnzb-moreinfo',):
nzo_info['more_info'] = value
elif item in ('x-dnzb-name',):
elif item in ("x-dnzb-moreinfo",):
nzo_info["more_info"] = value
elif item in ("x-dnzb-name",):
filename = value
if not filename.endswith('.nzb'):
filename += '.nzb'
elif item == 'x-dnzb-propername':
nzo_info['propername'] = value
elif item == 'x-dnzb-episodename':
nzo_info['episodename'] = value
elif item == 'x-dnzb-year':
nzo_info['year'] = value
elif item == 'x-dnzb-failure':
nzo_info['failure'] = value
elif item == 'x-dnzb-details':
nzo_info['details'] = value
elif item == 'x-dnzb-password':
nzo_info['password'] = value
elif item == 'retry-after':
if not filename.endswith(".nzb"):
filename += ".nzb"
elif item == "x-dnzb-propername":
nzo_info["propername"] = value
elif item == "x-dnzb-episodename":
nzo_info["episodename"] = value
elif item == "x-dnzb-year":
nzo_info["year"] = value
elif item == "x-dnzb-failure":
nzo_info["failure"] = value
elif item == "x-dnzb-details":
nzo_info["details"] = value
elif item == "x-dnzb-password":
nzo_info["password"] = value
elif item == "retry-after":
wait = misc.int_conv(value)
# Rating fields
@ -188,11 +201,11 @@ class URLGrabber(Thread):
# Get filename from Content-Disposition header
if not filename and "filename=" in value:
filename = value[value.index("filename=") + 9:].strip(';').strip('"')
filename = value[value.index("filename=") + 9 :].strip(";").strip('"')
if wait:
# For sites that have a rate-limiting attribute
msg = ''
msg = ""
retry = True
fetch_request = None
elif retry:
@ -200,7 +213,7 @@ class URLGrabber(Thread):
if not fetch_request:
if retry:
logging.info('Retry URL %s', url)
logging.info("Retry URL %s", url)
self.add(url, future_nzo, wait)
else:
self.fail_to_history(future_nzo, url, msg)
@ -213,56 +226,66 @@ class URLGrabber(Thread):
# Check if the original URL has extension
if url != fetch_request.url and sabnzbd.filesystem.get_ext(filename) not in VALID_NZB_FILES:
filename = os.path.basename(urllib.parse.unquote(fetch_request.url))
elif '&nzbname=' in filename:
elif "&nzbname=" in filename:
# Sometimes the filename contains the full URL, duh!
filename = filename[filename.find('&nzbname=') + 9:]
filename = filename[filename.find("&nzbname=") + 9 :]
pp = future_nzo.pp
script = future_nzo.script
cat = future_nzo.cat
if (cat is None or cat == '*') and category:
if (cat is None or cat == "*") and category:
cat = misc.cat_convert(category)
priority = future_nzo.priority
nzbname = future_nzo.custom_name
# process data
if gzipped:
filename += '.gz'
filename += ".gz"
if not data:
try:
data = fetch_request.read()
except (IncompleteRead, IOError):
self.fail_to_history(future_nzo, url, T('Server could not complete request'))
self.fail_to_history(future_nzo, url, T("Server could not complete request"))
fetch_request.close()
continue
fetch_request.close()
if b'<nzb' in data and sabnzbd.filesystem.get_ext(filename) != '.nzb':
filename += '.nzb'
if b"<nzb" in data and sabnzbd.filesystem.get_ext(filename) != ".nzb":
filename += ".nzb"
# Sanitize filename first (also removing forbidden Windows-names)
filename = sabnzbd.filesystem.sanitize_filename(filename)
# Write data to temp file
path = os.path.join(cfg.admin_dir.get_path(), FUTURE_Q_FOLDER, filename)
with open(path, 'wb') as temp_nzb:
with open(path, "wb") as temp_nzb:
temp_nzb.write(data)
# Check if nzb file
if sabnzbd.filesystem.get_ext(filename) in VALID_NZB_FILES:
res = dirscanner.process_single_nzb(filename, path, pp=pp, script=script, cat=cat, priority=priority,
nzbname=nzbname, nzo_info=nzo_info, url=future_nzo.url, keep=False,
nzo_id=future_nzo.nzo_id)[0]
res = dirscanner.process_single_nzb(
filename,
path,
pp=pp,
script=script,
cat=cat,
priority=priority,
nzbname=nzbname,
nzo_info=nzo_info,
url=future_nzo.url,
keep=False,
nzo_id=future_nzo.nzo_id,
)[0]
if res:
if res == -2:
logging.info('Incomplete NZB, retry after 5 min %s', url)
logging.info("Incomplete NZB, retry after 5 min %s", url)
when = 300
elif res == -1:
# Error, but no reason to retry. Warning is already given
NzbQueue.do.remove(future_nzo.nzo_id, add_to_history=False)
continue
else:
logging.info('Unknown error fetching NZB, retry after 2 min %s', url)
logging.info("Unknown error fetching NZB, retry after 2 min %s", url)
when = 120
self.add(url, future_nzo, when)
@ -270,27 +293,36 @@ class URLGrabber(Thread):
# Check if a supported archive
status, zf, exp_ext = dirscanner.is_archive(path)
if status == 0:
if sabnzbd.filesystem.get_ext(filename) not in ('.rar', '.zip', '.7z'):
if sabnzbd.filesystem.get_ext(filename) not in (".rar", ".zip", ".7z"):
filename = filename + exp_ext
os.rename(path, path + exp_ext)
path = path + exp_ext
dirscanner.process_nzb_archive_file(filename, path, pp, script, cat, priority=priority,
nzbname=nzbname, url=future_nzo.url, keep=False,
nzo_id=future_nzo.nzo_id)
dirscanner.process_nzb_archive_file(
filename,
path,
pp,
script,
cat,
priority=priority,
nzbname=nzbname,
url=future_nzo.url,
keep=False,
nzo_id=future_nzo.nzo_id,
)
else:
# Not a supported filetype, not an nzb (text/html ect)
try:
os.remove(fetch_request)
except:
pass
logging.info('Unknown filetype when fetching NZB, retry after 30s %s', url)
logging.info("Unknown filetype when fetching NZB, retry after 30s %s", url)
self.add(url, future_nzo, 30)
except:
logging.error(T('URLGRABBER CRASHED'), exc_info=True)
logging.error(T("URLGRABBER CRASHED"), exc_info=True)
logging.debug("URLGRABBER Traceback: ", exc_info=True)
def fail_to_history(self, nzo, url, msg='', content=False):
def fail_to_history(self, nzo, url, msg="", content=False):
""" Create History entry for failed URL Fetch
msg: message to be logged
content: report in history that cause is a bad NZB file
@ -302,16 +334,16 @@ class URLGrabber(Thread):
if content:
# Bad content
msg = T('Unusable NZB file')
msg = T("Unusable NZB file")
else:
# Failed fetch
msg = T('URL Fetching failed; %s') % msg
msg = T("URL Fetching failed; %s") % msg
# Mark as failed
nzo.status = Status.FAILED
nzo.fail_msg = msg
notifier.send_notification(T('URL Fetching failed; %s') % '', '%s\n%s' % (msg, url), 'other', nzo.cat)
notifier.send_notification(T("URL Fetching failed; %s") % "", "%s\n%s" % (msg, url), "other", nzo.cat)
if cfg.email_endjob() > 0:
emailer.badfetch_mail(msg, url)
@ -330,21 +362,21 @@ def _build_request(url):
u = urllib.parse.urlparse(url)
if u.username is not None or u.password is not None:
if u.username and u.password:
user_passwd = '%s:%s' % (u.username, u.password)
user_passwd = "%s:%s" % (u.username, u.password)
host_port = u.hostname
if u.port:
host_port += ':' + str(u.port)
host_port += ":" + str(u.port)
url = urllib.parse.urlunparse(u._replace(netloc=host_port))
# Start request
req = urllib.request.Request(url)
# Add headers
req.add_header('User-Agent', 'SABnzbd+/%s' % sabnzbd.version.__version__)
req.add_header("User-Agent", "SABnzbd+/%s" % sabnzbd.version.__version__)
if not any(item in url for item in _BAD_GZ_HOSTS):
req.add_header('Accept-encoding', 'gzip')
req.add_header("Accept-encoding", "gzip")
if user_passwd:
req.add_header('Authorization', 'Basic ' + ubtou(base64.b64encode(utob(user_passwd))).strip())
req.add_header("Authorization", "Basic " + ubtou(base64.b64encode(utob(user_passwd))).strip())
return urllib.request.urlopen(req)
@ -357,11 +389,11 @@ def _analyse(fetch_request, future_nzo):
if fetch_request:
msg = fetch_request.msg
else:
msg = ''
msg = ""
# Increasing wait-time in steps for standard errors
when = DEF_TIMEOUT * (future_nzo.url_tries + 1)
logging.debug('No usable response from indexer, retry after %s sec', when)
logging.debug("No usable response from indexer, retry after %s sec", when)
return None, msg, True, when, data
return fetch_request, fetch_request.msg, False, 0, data

38
sabnzbd/zconfig.py

@ -27,6 +27,7 @@ _HOST_PORT = (None, None)
try:
from sabnzbd.utils import pybonjour
from threading import Thread
_HAVE_BONJOUR = True
except:
_HAVE_BONJOUR = False
@ -41,16 +42,23 @@ _BONJOUR_OBJECT = None
def hostname():
""" Return host's pretty name """
if sabnzbd.WIN32:
return os.environ.get('computername', 'unknown')
return os.environ.get("computername", "unknown")
try:
return os.uname()[1]
except:
return 'unknown'
return "unknown"
def _zeroconf_callback(sdRef, flags, errorCode, name, regtype, domain):
logging.debug('Full Bonjour-callback sdRef=%s, flags=%s, errorCode=%s, name=%s, regtype=%s, domain=%s',
sdRef, flags, errorCode, name, regtype, domain)
logging.debug(
"Full Bonjour-callback sdRef=%s, flags=%s, errorCode=%s, name=%s, regtype=%s, domain=%s",
sdRef,
flags,
errorCode,
name,
regtype,
domain,
)
if errorCode == pybonjour.kDNSServiceErr_NoError:
logging.info('Registered in Bonjour as "%s" (%s)', name, domain)
@ -60,7 +68,7 @@ def set_bonjour(host=None, port=None):
global _HOST_PORT, _BONJOUR_OBJECT
if not _HAVE_BONJOUR or not cfg.enable_bonjour():
logging.info('No Bonjour/ZeroConfig support installed')
logging.info("No Bonjour/ZeroConfig support installed")
return
if host is None and port is None:
@ -72,7 +80,7 @@ def set_bonjour(host=None, port=None):
zhost = None
domain = None
if match_str(host, ('localhost', '127.0.', '::1')):
if match_str(host, ("localhost", "127.0.", "::1")):
logging.info('Bonjour/ZeroConfig does not support "localhost"')
# All implementations fail to implement "localhost" properly
# A false address is published even when scope==kDNSServiceInterfaceIndexLocalOnly
@ -83,30 +91,30 @@ def set_bonjour(host=None, port=None):
try:
refObject = pybonjour.DNSServiceRegister(
interfaceIndex=scope,
name='SABnzbd on %s:%s' % (name, port),
regtype='_http._tcp',
name="SABnzbd on %s:%s" % (name, port),
regtype="_http._tcp",
domain=domain,
host=zhost,
port=int(port),
txtRecord=pybonjour.TXTRecord({'path': cfg.url_base(),
'https': cfg.enable_https()}),
callBack=_zeroconf_callback)
txtRecord=pybonjour.TXTRecord({"path": cfg.url_base(), "https": cfg.enable_https()}),
callBack=_zeroconf_callback,
)
except sabnzbd.utils.pybonjour.BonjourError as e:
_BONJOUR_OBJECT = None
logging.debug('Failed to start Bonjour service: %s', str(e))
logging.debug("Failed to start Bonjour service: %s", str(e))
except:
_BONJOUR_OBJECT = None
logging.debug('Failed to start Bonjour service due to non-pybonjour related problem', exc_info=True)
logging.debug("Failed to start Bonjour service due to non-pybonjour related problem", exc_info=True)
else:
Thread(target=_bonjour_server, args=(refObject,))
_BONJOUR_OBJECT = refObject
logging.debug('Successfully started Bonjour service')
logging.debug("Successfully started Bonjour service")
def _bonjour_server(refObject):
while 1:
pybonjour.DNSServiceProcessResult(refObject)
logging.debug('GOT A BONJOUR CALL')
logging.debug("GOT A BONJOUR CALL")
def remove_server():

Loading…
Cancel
Save