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 uses: lgeiger/black-action@v1.0.1
with: with:
args: > args: >
SABnzbd.py
sabnzbd
scripts scripts
tools tools
tests 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 --line-length=120
--target-version=py35 --target-version=py35
--check --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" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=ASCII\n" "Content-Type: text/plain; charset=ASCII\n"
"Content-Transfer-Encoding: 7bit\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" "Generated-By: pygettext.py 1.5\n"
@ -24,11 +24,11 @@ msgstr ""
msgid "Cannot find web template: %s, trying standard template" msgid "Cannot find web template: %s, trying standard template"
msgstr "" msgstr ""
#: SABnzbd.py [Error message] #: SABnzbd.py
msgid "SABYenc disabled: no correct version found! (Found v%s, expecting v%s)" msgid "SABYenc disabled: no correct version found! (Found v%s, expecting v%s)"
msgstr "" msgstr ""
#: SABnzbd.py [Error message] #: SABnzbd.py
msgid "SABYenc module... NOT found! Expecting v%s - https://sabnzbd.org/sabyenc" msgid "SABYenc module... NOT found! Expecting v%s - https://sabnzbd.org/sabyenc"
msgstr "" msgstr ""
@ -36,15 +36,15 @@ msgstr ""
msgid "par2 binary... NOT found!" msgid "par2 binary... NOT found!"
msgstr "" msgstr ""
#: SABnzbd.py [Error message] #: SABnzbd.py
msgid "MultiPar binary... NOT found!" msgid "MultiPar binary... NOT found!"
msgstr "" msgstr ""
#: SABnzbd.py [Error message] #: SABnzbd.py
msgid "Verification and repair will not be possible." msgid "Verification and repair will not be possible."
msgstr "" msgstr ""
#: SABnzbd.py [Warning message] #: SABnzbd.py
msgid "Your UNRAR version is %s, we recommend version %s or higher.<br />" msgid "Your UNRAR version is %s, we recommend version %s or higher.<br />"
msgstr "" msgstr ""
@ -72,7 +72,7 @@ msgstr ""
msgid "HTTP and HTTPS ports cannot be the same" msgid "HTTP and HTTPS ports cannot be the same"
msgstr "" 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." msgid "SABnzbd was started with encoding %s, this should be UTF-8. Expect problems with Unicoded file and directory names in downloads."
msgstr "" msgstr ""
@ -236,11 +236,11 @@ msgstr ""
msgid "Fatal error in Assembler" msgid "Fatal error in Assembler"
msgstr "" 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)" msgid "WARNING: Paused job \"%s\" because of encrypted RAR file (if supplied, all passwords were tried)"
msgstr "" 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)" msgid "WARNING: Aborted job \"%s\" because of encrypted RAR file (if supplied, all passwords were tried)"
msgstr "" msgstr ""
@ -248,7 +248,7 @@ msgstr ""
msgid "Aborted, encryption detected" msgid "Aborted, encryption detected"
msgstr "" msgstr ""
#: sabnzbd/assembler.py [Warning message] #: sabnzbd/assembler.py
msgid "WARNING: In \"%s\" unwanted extension in RAR file. Unwanted file is %s " msgid "WARNING: In \"%s\" unwanted extension in RAR file. Unwanted file is %s "
msgstr "" msgstr ""
@ -260,11 +260,11 @@ msgstr ""
msgid "Aborted, unwanted extension detected" msgid "Aborted, unwanted extension detected"
msgstr "" msgstr ""
#: sabnzbd/assembler.py [Warning message] #: sabnzbd/assembler.py
msgid "WARNING: Paused job \"%s\" because of rating (%s)" msgid "WARNING: Paused job \"%s\" because of rating (%s)"
msgstr "" msgstr ""
#: sabnzbd/assembler.py [Warning message] #: sabnzbd/assembler.py
msgid "WARNING: Aborted job \"%s\" because of rating (%s)" msgid "WARNING: Aborted job \"%s\" because of rating (%s)"
msgstr "" msgstr ""
@ -272,7 +272,7 @@ msgstr ""
msgid "Aborted, rating filter matched (%s)" msgid "Aborted, rating filter matched (%s)"
msgstr "" msgstr ""
#: sabnzbd/assembler.py [Warning message] #: sabnzbd/assembler.py
msgid "Job \"%s\" is probably encrypted due to RAR with same name inside this RAR" msgid "Job \"%s\" is probably encrypted due to RAR with same name inside this RAR"
msgstr "" msgstr ""
@ -376,10 +376,6 @@ msgstr ""
msgid "UUencode detected, only yEnc encoding is supported [%s]" msgid "UUencode detected, only yEnc encoding is supported [%s]"
msgstr "" msgstr ""
#: sabnzbd/decoder.py
msgid "Badly formed yEnc article in %s"
msgstr ""
#: sabnzbd/decoder.py [Warning message] #: sabnzbd/decoder.py [Warning message]
msgid "Unknown Error while decoding %s" msgid "Unknown Error while decoding %s"
msgstr "" msgstr ""
@ -396,11 +392,11 @@ msgstr ""
msgid "Unpacked %s files/folders in %s" msgid "Unpacked %s files/folders in %s"
msgstr "" msgstr ""
#: sabnzbd/directunpacker.py [Warning message] #: sabnzbd/directunpacker.py
msgid "Direct Unpack was automatically enabled." msgid "Direct Unpack was automatically enabled."
msgstr "" 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." msgid "Jobs will start unpacking during the downloading to reduce post-processing time. Only works for jobs that do not need repair."
msgstr "" msgstr ""
@ -444,7 +440,7 @@ msgstr ""
msgid "Server %s will be ignored for %s minutes" msgid "Server %s will be ignored for %s minutes"
msgstr "" msgstr ""
#: sabnzbd/downloader.py [Error message] #: sabnzbd/downloader.py
msgid "Failed to initialize %s@%s with reason: %s" msgid "Failed to initialize %s@%s with reason: %s"
msgstr "" msgstr ""
@ -460,7 +456,7 @@ msgstr ""
msgid "Failed login for server %s" msgid "Failed login for server %s"
msgstr "" msgstr ""
#: sabnzbd/downloader.py [Error message] #: sabnzbd/downloader.py
msgid "Connecting %s@%s failed, message=%s" msgid "Connecting %s@%s failed, message=%s"
msgstr "" msgstr ""
@ -646,15 +642,15 @@ msgstr ""
msgid "Category folder cannot be a subfolder of the Temporary Download Folder." msgid "Category folder cannot be a subfolder of the Temporary Download Folder."
msgstr "" msgstr ""
#: sabnzbd/interface.py
msgid "Back"
msgstr ""
#: sabnzbd/interface.py # sabnzbd/skintext.py #: sabnzbd/interface.py # sabnzbd/skintext.py
msgid "ERROR:" msgid "ERROR:"
msgstr "" msgstr ""
#: sabnzbd/interface.py #: sabnzbd/interface.py
msgid "Back"
msgstr ""
#: sabnzbd/interface.py
msgid "Incorrect value for %s: %s" msgid "Incorrect value for %s: %s"
msgstr "" msgstr ""
@ -1014,7 +1010,7 @@ msgstr ""
msgid "Incompatible queuefile found, cannot proceed" msgid "Incompatible queuefile found, cannot proceed"
msgstr "" msgstr ""
#: sabnzbd/nzbqueue.py [Error message] #: sabnzbd/nzbqueue.py
msgid "Error loading %s, corrupt file detected" msgid "Error loading %s, corrupt file detected"
msgstr "" msgstr ""
@ -1291,11 +1287,11 @@ msgid "Program did not start!"
msgstr "" msgstr ""
#: sabnzbd/panic.py #: 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 "" msgstr ""
#: sabnzbd/panic.py #: 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 "" msgstr ""
#: sabnzbd/panic.py [Warning message] #: sabnzbd/panic.py [Warning message]
@ -2210,7 +2206,7 @@ msgstr ""
msgid "Purge Failed NZBs" msgid "Purge Failed NZBs"
msgstr "" msgstr ""
#: sabnzbd/skintext.py [Button to delete all failed jobs in History, including files] #: sabnzbd/skintext.py
msgid "Purge Failed NZBs & Delete Files" msgid "Purge Failed NZBs & Delete Files"
msgstr "" msgstr ""
@ -2254,7 +2250,7 @@ msgstr ""
msgid "Force Disconnect" msgid "Force Disconnect"
msgstr "" 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." msgid "Disconnect all active connections to usenet servers. Connections will be reopened after a few seconds if there are items in the queue."
msgstr "" msgstr ""
@ -3322,7 +3318,7 @@ msgstr ""
msgid "Optional" msgid "Optional"
msgstr "" msgstr ""
#: sabnzbd/skintext.py [Explain server optional tickbox] #: sabnzbd/skintext.py
msgid "For unreliable servers, will be ignored longer in case of failures" msgid "For unreliable servers, will be ignored longer in case of failures"
msgstr "" msgstr ""
@ -3606,7 +3602,7 @@ msgstr ""
msgid "Emergency retry" msgid "Emergency retry"
msgstr "" msgstr ""
#: sabnzbd/skintext.py [Pushover settings] #: sabnzbd/skintext.py
msgid "How often (in seconds) the same notification will be sent" msgid "How often (in seconds) the same notification will be sent"
msgstr "" msgstr ""
@ -3614,7 +3610,7 @@ msgstr ""
msgid "Emergency expire" msgid "Emergency expire"
msgstr "" msgstr ""
#: sabnzbd/skintext.py [Pushover settings] #: sabnzbd/skintext.py
msgid "How many seconds your notification will continue to be retried" msgid "How many seconds your notification will continue to be retried"
msgstr "" msgstr ""
@ -4110,7 +4106,7 @@ msgstr ""
msgid "Update Available!" msgid "Update Available!"
msgstr "" 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!" msgid "LocalStorage (cookies) are disabled in your browser, interface settings will be lost after you close the browser!"
msgstr "" msgstr ""
@ -4386,11 +4382,11 @@ msgstr ""
msgid "Closing any browser windows/tabs will NOT close SABnzbd." msgid "Closing any browser windows/tabs will NOT close SABnzbd."
msgstr "" 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." 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 "" 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" msgid "Further help can be found on our"
msgstr "" msgstr ""
@ -4426,7 +4422,7 @@ msgstr ""
msgid "Error getting TV info (%s)" msgid "Error getting TV info (%s)"
msgstr "" msgstr ""
#: sabnzbd/sorting.py [Error message] #: sabnzbd/sorting.py [Error message] # sabnzbd/sorting.py
msgid "Failed to rename: %s to %s" msgid "Failed to rename: %s to %s"
msgstr "" 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_org = 0
self.__cache_limit = 0 self.__cache_limit = 0
self.__cache_size = 0 self.__cache_size = 0
self.__article_list = [] # List of buffered articles self.__article_list = [] # List of buffered articles
self.__article_table = {} # Dict of buffered articles self.__article_table = {} # Dict of buffered articles
# Limit for the decoder is based on the total available cache # Limit for the decoder is based on the total available cache
# so it can be larger on memory-rich systems # 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 # For 64 bit we allow up to 4GB, in case somebody wants that
self.__cache_upper_limit = GIGI self.__cache_upper_limit = GIGI
if sabnzbd.DARWIN or sabnzbd.WIN64 or (struct.calcsize("P") * 8) == 64: 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 ArticleCache.do = self
@ -67,7 +67,7 @@ class ArticleCache:
# The decoder-limit should not be larger than 1/3th of the whole cache # The decoder-limit should not be larger than 1/3th of the whole cache
# Calculated in number of articles, assuming 1 article = 1MB max # 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 # The cache should also not be too small
self.decoder_cache_article_limit = max(decoder_cache_limit, MIN_DECODE_QUEUE) self.decoder_cache_article_limit = max(decoder_cache_limit, MIN_DECODE_QUEUE)
@ -128,8 +128,7 @@ class ArticleCache:
self.__article_list.remove(article) self.__article_list.remove(article)
self.free_reserved_space(len(data)) self.free_reserved_space(len(data))
elif article.art_id: elif article.art_id:
data = sabnzbd.load_data(article.art_id, nzo.workpath, remove=True, data = sabnzbd.load_data(article.art_id, nzo.workpath, remove=True, do_pickle=False, silent=True)
do_pickle=False, silent=True)
nzo.remove_saved_article(article) nzo.remove_saved_article(article)
return data return data

147
sabnzbd/assembler.py

@ -29,8 +29,7 @@ import hashlib
import sabnzbd import sabnzbd
from sabnzbd.misc import get_all_passwords from sabnzbd.misc import get_all_passwords
from sabnzbd.filesystem import set_permissions, clip_path, has_win_device, \ from sabnzbd.filesystem import set_permissions, clip_path, has_win_device, diskspace, get_filename, get_ext
diskspace, get_filename, get_ext
from sabnzbd.constants import Status, GIGI, MAX_ASSEMBLER_QUEUE from sabnzbd.constants import Status, GIGI, MAX_ASSEMBLER_QUEUE
import sabnzbd.cfg as cfg import sabnzbd.cfg as cfg
from sabnzbd.articlecache import ArticleCache from sabnzbd.articlecache import ArticleCache
@ -70,10 +69,13 @@ class Assembler(Thread):
if nzf: if nzf:
# Check if enough disk space is free after each file is done # Check if enough disk space is free after each file is done
# If not enough space left, pause downloader and send email # 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 # Only warn and email once
if not sabnzbd.downloader.Downloader.do.paused: 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! # Pause downloader, but don't save, since the disk is almost full!
sabnzbd.downloader.Downloader.do.pause() sabnzbd.downloader.Downloader.do.pause()
sabnzbd.emailer.diskfull_mail() sabnzbd.emailer.diskfull_mail()
@ -84,7 +86,7 @@ class Assembler(Thread):
filepath = nzf.prepare_filepath() filepath = nzf.prepare_filepath()
if filepath: if filepath:
logging.debug('Decoding part of %s', filepath) logging.debug("Decoding part of %s", filepath)
try: try:
self.assemble(nzf, file_done) self.assemble(nzf, file_done)
except IOError as err: 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: if not nzo.deleted and not nzo.is_gone() and not nzo.pp_active:
# 28 == disk full => pause downloader # 28 == disk full => pause downloader
if err.errno == 28: if err.errno == 28:
logging.error(T('Disk full! Forcing Pause')) logging.error(T("Disk full! Forcing Pause"))
else: 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 # Log traceback
logging.info('Traceback: ', exc_info=True) logging.info("Traceback: ", exc_info=True)
# Pause without saving # Pause without saving
sabnzbd.downloader.Downloader.do.pause() sabnzbd.downloader.Downloader.do.pause()
continue continue
except: except:
logging.error(T('Fatal error in Assembler'), exc_info=True) logging.error(T("Fatal error in Assembler"), exc_info=True)
break break
# Continue after partly written data # Continue after partly written data
@ -109,7 +111,7 @@ class Assembler(Thread):
continue continue
# Clean-up admin data # Clean-up admin data
logging.info('Decoding finished %s', filepath) logging.info("Decoding finished %s", filepath)
nzf.remove_admin() nzf.remove_admin()
# Do rar-related processing # Do rar-related processing
@ -118,23 +120,43 @@ class Assembler(Thread):
rar_encrypted, unwanted_file = check_encrypted_and_unwanted_files(nzo, filepath) rar_encrypted, unwanted_file = check_encrypted_and_unwanted_files(nzo, filepath)
if rar_encrypted: if rar_encrypted:
if cfg.pause_on_pwrar() == 1: 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() nzo.pause()
else: 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) logging.warning(
nzo.fail_msg = T('Aborted, encryption detected') 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) sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo)
if unwanted_file: 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.warning(
logging.debug(T('Unwanted extension is in rar file %s'), filepath) 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: 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.unwanted_ext = 1
nzo.pause() nzo.pause()
if cfg.action_on_unwanted_extensions() == 2: if cfg.action_on_unwanted_extensions() == 2:
logging.debug('Unwanted extension ... aborting') logging.debug("Unwanted extension ... aborting")
nzo.fail_msg = T('Aborted, unwanted extension detected') nzo.fail_msg = T("Aborted, unwanted extension detected")
sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo) sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo)
# Add to direct unpack # Add to direct unpack
@ -146,11 +168,19 @@ class Assembler(Thread):
filter_output, reason = nzo_filtered_by_rating(nzo) filter_output, reason = nzo_filtered_by_rating(nzo)
if filter_output == 1: 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() nzo.pause()
elif filter_output == 2: elif filter_output == 2:
logging.warning(remove_warning_label(T('WARNING: Aborted job "%s" because of rating (%s)')), nzo.final_name, reason) logging.warning(
nzo.fail_msg = T('Aborted, rating filter matched (%s)') % reason 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) sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo)
else: else:
@ -166,7 +196,7 @@ class Assembler(Thread):
if not nzf.md5: if not nzf.md5:
nzf.md5 = hashlib.md5() nzf.md5 = hashlib.md5()
with open(nzf.filepath, 'ab') as fout: with open(nzf.filepath, "ab") as fout:
for article in nzf.decodetable: for article in nzf.decodetable:
# Break if deleted during writing # Break if deleted during writing
if nzf.nzo.status is Status.DELETED: if nzf.nzo.status is Status.DELETED:
@ -214,21 +244,31 @@ def file_has_articles(nzf):
return has return has
RE_SUBS = re.compile(r'\W+sub|subs|subpack|subtitle|subtitles(?![a-z])', re.I) RE_SUBS = re.compile(r"\W+sub|subs|subpack|subtitle|subtitles(?![a-z])", re.I)
SAFE_EXTS = ('.mkv', '.mp4', '.avi', '.wmv', '.mpg', '.webm') SAFE_EXTS = (".mkv", ".mp4", ".avi", ".wmv", ".mpg", ".webm")
def is_cloaked(nzo, path, names): def is_cloaked(nzo, path, names):
""" Return True if this is likely to be a cloaked encrypted post """ """ Return True if this is likely to be a cloaked encrypted post """
fname = os.path.splitext(get_filename(path.lower()))[0] fname = os.path.splitext(get_filename(path.lower()))[0]
for name in names: for name in names:
name = get_filename(name.lower()) name = get_filename(name.lower())
name, ext = os.path.splitext(name) 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 # Only warn once
if nzo.encrypted == 0: 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 nzo.encrypted = 1
return True 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 # Only warn once
if nzo.encrypted == 0: if nzo.encrypted == 0:
logging.warning(T('Job "%s" is probably encrypted: "password" in filename "%s"'), nzo.final_name, name) 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 encrypted = False
unwanted = None 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 # These checks should not break the assembler
try: try:
# Rarfile freezes on Windows special names, so don't try those! # 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) zf = rarfile.RarFile(filepath, single_file_check=True)
# Check for encryption # 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 # Load all passwords
passwords = get_all_passwords(nzo) passwords = get_all_passwords(nzo)
@ -290,7 +336,7 @@ def check_encrypted_and_unwanted_files(nzo, filepath):
break break
except Exception as e: except Exception as e:
# Did we start from the right volume? # 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 return encrypted, unwanted
# This one failed # This one failed
pass pass
@ -312,15 +358,15 @@ def check_encrypted_and_unwanted_files(nzo, filepath):
# Check for unwanted extensions # Check for unwanted extensions
if cfg.unwanted_extensions() and cfg.action_on_unwanted_extensions(): if cfg.unwanted_extensions() and cfg.action_on_unwanted_extensions():
for somefile in zf.namelist(): for somefile in zf.namelist():
logging.debug('File contains: %s', somefile) logging.debug("File contains: %s", somefile)
if get_ext(somefile).replace('.', '').lower() in cfg.unwanted_extensions(): if get_ext(somefile).replace(".", "").lower() in cfg.unwanted_extensions():
logging.debug('Unwanted file %s', somefile) logging.debug("Unwanted file %s", somefile)
unwanted = somefile unwanted = somefile
zf.close() zf.close()
del zf del zf
except: except:
logging.info('Error during inspection of RAR-file %s', filepath) logging.info("Error during inspection of RAR-file %s", filepath)
logging.debug('Traceback: ', exc_info=True) logging.debug("Traceback: ", exc_info=True)
return encrypted, unwanted return encrypted, unwanted
@ -343,32 +389,39 @@ def rating_filtered(rating, filename, abort):
def check_keyword(keyword): def check_keyword(keyword):
clean_keyword = keyword.strip().lower() clean_keyword = keyword.strip().lower()
return (len(clean_keyword) > 0) and (clean_keyword in filename) return (len(clean_keyword) > 0) and (clean_keyword in filename)
audio = cfg.rating_filter_abort_audio() if abort else cfg.rating_filter_pause_audio() 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() 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 = 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() 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 = 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() 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() 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): 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): if (audio > 0) and (rating.avg_audio > 0) and (rating.avg_audio <= audio):
return T('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): if (spam and ((rating.avg_spam_cnt > 0) or rating.avg_encrypted_confirm)) or (
return T('spam') spam_confirm and rating.avg_spam_confirm
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("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): if downvoted and (rating.avg_vote_up < rating.avg_vote_down):
return T('downvoted') return T("downvoted")
if any(check_keyword(k) for k in keywords.split(',')): if any(check_keyword(k) for k in keywords.split(",")):
return T('keywords') return T("keywords")
return None return None
def remove_warning_label(msg): def remove_warning_label(msg):
""" Standardize errors by removing obsolete """ Standardize errors by removing obsolete
"WARNING:" part in all languages """ "WARNING:" part in all languages """
if ':' in msg: if ":" in msg:
return msg.split(':')[1].strip() return msg.split(":")[1].strip()
return msg 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) _DAYS = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
def last_month_day(tm): def last_month_day(tm):
""" Return last day of this month """ """ Return last day of this month """
year, month = tm[:2] year, month = tm[:2]
@ -105,31 +107,40 @@ class BPSMeter:
self.timeline_total = {} self.timeline_total = {}
self.day_label = time.strftime("%Y-%m-%d") self.day_label = time.strftime("%Y-%m-%d")
self.end_of_day = tomorrow(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_week = next_week(t) # Time that current day will end
self.end_of_month = next_month(t) # Time that current month 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_day = 1 # Day of quota reset
self.q_period = 'm' # Daily/Weekly/Monthly quota = d/w/m self.q_period = "m" # Daily/Weekly/Monthly quota = d/w/m
self.quota = self.left = 0.0 # Quota and remaining quota self.quota = self.left = 0.0 # Quota and remaining quota
self.have_quota = False # Flag for quota active self.have_quota = False # Flag for quota active
self.q_time = 0 # Next reset time for quota self.q_time = 0 # Next reset time for quota
self.q_hour = 0 # Quota reset hour self.q_hour = 0 # Quota reset hour
self.q_minute = 0 # Quota reset minute self.q_minute = 0 # Quota reset minute
self.quota_enabled = True # Scheduled quota enable/disable self.quota_enabled = True # Scheduled quota enable/disable
BPSMeter.do = self BPSMeter.do = self
def save(self): def save(self):
""" Save admin to disk """ """ Save admin to disk """
data = (self.last_update, self.grand_total, data = (
self.day_total, self.week_total, self.month_total, self.last_update,
self.end_of_day, self.end_of_week, self.end_of_month, self.grand_total,
self.quota, self.left, self.q_time, self.timeline_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) sabnzbd.save_admin(data, BYTES_FILE_NAME)
def defaults(self): def defaults(self):
""" Get the latest data from the database and assign to a fake server """ """ 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() history_db = sabnzbd.database.HistoryDB()
grand, month, week = history_db.get_history_size() grand, month, week = history_db.get_history_size()
history_db.close() history_db.close()
@ -138,11 +149,11 @@ class BPSMeter:
self.week_total = {} self.week_total = {}
self.day_total = {} self.day_total = {}
if grand: if grand:
self.grand_total['x'] = grand self.grand_total["x"] = grand
if month: if month:
self.month_total['x'] = month self.month_total["x"] = month
if week: if week:
self.week_total['x'] = week self.week_total["x"] = week
self.quota = self.left = cfg.quota_size.get_float() self.quota = self.left = cfg.quota_size.get_float()
def read(self): def read(self):
@ -152,10 +163,20 @@ class BPSMeter:
self.have_quota = bool(cfg.quota_size()) self.have_quota = bool(cfg.quota_size())
data = sabnzbd.load_admin(BYTES_FILE_NAME) data = sabnzbd.load_admin(BYTES_FILE_NAME)
try: try:
self.last_update, self.grand_total, \ (
self.day_total, self.week_total, self.month_total, \ self.last_update,
self.end_of_day, self.end_of_week, self.end_of_month, \ self.grand_total,
self.quota, self.left, self.q_time, self.timeline_total = data 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: if abs(quota - self.quota) > 0.5:
self.change_quota() self.change_quota()
res = self.reset_quota() res = self.reset_quota()
@ -210,7 +231,7 @@ class BPSMeter:
if server not in self.timeline_total: if server not in self.timeline_total:
self.timeline_total[server] = {} self.timeline_total[server] = {}
if self.day_label not in 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 self.timeline_total[server][self.day_label] += amount
# Quota check # Quota check
@ -219,7 +240,7 @@ class BPSMeter:
if self.left <= 0.0: if self.left <= 0.0:
if sabnzbd.downloader.Downloader.do and not sabnzbd.downloader.Downloader.do.paused: if sabnzbd.downloader.Downloader.do and not sabnzbd.downloader.Downloader.do.paused:
sabnzbd.downloader.Downloader.do.pause() sabnzbd.downloader.Downloader.do.pause()
logging.warning(T('Quota spent, pausing downloading')) logging.warning(T("Quota spent, pausing downloading"))
# Speedometer # Speedometer
try: try:
@ -261,22 +282,26 @@ class BPSMeter:
# Always trim the list to the max-length # Always trim the list to the max-length
if len(self.bps_list) > self.bps_list_max: 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): def get_sums(self):
""" return tuple of grand, month, week, day totals """ """ return tuple of grand, month, week, day totals """
return (sum([v for v in self.grand_total.values()]), return (
sum([v for v in self.month_total.values()]), sum([v for v in self.grand_total.values()]),
sum([v for v in self.week_total.values()]), sum([v for v in self.month_total.values()]),
sum([v for v in self.day_total.values()])) sum([v for v in self.week_total.values()]),
sum([v for v in self.day_total.values()]),
)
def amounts(self, server): def amounts(self, server):
""" Return grand, month, week, day totals for specified server """ """ Return grand, month, week, day totals for specified server """
return self.grand_total.get(server, 0), \ return (
self.month_total.get(server, 0), \ self.grand_total.get(server, 0),
self.week_total.get(server, 0), \ self.month_total.get(server, 0),
self.day_total.get(server, 0), \ self.week_total.get(server, 0),
self.timeline_total.get(server, {}) self.day_total.get(server, 0),
self.timeline_total.get(server, {}),
)
def clear_server(self, server): def clear_server(self, server):
""" Clean counters for specified 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)): if force or (self.have_quota and time.time() > (self.q_time - 50)):
self.quota = self.left = cfg.quota_size.get_float() 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(): 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: if sabnzbd.downloader.Downloader.do:
sabnzbd.downloader.Downloader.do.resume() sabnzbd.downloader.Downloader.do.resume()
self.next_reset() self.next_reset()
@ -344,20 +369,24 @@ class BPSMeter:
""" Determine next reset time """ """ Determine next reset time """
t = t or time.time() t = t or time.time()
tm = time.localtime(t) 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]) 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 (tm.tm_hour * 60 + tm.tm_min) >= (self.q_hour * 60 + self.q_minute):
# If today's moment has passed, it will happen tomorrow # If today's moment has passed, it will happen tomorrow
t = time.mktime(nx) + 24 * 3600 t = time.mktime(nx) + 24 * 3600
tm = time.localtime(t) tm = time.localtime(t)
elif self.q_period == 'w': 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)): 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)) tm = time.localtime(next_week(t))
dif = abs(self.q_day - tm.tm_wday - 1) dif = abs(self.q_day - tm.tm_wday - 1)
t = time.mktime(tm) + dif * 24 * 3600 t = time.mktime(tm) + dif * 24 * 3600
tm = time.localtime(t) tm = time.localtime(t)
elif self.q_period == 'm': 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)): 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)) tm = time.localtime(next_month(t))
day = min(last_month_day(tm), self.q_day) 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]) tm = (tm[0], tm[1], day, self.q_hour, self.q_minute, 0, 0, 0, tm[8])
@ -365,7 +394,7 @@ class BPSMeter:
return return
tm = (tm[0], tm[1], tm[2], self.q_hour, self.q_minute, 0, 0, 0, tm[8]) tm = (tm[0], tm[1], tm[2], self.q_hour, self.q_minute, 0, 0, 0, tm[8])
self.q_time = time.mktime(tm) 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): def change_quota(self, allow_resume=True):
""" Update quota, potentially pausing downloader """ """ Update quota, potentially pausing downloader """
@ -373,11 +402,11 @@ class BPSMeter:
# Never set, use last period's size # Never set, use last period's size
per = cfg.quota_period() per = cfg.quota_period()
sums = self.get_sums() sums = self.get_sums()
if per == 'd': if per == "d":
self.left = sums[3] self.left = sums[3]
elif per == 'w': elif per == "w":
self.left = sums[2] self.left = sums[2]
elif per == 'm': elif per == "m":
self.left = sums[1] self.left = sums[1]
self.have_quota = bool(cfg.quota_size()) self.have_quota = bool(cfg.quota_size())
@ -399,8 +428,9 @@ class BPSMeter:
# Pattern = <day#> <hh:mm> # Pattern = <day#> <hh:mm>
# The <day> and <hh:mm> part can both be optional # The <day> and <hh:mm> part can both be optional
__re_day = re.compile(r'^\s*(\d+)[^:]*') __re_day = re.compile(r"^\s*(\d+)[^:]*")
__re_hm = re.compile(r'(\d+):(\d+)\s*$') __re_hm = re.compile(r"(\d+):(\d+)\s*$")
def get_quota(self): def get_quota(self):
""" If quota active, return check-function, hour, minute """ """ If quota active, return check-function, hour, minute """
if self.have_quota: if self.have_quota:
@ -415,10 +445,10 @@ class BPSMeter:
if m: if m:
self.q_hour = int(m.group(1)) self.q_hour = int(m.group(1))
self.q_minute = int(m.group(2)) 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 = max(1, self.q_day)
self.q_day = min(7, 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 = max(1, self.q_day)
self.q_day = min(31, self.q_day) self.q_day = min(31, self.q_day)
else: else:
@ -447,7 +477,7 @@ class BPSMeter:
def quota_handler(): def quota_handler():
""" To be called from scheduler """ """ To be called from scheduler """
logging.debug('Checking quota') logging.debug("Checking quota")
BPSMeter.do.reset_quota() BPSMeter.do.reset_quota()

169
sabnzbd/directunpacker.py

@ -44,11 +44,10 @@ START_STOP_LOCK = threading.RLock()
ACTIVE_UNPACKERS = [] 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): class DirectUnpacker(threading.Thread):
def __init__(self, nzo): def __init__(self, nzo):
threading.Thread.__init__(self) threading.Thread.__init__(self)
@ -94,7 +93,13 @@ class DirectUnpacker(threading.Thread):
self.rarfile_nzf = None self.rarfile_nzf = None
def check_requirements(self): 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 False
return True return True
@ -137,12 +142,12 @@ class DirectUnpacker(threading.Thread):
# Are we doing this set? # Are we doing this set?
if self.cur_setname and self.cur_setname == nzf.setname: 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? # Is this the first one of the first set?
if not self.active_instance and not self.is_alive() and self.have_next_volume(): if not self.active_instance and not self.is_alive() and self.have_next_volume():
# Too many runners already? # Too many runners already?
if len(ACTIVE_UNPACKERS) >= cfg.direct_unpack_threads(): 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 return
# Start the unrar command and the loop # Start the unrar command and the loop
@ -158,8 +163,8 @@ class DirectUnpacker(threading.Thread):
def run(self): def run(self):
# Input and output # Input and output
linebuf = '' linebuf = ""
last_volume_linebuf = '' last_volume_linebuf = ""
unrar_log = [] unrar_log = []
rarfiles = [] rarfiles = []
extracted = [] extracted = []
@ -179,17 +184,30 @@ class DirectUnpacker(threading.Thread):
linebuf += char linebuf += char
# Error? Let PP-handle it # Error? Let PP-handle it
if linebuf.endswith(('ERROR: ', 'Cannot create', 'in the encrypted file', 'CRC failed', 'checksum failed', if linebuf.endswith(
'You need to start extraction from a previous volume', 'password is incorrect', (
'Incorrect password', 'Write error', 'checksum error', 'Cannot open', "ERROR: ",
'start extraction from a previous volume', 'Unexpected end of archive')): "Cannot create",
logging.info('Error in DirectUnpack of %s: %s', self.cur_setname, linebuf.strip()) "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() self.abort()
if linebuf.endswith('\n'): if linebuf.endswith("\n"):
# List files we used # List files we used
if linebuf.startswith('Extracting from'): if linebuf.startswith("Extracting from"):
filename = (re.search(EXTRACTFROM_RE, linebuf.strip()).group(1)) filename = re.search(EXTRACTFROM_RE, linebuf.strip()).group(1)
if filename not in rarfiles: if filename not in rarfiles:
rarfiles.append(filename) rarfiles.append(filename)
@ -203,27 +221,30 @@ class DirectUnpacker(threading.Thread):
extracted.append(real_path(self.unpack_dir_info[0], unpacked_file)) extracted.append(real_path(self.unpack_dir_info[0], unpacked_file))
# Did we reach the end? # Did we reach the end?
if linebuf.endswith('All OK'): if linebuf.endswith("All OK"):
# Stop timer and finish # Stop timer and finish
self.unpack_time += time.time() - start_time self.unpack_time += time.time() - start_time
ACTIVE_UNPACKERS.remove(self) ACTIVE_UNPACKERS.remove(self)
# Add to success # Add to success
rarfile_path = os.path.join(self.nzo.downpath, self.rarfile_nzf.filename) 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) self.success_sets[self.cur_setname] = (
logging.info('DirectUnpack completed for %s', self.cur_setname) rar_volumelist(rarfile_path, self.nzo.password, rarfiles),
self.nzo.set_action_line(T('Direct Unpack'), T('Completed')) 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 # List success in history-info
msg = T('Unpacked %s files/folders in %s') % (len(extracted), format_time_string(self.unpack_time)) msg = T("Unpacked %s files/folders in %s") % (len(extracted), format_time_string(self.unpack_time))
msg = '%s - %s' % (T('Direct Unpack'), msg) msg = "%s - %s" % (T("Direct Unpack"), msg)
self.nzo.set_unpack_info('Unpack', msg, self.cur_setname) self.nzo.set_unpack_info("Unpack", msg, self.cur_setname)
# Write current log and clear # Write current log and clear
unrar_log.append(linebuf.strip()) unrar_log.append(linebuf.strip())
linebuf = '' linebuf = ""
last_volume_linebuf = '' last_volume_linebuf = ""
logging.debug('DirectUnpack Unrar output %s', '\n'.join(unrar_log)) logging.debug("DirectUnpack Unrar output %s", "\n".join(unrar_log))
unrar_log = [] unrar_log = []
rarfiles = [] rarfiles = []
extracted = [] extracted = []
@ -247,7 +268,7 @@ class DirectUnpacker(threading.Thread):
self.killed = True self.killed = True
break break
if linebuf.endswith('[C]ontinue, [Q]uit '): if linebuf.endswith("[C]ontinue, [Q]uit "):
# Stop timer # Stop timer
self.unpack_time += time.time() - start_time 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 # If unrar stopped or is killed somehow, writing will cause a crash
try: try:
# Give unrar some time to do it's thing # 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() start_time = time.time()
time.sleep(0.1) time.sleep(0.1)
except IOError: except IOError:
@ -270,14 +291,14 @@ class DirectUnpacker(threading.Thread):
if not last_volume_linebuf or last_volume_linebuf != linebuf: if not last_volume_linebuf or last_volume_linebuf != linebuf:
# Next volume # Next volume
self.cur_volume += 1 self.cur_volume += 1
self.nzo.set_action_line(T('Direct Unpack'), self.get_formatted_stats()) 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) 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! # 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 # In rare occasions we can get stuck forever with repeating lines
if last_volume_linebuf == linebuf: if last_volume_linebuf == linebuf:
if not self.have_next_volume() or self.duplicate_lines > 10: 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() self.abort()
else: else:
logging.debug('Duplicate output line detected: "%s"', last_volume_linebuf) logging.debug('Duplicate output line detected: "%s"', last_volume_linebuf)
@ -287,13 +308,13 @@ class DirectUnpacker(threading.Thread):
last_volume_linebuf = linebuf last_volume_linebuf = linebuf
# Show the log # Show the log
if linebuf.endswith('\n'): if linebuf.endswith("\n"):
unrar_log.append(linebuf.strip()) unrar_log.append(linebuf.strip())
linebuf = '' linebuf = ""
# Add last line # Add last line
unrar_log.append(linebuf.strip()) 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 # Make more space
self.reset_active() self.reset_active()
@ -309,7 +330,7 @@ class DirectUnpacker(threading.Thread):
Make sure that files are 100% written to disk by checking md5sum Make sure that files are 100% written to disk by checking md5sum
""" """
for nzf_search in reversed(self.nzo.finished_files): 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 nzf_search
return False return False
@ -338,14 +359,14 @@ class DirectUnpacker(threading.Thread):
# Set options # Set options
if self.nzo.password: if self.nzo.password:
password_command = '-p%s' % self.nzo.password password_command = "-p%s" % self.nzo.password
else: else:
password_command = '-p-' password_command = "-p-"
if one_folder or cfg.flat_unpack(): if one_folder or cfg.flat_unpack():
action = 'e' action = "e"
else: else:
action = 'x' action = "x"
# The first NZF # The first NZF
self.rarfile_nzf = self.have_next_volume() self.rarfile_nzf = self.have_next_volume()
@ -360,36 +381,60 @@ class DirectUnpacker(threading.Thread):
if sabnzbd.WIN32: if sabnzbd.WIN32:
# For Unrar to support long-path, we need to cricumvent Python's list2cmdline # For Unrar to support long-path, we need to cricumvent Python's list2cmdline
# See: https://github.com/sabnzbd/sabnzbd/issues/1043 # See: https://github.com/sabnzbd/sabnzbd/issues/1043
command = ['%s' % sabnzbd.newsunpack.RAR_COMMAND, action, '-vp', '-idp', '-o+', '-ai', password_command, command = [
'%s' % clip_path(rarfile_path), '%s\\' % long_path(extraction_path)] "%s" % sabnzbd.newsunpack.RAR_COMMAND,
action,
"-vp",
"-idp",
"-o+",
"-ai",
password_command,
"%s" % clip_path(rarfile_path),
"%s\\" % long_path(extraction_path),
]
else: else:
# Don't use "-ai" (not needed for non-Windows) # Don't use "-ai" (not needed for non-Windows)
command = ['%s' % sabnzbd.newsunpack.RAR_COMMAND, action, '-vp', '-idp', '-o+', password_command, command = [
'%s' % rarfile_path, '%s/' % extraction_path] "%s" % sabnzbd.newsunpack.RAR_COMMAND,
action,
"-vp",
"-idp",
"-o+",
password_command,
"%s" % rarfile_path,
"%s/" % extraction_path,
]
if cfg.ignore_unrar_dates(): if cfg.ignore_unrar_dates():
command.insert(3, '-tsm-') command.insert(3, "-tsm-")
# Let's start from the first one! # Let's start from the first one!
self.cur_volume = 1 self.cur_volume = 1
stup, need_shell, command, creationflags = build_command(command, flatten_command=True) 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 # Need to disable buffer to have direct feedback
self.active_instance = Popen(command, shell=False, stdin=subprocess.PIPE, self.active_instance = Popen(
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, command,
startupinfo=stup, creationflags=creationflags, bufsize=0) shell=False,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
startupinfo=stup,
creationflags=creationflags,
bufsize=0,
)
# Add to runners # Add to runners
ACTIVE_UNPACKERS.append(self) ACTIVE_UNPACKERS.append(self)
# Doing the first # 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) @synchronized(START_STOP_LOCK)
def abort(self): def abort(self):
""" Abort running instance and delete generated files """ """ Abort running instance and delete generated files """
if not self.killed and self.cur_setname: 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 self.killed = True
# Save reference to the first rarfile # Save reference to the first rarfile
@ -399,7 +444,7 @@ class DirectUnpacker(threading.Thread):
if self.active_instance: if self.active_instance:
# First we try to abort gracefully # First we try to abort gracefully
try: try:
self.active_instance.stdin.write(b'Q\n') self.active_instance.stdin.write(b"Q\n")
time.sleep(0.2) time.sleep(0.2)
except IOError: except IOError:
pass pass
@ -427,14 +472,18 @@ class DirectUnpacker(threading.Thread):
if one_folder: if one_folder:
# RarFile can fail for mysterious reasons # RarFile can fail for mysterious reasons
try: 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: for rm_file in rar_contents:
# Flat-unpack, so remove foldername from RarFile output # Flat-unpack, so remove foldername from RarFile output
f = os.path.join(extraction_path, os.path.basename(rm_file)) f = os.path.join(extraction_path, os.path.basename(rm_file))
remove_file(f) remove_file(f)
except: except:
# The user will have to remove it themselves # 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: else:
# We can just remove the whole path # We can just remove the whole path
remove_all(extraction_path, recursive=True) 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: if self.cur_setname and self.cur_setname in self.total_volumes:
# This won't work on obfuscated posts # This won't work on obfuscated posts
if self.total_volumes[self.cur_setname] >= self.cur_volume and self.cur_volume: 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 return self.cur_volume
@ -465,14 +514,14 @@ def analyze_rar_filename(filename):
return m.group(1), int_conv(m.group(3)) return m.group(1), int_conv(m.group(3))
else: else:
# Detect if first of "rxx" set # Detect if first of "rxx" set
if filename.endswith('.rar'): if filename.endswith(".rar"):
return os.path.splitext(filename)[0], 1 return os.path.splitext(filename)[0], 1
return None, None return None, None
def abort_all(): def abort_all():
""" Abort all running DirectUnpackers """ """ Abort all running DirectUnpackers """
logging.info('Aborting all DirectUnpackers') logging.info("Aborting all DirectUnpackers")
for direct_unpacker in ACTIVE_UNPACKERS: for direct_unpacker in ACTIVE_UNPACKERS:
direct_unpacker.abort() direct_unpacker.abort()
@ -483,8 +532,14 @@ def test_disk_performance():
""" """
if diskspeedmeasure(sabnzbd.cfg.download_dir.get_path()) > 40: if diskspeedmeasure(sabnzbd.cfg.download_dir.get_path()) > 40:
cfg.direct_unpack.set(True) 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: 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) cfg.direct_unpack_tested.set(True)
sabnzbd.config.save_config() 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 # Timeout penalty in minutes for each cause
_PENALTY_UNKNOWN = 3 # Unknown cause _PENALTY_UNKNOWN = 3 # Unknown cause
_PENALTY_502 = 5 # Unknown 502 _PENALTY_502 = 5 # Unknown 502
_PENALTY_TIMEOUT = 10 # Server doesn't give an answer (multiple times) _PENALTY_TIMEOUT = 10 # Server doesn't give an answer (multiple times)
_PENALTY_SHARE = 10 # Account sharing detected _PENALTY_SHARE = 10 # Account sharing detected
_PENALTY_TOOMANY = 10 # Too many connections _PENALTY_TOOMANY = 10 # Too many connections
_PENALTY_PERM = 10 # Permanent error, like bad username/password _PENALTY_PERM = 10 # Permanent error, like bad username/password
_PENALTY_SHORT = 1 # Minimal penalty when no_penalties is set _PENALTY_SHORT = 1 # Minimal penalty when no_penalties is set
_PENALTY_VERYSHORT = 0.1 # Error 400 without cause clues _PENALTY_VERYSHORT = 0.1 # Error 400 without cause clues
@ -55,9 +55,24 @@ TIMER_LOCK = RLock()
class Server: class Server:
def __init__(
def __init__(self, server_id, displayname, host, port, timeout, threads, priority, ssl, ssl_verify, ssl_ciphers, self,
send_group, username=None, password=None, optional=False, retention=0): 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.id = server_id
self.newid = None self.newid = None
@ -82,12 +97,12 @@ class Server:
self.idle_threads = [] self.idle_threads = []
self.active = True self.active = True
self.bad_cons = 0 self.bad_cons = 0
self.errormsg = '' self.errormsg = ""
self.warning = '' self.warning = ""
self.info = None # Will hold getaddrinfo() list self.info = None # Will hold getaddrinfo() list
self.ssl_info = '' # Will hold the type and cipher of SSL connection self.ssl_info = "" # Will hold the type and cipher of SSL connection
self.request = False # True if a getaddrinfo() request is pending 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 self.have_stat = True # Assume server has "STAT", until proven otherwise
for i in range(threads): for i in range(threads):
@ -105,30 +120,30 @@ class Server:
# Check if already a successful ongoing connection # Check if already a successful ongoing connection
if self.busy_threads and self.busy_threads[0].nntp: if self.busy_threads and self.busy_threads[0].nntp:
# Re-use that IP # 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 return self.busy_threads[0].nntp.host
# Determine new IP # Determine new IP
if cfg.load_balancing() == 0 and self.info: if cfg.load_balancing() == 0 and self.info:
# Just return the first one, so all next threads use the same IP # Just return the first one, so all next threads use the same IP
ip = self.info[0][4][0] 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: elif cfg.load_balancing() == 1 and self.info and len(self.info) > 1:
# Return a random entry from the possible IPs # Return a random entry from the possible IPs
rnd = random.randint(0, len(self.info) - 1) rnd = random.randint(0, len(self.info) - 1)
ip = self.info[rnd][4][0] 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: elif cfg.load_balancing() == 2 and self.info and len(self.info) > 1:
# RFC6555 / Happy Eyeballs: # RFC6555 / Happy Eyeballs:
ip = happyeyeballs(self.host, port=self.port, ssl=self.ssl) ip = happyeyeballs(self.host, port=self.port, ssl=self.ssl)
if ip: if ip:
logging.debug('%s: Connecting to address %s', self.host, ip) logging.debug("%s: Connecting to address %s", self.host, ip)
else: else:
# nothing returned, so there was a connection problem # nothing returned, so there was a connection problem
ip = self.host 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: else:
ip = self.host ip = self.host
return ip return ip
@ -152,6 +167,7 @@ class Server:
class Downloader(Thread): class Downloader(Thread):
""" Singleton Downloader Thread """ """ Singleton Downloader Thread """
do = None do = None
def __init__(self, paused=False): def __init__(self, paused=False):
@ -187,7 +203,7 @@ class Downloader(Thread):
self.write_fds = {} self.write_fds = {}
self.servers = [] 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.server_nr = 0
self._timers = {} self._timers = {}
@ -235,8 +251,23 @@ class Downloader(Thread):
break break
if create and enabled and host and port and threads: if create and enabled and host and port and threads:
server = Server(newserver, displayname, host, port, timeout, threads, priority, ssl, ssl_verify, server = Server(
ssl_ciphers, send_group, username, password, optional, retention) newserver,
displayname,
host,
port,
timeout,
threads,
priority,
ssl,
ssl_verify,
ssl_ciphers,
send_group,
username,
password,
optional,
retention,
)
self.servers.append(server) self.servers.append(server)
self.server_dict[newserver] = server self.server_dict[newserver] = server
@ -255,7 +286,7 @@ class Downloader(Thread):
# Do not notify when SABnzbd is still starting # Do not notify when SABnzbd is still starting
if self.paused and sabnzbd.WEB_DIR: if self.paused and sabnzbd.WEB_DIR:
logging.info("Resuming") logging.info("Resuming")
notifier.send_notification("SABnzbd", T('Resuming'), 'download') notifier.send_notification("SABnzbd", T("Resuming"), "download")
self.paused = False self.paused = False
@NzbQueueLocker @NzbQueueLocker
@ -264,7 +295,7 @@ class Downloader(Thread):
if not self.paused: if not self.paused:
self.paused = True self.paused = True
logging.info("Pausing") logging.info("Pausing")
notifier.send_notification("SABnzbd", T('Paused'), 'download') notifier.send_notification("SABnzbd", T("Paused"), "download")
if self.is_paused(): if self.is_paused():
BPSMeter.do.reset() BPSMeter.do.reset()
if cfg.autodisconnect(): if cfg.autodisconnect():
@ -289,13 +320,13 @@ class Downloader(Thread):
""" """
if value: if value:
mx = cfg.bandwidth_max.get_int() mx = cfg.bandwidth_max.get_int()
if '%' in str(value) or (0 < from_units(value) < 101): if "%" in str(value) or (0 < from_units(value) < 101):
limit = value.strip(' %') limit = value.strip(" %")
self.bandwidth_perc = from_units(limit) self.bandwidth_perc = from_units(limit)
if mx: if mx:
self.bandwidth_limit = mx * self.bandwidth_perc / 100 self.bandwidth_limit = mx * self.bandwidth_perc / 100
else: 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: else:
self.bandwidth_limit = from_units(value) self.bandwidth_limit = from_units(value)
if mx: if mx:
@ -347,11 +378,11 @@ class Downloader(Thread):
# Was it resolving problem? # Was it resolving problem?
if server.info is False: if server.info is False:
# Warn about resolving issues # 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: if server.errormsg != errormsg:
server.errormsg = errormsg server.errormsg = errormsg
logging.warning(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 # Not fully the same as the code below for optional servers
server.bad_cons = 0 server.bad_cons = 0
@ -363,7 +394,7 @@ class Downloader(Thread):
if server.optional and server.active and (server.bad_cons / server.threads) > 3: if server.optional and server.active and (server.bad_cons / server.threads) > 3:
server.bad_cons = 0 server.bad_cons = 0
server.active = False 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) self.plan_server(server, _PENALTY_TIMEOUT)
# Remove all connections to server # Remove all connections to server
@ -389,23 +420,27 @@ class Downloader(Thread):
# See if we need to delay because the queues are full # See if we need to delay because the queues are full
logged = False 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: if not logged:
# Only log once, to not waste any CPU-cycles # Only log once, to not waste any CPU-cycles
logging.debug("Delaying - Decoder queue: %s - Assembler queue: %s", logging.debug(
sabnzbd.decoder.Decoder.do.decoder_queue.qsize(), "Delaying - Decoder queue: %s - Assembler queue: %s",
sabnzbd.assembler.Assembler.do.queue.qsize()) sabnzbd.decoder.Decoder.do.decoder_queue.qsize(),
sabnzbd.assembler.Assembler.do.queue.qsize(),
)
logged = True logged = True
time.sleep(0.05) time.sleep(0.05)
def run(self): def run(self):
# First check IPv6 connectivity # First check IPv6 connectivity
sabnzbd.EXTERNAL_IPV6 = sabnzbd.test_ipv6() 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 # Then we check SSL certificate checking
sabnzbd.CERTIFICATE_VALIDATION = sabnzbd.test_cert_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 # Kick BPS-Meter to check quota
BPSMeter.do.update() BPSMeter.do.update()
@ -462,7 +497,7 @@ class Downloader(Thread):
if server.retention and article.nzf.nzo.avg_stamp < time.time() - server.retention: 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 # 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: while article:
self.decode(article, None) self.decode(article, None)
article = article.nzf.nzo.get_article(server, self.servers) 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) logging.info("%s@%s: Initiating connection", nw.thrdnum, server.host)
nw.init_connect(self.write_fds) nw.init_connect(self.write_fds)
except: 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") self.__reset_nw(nw, "failed to initialize")
# Exit-point # Exit-point
@ -530,7 +570,7 @@ class Downloader(Thread):
# Now let's check if it was stable in the last 10 seconds # 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 = BPSMeter.do.get_stable_speed(timespan=10)
self.can_be_slowed_timer = 0 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: else:
read, write, error = ([], [], []) read, write, error = ([], [], [])
@ -540,8 +580,11 @@ class Downloader(Thread):
time.sleep(1.0) time.sleep(1.0)
DOWNLOADER_CV.acquire() DOWNLOADER_CV.acquire()
while (sabnzbd.nzbqueue.NzbQueue.do.is_empty() or self.is_paused() or self.postproc) and not \ while (
self.shutdown and not self.__restart: (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.wait()
DOWNLOADER_CV.release() DOWNLOADER_CV.release()
@ -599,7 +642,9 @@ class Downloader(Thread):
try: try:
nw.finish_connect(nw.status_code) nw.finish_connect(nw.status_code)
if sabnzbd.LOG_ALL: 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() nw.clear_data()
except NNTPPermanentError as error: except NNTPPermanentError as error:
# Handle login problems # Handle login problems
@ -607,45 +652,45 @@ class Downloader(Thread):
penalty = 0 penalty = 0
msg = error.response msg = error.response
ecode = int_conv(msg[:3]) ecode = int_conv(msg[:3])
display_msg = ' [%s]' % msg display_msg = " [%s]" % msg
logging.debug('Server login problem: %s, %s', ecode, msg) logging.debug("Server login problem: %s, %s", ecode, msg)
if ecode in (502, 400, 481, 482) and clues_too_many(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 # Too many connections: remove this thread and reduce thread-setting for server
# Plan to go back to the full number after a penalty timeout # Plan to go back to the full number after a penalty timeout
if server.active: 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: if server.errormsg != errormsg:
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.__reset_nw(nw, None, warn=False, destroy=True, send_quit=True)
self.plan_server(server, _PENALTY_TOOMANY) self.plan_server(server, _PENALTY_TOOMANY)
server.threads -= 1 server.threads -= 1
elif ecode in (502, 481, 482) and clues_too_many_ip(msg): elif ecode in (502, 481, 482) and clues_too_many_ip(msg):
# Account sharing? # Account sharing?
if server.active: if server.active:
errormsg = T('Probable account sharing') + display_msg errormsg = T("Probable account sharing") + display_msg
if server.errormsg != errormsg: if server.errormsg != errormsg:
server.errormsg = errormsg server.errormsg = errormsg
name = ' (%s)' % server.host name = " (%s)" % server.host
logging.warning(T('Probable account sharing') + name) logging.warning(T("Probable account sharing") + name)
penalty = _PENALTY_SHARE penalty = _PENALTY_SHARE
block = True block = True
elif ecode in (452, 481, 482, 381) or (ecode == 502 and clues_login(msg)): elif ecode in (452, 481, 482, 381) or (ecode == 502 and clues_login(msg)):
# Cannot login, block this server # Cannot login, block this server
if server.active: 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: if server.errormsg != errormsg:
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 penalty = _PENALTY_PERM
block = True block = True
elif ecode in (502, 482): elif ecode in (502, 482):
# Cannot connect (other reasons), block this server # Cannot connect (other reasons), block this server
if server.active: 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: if server.errormsg != errormsg:
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): if clues_pay(msg):
penalty = _PENALTY_PERM penalty = _PENALTY_PERM
else: else:
@ -654,16 +699,16 @@ class Downloader(Thread):
elif ecode == 400: elif ecode == 400:
# Temp connection problem? # Temp connection problem?
if server.active: 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 penalty = _PENALTY_VERYSHORT
block = True block = True
else: else:
# Unknown error, just keep trying # Unknown error, just keep trying
if server.active: 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: if server.errormsg != errormsg:
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 penalty = _PENALTY_UNKNOWN
block = True block = True
if block or (penalty and server.optional): if block or (penalty and server.optional):
@ -675,8 +720,12 @@ class Downloader(Thread):
self.__reset_nw(nw, None, warn=False, send_quit=True) self.__reset_nw(nw, None, warn=False, send_quit=True)
continue continue
except: except:
logging.error(T('Connecting %s@%s failed, message=%s'), logging.error(
nw.thrdnum, nw.server.host, nntp_to_msg(nw.data)) 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 # No reset-warning needed, above logging is sufficient
self.__reset_nw(nw, None, warn=False) self.__reset_nw(nw, None, warn=False)
@ -686,7 +735,7 @@ class Downloader(Thread):
elif nw.status_code == 223: elif nw.status_code == 223:
done = True done = True
logging.debug('Article <%s> is present', article.article) logging.debug("Article <%s> is present", article.article)
elif nw.status_code == 211: elif nw.status_code == 211:
done = False done = False
@ -697,27 +746,32 @@ class Downloader(Thread):
elif nw.status_code in (411, 423, 430): elif nw.status_code in (411, 423, 430):
done = True done = True
logging.debug('Thread %s@%s: Article %s missing (error=%s)', logging.debug(
nw.thrdnum, nw.server.host, article.article, nw.status_code) "Thread %s@%s: Article %s missing (error=%s)",
nw.thrdnum,
nw.server.host,
article.article,
nw.status_code,
)
nw.clear_data() nw.clear_data()
elif nw.status_code == 500: elif nw.status_code == 500:
if nzo.precheck: if nzo.precheck:
# Assume "STAT" command is not supported # Assume "STAT" command is not supported
server.have_stat = False 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: else:
# Assume "BODY" command is not supported # Assume "BODY" command is not supported
server.have_body = False 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() nw.clear_data()
self.__request_article(nw) self.__request_article(nw)
if done: if done:
server.bad_cons = 0 # Successful data, clear "bad" counter server.bad_cons = 0 # Successful data, clear "bad" counter
server.errormsg = server.warning = '' server.errormsg = server.warning = ""
if sabnzbd.LOG_ALL: 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) self.decode(article, nw.data)
nw.soft_reset() nw.soft_reset()
@ -749,9 +803,9 @@ class Downloader(Thread):
if warn and reset_msg: if warn and reset_msg:
server.warning = 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: 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: if nw in server.busy_threads:
server.busy_threads.remove(nw) server.busy_threads.remove(nw)
@ -777,7 +831,7 @@ class Downloader(Thread):
nw.hard_reset(wait, send_quit=send_quit) nw.hard_reset(wait, send_quit=send_quit)
# Empty SSL info, it might change on next connect # Empty SSL info, it might change on next connect
server.ssl_info = '' server.ssl_info = ""
def __request_article(self, nw): def __request_article(self, nw):
try: try:
@ -785,25 +839,25 @@ class Downloader(Thread):
if nw.server.send_group and nzo.group != nw.group: if nw.server.send_group and nzo.group != nw.group:
group = nzo.group group = nzo.group
if sabnzbd.LOG_ALL: 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) nw.send_group(group)
else: else:
if sabnzbd.LOG_ALL: 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) nw.body(nzo.precheck)
fileno = nw.nntp.sock.fileno() fileno = nw.nntp.sock.fileno()
if fileno not in self.read_fds: if fileno not in self.read_fds:
self.read_fds[fileno] = nw self.read_fds[fileno] = nw
except socket.error as err: 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) self.__reset_nw(nw, "server broke off connection", send_quit=False)
except: except:
logging.error(T('Suspect error in downloader')) logging.error(T("Suspect error in downloader"))
logging.info("Traceback: ", exc_info=True) logging.info("Traceback: ", exc_info=True)
self.__reset_nw(nw, "server broke off connection", send_quit=False) self.__reset_nw(nw, "server broke off connection", send_quit=False)
#------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Timed restart of servers admin. # Timed restart of servers admin.
# For each server all planned events are kept in a list. # For each server all planned events are kept in a list.
# When the first timer of a server fires, all other existing timers # 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 # Overwrite in case of no_penalties
interval = _PENALTY_SHORT 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: if server.id not in self._timers:
self._timers[server.id] = [] self._timers[server.id] = []
stamp = time.time() + 60.0 * interval stamp = time.time() + 60.0 * interval
@ -828,7 +882,7 @@ class Downloader(Thread):
@synchronized(TIMER_LOCK) @synchronized(TIMER_LOCK)
def trigger_server(self, server_id, timestamp): def trigger_server(self, server_id, timestamp):
""" Called by scheduler, start server if timer still valid """ """ 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 server_id in self._timers:
if timestamp in self._timers[server_id]: if timestamp in self._timers[server_id]:
del self._timers[server_id] del self._timers[server_id]
@ -845,7 +899,7 @@ class Downloader(Thread):
# Activate server if it was inactive # Activate server if it was inactive
for server in self.servers: for server in self.servers:
if server.id == server_id and not server.active: 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) self.init_server(server_id, server_id)
break break
@ -862,7 +916,7 @@ class Downloader(Thread):
kicked = [] kicked = []
for server_id in self._timers.keys(): for server_id in self._timers.keys():
if not [stamp for stamp in self._timers[server_id] if stamp >= now]: 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] del self._timers[server_id]
self.init_server(server_id, server_id) self.init_server(server_id, server_id)
kicked.append(server_id) kicked.append(server_id)
@ -870,7 +924,7 @@ class Downloader(Thread):
for server in self.servers: for server in self.servers:
if server.id not in self._timers: if server.id not in self._timers:
if server.id not in kicked and not server.active: 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) self.init_server(server.id, server.id)
def update_server(self, oldserver, newserver): def update_server(self, oldserver, newserver):
@ -886,7 +940,7 @@ class Downloader(Thread):
def stop(self): def stop(self):
self.shutdown = True self.shutdown = True
notifier.send_notification("SABnzbd", T('Shutting down'), 'startup') notifier.send_notification("SABnzbd", T("Shutting down"), "startup")
def stop(): def stop():
@ -905,7 +959,7 @@ def stop():
def clues_login(text): def clues_login(text):
""" Check for any "failed login" clues in the response code """ """ Check for any "failed login" clues in the response code """
text = text.lower() 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: if clue in text:
return True return True
return False return False
@ -914,9 +968,9 @@ def clues_login(text):
def clues_too_many(text): def clues_too_many(text):
""" Check for any "too many connections" clues in the response code """ """ Check for any "too many connections" clues in the response code """
text = text.lower() 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 # 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 True
return False return False
@ -924,7 +978,7 @@ def clues_too_many(text):
def clues_too_many_ip(text): def clues_too_many_ip(text):
""" Check for any "account sharing" clues in the response code """ """ Check for any "account sharing" clues in the response code """
text = text.lower() text = text.lower()
for clue in ('simultaneous ip', 'multiple ip'): for clue in ("simultaneous ip", "multiple ip"):
if clue in text: if clue in text:
return True return True
return False return False
@ -933,7 +987,7 @@ def clues_too_many_ip(text):
def clues_pay(text): def clues_pay(text):
""" Check for messages about payments """ """ Check for messages about payments """
text = text.lower() text = text.lower()
for clue in ('credits', 'paym', 'expired', 'exceeded'): for clue in ("credits", "paym", "expired", "exceeded"):
if clue in text: if clue in text:
return True return True
return False return False

14
sabnzbd/encoding.py

@ -31,14 +31,14 @@ def utob(str_in):
""" Shorthand for converting UTF-8 to bytes """ """ Shorthand for converting UTF-8 to bytes """
if isinstance(str_in, bytes): if isinstance(str_in, bytes):
return str_in return str_in
return str_in.encode('utf-8') return str_in.encode("utf-8")
def ubtou(str_in): def ubtou(str_in):
""" Shorthand for converting unicode bytes to UTF-8 """ """ Shorthand for converting unicode bytes to UTF-8 """
if not isinstance(str_in, bytes): if not isinstance(str_in, bytes):
return str_in return str_in
return str_in.decode('utf-8') return str_in.decode("utf-8")
def platform_btou(str_in): def platform_btou(str_in):
@ -50,7 +50,7 @@ def platform_btou(str_in):
try: try:
return ubtou(str_in) return ubtou(str_in)
except UnicodeDecodeError: except UnicodeDecodeError:
return str_in.decode(CODEPAGE, errors='replace').replace('?', '!') return str_in.decode(CODEPAGE, errors="replace").replace("?", "!")
else: else:
return str_in return str_in
@ -63,7 +63,7 @@ def correct_unknown_encoding(str_or_bytes_in):
""" """
# If already string, back to bytes # If already string, back to bytes
if not isinstance(str_or_bytes_in, 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 simple bytes-to-string
try: try:
@ -71,14 +71,12 @@ def correct_unknown_encoding(str_or_bytes_in):
except UnicodeDecodeError: except UnicodeDecodeError:
try: try:
# Try using 8-bit ASCII, if came from Windows # 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: except ValueError:
# Last resort we use the slow chardet package # 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): def xml_name(p):
""" Prepare name for use in HTML/XML contect """ """ Prepare name for use in HTML/XML contect """
return escape(str(p)) 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): def _retrieve_info(server):
""" Async attempt to run getaddrinfo() for specified 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) info = GetServerParms(server.host, server.port)
if not info: if not info:
server.bad_cons += server.threads server.bad_cons += server.threads
@ -67,12 +67,12 @@ def GetServerParms(host, port):
except: except:
port = 119 port = 119
opt = sabnzbd.cfg.ipv6_servers() 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: Control the use of IPv6 Usenet server addresses. Meaning:
0 = don't use 0 = don't use
1 = use when available and reachable (DEFAULT) 1 = use when available and reachable (DEFAULT)
2 = force usage (when SABnzbd's detection fails) 2 = force usage (when SABnzbd's detection fails)
''' """
try: try:
# Standard IPV4 or IPV6 # Standard IPV4 or IPV6
ips = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM) ips = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
@ -82,13 +82,14 @@ def GetServerParms(host, port):
return ips return ips
else: else:
# IPv6 unreachable or not allowed by user, so only return IPv4 address(es): # 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: except:
if opt == 2 or (opt == 1 and sabnzbd.EXTERNAL_IPV6) or (opt == 1 and sabnzbd.cfg.load_balancing() == 2): if opt == 2 or (opt == 1 and sabnzbd.EXTERNAL_IPV6) or (opt == 1 and sabnzbd.cfg.load_balancing() == 2):
try: try:
# Try IPV6 explicitly # Try IPV6 explicitly
return socket.getaddrinfo(host, port, socket.AF_INET6, return socket.getaddrinfo(
socket.SOCK_STREAM, socket.IPPROTO_IP, socket.AI_CANONNAME) host, port, socket.AF_INET6, socket.SOCK_STREAM, socket.IPPROTO_IP, socket.AI_CANONNAME
)
except: except:
# Nothing found! # Nothing found!
pass pass
@ -101,8 +102,9 @@ def con(sock, host, port, sslenabled, write_fds, nntp):
sock.setblocking(0) sock.setblocking(0)
if sslenabled: if sslenabled:
# Log SSL/TLS info # Log SSL/TLS info
logging.info("%s@%s: Connected using %s (%s)", logging.info(
nntp.nw.thrdnum, nntp.nw.server.host, sock.version(), sock.cipher()[0]) "%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]) 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. # 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: class NNTP:
# Pre-define attributes to save memory # 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): def __init__(self, host, port, info, sslenabled, nw, block=False, write_fds=None):
self.host = host self.host = host
@ -192,8 +194,13 @@ class NNTP:
self.sock.connect((self.host, self.port)) self.sock.connect((self.host, self.port))
if sslenabled: if sslenabled:
# Log SSL/TLS info # Log SSL/TLS info
logging.info("%s@%s: Connected using %s (%s)", logging.info(
self.nw.thrdnum, self.nw.server.host, self.sock.version(), self.sock.cipher()[0]) "%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]) self.nw.server.ssl_info = "%s (%s)" % (self.sock.version(), self.sock.cipher()[0])
except (ssl.SSLError, ssl.CertificateError) as e: except (ssl.SSLError, ssl.CertificateError) as e:
@ -216,23 +223,25 @@ class NNTP:
def error(self, error): def error(self, error):
raw_error_str = str(error) raw_error_str = str(error)
if 'SSL23_GET_SERVER_HELLO' in str(error) or 'SSL3_GET_RECORD' in raw_error_str: 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') error = T("This server does not allow SSL on this port")
# Catch certificate errors # 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 # 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 # Try to see if we should catch this message and provide better text
if 'hostname' in raw_error_str: 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.') raw_error_str = T(
elif 'certificate verify failed' in raw_error_str: "Certificate hostname mismatch: the server hostname is not listed in the certificate. This is a server issue."
raw_error_str = T('Certificate not valid. This is most probably 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 # Reformat error
error = T('Server %s uses an untrusted certificate [%s]') % (self.nw.server.host, raw_error_str) 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 = "%s - %s: %s" % (error, T("Wiki"), "https://sabnzbd.org/certificate-errors")
# Prevent throwing a lot of errors or when testing server # Prevent throwing a lot of errors or when testing server
if error not in self.nw.server.warning and not self.blocking: if error not in self.nw.server.warning and not self.blocking:
@ -254,8 +263,24 @@ class NNTP:
class NewsWrapper: class NewsWrapper:
# Pre-define attributes to save memory # Pre-define attributes to save memory
__slots__ = ('server', 'thrdnum', 'blocking', 'timeout', 'article', 'data', 'last_line', 'nntp', __slots__ = (
'recv', 'connected', 'user_sent', 'pass_sent', 'group', 'user_ok', 'pass_ok', 'force_login') "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): def __init__(self, server, thrdnum, block=False):
self.server = server self.server = server
@ -265,7 +290,7 @@ class NewsWrapper:
self.timeout = None self.timeout = None
self.article = None self.article = None
self.data = [] self.data = []
self.last_line = '' self.last_line = ""
self.nntp = None self.nntp = None
self.recv = None self.recv = None
@ -296,8 +321,9 @@ class NewsWrapper:
self.server.info = GetServerParms(self.server.host, self.server.port) self.server.info = GetServerParms(self.server.host, self.server.port)
# Construct NNTP object and shorthands # Construct NNTP object and shorthands
self.nntp = NNTP(self.server.hostip, self.server.port, self.server.info, self.server.ssl, self.nntp = NNTP(
self, self.blocking, write_fds) self.server.hostip, self.server.port, self.server.info, self.server.ssl, self, self.blocking, write_fds
)
self.recv = self.nntp.sock.recv self.recv = self.nntp.sock.recv
self.timeout = time.time() + self.server.timeout self.timeout = time.time() + self.server.timeout
@ -312,7 +338,7 @@ class NewsWrapper:
if code == 501 and self.user_sent: if code == 501 and self.user_sent:
# Change to a sensible text # Change to a sensible text
code = 481 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.user_ok = True
self.pass_sent = True self.pass_sent = True
@ -327,7 +353,7 @@ class NewsWrapper:
if code in (400, 502): if code in (400, 502):
raise NNTPPermanentError(nntp_to_msg(self.data)) raise NNTPPermanentError(nntp_to_msg(self.data))
elif not self.user_sent: 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.nntp.sock.sendall(command)
self.data = [] self.data = []
self.user_sent = True self.user_sent = True
@ -342,7 +368,7 @@ class NewsWrapper:
self.connected = True self.connected = True
if self.user_ok and not self.pass_sent: 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.nntp.sock.sendall(command)
self.data = [] self.data = []
self.pass_sent = True self.pass_sent = True
@ -359,19 +385,19 @@ class NewsWrapper:
self.timeout = time.time() + self.server.timeout self.timeout = time.time() + self.server.timeout
if precheck: if precheck:
if self.server.have_stat: if self.server.have_stat:
command = utob('STAT <%s>\r\n' % (self.article.article)) command = utob("STAT <%s>\r\n" % (self.article.article))
else: else:
command = utob('HEAD <%s>\r\n' % (self.article.article)) command = utob("HEAD <%s>\r\n" % (self.article.article))
elif self.server.have_body: elif self.server.have_body:
command = utob('BODY <%s>\r\n' % (self.article.article)) command = utob("BODY <%s>\r\n" % (self.article.article))
else: 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.nntp.sock.sendall(command)
self.data = [] self.data = []
def send_group(self, group): def send_group(self, group):
self.timeout = time.time() + self.server.timeout 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.nntp.sock.sendall(command)
self.data = [] self.data = []
@ -403,13 +429,13 @@ class NewsWrapper:
# Official end-of-article is ".\r\n" but sometimes it can get lost between 2 chunks # Official end-of-article is ".\r\n" but sometimes it can get lost between 2 chunks
chunk_len = len(chunk) 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) return (chunk_len, True, False)
elif chunk_len < 5 and len(self.data) > 1: elif chunk_len < 5 and len(self.data) > 1:
# We need to make sure the end is not split over 2 chunks # We need to make sure the end is not split over 2 chunks
# This is faster than join() # This is faster than join()
combine_chunk = self.data[-2][-5:] + chunk 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) return (chunk_len, True, False)
# Still in middle of data, so continue! # Still in middle of data, so continue!
@ -422,13 +448,13 @@ class NewsWrapper:
def clear_data(self): def clear_data(self):
self.data = [] self.data = []
self.last_line = '' self.last_line = ""
def hard_reset(self, wait=True, send_quit=True): def hard_reset(self, wait=True, send_quit=True):
if self.nntp: if self.nntp:
try: try:
if send_quit: if send_quit:
self.nntp.sock.sendall(b'QUIT\r\n') self.nntp.sock.sendall(b"QUIT\r\n")
time.sleep(0.1) time.sleep(0.1)
self.nntp.sock.close() self.nntp.sock.close()
except: except:
@ -449,7 +475,7 @@ class NewsWrapper:
if self.nntp: if self.nntp:
try: try:
if quit: if quit:
self.nntp.sock.sendall(b'QUIT\r\n') self.nntp.sock.sendall(b"QUIT\r\n")
time.sleep(0.1) time.sleep(0.1)
self.nntp.sock.close() self.nntp.sock.close()
except: 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 from sabnzbd.panic import panic_queue
import sabnzbd.database as database import sabnzbd.database as database
from sabnzbd.decorators import NzbQueueLocker from sabnzbd.decorators import NzbQueueLocker
from sabnzbd.constants import QUEUE_FILE_NAME, QUEUE_VERSION, FUTURE_Q_FOLDER, \ from sabnzbd.constants import (
JOB_ADMIN, LOW_PRIORITY, NORMAL_PRIORITY, HIGH_PRIORITY, TOP_PRIORITY, \ QUEUE_FILE_NAME,
REPAIR_PRIORITY, STOP_PRIORITY, VERIFIED_FILE, \ QUEUE_VERSION,
Status, IGNORED_FOLDERS, QNFO, DIRECT_WRITE_TRIGGER 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.cfg as cfg
import sabnzbd.downloader import sabnzbd.downloader
@ -47,6 +60,7 @@ from sabnzbd.dirscanner import process_single_nzb
class NzbQueue: class NzbQueue:
""" Singleton NzbQueue """ """ Singleton NzbQueue """
do = None do = None
def __init__(self): def __init__(self):
@ -71,14 +85,16 @@ class NzbQueue:
queue_vers, nzo_ids, _ = data queue_vers, nzo_ids, _ = data
if not queue_vers == QUEUE_VERSION: if not queue_vers == QUEUE_VERSION:
nzo_ids = [] nzo_ids = []
logging.error(T('Incompatible queuefile found, cannot proceed')) logging.error(T("Incompatible queuefile found, cannot proceed"))
if not repair: if not repair:
panic_queue(os.path.join(cfg.admin_dir.get_path(), QUEUE_FILE_NAME)) panic_queue(os.path.join(cfg.admin_dir.get_path(), QUEUE_FILE_NAME))
exit_sab(2) exit_sab(2)
except: except:
nzo_ids = [] nzo_ids = []
logging.error(T('Error loading %s, corrupt file detected'), logging.error(
os.path.join(cfg.admin_dir.get_path(), QUEUE_FILE_NAME)) 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 # First handle jobs in the queue file
folders = [] folders = []
@ -103,7 +119,7 @@ class NzbQueue:
for item in globber_full(os.path.join(cfg.admin_dir.get_path(), FUTURE_Q_FOLDER)): for item in globber_full(os.path.join(cfg.admin_dir.get_path(), FUTURE_Q_FOLDER)):
path, nzo_id = os.path.split(item) path, nzo_id = os.path.split(item)
if nzo_id not in self.__nzo_table: 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) nzo = sabnzbd.load_data(nzo_id, path, remove=True)
if nzo: if nzo:
self.add(nzo, save=True) self.add(nzo, save=True)
@ -130,20 +146,25 @@ class NzbQueue:
# Retryable folders from History # Retryable folders from History
items = sabnzbd.api.build_history(output=True)[0] items = sabnzbd.api.build_history(output=True)[0]
# Anything waiting or active or retryable is a known item # Anything waiting or active or retryable is a known item
registered.extend([os.path.basename(item['path']) registered.extend(
for item in items if item['retry'] or item['loaded'] or item['status'] == Status.QUEUED]) [
os.path.basename(item["path"])
for item in items
if item["retry"] or item["loaded"] or item["status"] == Status.QUEUED
]
)
# Repair unregistered folders # Repair unregistered folders
for folder in globber_full(cfg.download_dir.get_path()): for folder in globber_full(cfg.download_dir.get_path()):
name = os.path.basename(folder) name = os.path.basename(folder)
if os.path.isdir(folder) and name not in registered and name not in IGNORED_FOLDERS: if os.path.isdir(folder) and name not in registered and name not in IGNORED_FOLDERS:
if action: if action:
logging.info('Repairing job %s', folder) logging.info("Repairing job %s", folder)
self.repair_job(folder) self.repair_job(folder)
result.append(os.path.basename(folder)) result.append(os.path.basename(folder))
else: else:
if action: if action:
logging.info('Skipping repair for job %s', folder) logging.info("Skipping repair for job %s", folder)
return result return result
def repair_job(self, folder, new_nzb=None, password=None): def repair_job(self, folder, new_nzb=None, password=None):
@ -154,40 +175,49 @@ class NzbQueue:
name = os.path.basename(folder) name = os.path.basename(folder)
path = os.path.join(folder, JOB_ADMIN) path = os.path.join(folder, JOB_ADMIN)
if hasattr(new_nzb, 'filename'): if hasattr(new_nzb, "filename"):
filename = new_nzb.filename filename = new_nzb.filename
else: else:
filename = '' filename = ""
if not filename: if not filename:
# Was this file already post-processed? # Was this file already post-processed?
verified = sabnzbd.load_data(VERIFIED_FILE, path, remove=False) verified = sabnzbd.load_data(VERIFIED_FILE, path, remove=False)
if not verified or not all(verified[x] for x in verified): 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: if len(filename) > 0:
logging.debug('Repair job %s by re-parsing stored NZB', name) 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, nzo_id = sabnzbd.add_nzbfile(
reuse=True, password=password)[1] filename[0],
pp=None,
script=None,
cat=None,
priority=None,
nzbname=name,
reuse=True,
password=password,
)[1]
else: else:
logging.debug('Repair job %s without stored NZB', name) 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 = NzbObject(name, pp=None, script=None, nzb="", cat=None, priority=None, nzbname=name, reuse=True)
nzo.password = password nzo.password = password
self.add(nzo) self.add(nzo)
nzo_id = nzo.nzo_id nzo_id = nzo.nzo_id
else: else:
remove_all(path, '*.gz') remove_all(path, "*.gz")
logging.debug('Repair job %s with new NZB (%s)', name, filename) 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, nzo_id = sabnzbd.add_nzbfile(
reuse=True, password=password)[1] new_nzb, pp=None, script=None, cat=None, priority=None, nzbname=name, reuse=True, password=password
)[1]
return nzo_id return nzo_id
@NzbQueueLocker @NzbQueueLocker
def send_back(self, nzo): def send_back(self, nzo):
""" Send back job to queue after successful pre-check """ """ Send back job to queue after successful pre-check """
try: try:
nzb_path = globber_full(nzo.workpath, '*.gz')[0] nzb_path = globber_full(nzo.workpath, "*.gz")[0]
except: 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 return
# Need to remove it first, otherwise it might still be downloading # 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): 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 """ """ Create and return a placeholder nzo object """
logging.debug('Creating placeholder NZO') 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) future_nzo = NzbObject(
msg,
pp,
script,
None,
futuretype=True,
cat=cat,
url=url,
priority=priority,
nzbname=nzbname,
status=Status.GRABBING,
)
self.add(future_nzo) self.add(future_nzo)
return future_nzo return future_nzo
def change_opts(self, nzo_ids, pp): def change_opts(self, nzo_ids, pp):
result = 0 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: if nzo_id in self.__nzo_table:
self.__nzo_table[nzo_id].set_pp(pp) self.__nzo_table[nzo_id].set_pp(pp)
result += 1 result += 1
@ -236,20 +277,20 @@ class NzbQueue:
def change_script(self, nzo_ids, script): def change_script(self, nzo_ids, script):
result = 0 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: if nzo_id in self.__nzo_table:
self.__nzo_table[nzo_id].script = script 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 result += 1
return result return result
def change_cat(self, nzo_ids, cat, explicit_priority=None): def change_cat(self, nzo_ids, cat, explicit_priority=None):
result = 0 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: if nzo_id in self.__nzo_table:
nzo = self.__nzo_table[nzo_id] nzo = self.__nzo_table[nzo_id]
nzo.cat, pp, nzo.script, prio = cat_to_opts(cat) 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) nzo.set_pp(pp)
if explicit_priority is None: if explicit_priority is None:
self.set_priority(nzo_id, prio) self.set_priority(nzo_id, prio)
@ -261,7 +302,7 @@ class NzbQueue:
def change_name(self, nzo_id, name, password=None): def change_name(self, nzo_id, name, password=None):
if nzo_id in self.__nzo_table: if nzo_id in self.__nzo_table:
nzo = self.__nzo_table[nzo_id] 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) # Abort any ongoing unpacking if the name changed (dirs change)
nzo.abort_direct_unpacker() nzo.abort_direct_unpacker()
if not nzo.futuretype: if not nzo.futuretype:
@ -283,12 +324,12 @@ class NzbQueue:
@NzbQueueLocker @NzbQueueLocker
def add(self, nzo, save=True, quiet=False): def add(self, nzo, save=True, quiet=False):
if not nzo.nzo_id: 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 no files are to be downloaded anymore, send to postproc
if not nzo.files and not nzo.futuretype: if not nzo.files and not nzo.futuretype:
self.end_job(nzo) self.end_job(nzo)
return '' return ""
# Reset try_lists # Reset try_lists
nzo.reset_try_list() nzo.reset_try_list()
@ -330,7 +371,7 @@ class NzbQueue:
self.save(nzo) self.save(nzo)
if not (quiet or nzo.status == Status.FETCHING): 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(): if not quiet and cfg.auto_sort():
self.sort_by_avg_age() self.sort_by_avg_age()
@ -344,7 +385,7 @@ class NzbQueue:
""" """
if nzo_id in self.__nzo_table: if nzo_id in self.__nzo_table:
nzo = self.__nzo_table.pop(nzo_id) 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 # Set statuses
nzo.deleted = True nzo.deleted = True
@ -410,10 +451,10 @@ class NzbQueue:
elif force_delete: elif force_delete:
# Force-remove all trace # Force-remove all trace
nzo.bytes -= nzf.bytes 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] del nzo.files_table[nzf_id]
nzo.finished_files.remove(nzf) 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 return removed
def pause_multiple_nzo(self, nzo_ids): def pause_multiple_nzo(self, nzo_ids):
@ -492,7 +533,13 @@ class NzbQueue:
item_id_pos2 = i item_id_pos2 = i
if (item_id_pos1 > -1) and (item_id_pos2 > -1): if (item_id_pos1 > -1) and (item_id_pos2 > -1):
item = self.__nzo_list[item_id_pos1] 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] del self.__nzo_list[item_id_pos1]
self.__nzo_list.insert(item_id_pos2, item) self.__nzo_list.insert(item_id_pos2, item)
return item_id_pos2, nzo1.priority return item_id_pos2, nzo1.priority
@ -538,17 +585,17 @@ class NzbQueue:
def sort_queue(self, field, reverse=None): def sort_queue(self, field, reverse=None):
if isinstance(reverse, str): if isinstance(reverse, str):
if reverse.lower() == 'desc': if reverse.lower() == "desc":
reverse = True reverse = True
else: else:
reverse = False reverse = False
if reverse is None: if reverse is None:
reverse = False reverse = False
if field.lower() == 'name': if field.lower() == "name":
self.sort_by_name(reverse) 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) self.sort_by_size(reverse)
elif field.lower() == 'avg_age': elif field.lower() == "avg_age":
self.sort_by_avg_age(reverse) self.sort_by_avg_age(reverse)
else: else:
logging.debug("Sort: %s not recognized", field) logging.debug("Sort: %s not recognized", field)
@ -578,8 +625,11 @@ class NzbQueue:
return nzo_id_pos1 return nzo_id_pos1
nzo.set_priority(priority) nzo.set_priority(priority)
if sabnzbd.scheduler.analyse(False, priority) and \ if sabnzbd.scheduler.analyse(False, priority) and nzo.status in (
nzo.status in (Status.CHECKING, Status.DOWNLOADING, Status.QUEUED): Status.CHECKING,
Status.DOWNLOADING,
Status.QUEUED,
):
nzo.status = Status.PAUSED nzo.status = Status.PAUSED
elif nzo.status == Status.PAUSED: elif nzo.status == Status.PAUSED:
nzo.status = Status.QUEUED nzo.status = Status.QUEUED
@ -620,7 +670,9 @@ class NzbQueue:
self.__nzo_list.append(nzo) self.__nzo_list.append(nzo)
pos = 0 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 return pos
except: except:
@ -630,7 +682,7 @@ class NzbQueue:
def set_priority(self, nzo_ids, priority): def set_priority(self, nzo_ids, priority):
try: try:
n = -1 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) n = self.__set_priority(nzo_id, priority)
return n return n
except: except:
@ -667,7 +719,11 @@ class NzbQueue:
# Not when queue paused and not a forced item # Not when queue paused and not a forced item
if nzo.status not in (Status.PAUSED, Status.GRABBING) or nzo.priority == TOP_PRIORITY: if nzo.status not in (Status.PAUSED, Status.GRABBING) or nzo.priority == TOP_PRIORITY:
# Check if past propagation delay, or forced # 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): if not nzo.server_in_try_list(server):
article = nzo.get_article(server, servers) article = nzo.get_article(server, servers)
if article: if article:
@ -690,7 +746,7 @@ class NzbQueue:
articles_left, file_done, post_done = nzo.remove_article(article, success) articles_left, file_done, post_done = nzo.remove_article(article, success)
if nzo.is_gone(): 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: else:
# Write data if file is done or at trigger time # Write data if file is done or at trigger time
if file_done or (articles_left and (articles_left % DIRECT_WRITE_TRIGGER) == 0): 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 # The type is only set if sabyenc could decode the article
if nzf.filename and nzf.type: if nzf.filename and nzf.type:
Assembler.do.process((nzo, nzf, file_done)) 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 # Broken par2 file, try to get another one
nzo.promote_par2(nzf) nzo.promote_par2(nzf)
else: else:
if file_has_articles(nzf): 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 # Save bookkeeping in case of crash
if file_done and (nzo.next_save is None or time.time() > nzo.next_save): 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): def end_job(self, nzo):
""" Send NZO to the post-processing queue """ """ 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 # Notify assembler to call postprocessor
if not nzo.deleted: if not nzo.deleted:
@ -808,7 +864,7 @@ class NzbQueue:
empty = [] empty = []
for nzo in self.__nzo_list: for nzo in self.__nzo_list:
if not nzo.futuretype and not nzo.files and nzo.status not in (Status.PAUSED, Status.GRABBING): 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) empty.append(nzo)
# Stall prevention by checking if all servers are in the trylist # Stall prevention by checking if all servers are in the trylist
@ -818,11 +874,11 @@ class NzbQueue:
for nzf in nzo.files: for nzf in nzo.files:
if len(nzf.try_list) == sabnzbd.downloader.Downloader.do.server_nr: if len(nzf.try_list) == sabnzbd.downloader.Downloader.do.server_nr:
# We do not want to reset all article trylists, they are good # 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() nzf.reset_try_list()
# Reset main trylist, minimal performance impact # 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() nzo.reset_try_list()
for nzo in empty: for nzo in empty:
@ -859,7 +915,7 @@ class NzbQueue:
nzo = self.__nzo_table[nzo_id] nzo = self.__nzo_table[nzo_id]
if nzo.futuretype: if nzo.futuretype:
url = nzo.url url = nzo.url
if nzo.futuretype and url.lower().startswith('http'): if nzo.futuretype and url.lower().startswith("http"):
lst.append((url, nzo)) lst.append((url, nzo))
return lst 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 from sabnzbd.bpsmeter import BPSMeter
status_icons = { status_icons = {
'idle': 'icons/sabnzbd_osx_idle.tiff', "idle": "icons/sabnzbd_osx_idle.tiff",
'pause': 'icons/sabnzbd_osx_pause.tiff', "pause": "icons/sabnzbd_osx_pause.tiff",
'clicked': 'icons/sabnzbd_osx_clicked.tiff' "clicked": "icons/sabnzbd_osx_clicked.tiff",
} }
start_time = NSDate.date() start_time = NSDate.date()
debug = 0 debug = 0
@ -71,7 +71,9 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] awake") NSLog("[osx] awake")
self.buildMenu() self.buildMenu()
# Timer for updating menu # 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, NSDefaultRunLoopMode)
NSRunLoop.currentRunLoop().addTimer_forMode_(self.timer, NSEventTrackingRunLoopMode) NSRunLoop.currentRunLoop().addTimer_forMode_(self.timer, NSEventTrackingRunLoopMode)
# NSRunLoop.currentRunLoop().addTimer_forMode_(self.timer, NSModalPanelRunLoopMode) # NSRunLoop.currentRunLoop().addTimer_forMode_(self.timer, NSModalPanelRunLoopMode)
@ -86,15 +88,15 @@ class SABnzbdDelegate(NSObject):
icon_path = status_icons[icon] icon_path = status_icons[icon]
if hasattr(sys, "frozen"): if hasattr(sys, "frozen"):
# Path is modified for the binary # 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) self.icons[icon] = NSImage.alloc().initByReferencingFile_(icon_path)
if sabnzbd.DARWIN_VERSION > 9: if sabnzbd.DARWIN_VERSION > 9:
# Support for Yosemite Dark Mode # Support for Yosemite Dark Mode
self.icons[icon].setTemplate_(YES) self.icons[icon].setTemplate_(YES)
self.status_item.setImage_(self.icons['idle']) self.status_item.setImage_(self.icons["idle"])
self.status_item.setAlternateImage_(self.icons['clicked']) self.status_item.setAlternateImage_(self.icons["clicked"])
self.status_item.setHighlightMode_(1) self.status_item.setHighlightMode_(1)
self.status_item.setToolTip_('SABnzbd') self.status_item.setToolTip_("SABnzbd")
self.status_item.setEnabled_(YES) self.status_item.setEnabled_(YES)
if debug == 1: if debug == 1:
@ -125,7 +127,7 @@ class SABnzbdDelegate(NSObject):
self.menu = NSMenu.alloc().init() self.menu = NSMenu.alloc().init()
try: try:
menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Dummy", '', '') menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Dummy", "", "")
menu_item.setHidden_(YES) menu_item.setHidden_(YES)
self.isLeopard = 1 self.isLeopard = 1
except: except:
@ -135,7 +137,9 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 3 construction") NSLog("[osx] menu 3 construction")
# Warnings Item # 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: if self.isLeopard:
self.warnings_menu_item.setHidden_(YES) self.warnings_menu_item.setHidden_(YES)
else: else:
@ -147,7 +151,9 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 4 warning added") NSLog("[osx] menu 4 warning added")
# State Item # 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.state_menu_item.setRepresentedObject_("")
self.menu.addItem_(self.state_menu_item) self.menu.addItem_(self.state_menu_item)
@ -155,7 +161,7 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 5 state added") NSLog("[osx] menu 5 state added")
# Config Item # 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.setRepresentedObject_("config/general/")
menu_item.setAlternate_(YES) menu_item.setAlternate_(YES)
menu_item.setKeyEquivalentModifierMask_(NSAlternateKeyMask) menu_item.setKeyEquivalentModifierMask_(NSAlternateKeyMask)
@ -165,7 +171,9 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 6 config added") NSLog("[osx] menu 6 config added")
# Queue Item # 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.queue_menu_item.setRepresentedObject_("")
self.menu.addItem_(self.queue_menu_item) self.menu.addItem_(self.queue_menu_item)
@ -173,7 +181,9 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 7 queue added") NSLog("[osx] menu 7 queue added")
# Purge Queue Item # 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.setRepresentedObject_("queue")
self.purgequeue_menu_item.setAlternate_(YES) self.purgequeue_menu_item.setAlternate_(YES)
self.purgequeue_menu_item.setKeyEquivalentModifierMask_(NSAlternateKeyMask) self.purgequeue_menu_item.setKeyEquivalentModifierMask_(NSAlternateKeyMask)
@ -183,7 +193,9 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 8 purge queue added") NSLog("[osx] menu 8 purge queue added")
# History Item # 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.history_menu_item.setRepresentedObject_("")
self.menu.addItem_(self.history_menu_item) self.menu.addItem_(self.history_menu_item)
@ -191,7 +203,9 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 9 history added") NSLog("[osx] menu 9 history added")
# Purge History Item # 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.setRepresentedObject_("history")
self.purgehistory_menu_item.setAlternate_(YES) self.purgehistory_menu_item.setAlternate_(YES)
self.purgehistory_menu_item.setKeyEquivalentModifierMask_(NSAlternateKeyMask) self.purgehistory_menu_item.setKeyEquivalentModifierMask_(NSAlternateKeyMask)
@ -204,16 +218,27 @@ class SABnzbdDelegate(NSObject):
self.menu.addItem_(self.separator_menu_item) self.menu.addItem_(self.separator_menu_item)
# Limit Speed Item & Submenu # 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() self.menu_speed = NSMenu.alloc().init()
speeds = {10: '10%', 20: '20%', 30: '30%', 40: '40%', 50: '50%', speeds = {
60: '60%', 70: '70%', 80: '80%', 90: '90%', 100: '100%' 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()): 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) menu_speed_item.setRepresentedObject_("%s" % speed)
self.menu_speed.addItem_(menu_speed_item) self.menu_speed.addItem_(menu_speed_item)
@ -224,13 +249,15 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 11 limit speed added") NSLog("[osx] menu 11 limit speed added")
# Pause Item & Submenu # Pause Item & Submenu
self.pause_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T('Pause'), 'pauseAction:', '') self.pause_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(T("Pause"), "pauseAction:", "")
self.pause_menu_item.setRepresentedObject_('0') self.pause_menu_item.setRepresentedObject_("0")
self.menu_pause = NSMenu.alloc().init() self.menu_pause = NSMenu.alloc().init()
for i in range(6): 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)) menu_pause_item.setRepresentedObject_("%s" % ((i + 1) * 10))
self.menu_pause.addItem_(menu_pause_item) self.menu_pause.addItem_(menu_pause_item)
@ -241,7 +268,7 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 12 pause added") NSLog("[osx] menu 12 pause added")
# Resume Item # 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: if self.isLeopard:
self.resume_menu_item.setHidden_(YES) self.resume_menu_item.setHidden_(YES)
else: else:
@ -252,7 +279,9 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 13 resume added") NSLog("[osx] menu 13 resume added")
# Watched folder Item # 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: if self.isLeopard:
self.watched_menu_item.setHidden_(YES) self.watched_menu_item.setHidden_(YES)
else: else:
@ -260,7 +289,9 @@ class SABnzbdDelegate(NSObject):
self.menu.addItem_(self.watched_menu_item) self.menu.addItem_(self.watched_menu_item)
# All RSS feeds # 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: if self.isLeopard:
self.rss_menu_item.setHidden_(YES) self.rss_menu_item.setHidden_(YES)
else: else:
@ -274,12 +305,16 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 14 watched folder added") NSLog("[osx] menu 14 watched folder added")
# Complete Folder Item # 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.completefolder_menu_item.setRepresentedObject_(sabnzbd.cfg.complete_dir.get_path())
self.menu.addItem_(self.completefolder_menu_item) self.menu.addItem_(self.completefolder_menu_item)
# Incomplete Folder 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.incompletefolder_menu_item.setRepresentedObject_(sabnzbd.cfg.download_dir.get_path())
self.menu.addItem_(self.incompletefolder_menu_item) self.menu.addItem_(self.incompletefolder_menu_item)
@ -289,14 +324,15 @@ class SABnzbdDelegate(NSObject):
self.menu.addItem_(NSMenuItem.separatorItem()) self.menu.addItem_(NSMenuItem.separatorItem())
# Set diagnostic menu # 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() self.menu_diagnostic = NSMenu.alloc().init()
diag_items = ((T('Restart'), 'restartAction:'), diag_items = (
(T('Restart') + ' - 127.0.0.1:8080', 'restartSafeHost:'), (T("Restart"), "restartAction:"),
(T('Restart without login'), 'restartNoLogin:') (T("Restart") + " - 127.0.0.1:8080", "restartSafeHost:"),
) (T("Restart without login"), "restartNoLogin:"),
)
for item in diag_items: 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]) menu_diag_item.setRepresentedObject_(item[0])
self.menu_diagnostic.addItem_(menu_diag_item) self.menu_diagnostic.addItem_(menu_diag_item)
@ -307,7 +343,7 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] menu 16 Diagnostic added") NSLog("[osx] menu 16 Diagnostic added")
# Quit Item # 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) self.menu.addItem_(menu_item)
if debug == 1: if debug == 1:
@ -361,7 +397,9 @@ class SABnzbdDelegate(NSObject):
if len(pnfo_list): 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_(menu_queue_item)
self.menu_queue.addItem_(NSMenuItem.separatorItem()) self.menu_queue.addItem_(NSMenuItem.separatorItem())
@ -373,13 +411,17 @@ class SABnzbdDelegate(NSObject):
timeleft = self.calc_timeleft_(bytesleftprogess, BPSMeter.do.bps) timeleft = self.calc_timeleft_(bytesleftprogess, BPSMeter.do.bps)
job = "%s\t(%d/%d MB) %s" % (pnfo.filename, bytesleft, bytes_total, timeleft) 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.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: 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.menu_queue.addItem_(menu_queue_item)
self.queue_menu_item.setSubmenu_(self.menu_queue) 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) items, fetched_items, _total_items = self.history_db.fetch_history(0, 10, None)
self.menu_history = NSMenu.alloc().init() 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_(menu_history_item)
self.menu_history.addItem_(NSMenuItem.separatorItem()) self.menu_history.addItem_(NSMenuItem.separatorItem())
if fetched_items: if fetched_items:
for history in items: for history in items:
# logging.info("[osx] history : %s" % (history)) # logging.info("[osx] history : %s" % (history))
job = "%s" % (history['name']) job = "%s" % (history["name"])
path = "" path = ""
if os.path.isdir(history['storage']) or os.path.isfile(history['storage']): if os.path.isdir(history["storage"]) or os.path.isfile(history["storage"]):
if os.path.isfile(history['storage']): if os.path.isfile(history["storage"]):
path = os.path.dirname(history['storage']) path = os.path.dirname(history["storage"])
else: else:
path = history['storage'] path = history["storage"]
if path: if path:
menu_history_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(job, 'openFolderAction:', '') menu_history_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
job, "openFolderAction:", ""
)
else: else:
menu_history_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(job, '', '') menu_history_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(job, "", "")
if history['status'] != Status.COMPLETED: if history["status"] != Status.COMPLETED:
jobfailed = NSAttributedString.alloc().initWithString_attributes_(job, self.failedAttributes) jobfailed = NSAttributedString.alloc().initWithString_attributes_(job, self.failedAttributes)
menu_history_item.setAttributedTitle_(jobfailed) menu_history_item.setAttributedTitle_(jobfailed)
menu_history_item.setRepresentedObject_("%s" % path) menu_history_item.setRepresentedObject_("%s" % path)
self.menu_history.addItem_(menu_history_item) self.menu_history.addItem_(menu_history_item)
else: 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.menu_history.addItem_(menu_history_item)
self.history_menu_item.setSubmenu_(self.menu_history) self.history_menu_item.setSubmenu_(self.menu_history)
@ -434,10 +483,12 @@ class SABnzbdDelegate(NSObject):
if warnings: if warnings:
warningsAttributes = { warningsAttributes = {
NSForegroundColorAttributeName: NSColor.redColor(), 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) self.warnings_menu_item.setAttributedTitle_(warningsTitle)
if self.isLeopard: if self.isLeopard:
@ -445,7 +496,7 @@ class SABnzbdDelegate(NSObject):
else: else:
self.warnings_menu_item.setEnabled_(YES) self.warnings_menu_item.setEnabled_(YES)
else: else:
self.warnings_menu_item.setTitle_("%s : 0" % (T('Warnings'))) self.warnings_menu_item.setTitle_("%s : 0" % (T("Warnings")))
if self.isLeopard: if self.isLeopard:
self.warnings_menu_item.setHidden_(YES) self.warnings_menu_item.setHidden_(YES)
else: else:
@ -458,7 +509,7 @@ class SABnzbdDelegate(NSObject):
paused, bytes_left, bpsnow, time_left = fast_queue() paused, bytes_left, bpsnow, time_left = fast_queue()
if paused: if paused:
self.state = T('Paused') self.state = T("Paused")
if sabnzbd.scheduler.pause_int() != "0": if sabnzbd.scheduler.pause_int() != "0":
self.setMenuTitle_("\n\n%s\n" % (sabnzbd.scheduler.pause_int())) self.setMenuTitle_("\n\n%s\n" % (sabnzbd.scheduler.pause_int()))
else: else:
@ -467,8 +518,8 @@ class SABnzbdDelegate(NSObject):
self.state = "" self.state = ""
speed = to_units(bpsnow) speed = to_units(bpsnow)
# "10.1 MB/s" doesn't fit, remove space char # "10.1 MB/s" doesn't fit, remove space char
if 'M' in speed and len(speed) > 5: if "M" in speed and len(speed) > 5:
speed = speed.replace(' ', '') speed = speed.replace(" ", "")
time_left = (bpsnow > 10 and time_left) or "------" time_left = (bpsnow > 10 and time_left) or "------"
statusbarText = "\n\n%s\n%sB/s\n" % (time_left, speed) statusbarText = "\n\n%s\n%sB/s\n" % (time_left, speed)
@ -481,7 +532,7 @@ class SABnzbdDelegate(NSObject):
self.setMenuTitle_(statusbarText) self.setMenuTitle_(statusbarText)
else: else:
self.state = T('Idle') self.state = T("Idle")
self.setMenuTitle_("") self.setMenuTitle_("")
if self.state != "" and self.info != "": if self.state != "" and self.info != "":
@ -497,9 +548,9 @@ class SABnzbdDelegate(NSObject):
def iconUpdate(self): def iconUpdate(self):
try: try:
if sabnzbd.downloader.Downloader.do.paused: if sabnzbd.downloader.Downloader.do.paused:
self.status_item.setImage_(self.icons['pause']) self.status_item.setImage_(self.icons["pause"])
else: else:
self.status_item.setImage_(self.icons['idle']) self.status_item.setImage_(self.icons["idle"])
except: except:
logging.info("[osx] iconUpdate Exception %s" % (sys.exc_info()[0])) 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: if sabnzbd.NEW_VERSION and self.version_notify:
# logging.info("[osx] New Version : %s" % (sabnzbd.NEW_VERSION)) # logging.info("[osx] New Version : %s" % (sabnzbd.NEW_VERSION))
new_release, _new_rel_url = 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 self.version_notify = 0
except: except:
logging.info("[osx] versionUpdate Exception %s" % (sys.exc_info()[0])) logging.info("[osx] versionUpdate Exception %s" % (sys.exc_info()[0]))
@ -574,7 +625,7 @@ class SABnzbdDelegate(NSObject):
def serverUpdate(self): def serverUpdate(self):
try: try:
if not config.get_servers(): 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 hide = YES
alternate = NO alternate = NO
value = 0 value = 0
@ -622,8 +673,12 @@ class SABnzbdDelegate(NSObject):
def diskspaceUpdate(self): def diskspaceUpdate(self):
try: try:
self.completefolder_menu_item.setTitle_("%s%.2f GB" % (T('Complete Folder') + '\t\t\t', diskspace()['complete_dir'][1])) self.completefolder_menu_item.setTitle_(
self.incompletefolder_menu_item.setTitle_("%s%.2f GB" % (T('Incomplete Folder') + '\t\t', diskspace()['download_dir'][1])) "%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: except:
logging.info("[osx] diskspaceUpdate Exception %s" % (sys.exc_info()[0])) logging.info("[osx] diskspaceUpdate Exception %s" % (sys.exc_info()[0]))
@ -648,7 +703,7 @@ class SABnzbdDelegate(NSObject):
NSBaselineOffsetAttributeName: 5.0, NSBaselineOffsetAttributeName: 5.0,
NSFontAttributeName: NSFont.menuFontOfSize_(9.0), NSFontAttributeName: NSFont.menuFontOfSize_(9.0),
NSParagraphStyleAttributeName: style NSParagraphStyleAttributeName: style
#,NSForegroundColorAttributeName: titleColor # ,NSForegroundColorAttributeName: titleColor
} }
title = NSAttributedString.alloc().initWithString_attributes_(text, titleAttributes) title = NSAttributedString.alloc().initWithString_attributes_(text, titleAttributes)
@ -663,12 +718,12 @@ class SABnzbdDelegate(NSObject):
minutes, seconds = divmod(totalseconds, 60) minutes, seconds = divmod(totalseconds, 60)
hours, minutes = divmod(minutes, 60) hours, minutes = divmod(minutes, 60)
if minutes < 10: if minutes < 10:
minutes = '0%s' % minutes minutes = "0%s" % minutes
if seconds < 10: if seconds < 10:
seconds = '0%s' % seconds seconds = "0%s" % seconds
return '%s:%s:%s' % (hours, minutes, seconds) return "%s:%s:%s" % (hours, minutes, seconds)
except: except:
return '0:00:00' return "0:00:00"
def openBrowserAction_(self, sender): def openBrowserAction_(self, sender):
if sender.representedObject: if sender.representedObject:
@ -681,7 +736,7 @@ class SABnzbdDelegate(NSObject):
# logging.info("[osx] speed limit to %s" % (sender.representedObject())) # logging.info("[osx] speed limit to %s" % (sender.representedObject()))
speed = int(sender.representedObject()) speed = int(sender.representedObject())
if speed != self.speed: if speed != self.speed:
sabnzbd.downloader.Downloader.do.limit_speed('%s%%' % speed) sabnzbd.downloader.Downloader.do.limit_speed("%s%%" % speed)
self.speedlimitUpdate() self.speedlimitUpdate()
def purgeAction_(self, sender): def purgeAction_(self, sender):
@ -717,38 +772,38 @@ class SABnzbdDelegate(NSObject):
NSLog("[osx] %@", folder2open) NSLog("[osx] %@", folder2open)
os.system('open "%s"' % folder2open) os.system('open "%s"' % folder2open)
# def aboutAction_(self, sender): # def aboutAction_(self, sender):
# app = NSApplication.sharedApplication() # app = NSApplication.sharedApplication()
# app.orderFrontStandardAboutPanel_(nil) # app.orderFrontStandardAboutPanel_(nil)
def restartAction_(self, sender): def restartAction_(self, sender):
self.setMenuTitle_("\n\n%s\n" % (T('Stopping...'))) self.setMenuTitle_("\n\n%s\n" % (T("Stopping...")))
logging.info('Restart requested by tray') logging.info("Restart requested by tray")
sabnzbd.trigger_restart() sabnzbd.trigger_restart()
self.setMenuTitle_("\n\n%s\n" % (T('Stopping...'))) self.setMenuTitle_("\n\n%s\n" % (T("Stopping...")))
def restartSafeHost_(self, sender): def restartSafeHost_(self, sender):
sabnzbd.cfg.cherryhost.set('127.0.0.1') sabnzbd.cfg.cherryhost.set("127.0.0.1")
sabnzbd.cfg.cherryport.set('8080') sabnzbd.cfg.cherryport.set("8080")
sabnzbd.cfg.enable_https.set(False) sabnzbd.cfg.enable_https.set(False)
sabnzbd.config.save_config() sabnzbd.config.save_config()
self.setMenuTitle_("\n\n%s\n" % (T('Stopping...'))) self.setMenuTitle_("\n\n%s\n" % (T("Stopping...")))
sabnzbd.trigger_restart() sabnzbd.trigger_restart()
self.setMenuTitle_("\n\n%s\n" % (T('Stopping...'))) self.setMenuTitle_("\n\n%s\n" % (T("Stopping...")))
def restartNoLogin_(self, sender): def restartNoLogin_(self, sender):
sabnzbd.cfg.username.set('') sabnzbd.cfg.username.set("")
sabnzbd.cfg.password.set('') sabnzbd.cfg.password.set("")
sabnzbd.config.save_config() sabnzbd.config.save_config()
self.setMenuTitle_("\n\n%s\n" % (T('Stopping...'))) self.setMenuTitle_("\n\n%s\n" % (T("Stopping...")))
sabnzbd.trigger_restart() 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): def application_openFiles_(self, nsapp, filenames):
# logging.info('[osx] file open') # logging.info('[osx] file open')
# logging.info('[osx] file : %s' % (filenames)) # logging.info('[osx] file : %s' % (filenames))
for name in 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): if os.path.exists(name):
fn = get_filename(name) fn = get_filename(name)
# logging.info('[osx] filename : %s' % (fn)) # logging.info('[osx] filename : %s' % (fn))
@ -762,8 +817,8 @@ class SABnzbdDelegate(NSObject):
# logging.info('opening done') # logging.info('opening done')
def applicationShouldTerminate_(self, sender): def applicationShouldTerminate_(self, sender):
logging.info('[osx] application terminating') logging.info("[osx] application terminating")
self.setMenuTitle_("\n\n%s\n" % (T('Stopping...'))) self.setMenuTitle_("\n\n%s\n" % (T("Stopping...")))
self.status_item.setHighlightMode_(NO) self.status_item.setHighlightMode_(NO)
self.osx_icon = False self.osx_icon = False
sabnzbd.shutdown_program() sabnzbd.shutdown_program()

131
sabnzbd/panic.py

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

92
sabnzbd/powersup.py

@ -51,7 +51,7 @@ def win_hibernate():
win_power_privileges() win_power_privileges()
win32api.SetSystemPowerState(False, True) win32api.SetSystemPowerState(False, True)
except: except:
logging.error(T('Failed to hibernate system')) logging.error(T("Failed to hibernate system"))
logging.info("Traceback: ", exc_info=True) logging.info("Traceback: ", exc_info=True)
@ -61,7 +61,7 @@ def win_standby():
win_power_privileges() win_power_privileges()
win32api.SetSystemPowerState(True, True) win32api.SetSystemPowerState(True, True)
except: except:
logging.error(T('Failed to standby system')) logging.error(T("Failed to standby system"))
logging.info("Traceback: ", exc_info=True) logging.info("Traceback: ", exc_info=True)
@ -78,12 +78,13 @@ def win_shutdown():
# Power management for OSX # Power management for OSX
############################################################################## ##############################################################################
def osx_shutdown(): def osx_shutdown():
""" Shutdown OSX system, never returns """ """ Shutdown OSX system, never returns """
try: try:
subprocess.call(['osascript', '-e', 'tell app "System Events" to shut down']) subprocess.call(["osascript", "-e", 'tell app "System Events" to shut down'])
except: except:
logging.error(T('Error while shutting down system')) logging.error(T("Error while shutting down system"))
logging.info("Traceback: ", exc_info=True) logging.info("Traceback: ", exc_info=True)
os._exit(0) os._exit(0)
@ -91,10 +92,10 @@ def osx_shutdown():
def osx_standby(): def osx_standby():
""" Make OSX system sleep, returns after wakeup """ """ Make OSX system sleep, returns after wakeup """
try: try:
subprocess.call(['osascript', '-e', 'tell app "System Events" to sleep']) subprocess.call(["osascript", "-e", 'tell app "System Events" to sleep'])
time.sleep(10) time.sleep(10)
except: except:
logging.error(T('Failed to standby system')) logging.error(T("Failed to standby system"))
logging.info("Traceback: ", exc_info=True) logging.info("Traceback: ", exc_info=True)
@ -119,20 +120,21 @@ def osx_hibernate():
try: try:
import dbus import dbus
HAVE_DBUS = True HAVE_DBUS = True
except ImportError: except ImportError:
HAVE_DBUS = False HAVE_DBUS = False
_IS_NOT_INTERACTIVE = False _IS_NOT_INTERACTIVE = False
_LOGIND_SUCCESSFUL_RESULT = 'yes' _LOGIND_SUCCESSFUL_RESULT = "yes"
def _get_sessionproxy(): def _get_sessionproxy():
""" Return (proxy-object, interface), (None, None) if not available """ """ Return (proxy-object, interface), (None, None) if not available """
name = 'org.freedesktop.PowerManagement' name = "org.freedesktop.PowerManagement"
path = '/org/freedesktop/PowerManagement' path = "/org/freedesktop/PowerManagement"
interface = 'org.freedesktop.PowerManagement' interface = "org.freedesktop.PowerManagement"
try: try:
bus = dbus.SessionBus() bus = dbus.SessionBus()
return bus.get_object(name, path), interface return bus.get_object(name, path), interface
@ -142,31 +144,31 @@ def _get_sessionproxy():
def _get_systemproxy(method): def _get_systemproxy(method):
""" Return (proxy-object, interface, pinterface), (None, None, None) if not available """ """ Return (proxy-object, interface, pinterface), (None, None, None) if not available """
if method == 'ConsoleKit': if method == "ConsoleKit":
name = 'org.freedesktop.ConsoleKit' name = "org.freedesktop.ConsoleKit"
path = '/org/freedesktop/ConsoleKit/Manager' path = "/org/freedesktop/ConsoleKit/Manager"
interface = 'org.freedesktop.ConsoleKit.Manager' interface = "org.freedesktop.ConsoleKit.Manager"
pinterface = None pinterface = None
elif method == 'DeviceKit': elif method == "DeviceKit":
name = 'org.freedesktop.DeviceKit.Power' name = "org.freedesktop.DeviceKit.Power"
path = '/org/freedesktop/DeviceKit/Power' path = "/org/freedesktop/DeviceKit/Power"
interface = 'org.freedesktop.DeviceKit.Power' interface = "org.freedesktop.DeviceKit.Power"
pinterface = 'org.freedesktop.DBus.Properties' pinterface = "org.freedesktop.DBus.Properties"
elif method == 'UPower': elif method == "UPower":
name = 'org.freedesktop.UPower' name = "org.freedesktop.UPower"
path = '/org/freedesktop/UPower' path = "/org/freedesktop/UPower"
interface = 'org.freedesktop.UPower' interface = "org.freedesktop.UPower"
pinterface = 'org.freedesktop.DBus.Properties' pinterface = "org.freedesktop.DBus.Properties"
elif method == 'Logind': elif method == "Logind":
name = 'org.freedesktop.login1' name = "org.freedesktop.login1"
path = '/org/freedesktop/login1' path = "/org/freedesktop/login1"
interface = 'org.freedesktop.login1.Manager' interface = "org.freedesktop.login1.Manager"
pinterface = None pinterface = None
try: try:
bus = dbus.SystemBus() bus = dbus.SystemBus()
return bus.get_object(name, path), interface, pinterface return bus.get_object(name, path), interface, pinterface
except dbus.exceptions.DBusException as msg: except dbus.exceptions.DBusException as msg:
logging.info('DBus not reachable (%s)', msg) logging.info("DBus not reachable (%s)", msg)
return None, None, None return None, None, None
@ -182,19 +184,19 @@ def linux_shutdown():
if proxy.CanShutdown(): if proxy.CanShutdown():
proxy.Shutdown(dbus_interface=interface) proxy.Shutdown(dbus_interface=interface)
else: else:
proxy, interface, pinterface = _get_systemproxy('Logind') proxy, interface, pinterface = _get_systemproxy("Logind")
if proxy: if proxy:
if proxy.CanPowerOff(dbus_interface=interface) == _LOGIND_SUCCESSFUL_RESULT: if proxy.CanPowerOff(dbus_interface=interface) == _LOGIND_SUCCESSFUL_RESULT:
proxy.PowerOff(_IS_NOT_INTERACTIVE, dbus_interface=interface) proxy.PowerOff(_IS_NOT_INTERACTIVE, dbus_interface=interface)
else: else:
proxy, interface, _pinterface = _get_systemproxy('ConsoleKit') proxy, interface, _pinterface = _get_systemproxy("ConsoleKit")
if proxy: if proxy:
if proxy.CanStop(dbus_interface=interface): if proxy.CanStop(dbus_interface=interface):
proxy.Stop(dbus_interface=interface) proxy.Stop(dbus_interface=interface)
else: else:
logging.info('DBus does not support Stop (shutdown)') logging.info("DBus does not support Stop (shutdown)")
except dbus.exceptions.DBusException as msg: 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) os._exit(0)
@ -209,22 +211,22 @@ def linux_hibernate():
if proxy.CanHibernate(): if proxy.CanHibernate():
proxy.Hibernate(dbus_interface=interface) proxy.Hibernate(dbus_interface=interface)
else: else:
proxy, interface, pinterface = _get_systemproxy('Logind') proxy, interface, pinterface = _get_systemproxy("Logind")
if proxy: if proxy:
if proxy.CanHibernate(dbus_interface=interface) == _LOGIND_SUCCESSFUL_RESULT: if proxy.CanHibernate(dbus_interface=interface) == _LOGIND_SUCCESSFUL_RESULT:
proxy.Hibernate(_IS_NOT_INTERACTIVE, dbus_interface=interface) proxy.Hibernate(_IS_NOT_INTERACTIVE, dbus_interface=interface)
else: else:
proxy, interface, pinterface = _get_systemproxy('UPower') proxy, interface, pinterface = _get_systemproxy("UPower")
if not proxy: if not proxy:
proxy, interface, pinterface = _get_systemproxy('DeviceKit') proxy, interface, pinterface = _get_systemproxy("DeviceKit")
if proxy: 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) proxy.Hibernate(dbus_interface=interface)
else: else:
logging.info('DBus does not support Hibernate') logging.info("DBus does not support Hibernate")
time.sleep(10) time.sleep(10)
except dbus.exceptions.DBusException as msg: 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(): def linux_standby():
@ -238,19 +240,19 @@ def linux_standby():
if proxy.CanSuspend(): if proxy.CanSuspend():
proxy.Suspend(dbus_interface=interface) proxy.Suspend(dbus_interface=interface)
else: else:
proxy, interface, pinterface = _get_systemproxy('Logind') proxy, interface, pinterface = _get_systemproxy("Logind")
if proxy: if proxy:
if proxy.CanSuspend(dbus_interface=interface) == _LOGIND_SUCCESSFUL_RESULT: if proxy.CanSuspend(dbus_interface=interface) == _LOGIND_SUCCESSFUL_RESULT:
proxy.Suspend(_IS_NOT_INTERACTIVE, dbus_interface=interface) proxy.Suspend(_IS_NOT_INTERACTIVE, dbus_interface=interface)
else: else:
proxy, interface, pinterface = _get_systemproxy('UPower') proxy, interface, pinterface = _get_systemproxy("UPower")
if not proxy: if not proxy:
proxy, interface, pinterface = _get_systemproxy('DeviceKit') proxy, interface, pinterface = _get_systemproxy("DeviceKit")
if proxy: 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) proxy.Suspend(dbus_interface=interface)
else: else:
logging.info('DBus does not support Suspend (standby)') logging.info("DBus does not support Suspend (standby)")
time.sleep(10) time.sleep(10)
except dbus.exceptions.DBusException as msg: 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): def _init(self, maxsize):
self.maxsize = maxsize self.maxsize = maxsize
self.queue = collections.OrderedDict() self.queue = collections.OrderedDict()
def _put(self, item): def _put(self, item):
self.queue[item] = None self.queue[item] = None
def _get(self): def _get(self):
return self.queue.popitem()[0] return self.queue.popitem()[0]
_RATING_URL = "/releaseRatings/releaseRatings.php" _RATING_URL = "/releaseRatings/releaseRatings.php"
RATING_LOCK = RLock() RATING_LOCK = RLock()
@ -60,7 +63,6 @@ def _reset_warn():
class NzbRating: class NzbRating:
def __init__(self): def __init__(self):
self.avg_video = 0 self.avg_video = 0
self.avg_video_cnt = 0 self.avg_video_cnt = 0
@ -77,7 +79,6 @@ class NzbRating:
class NzbRatingV2(NzbRating): class NzbRatingV2(NzbRating):
def __init__(self): def __init__(self):
super(NzbRatingV2, self).__init__() super(NzbRatingV2, self).__init__()
self.avg_spam_cnt = 0 self.avg_spam_cnt = 0
@ -116,8 +117,9 @@ class Rating(Thread):
self.shutdown = False self.shutdown = False
self.queue = OrderedSetQueue() self.queue = OrderedSetQueue()
try: try:
self.version, self.ratings, self.nzo_indexer_map = sabnzbd.load_admin("Rating.sab", self.version, self.ratings, self.nzo_indexer_map = sabnzbd.load_admin(
silent=not cfg.rating_enable()) "Rating.sab", silent=not cfg.rating_enable()
)
if self.version == 1: if self.version == 1:
ratings = {} ratings = {}
for k, v in self.ratings.items(): for k, v in self.ratings.items():
@ -152,7 +154,7 @@ class Rating(Thread):
self.queue.put(indexer_id) self.queue.put(indexer_id)
except: except:
pass pass
logging.debug('Stopping ratings') logging.debug("Stopping ratings")
@synchronized(RATING_LOCK) @synchronized(RATING_LOCK)
def save(self): def save(self):
@ -163,32 +165,44 @@ class Rating(Thread):
@synchronized(RATING_LOCK) @synchronized(RATING_LOCK)
def add_rating(self, indexer_id, nzo_id, fields): def add_rating(self, indexer_id, nzo_id, fields):
if indexer_id and nzo_id: 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: try:
rating = self.ratings.get(indexer_id, NzbRatingV2()) rating = self.ratings.get(indexer_id, NzbRatingV2())
if fields['video'] and fields['videocnt']: if fields["video"] and fields["videocnt"]:
rating.avg_video = int(float(fields['video'])) rating.avg_video = int(float(fields["video"]))
rating.avg_video_cnt = int(float(fields['videocnt'])) rating.avg_video_cnt = int(float(fields["videocnt"]))
if fields['audio'] and fields['audiocnt']: if fields["audio"] and fields["audiocnt"]:
rating.avg_audio = int(float(fields['audio'])) rating.avg_audio = int(float(fields["audio"]))
rating.avg_audio_cnt = int(float(fields['audiocnt'])) rating.avg_audio_cnt = int(float(fields["audiocnt"]))
if fields['voteup']: if fields["voteup"]:
rating.avg_vote_up = int(float(fields['voteup'])) rating.avg_vote_up = int(float(fields["voteup"]))
if fields['votedown']: if fields["votedown"]:
rating.avg_vote_down = int(float(fields['votedown'])) rating.avg_vote_down = int(float(fields["votedown"]))
if fields['spam']: if fields["spam"]:
rating.avg_spam_cnt = int(float(fields['spam'])) rating.avg_spam_cnt = int(float(fields["spam"]))
if fields['confirmed-spam']: if fields["confirmed-spam"]:
rating.avg_spam_confirm = (fields['confirmed-spam'].lower() == 'yes') rating.avg_spam_confirm = fields["confirmed-spam"].lower() == "yes"
if fields['passworded']: if fields["passworded"]:
rating.avg_encrypted_cnt = int(float(fields['passworded'])) rating.avg_encrypted_cnt = int(float(fields["passworded"]))
if fields['confirmed-passworded']: if fields["confirmed-passworded"]:
rating.avg_encrypted_confirm = (fields['confirmed-passworded'].lower() == 'yes') rating.avg_encrypted_confirm = fields["confirmed-passworded"].lower() == "yes"
# Indexers can supply a full URL or just a host # Indexers can supply a full URL or just a host
if fields['host']: if fields["host"]:
rating.host = fields['host'][0] if fields['host'] and isinstance(fields['host'], list) else fields['host'] rating.host = (
if fields['url']: fields["host"][0] if fields["host"] and isinstance(fields["host"], list) else fields["host"]
rating.host = fields['url'][0] if fields['url'] and isinstance(fields['url'], list) else fields['url'] )
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.ratings[indexer_id] = rating
self.nzo_indexer_map[nzo_id] = indexer_id self.nzo_indexer_map[nzo_id] = indexer_id
except: except:
@ -196,22 +210,26 @@ class Rating(Thread):
@synchronized(RATING_LOCK) @synchronized(RATING_LOCK)
def update_user_rating(self, nzo_id, video, audio, vote, flag, flag_detail=None): 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: 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 return
indexer_id = self.nzo_indexer_map[nzo_id] indexer_id = self.nzo_indexer_map[nzo_id]
rating = self.ratings[indexer_id] rating = self.ratings[indexer_id]
if video: if video:
rating.user_video = int(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 rating.changed = rating.changed | Rating.CHANGED_USER_VIDEO
if audio: if audio:
rating.user_audio = int(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 rating.changed = rating.changed | Rating.CHANGED_USER_AUDIO
if flag: 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 rating.changed = rating.changed | Rating.CHANGED_USER_FLAG
if vote: if vote:
rating.changed = rating.changed | Rating.CHANGED_USER_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): 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): if not flag or not cfg.rating_enable() or (nzo_id not in self.nzo_indexer_map):
return 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] indexer_id = self.nzo_indexer_map[nzo_id]
rating = self.ratings[indexer_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 rating.changed = rating.changed | Rating.CHANGED_AUTO_FLAG
self.queue.put(indexer_id) self.queue.put(indexer_id)
@ -252,73 +270,86 @@ class Rating(Thread):
def _flag_request(self, val, flag_detail, auto): def _flag_request(self, val, flag_detail, auto):
if val == Rating.FLAG_SPAM: if val == Rating.FLAG_SPAM:
return {'m': 'rs', 'auto': auto} return {"m": "rs", "auto": auto}
if val == Rating.FLAG_ENCRYPTED: if val == Rating.FLAG_ENCRYPTED:
return {'m': 'rp', 'auto': auto} return {"m": "rp", "auto": auto}
if val == Rating.FLAG_EXPIRED: if val == Rating.FLAG_EXPIRED:
expired_host = flag_detail if flag_detail and len(flag_detail) > 0 else 'Other' expired_host = flag_detail if flag_detail and len(flag_detail) > 0 else "Other"
return {'m': 'rpr', 'pr': expired_host, 'auto': auto} return {"m": "rpr", "pr": expired_host, "auto": auto}
if (val == Rating.FLAG_OTHER) and flag_detail and len(flag_detail) > 0: 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: 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): 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() api_key = cfg.rating_api_key()
rating_host = cfg.rating_host() rating_host = cfg.rating_host()
rating_url = _RATING_URL rating_url = _RATING_URL
requests = [] requests = []
_headers = {'User-agent': 'SABnzbd+/%s' % sabnzbd.version.__version__, 'Content-type': 'application/x-www-form-urlencoded'} _headers = {
rating = self._get_rating_by_indexer(indexer_id) # Requesting info here ensures always have latest information even on retry "User-agent": "SABnzbd+/%s" % sabnzbd.version.__version__,
if hasattr(rating, 'host') and rating.host: "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) host_parsed = urllib.parse.urlparse(rating.host)
rating_host = host_parsed.netloc rating_host = host_parsed.netloc
# Is it an URL or just a HOST? # Is it an URL or just a HOST?
if host_parsed.path and 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 rating_url = host_parsed.path + "?" + host_parsed.query if host_parsed.query else host_parsed.path
if not rating_host: 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 return True
if not api_key: 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 return True
if rating.changed & Rating.CHANGED_USER_VIDEO: 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: 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: if rating.changed & Rating.CHANGED_USER_VOTE:
up_down = 'up' if rating.user_vote == Rating.VOTE_UP else 'down' up_down = "up" if rating.user_vote == Rating.VOTE_UP else "down"
requests.append({'m': 'v', 'v': up_down, 'r': 'overall'}) requests.append({"m": "v", "v": up_down, "r": "overall"})
if rating.changed & Rating.CHANGED_USER_FLAG: 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: 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: try:
conn = http.client.HTTPSConnection(rating_host) conn = http.client.HTTPSConnection(rating_host)
for request in [r for r in requests if r is not None]: for request in [r for r in requests if r is not None]:
if api_key: if api_key:
request['apikey'] = api_key request["apikey"] = api_key
request['i'] = indexer_id request["i"] = indexer_id
conn.request('POST', rating_url, urllib.parse.urlencode(request), headers=_headers) conn.request("POST", rating_url, urllib.parse.urlencode(request), headers=_headers)
response = conn.getresponse() response = conn.getresponse()
response.read() response.read()
if response.status == http.client.UNAUTHORIZED: if response.status == http.client.UNAUTHORIZED:
_warn('Ratings server unauthorized user') _warn("Ratings server unauthorized user")
return False return False
elif response.status != http.client.OK: 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 return False
self.ratings[indexer_id].changed = self.ratings[indexer_id].changed & ~rating.changed self.ratings[indexer_id].changed = self.ratings[indexer_id].changed & ~rating.changed
_reset_warn() _reset_warn()
return True return True
except: except:
_warn('Problem accessing ratings server: %s' % rating_host) _warn("Problem accessing ratings server: %s" % rating_host)
return False return False

90
sabnzbd/sabtray.py

@ -37,48 +37,60 @@ from sabnzbd.utils.systrayiconthread import SysTrayIconThread
class SABTrayThread(SysTrayIconThread): class SABTrayThread(SysTrayIconThread):
sabicons = { sabicons = {
'default': 'icons/sabnzbd16_32.ico', "default": "icons/sabnzbd16_32.ico",
'green': 'icons/sabnzbd16_32green.ico', "green": "icons/sabnzbd16_32green.ico",
'pause': 'icons/sabnzbd16_32paused.ico' "pause": "icons/sabnzbd16_32paused.ico",
} }
def __init__(self): def __init__(self):
# Wait for translated texts to be loaded # Wait for translated texts to be loaded
while not sabnzbd.WEBUI_READY: while not sabnzbd.WEBUI_READY:
sleep(0.2) sleep(0.2)
logging.debug('language file not loaded, waiting') logging.debug("language file not loaded, waiting")
self.sabpaused = False self.sabpaused = False
self.counter = 0 self.counter = 0
self.set_texts() self.set_texts()
menu_options = ( menu_options = (
(T('Show interface'), None, self.browse), (T("Show interface"), None, self.browse),
(T('Open complete folder'), None, self.opencomplete), (T("Open complete folder"), None, self.opencomplete),
('SEPARATOR', None, None), ("SEPARATOR", None, None),
(T('Pause') + '/' + T('Resume'), None, self.pauseresume), (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"),
(T('Pause for 30 minutes'), None, self.pausefor30min), None,
(T('Pause for 1 hour'), None, self.pausefor1hour), (
(T('Pause for 3 hours'), None, self.pausefor3hour), (T("Pause for 5 minutes"), None, self.pausefor5min),
(T('Pause for 6 hours'), None, self.pausefor6hour))), (T("Pause for 15 minutes"), None, self.pausefor15min),
('SEPARATOR', None, None), (T("Pause for 30 minutes"), None, self.pausefor30min),
(T('Read all RSS feeds'), None, self.rss), (T("Pause for 1 hour"), None, self.pausefor1hour),
('SEPARATOR', None, None), (T("Pause for 3 hours"), None, self.pausefor3hour),
(T('Troubleshoot'), None, ((T('Restart'), None, self.restart_sab), (T("Pause for 6 hours"), None, self.pausefor6hour),
(T('Restart without login'), None, self.nologin), ),
(T('Restart') + ' - 127.0.0.1:8080', None, self.defhost))), ),
(T('Shutdown'), None, self.shutdown), ("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): def set_texts(self):
""" Cache texts for performance, doUpdates is called often """ """ Cache texts for performance, doUpdates is called often """
self.txt_idle = T('Idle') self.txt_idle = T("Idle")
self.txt_paused = T('Paused') self.txt_paused = T("Paused")
self.txt_remaining = T('Remaining') self.txt_remaining = T("Remaining")
# called every few ms by SysTrayIconThread # called every few ms by SysTrayIconThread
def doUpdates(self): def doUpdates(self):
@ -94,13 +106,13 @@ class SABTrayThread(SysTrayIconThread):
self.hover_text = "%s - %s: %sB" % (self.txt_paused, self.txt_remaining, mb_left) self.hover_text = "%s - %s: %sB" % (self.txt_paused, self.txt_remaining, mb_left)
else: else:
self.hover_text = self.txt_paused self.hover_text = self.txt_paused
self.icon = self.sabicons['pause'] self.icon = self.sabicons["pause"]
elif bytes_left > 0: elif bytes_left > 0:
self.hover_text = "%sB/s - %s: %sB (%s)" % (speed, self.txt_remaining, mb_left, time_left) 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: else:
self.hover_text = self.txt_idle self.hover_text = self.txt_idle
self.icon = self.sabicons['default'] self.icon = self.sabicons["default"]
self.refresh_icon() self.refresh_icon()
self.counter = 0 self.counter = 0
@ -148,36 +160,36 @@ class SABTrayThread(SysTrayIconThread):
self.pausefor(60) self.pausefor(60)
def pausefor3hour(self, icon): def pausefor3hour(self, icon):
self.pausefor(3*60) self.pausefor(3 * 60)
def pausefor6hour(self, icon): def pausefor6hour(self, icon):
self.pausefor(6*60) self.pausefor(6 * 60)
def restart_sab(self, icon): def restart_sab(self, icon):
self.hover_text = T('Restart') self.hover_text = T("Restart")
logging.info('Restart requested by tray') logging.info("Restart requested by tray")
sabnzbd.trigger_restart() sabnzbd.trigger_restart()
def rss(self, icon): def rss(self, icon):
self.hover_text = T('Read all RSS feeds') self.hover_text = T("Read all RSS feeds")
scheduler.force_rss() scheduler.force_rss()
def nologin(self, icon): def nologin(self, icon):
sabnzbd.cfg.username.set('') sabnzbd.cfg.username.set("")
sabnzbd.cfg.password.set('') sabnzbd.cfg.password.set("")
sabnzbd.config.save_config() sabnzbd.config.save_config()
self.hover_text = T('Restart') self.hover_text = T("Restart")
sabnzbd.trigger_restart() sabnzbd.trigger_restart()
def defhost(self, icon): 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.cfg.enable_https.set(False)
sabnzbd.config.save_config() sabnzbd.config.save_config()
self.hover_text = T('Restart') self.hover_text = T("Restart")
sabnzbd.trigger_restart() sabnzbd.trigger_restart()
def shutdown(self, icon): def shutdown(self, icon):
self.hover_text = T('Shutdown') self.hover_text = T("Shutdown")
sabnzbd.shutdown_program() sabnzbd.shutdown_program()
def pause(self): 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 import gi
from gi.repository import Gtk, GLib from gi.repository import Gtk, GLib
import logging import logging
try: try:
gi.require_version('XApp', '1.0') gi.require_version("XApp", "1.0")
from gi.repository import XApp from gi.repository import XApp
if not hasattr(XApp, 'StatusIcon'):
if not hasattr(XApp, "StatusIcon"):
raise ImportError raise ImportError
HAVE_XAPP = True HAVE_XAPP = True
logging.debug("XApp found: %s" % XApp) logging.debug("XApp found: %s" % XApp)
@ -49,9 +51,9 @@ from sabnzbd.utils.upload import add_local
class StatusIcon(Thread): class StatusIcon(Thread):
sabicons = { sabicons = {
'default': abspath('icons/logo-arrow.svg'), "default": abspath("icons/logo-arrow.svg"),
'green': abspath('icons/logo-arrow_green.svg'), "green": abspath("icons/logo-arrow_green.svg"),
'pause': abspath('icons/logo-arrow_gray.svg') "pause": abspath("icons/logo-arrow_gray.svg"),
} }
updatefreq = 1000 # ms updatefreq = 1000 # ms
@ -64,7 +66,7 @@ class StatusIcon(Thread):
# Wait for translated texts to be loaded # Wait for translated texts to be loaded
while not sabnzbd.WEBUI_READY: while not sabnzbd.WEBUI_READY:
sleep(0.2) sleep(0.2)
logging.debug('language file not loaded, waiting') logging.debug("language file not loaded, waiting")
self.sabpaused = False self.sabpaused = False
if HAVE_XAPP: if HAVE_XAPP:
@ -73,7 +75,7 @@ class StatusIcon(Thread):
self.statusicon = Gtk.StatusIcon() self.statusicon = Gtk.StatusIcon()
self.statusicon.set_name("SABnzbd") self.statusicon.set_name("SABnzbd")
self.statusicon.set_visible(True) self.statusicon.set_visible(True)
self.icon = self.sabicons['default'] self.icon = self.sabicons["default"]
self.refresh_icon() self.refresh_icon()
self.tooltip = "SABnzbd" self.tooltip = "SABnzbd"
self.refresh_tooltip() self.refresh_tooltip()
@ -102,14 +104,14 @@ class StatusIcon(Thread):
speed = to_units(bpsnow) speed = to_units(bpsnow)
if self.sabpaused: if self.sabpaused:
self.tooltip = T('Paused') self.tooltip = T("Paused")
self.icon = self.sabicons['pause'] self.icon = self.sabicons["pause"]
elif bytes_left > 0: elif bytes_left > 0:
self.tooltip = "%sB/s %s: %sB (%s)" % (speed, T('Remaining'), mb_left, time_left) self.tooltip = "%sB/s %s: %sB (%s)" % (speed, T("Remaining"), mb_left, time_left)
self.icon = self.sabicons['green'] self.icon = self.sabicons["green"]
else: else:
self.tooltip = T('Idle') self.tooltip = T("Idle")
self.icon = self.sabicons['default'] self.icon = self.sabicons["default"]
self.refresh_icon() self.refresh_icon()
self.refresh_tooltip() self.refresh_tooltip()
@ -185,11 +187,11 @@ class StatusIcon(Thread):
self.pause() self.pause()
def restart(self, icon): def restart(self, icon):
self.hover_text = T('Restart') self.hover_text = T("Restart")
sabnzbd.trigger_restart() sabnzbd.trigger_restart()
def shutdown(self, icon): def shutdown(self, icon):
self.hover_text = T('Shutdown') self.hover_text = T("Shutdown")
sabnzbd.shutdown_program() sabnzbd.shutdown_program()
def pause(self): def pause(self):

183
sabnzbd/scheduler.py

@ -86,7 +86,7 @@ def init():
m = int(m) m = int(m)
h = int(h) h = int(h)
except: 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 continue
if d.isdigit(): if d.isdigit():
@ -94,84 +94,82 @@ def init():
else: else:
d = list(range(1, 8)) d = list(range(1, 8))
if action_name == 'resume': if action_name == "resume":
action = scheduled_resume action = scheduled_resume
arguments = [] arguments = []
elif action_name == 'pause': elif action_name == "pause":
action = sabnzbd.downloader.Downloader.do.pause action = sabnzbd.downloader.Downloader.do.pause
arguments = [] arguments = []
elif action_name == 'pause_all': elif action_name == "pause_all":
action = sabnzbd.pause_all action = sabnzbd.pause_all
arguments = [] arguments = []
elif action_name == 'shutdown': elif action_name == "shutdown":
action = sabnzbd.shutdown_program action = sabnzbd.shutdown_program
arguments = [] arguments = []
elif action_name == 'restart': elif action_name == "restart":
action = sabnzbd.restart_program action = sabnzbd.restart_program
arguments = [] arguments = []
elif action_name == 'pause_post': elif action_name == "pause_post":
action = pp_pause action = pp_pause
elif action_name == 'resume_post': elif action_name == "resume_post":
action = pp_resume action = pp_resume
elif action_name == 'speedlimit' and arguments != []: elif action_name == "speedlimit" and arguments != []:
action = sabnzbd.downloader.Downloader.do.limit_speed 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 action = sabnzbd.enable_server
elif action_name == 'disable_server' and arguments != []: elif action_name == "disable_server" and arguments != []:
action = sabnzbd.disable_server action = sabnzbd.disable_server
elif action_name == 'scan_folder': elif action_name == "scan_folder":
action = sabnzbd.dirscanner.dirscan action = sabnzbd.dirscanner.dirscan
elif action_name == 'rss_scan': elif action_name == "rss_scan":
action = rss.run_method action = rss.run_method
rss_planned = True rss_planned = True
elif action_name == 'remove_failed': elif action_name == "remove_failed":
action = sabnzbd.api.history_remove_failed action = sabnzbd.api.history_remove_failed
elif action_name == 'remove_completed': elif action_name == "remove_completed":
action = sabnzbd.api.history_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 action = sabnzbd.bpsmeter.BPSMeter.do.set_status
arguments = [True] arguments = [True]
elif action_name == 'disable_quota': elif action_name == "disable_quota":
action = sabnzbd.bpsmeter.BPSMeter.do.set_status action = sabnzbd.bpsmeter.BPSMeter.do.set_status
arguments = [False] arguments = [False]
elif action_name == 'pause_all_low': elif action_name == "pause_all_low":
action = sabnzbd.nzbqueue.NzbQueue.do.pause_on_prio action = sabnzbd.nzbqueue.NzbQueue.do.pause_on_prio
arguments = [LOW_PRIORITY] arguments = [LOW_PRIORITY]
elif action_name == 'pause_all_normal': elif action_name == "pause_all_normal":
action = sabnzbd.nzbqueue.NzbQueue.do.pause_on_prio action = sabnzbd.nzbqueue.NzbQueue.do.pause_on_prio
arguments = [NORMAL_PRIORITY] arguments = [NORMAL_PRIORITY]
elif action_name == 'pause_all_high': elif action_name == "pause_all_high":
action = sabnzbd.nzbqueue.NzbQueue.do.pause_on_prio action = sabnzbd.nzbqueue.NzbQueue.do.pause_on_prio
arguments = [HIGH_PRIORITY] arguments = [HIGH_PRIORITY]
elif action_name == 'resume_all_low': elif action_name == "resume_all_low":
action = sabnzbd.nzbqueue.NzbQueue.do.resume_on_prio action = sabnzbd.nzbqueue.NzbQueue.do.resume_on_prio
arguments = [LOW_PRIORITY] arguments = [LOW_PRIORITY]
elif action_name == 'resume_all_normal': elif action_name == "resume_all_normal":
action = sabnzbd.nzbqueue.NzbQueue.do.resume_on_prio action = sabnzbd.nzbqueue.NzbQueue.do.resume_on_prio
arguments = [NORMAL_PRIORITY] arguments = [NORMAL_PRIORITY]
elif action_name == 'resume_all_high': elif action_name == "resume_all_high":
action = sabnzbd.nzbqueue.NzbQueue.do.resume_on_prio action = sabnzbd.nzbqueue.NzbQueue.do.resume_on_prio
arguments = [HIGH_PRIORITY] arguments = [HIGH_PRIORITY]
elif action_name == 'pause_cat': elif action_name == "pause_cat":
action = sabnzbd.nzbqueue.NzbQueue.do.pause_on_cat action = sabnzbd.nzbqueue.NzbQueue.do.pause_on_cat
arguments = [argument_list] arguments = [argument_list]
elif action_name == 'resume_cat': elif action_name == "resume_cat":
action = sabnzbd.nzbqueue.NzbQueue.do.resume_on_cat action = sabnzbd.nzbqueue.NzbQueue.do.resume_on_cat
arguments = [argument_list] arguments = [argument_list]
else: else:
logging.warning(T('Unknown action: %s'), action_name) logging.warning(T("Unknown action: %s"), action_name)
continue continue
if enabled == '1': if enabled == "1":
logging.debug("Scheduling %s(%s) on days %s at %02d:%02d", action_name, arguments, d, h, m) 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), __SCHED.add_daytime_task(action, action_name, d, None, (h, m), kronos.method.sequential, arguments, None)
kronos.method.sequential, arguments, None)
else: else:
logging.debug("Skipping %s(%s) on days %s at %02d:%02d", action_name, arguments, d, h, m) logging.debug("Skipping %s(%s) on days %s at %02d:%02d", action_name, arguments, d, h, m)
# Set Guardian interval to 30 seconds # Set Guardian interval to 30 seconds
__SCHED.add_interval_task(sched_guardian, "Guardian", 15, 30, __SCHED.add_interval_task(sched_guardian, "Guardian", 15, 30, kronos.method.sequential, None, None)
kronos.method.sequential, None, None)
# Set RSS check interval # Set RSS check interval
if not rss_planned: if not rss_planned:
@ -179,34 +177,53 @@ def init():
delay = random.randint(0, interval - 1) delay = random.randint(0, interval - 1)
logging.debug("Scheduling RSS interval task every %s min (delay=%s)", interval, delay) logging.debug("Scheduling RSS interval task every %s min (delay=%s)", interval, delay)
sabnzbd.rss.next_run(time.time() + delay * 60) sabnzbd.rss.next_run(time.time() + delay * 60)
__SCHED.add_interval_task(rss.run_method, "RSS", delay * 60, interval * 60, __SCHED.add_interval_task(
kronos.method.sequential, None, None) 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_single_task(rss.run_method, "RSS", 15, kronos.method.sequential, None, None)
if cfg.version_check(): if cfg.version_check():
# Check for new release, once per week on random time # Check for new release, once per week on random time
m = random.randint(0, 59) m = random.randint(0, 59)
h = random.randint(0, 23) 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) 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), __SCHED.add_daytime_task(
kronos.method.sequential, [], None) sabnzbd.misc.check_latest_version, "VerCheck", d, None, (h, m), kronos.method.sequential, [], None
)
action, hour, minute = sabnzbd.bpsmeter.BPSMeter.do.get_quota() action, hour, minute = sabnzbd.bpsmeter.BPSMeter.do.get_quota()
if action: if action:
logging.info('Setting schedule for quota check daily at %s:%s', hour, minute) 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), __SCHED.add_daytime_task(
kronos.method.sequential, [], None) action, "quota_reset", list(range(1, 8)), None, (hour, minute), kronos.method.sequential, [], None
)
if sabnzbd.misc.int_conv(cfg.history_retention()) > 0: if sabnzbd.misc.int_conv(cfg.history_retention()) > 0:
logging.info('Setting schedule for midnight auto history-purge') 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), __SCHED.add_daytime_task(
kronos.method.sequential, [], None) sabnzbd.database.midnight_history_purge,
"midnight_history_purge",
logging.info('Setting schedule for midnight BPS reset') list(range(1, 8)),
__SCHED.add_daytime_task(sabnzbd.bpsmeter.midnight_action, 'midnight_bps', list(range(1, 8)), None, (0, 0), None,
kronos.method.sequential, [], 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 # Subscribe to special schedule changes
cfg.rss_rate.callback(schedule_guard) cfg.rss_rate.callback(schedule_guard)
@ -216,7 +233,7 @@ def start():
""" Start the scheduler """ """ Start the scheduler """
global __SCHED global __SCHED
if __SCHED: if __SCHED:
logging.debug('Starting scheduler') logging.debug("Starting scheduler")
__SCHED.start() __SCHED.start()
@ -241,7 +258,7 @@ def stop():
""" Stop the scheduler, destroy instance """ """ Stop the scheduler, destroy instance """
global __SCHED global __SCHED
if __SCHED: if __SCHED:
logging.debug('Stopping scheduler') logging.debug("Stopping scheduler")
try: try:
__SCHED.stop() __SCHED.stop()
except IndexError: except IndexError:
@ -254,7 +271,7 @@ def abort():
""" Emergency stop, just set the running attribute false """ """ Emergency stop, just set the running attribute false """
global __SCHED global __SCHED
if __SCHED: if __SCHED:
logging.debug('Terminating scheduler') logging.debug("Terminating scheduler")
__SCHED.running = False __SCHED.running = False
@ -284,8 +301,8 @@ def sort_schedules(all_events, now=None):
except: except:
continue # Bad schedule, ignore continue # Bad schedule, ignore
action = action.strip() action = action.strip()
if dd == '*': if dd == "*":
dd = '1234567' dd = "1234567"
if not dd.isdigit(): if not dd.isdigit():
continue # Bad schedule, ignore continue # Bad schedule, ignore
for d in dd: for d in dd:
@ -319,10 +336,10 @@ def analyse(was_paused=False, priority=None):
for ev in sort_schedules(all_events=True): for ev in sort_schedules(all_events=True):
if priority is None: if priority is None:
logging.debug('Schedule check result = %s', ev) logging.debug("Schedule check result = %s", ev)
# Skip if disabled # Skip if disabled
if ev[4] == '0': if ev[4] == "0":
continue continue
action = ev[1] action = ev[1]
@ -330,48 +347,48 @@ def analyse(was_paused=False, priority=None):
value = ev[2] value = ev[2]
except: except:
value = None value = None
if action == 'pause': if action == "pause":
paused = True paused = True
elif action == 'pause_all': elif action == "pause_all":
paused_all = True paused_all = True
PP_PAUSE_EVENT = True PP_PAUSE_EVENT = True
elif action == 'resume': elif action == "resume":
paused = False paused = False
paused_all = False paused_all = False
elif action == 'pause_post': elif action == "pause_post":
pause_post = True pause_post = True
PP_PAUSE_EVENT = True PP_PAUSE_EVENT = True
elif action == 'resume_post': elif action == "resume_post":
pause_post = False pause_post = False
PP_PAUSE_EVENT = True PP_PAUSE_EVENT = True
elif action == 'speedlimit' and value is not None: elif action == "speedlimit" and value is not None:
speedlimit = ev[2] speedlimit = ev[2]
elif action == 'pause_all_low': elif action == "pause_all_low":
pause_low = True pause_low = True
elif action == 'pause_all_normal': elif action == "pause_all_normal":
pause_normal = True pause_normal = True
elif action == 'pause_all_high': elif action == "pause_all_high":
pause_high = True pause_high = True
elif action == 'resume_all_low': elif action == "resume_all_low":
pause_low = False pause_low = False
elif action == 'resume_all_normal': elif action == "resume_all_normal":
pause_normal = False pause_normal = False
elif action == 'resume_all_high': elif action == "resume_all_high":
pause_high = False pause_high = False
elif action == 'enable_quota': elif action == "enable_quota":
quota = True quota = True
elif action == 'disable_quota': elif action == "disable_quota":
quota = False quota = False
elif action == 'enable_server': elif action == "enable_server":
try: try:
servers[value] = 1 servers[value] = 1
except: except:
logging.warning(T('Schedule for non-existing server %s'), value) logging.warning(T("Schedule for non-existing server %s"), value)
elif action == 'disable_server': elif action == "disable_server":
try: try:
servers[value] = 0 servers[value] = 0
except: 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 # Special case, a priority was passed, so evaluate only that and return state
if priority == LOW_PRIORITY: if priority == LOW_PRIORITY:
@ -399,7 +416,7 @@ def analyse(was_paused=False, priority=None):
for serv in servers: for serv in servers:
try: try:
item = config.get_config('servers', serv) item = config.get_config("servers", serv)
value = servers[serv] value = servers[serv]
if bool(item.enable()) != bool(value): if bool(item.enable()) != bool(value):
item.enable.set(value) item.enable.set(value)
@ -410,7 +427,7 @@ def analyse(was_paused=False, priority=None):
# Support for single shot pause (=delayed resume) # 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(): def scheduled_resume():
@ -427,10 +444,10 @@ def __oneshot_resume(when):
global __PAUSE_END global __PAUSE_END
if __PAUSE_END is not None and (when > __PAUSE_END - 5) and (when < __PAUSE_END + 55): if __PAUSE_END is not None and (when > __PAUSE_END - 5) and (when < __PAUSE_END + 55):
__PAUSE_END = None __PAUSE_END = None
logging.debug('Resume after pause-interval') logging.debug("Resume after pause-interval")
sabnzbd.unpause_all() sabnzbd.unpause_all()
else: else:
logging.debug('Ignoring cancelled resume') logging.debug("Ignoring cancelled resume")
def plan_resume(interval): def plan_resume(interval):
@ -438,8 +455,8 @@ def plan_resume(interval):
global __SCHED, __PAUSE_END global __SCHED, __PAUSE_END
if interval > 0: if interval > 0:
__PAUSE_END = time.time() + (interval * 60) __PAUSE_END = time.time() + (interval * 60)
logging.debug('Schedule resume at %s', __PAUSE_END) logging.debug("Schedule resume at %s", __PAUSE_END)
__SCHED.add_single_task(__oneshot_resume, '', interval * 60, kronos.method.sequential, [__PAUSE_END], None) __SCHED.add_single_task(__oneshot_resume, "", interval * 60, kronos.method.sequential, [__PAUSE_END], None)
sabnzbd.downloader.Downloader.do.pause() sabnzbd.downloader.Downloader.do.pause()
else: else:
__PAUSE_END = None __PAUSE_END = None
@ -454,10 +471,10 @@ def pause_int():
else: else:
val = __PAUSE_END - time.time() val = __PAUSE_END - time.time()
if val < 0: if val < 0:
sign = '-' sign = "-"
val = abs(val) val = abs(val)
else: else:
sign = '' sign = ""
mins = int(val / 60) mins = int(val / 60)
sec = int(val - mins * 60) sec = int(val - mins * 60)
return "%s%d:%02d" % (sign, mins, sec) return "%s%d:%02d" % (sign, mins, sec)
@ -468,18 +485,18 @@ def pause_check():
global __PAUSE_END global __PAUSE_END
if __PAUSE_END is not None and (__PAUSE_END - time.time()) < 0: if __PAUSE_END is not None and (__PAUSE_END - time.time()) < 0:
__PAUSE_END = None __PAUSE_END = None
logging.debug('Force resume, negative timer') logging.debug("Force resume, negative timer")
sabnzbd.unpause_all() sabnzbd.unpause_all()
def plan_server(action, parms, interval): def plan_server(action, parms, interval):
""" Plan to re-activate server after 'interval' minutes """ """ 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(): def force_rss():
""" Add a one-time RSS scan, one second from now """ """ 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 # 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 from sabnzbd.encoding import ubtou, utob
_BAD_GZ_HOSTS = ('.zip', 'nzbsa.co.za', 'newshost.za.net') _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', _RARTING_FIELDS = (
'x-rating-voteup', 'x-rating-votedown', 'x-rating-spam', 'x-rating-confirmed-spam', 'x-rating-passworded', 'x-rating-confirmed-passworded') "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): class URLGrabber(Thread):
@ -69,7 +82,7 @@ class URLGrabber(Thread):
# Too many tries? Cancel # Too many tries? Cancel
if future_nzo.url_tries > cfg.max_url_retries(): 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 return
future_nzo.url_wait = time.time() + when future_nzo.url_wait = time.time() + when
@ -77,12 +90,12 @@ class URLGrabber(Thread):
self.queue.put((url, future_nzo)) self.queue.put((url, future_nzo))
def stop(self): def stop(self):
logging.info('URLGrabber shutting down') logging.info("URLGrabber shutting down")
self.shutdown = True self.shutdown = True
self.add(None, None) self.add(None, None)
def run(self): def run(self):
logging.info('URLGrabber starting up') logging.info("URLGrabber starting up")
self.shutdown = False self.shutdown = False
while not self.shutdown: while not self.shutdown:
@ -104,7 +117,7 @@ class URLGrabber(Thread):
time.sleep(1.0) time.sleep(1.0)
continue continue
url = url.replace(' ', '') url = url.replace(" ", "")
try: try:
if future_nzo: if future_nzo:
@ -114,7 +127,7 @@ class URLGrabber(Thread):
except AttributeError: except AttributeError:
deleted = True deleted = True
if deleted: if deleted:
logging.debug('Dropping URL %s, job entry missing', url) logging.debug("Dropping URL %s, job entry missing", url)
continue continue
filename = None filename = None
@ -125,7 +138,7 @@ class URLGrabber(Thread):
retry = True retry = True
fetch_request = None fetch_request = None
logging.info('Grabbing URL %s', url) logging.info("Grabbing URL %s", url)
try: try:
fetch_request = _build_request(url) fetch_request = _build_request(url)
except Exception as e: except Exception as e:
@ -133,22 +146,22 @@ class URLGrabber(Thread):
error0 = str(sys.exc_info()[0]).lower() error0 = str(sys.exc_info()[0]).lower()
error1 = str(sys.exc_info()[1]).lower() error1 = str(sys.exc_info()[1]).lower()
logging.debug('Error "%s" trying to get the url %s', error1, url) logging.debug('Error "%s" trying to get the url %s', error1, url)
if 'certificate_verify_failed' in error1 or 'certificateerror' in error0: if "certificate_verify_failed" in error1 or "certificateerror" in error0:
msg = T('Server %s uses an untrusted HTTPS certificate') % '' msg = T("Server %s uses an untrusted HTTPS certificate") % ""
msg += ' - https://sabnzbd.org/certificate-errors' msg += " - https://sabnzbd.org/certificate-errors"
retry = False retry = False
elif 'nodename nor servname provided' in error1: elif "nodename nor servname provided" in error1:
msg = T('Server name does not resolve') msg = T("Server name does not resolve")
retry = False retry = False
elif '401' in error1 or 'unauthorized' in error1: elif "401" in error1 or "unauthorized" in error1:
msg = T('Unauthorized access') msg = T("Unauthorized access")
retry = False retry = False
elif '404' in error1: elif "404" in error1:
msg = T('File not on server') msg = T("File not on server")
retry = False 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) # 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: if fetch_request:
for hdr in fetch_request.headers: for hdr in fetch_request.headers:
@ -157,29 +170,29 @@ class URLGrabber(Thread):
value = fetch_request.headers[hdr] value = fetch_request.headers[hdr]
except: except:
continue continue
if item in ('content-encoding',) and value == 'gzip': if item in ("content-encoding",) and value == "gzip":
gzipped = True gzipped = True
if item in ('category_id', 'x-dnzb-category'): if item in ("category_id", "x-dnzb-category"):
category = value category = value
elif item in ('x-dnzb-moreinfo',): elif item in ("x-dnzb-moreinfo",):
nzo_info['more_info'] = value nzo_info["more_info"] = value
elif item in ('x-dnzb-name',): elif item in ("x-dnzb-name",):
filename = value filename = value
if not filename.endswith('.nzb'): if not filename.endswith(".nzb"):
filename += '.nzb' filename += ".nzb"
elif item == 'x-dnzb-propername': elif item == "x-dnzb-propername":
nzo_info['propername'] = value nzo_info["propername"] = value
elif item == 'x-dnzb-episodename': elif item == "x-dnzb-episodename":
nzo_info['episodename'] = value nzo_info["episodename"] = value
elif item == 'x-dnzb-year': elif item == "x-dnzb-year":
nzo_info['year'] = value nzo_info["year"] = value
elif item == 'x-dnzb-failure': elif item == "x-dnzb-failure":
nzo_info['failure'] = value nzo_info["failure"] = value
elif item == 'x-dnzb-details': elif item == "x-dnzb-details":
nzo_info['details'] = value nzo_info["details"] = value
elif item == 'x-dnzb-password': elif item == "x-dnzb-password":
nzo_info['password'] = value nzo_info["password"] = value
elif item == 'retry-after': elif item == "retry-after":
wait = misc.int_conv(value) wait = misc.int_conv(value)
# Rating fields # Rating fields
@ -188,11 +201,11 @@ class URLGrabber(Thread):
# Get filename from Content-Disposition header # Get filename from Content-Disposition header
if not filename and "filename=" in value: 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: if wait:
# For sites that have a rate-limiting attribute # For sites that have a rate-limiting attribute
msg = '' msg = ""
retry = True retry = True
fetch_request = None fetch_request = None
elif retry: elif retry:
@ -200,7 +213,7 @@ class URLGrabber(Thread):
if not fetch_request: if not fetch_request:
if retry: if retry:
logging.info('Retry URL %s', url) logging.info("Retry URL %s", url)
self.add(url, future_nzo, wait) self.add(url, future_nzo, wait)
else: else:
self.fail_to_history(future_nzo, url, msg) self.fail_to_history(future_nzo, url, msg)
@ -213,56 +226,66 @@ class URLGrabber(Thread):
# Check if the original URL has extension # Check if the original URL has extension
if url != fetch_request.url and sabnzbd.filesystem.get_ext(filename) not in VALID_NZB_FILES: 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)) 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! # Sometimes the filename contains the full URL, duh!
filename = filename[filename.find('&nzbname=') + 9:] filename = filename[filename.find("&nzbname=") + 9 :]
pp = future_nzo.pp pp = future_nzo.pp
script = future_nzo.script script = future_nzo.script
cat = future_nzo.cat 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) cat = misc.cat_convert(category)
priority = future_nzo.priority priority = future_nzo.priority
nzbname = future_nzo.custom_name nzbname = future_nzo.custom_name
# process data # process data
if gzipped: if gzipped:
filename += '.gz' filename += ".gz"
if not data: if not data:
try: try:
data = fetch_request.read() data = fetch_request.read()
except (IncompleteRead, IOError): 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() fetch_request.close()
continue continue
fetch_request.close() fetch_request.close()
if b'<nzb' in data and sabnzbd.filesystem.get_ext(filename) != '.nzb': if b"<nzb" in data and sabnzbd.filesystem.get_ext(filename) != ".nzb":
filename += '.nzb' filename += ".nzb"
# Sanitize filename first (also removing forbidden Windows-names) # Sanitize filename first (also removing forbidden Windows-names)
filename = sabnzbd.filesystem.sanitize_filename(filename) filename = sabnzbd.filesystem.sanitize_filename(filename)
# Write data to temp file # Write data to temp file
path = os.path.join(cfg.admin_dir.get_path(), FUTURE_Q_FOLDER, filename) 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) temp_nzb.write(data)
# Check if nzb file # Check if nzb file
if sabnzbd.filesystem.get_ext(filename) in VALID_NZB_FILES: 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, res = dirscanner.process_single_nzb(
nzbname=nzbname, nzo_info=nzo_info, url=future_nzo.url, keep=False, filename,
nzo_id=future_nzo.nzo_id)[0] 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:
if res == -2: 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 when = 300
elif res == -1: elif res == -1:
# Error, but no reason to retry. Warning is already given # Error, but no reason to retry. Warning is already given
NzbQueue.do.remove(future_nzo.nzo_id, add_to_history=False) NzbQueue.do.remove(future_nzo.nzo_id, add_to_history=False)
continue continue
else: 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 when = 120
self.add(url, future_nzo, when) self.add(url, future_nzo, when)
@ -270,27 +293,36 @@ class URLGrabber(Thread):
# Check if a supported archive # Check if a supported archive
status, zf, exp_ext = dirscanner.is_archive(path) status, zf, exp_ext = dirscanner.is_archive(path)
if status == 0: 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 filename = filename + exp_ext
os.rename(path, path + exp_ext) os.rename(path, path + exp_ext)
path = path + exp_ext path = path + exp_ext
dirscanner.process_nzb_archive_file(filename, path, pp, script, cat, priority=priority, dirscanner.process_nzb_archive_file(
nzbname=nzbname, url=future_nzo.url, keep=False, filename,
nzo_id=future_nzo.nzo_id) path,
pp,
script,
cat,
priority=priority,
nzbname=nzbname,
url=future_nzo.url,
keep=False,
nzo_id=future_nzo.nzo_id,
)
else: else:
# Not a supported filetype, not an nzb (text/html ect) # Not a supported filetype, not an nzb (text/html ect)
try: try:
os.remove(fetch_request) os.remove(fetch_request)
except: except:
pass 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) self.add(url, future_nzo, 30)
except: except:
logging.error(T('URLGRABBER CRASHED'), exc_info=True) logging.error(T("URLGRABBER CRASHED"), exc_info=True)
logging.debug("URLGRABBER Traceback: ", 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 """ Create History entry for failed URL Fetch
msg: message to be logged msg: message to be logged
content: report in history that cause is a bad NZB file content: report in history that cause is a bad NZB file
@ -302,16 +334,16 @@ class URLGrabber(Thread):
if content: if content:
# Bad content # Bad content
msg = T('Unusable NZB file') msg = T("Unusable NZB file")
else: else:
# Failed fetch # Failed fetch
msg = T('URL Fetching failed; %s') % msg msg = T("URL Fetching failed; %s") % msg
# Mark as failed # Mark as failed
nzo.status = Status.FAILED nzo.status = Status.FAILED
nzo.fail_msg = msg 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: if cfg.email_endjob() > 0:
emailer.badfetch_mail(msg, url) emailer.badfetch_mail(msg, url)
@ -330,21 +362,21 @@ def _build_request(url):
u = urllib.parse.urlparse(url) u = urllib.parse.urlparse(url)
if u.username is not None or u.password is not None: if u.username is not None or u.password is not None:
if u.username and u.password: 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 host_port = u.hostname
if u.port: if u.port:
host_port += ':' + str(u.port) host_port += ":" + str(u.port)
url = urllib.parse.urlunparse(u._replace(netloc=host_port)) url = urllib.parse.urlunparse(u._replace(netloc=host_port))
# Start request # Start request
req = urllib.request.Request(url) req = urllib.request.Request(url)
# Add headers # 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): 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: 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) return urllib.request.urlopen(req)
@ -357,11 +389,11 @@ def _analyse(fetch_request, future_nzo):
if fetch_request: if fetch_request:
msg = fetch_request.msg msg = fetch_request.msg
else: else:
msg = '' msg = ""
# Increasing wait-time in steps for standard errors # Increasing wait-time in steps for standard errors
when = DEF_TIMEOUT * (future_nzo.url_tries + 1) 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 None, msg, True, when, data
return fetch_request, fetch_request.msg, False, 0, data return fetch_request, fetch_request.msg, False, 0, data

38
sabnzbd/zconfig.py

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

Loading…
Cancel
Save