mirror of
https://github.com/sabnzbd/sabnzbd.git
synced 2025-12-24 16:19:31 -05:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af0b53990c | ||
|
|
e3861954ba | ||
|
|
006dd8dc77 | ||
|
|
dbff203c62 | ||
|
|
f45eb891cd | ||
|
|
77b58240cf | ||
|
|
97ae1ff10e | ||
|
|
8734a4f24b | ||
|
|
480fce55a8 | ||
|
|
d4136fadd2 | ||
|
|
308bc375bd | ||
|
|
3bbcf6a41e | ||
|
|
3d5d10a4c1 | ||
|
|
0e979c14f0 | ||
|
|
70f49114ac | ||
|
|
699d75bb9f | ||
|
|
95822704c8 | ||
|
|
76e5f69e67 | ||
|
|
abd31d0249 | ||
|
|
9ae7ee6e2d |
2
.github/workflows/build_release.yml
vendored
2
.github/workflows/build_release.yml
vendored
@@ -81,7 +81,7 @@ jobs:
|
||||
# We need the official Python, because the GA ones only support newer macOS versions
|
||||
# The deployment target is picked up by the Python build tools automatically
|
||||
# If updated, make sure to also set LSMinimumSystemVersion in SABnzbd.spec
|
||||
PYTHON_VERSION: "3.12.1"
|
||||
PYTHON_VERSION: "3.12.2"
|
||||
MACOSX_DEPLOYMENT_TARGET: "10.9"
|
||||
# We need to force compile for universal2 support
|
||||
CFLAGS: -arch x86_64 -arch arm64
|
||||
|
||||
20
README.mkd
20
README.mkd
@@ -1,14 +1,28 @@
|
||||
Release Notes - SABnzbd 4.2.2 Release Candidate 1
|
||||
Release Notes - SABnzbd 4.2.3 Release Candidate 3
|
||||
=========================================================
|
||||
|
||||
This is the second bug-fix release of SABnzbd 4.2.0.
|
||||
This is the third bug-fix release of SABnzbd 4.2.0.
|
||||
|
||||
## Bug-fixes and changes since 4.2.2:
|
||||
|
||||
* **Bug-fixes:**
|
||||
* Handle new status code for missing articles, which would result in timeouts.
|
||||
This specifically affects Giganews and its resellers.
|
||||
* Retry of failed job would not use the password provided.
|
||||
* Optimize database handling in order to prevent locking errors.
|
||||
* macOS: System standby after finishing the queue would not always work.
|
||||
|
||||
* **Changes:**
|
||||
* Remove `Send Group` option for Servers.
|
||||
|
||||
## Bug-fixes and changes since 4.2.1:
|
||||
|
||||
* **Bug-fixes:**
|
||||
* RSS readout could result in a crash if duplicate detection is enabled.
|
||||
* RSS readout could result in a crash if Duplicate Detection was enabled.
|
||||
* Passwords were not always correctly parsed.
|
||||
* Warnings could show even if `helpful_warnings` was disabled.
|
||||
* Duplicate Detection would trigger again on URLs if they were resumed.
|
||||
* Windows: Fatal crash could occur if ran as Service or on older Windows versions.
|
||||
|
||||
* **Changes:**
|
||||
* Parsing of filenames from the NZB was extended to allow more exotic formatting.
|
||||
|
||||
@@ -106,11 +106,6 @@
|
||||
<span class="desc">$T('explain-ssl_ciphers') <br>$T('readwiki')
|
||||
<a href="https://sabnzbd.org/wiki/advanced/ssl-ciphers" target="_blank">https://sabnzbd.org/wiki/advanced/ssl-ciphers</a></span>
|
||||
</div>
|
||||
<div class="field-pair advanced-settings">
|
||||
<label class="config" for="send_group">$T('srv-send_group')</label>
|
||||
<input type="checkbox" name="send_group" id="send_group" value="1" />
|
||||
<span class="desc">$T('srv-explain-send_group')</span>
|
||||
</div>
|
||||
<div class="field-pair advanced-settings">
|
||||
<label class="config" for="required">$T('srv-required')</label>
|
||||
<input type="checkbox" name="required" id="required" value="1" />
|
||||
@@ -248,11 +243,6 @@
|
||||
<input type="checkbox" name="optional" id="optional$cur" value="1" <!--#if int($server['optional']) != 0 then 'checked="checked"' else ""#--> />
|
||||
<span class="desc">$T('explain-optional')</span>
|
||||
</div>
|
||||
<div class="field-pair advanced-settings">
|
||||
<label class="config" for="send_group$cur">$T('srv-send_group')</label>
|
||||
<input type="checkbox" name="send_group" id="send_group$cur" value="1" <!--#if int($server['send_group']) != 0 then 'checked="checked"' else ""#--> />
|
||||
<span class="desc">$T('srv-explain-send_group')</span>
|
||||
</div>
|
||||
<div class="field-pair advanced-settings">
|
||||
<label class="config" for="expire_date$cur">$T('srv-expire_date')</label>
|
||||
<input type="date" name="expire_date" id="expire_date$cur" value="$server['expire_date']" />
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<a href="#" class="hover-button" data-bind="visible: history.isMultiEditing(), click: history.doMultiDelete">
|
||||
<a href="#" class="hover-button" title="$T('nzo-delete')" data-bind="visible: history.isMultiEditing(), click: history.doMultiDelete">
|
||||
<span class="glyphicon glyphicon-trash"></span>
|
||||
</a>
|
||||
<a href="#modal-purge-history" class="hover-button" title="$T('purgeHist')" data-bind="visible: !history.isMultiEditing()" data-toggle="modal" data-tooltip="true" data-placement="left">
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
<label for="multiedit-checkall-queue">
|
||||
<input type="checkbox" name="multieditCheckAll" id="multiedit-checkall-queue" title="$T('Glitter-checkAll')" data-bind="click: queue.checkAllJobs" data-tooltip="true" data-placement="top" />
|
||||
</label>
|
||||
<a href="#" class="hover-button" data-bind="click: queue.doMultiDelete">
|
||||
<a href="#" class="hover-button" title="$T('removeNZB-Files')" data-bind="click: queue.doMultiDelete">
|
||||
<span class="glyphicon glyphicon-trash"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
<url type="faq">https://sabnzbd.org/wiki/faq</url>
|
||||
<url type="contact">https://sabnzbd.org/live-chat.html</url>
|
||||
<releases>
|
||||
<release version="4.2.2" date="2024-02-01" type="stable"/>
|
||||
<release version="4.2.3" date="2024-03-10" type="stable"/>
|
||||
<release version="4.2.2" date="2024-01-31" type="stable"/>
|
||||
<release version="4.2.1" date="2024-01-05" type="stable"/>
|
||||
<release version="4.2.0" date="2024-01-03" type="stable"/>
|
||||
<release version="4.1.0" date="2023-09-26" type="stable"/>
|
||||
|
||||
@@ -447,7 +447,6 @@ class ConfigServer:
|
||||
self.expire_date = OptionStr(name, "expire_date", add=False)
|
||||
self.quota = OptionStr(name, "quota", add=False)
|
||||
self.usage_at_start = OptionNumber(name, "usage_at_start", add=False)
|
||||
self.send_group = OptionBool(name, "send_group", False, add=False)
|
||||
self.priority = OptionNumber(name, "priority", 0, 0, 99, add=False)
|
||||
self.notes = OptionStr(name, "notes", add=False)
|
||||
|
||||
@@ -473,7 +472,6 @@ class ConfigServer:
|
||||
"ssl",
|
||||
"ssl_verify",
|
||||
"ssl_ciphers",
|
||||
"send_group",
|
||||
"enable",
|
||||
"required",
|
||||
"optional",
|
||||
@@ -516,7 +514,6 @@ class ConfigServer:
|
||||
output_dict["expire_date"] = self.expire_date()
|
||||
output_dict["quota"] = self.quota()
|
||||
output_dict["usage_at_start"] = self.usage_at_start()
|
||||
output_dict["send_group"] = self.send_group()
|
||||
output_dict["priority"] = self.priority()
|
||||
output_dict["notes"] = self.notes()
|
||||
return output_dict
|
||||
|
||||
@@ -75,6 +75,7 @@ DEF_LOG_CHERRY = "cherrypy.log"
|
||||
DEF_ARTICLE_CACHE_DEFAULT = "500M"
|
||||
DEF_ARTICLE_CACHE_MAX = "1G"
|
||||
DEF_TIMEOUT = 60
|
||||
DEF_TEST_TIMEOUT = 10
|
||||
DEF_SCANRATE = 5
|
||||
DEF_HTTPS_CERT_FILE = "server.cert"
|
||||
DEF_HTTPS_KEY_FILE = "server.key"
|
||||
|
||||
@@ -38,7 +38,7 @@ from sabnzbd.encoding import ubtou, utob
|
||||
from sabnzbd.misc import int_conv, caller_name, opts_to_pp, to_units
|
||||
from sabnzbd.filesystem import remove_file, clip_path
|
||||
|
||||
DB_LOCK = threading.RLock()
|
||||
DB_LOCK = threading.Lock()
|
||||
|
||||
|
||||
class HistoryDB:
|
||||
@@ -50,66 +50,69 @@ class HistoryDB:
|
||||
|
||||
# These class attributes will be accessed directly because
|
||||
# they need to be shared by all instances
|
||||
db_path = None # Will contain full path to history database
|
||||
done_cleaning = False # Ensure we only do one Vacuum per session
|
||||
db_path = None # Full path to history database
|
||||
startup_done = False
|
||||
|
||||
@synchronized(DB_LOCK)
|
||||
def __init__(self):
|
||||
"""Determine database path and create connection"""
|
||||
self.connection: Optional[Connection] = None
|
||||
self.cursor: Optional[Cursor] = None
|
||||
if not HistoryDB.db_path:
|
||||
HistoryDB.db_path = os.path.join(sabnzbd.cfg.admin_dir.get_path(), DB_HISTORY_NAME)
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
"""Create a connection to the database"""
|
||||
create_table = not os.path.exists(HistoryDB.db_path)
|
||||
if not HistoryDB.db_path:
|
||||
HistoryDB.db_path = os.path.join(sabnzbd.cfg.admin_dir.get_path(), DB_HISTORY_NAME)
|
||||
create_table = not HistoryDB.startup_done and not os.path.exists(HistoryDB.db_path)
|
||||
|
||||
self.connection = sqlite3.connect(HistoryDB.db_path)
|
||||
self.connection.isolation_level = None # autocommit attribute only introduced in Python 3.12
|
||||
self.connection.row_factory = sqlite3.Row
|
||||
self.cursor = self.connection.cursor()
|
||||
if create_table:
|
||||
self.create_history_db()
|
||||
elif not HistoryDB.done_cleaning:
|
||||
# Run VACUUM on sqlite
|
||||
|
||||
# Perform initialization only once
|
||||
if not HistoryDB.startup_done:
|
||||
if create_table:
|
||||
self.create_history_db()
|
||||
|
||||
# When an object (table, index, or trigger) is dropped from the database, it leaves behind empty space
|
||||
# http://www.sqlite.org/lang_vacuum.html
|
||||
HistoryDB.done_cleaning = True
|
||||
self.execute("VACUUM")
|
||||
|
||||
self.execute("PRAGMA user_version;")
|
||||
try:
|
||||
version = self.cursor.fetchone()["user_version"]
|
||||
except (IndexError, TypeError):
|
||||
version = 0
|
||||
# See if we need to perform any updates
|
||||
self.execute("PRAGMA user_version;")
|
||||
try:
|
||||
version = self.cursor.fetchone()["user_version"]
|
||||
except (IndexError, TypeError):
|
||||
version = 0
|
||||
|
||||
# Add any new columns added since last DB version
|
||||
# Use "and" to stop when database has been reset due to corruption
|
||||
if version < 1:
|
||||
_ = (
|
||||
self.execute("PRAGMA user_version = 1;", save=True)
|
||||
and self.execute("ALTER TABLE history ADD COLUMN series TEXT;", save=True)
|
||||
and self.execute("ALTER TABLE history ADD COLUMN md5sum TEXT;", save=True)
|
||||
)
|
||||
if version < 2:
|
||||
_ = self.execute("PRAGMA user_version = 2;", save=True) and self.execute(
|
||||
"ALTER TABLE history ADD COLUMN password TEXT;", save=True
|
||||
)
|
||||
if version < 3:
|
||||
# Transfer data to new column (requires WHERE-hack), original column should be removed later
|
||||
_ = (
|
||||
self.execute("PRAGMA user_version = 3;", save=True)
|
||||
and self.execute("ALTER TABLE history ADD COLUMN duplicate_key TEXT;", save=True)
|
||||
and self.execute("UPDATE history SET duplicate_key = series WHERE 1 = 1;", save=True)
|
||||
)
|
||||
# Add any new columns added since last DB version
|
||||
# Use "and" to stop when database has been reset due to corruption
|
||||
if version < 1:
|
||||
_ = (
|
||||
self.execute("PRAGMA user_version = 1;")
|
||||
and self.execute("ALTER TABLE history ADD COLUMN series TEXT;")
|
||||
and self.execute("ALTER TABLE history ADD COLUMN md5sum TEXT;")
|
||||
)
|
||||
if version < 2:
|
||||
_ = self.execute("PRAGMA user_version = 2;") and self.execute(
|
||||
"ALTER TABLE history ADD COLUMN password TEXT;"
|
||||
)
|
||||
if version < 3:
|
||||
# Transfer data to new column (requires WHERE-hack), original column should be removed later
|
||||
_ = (
|
||||
self.execute("PRAGMA user_version = 3;")
|
||||
and self.execute("ALTER TABLE history ADD COLUMN duplicate_key TEXT;")
|
||||
and self.execute("UPDATE history SET duplicate_key = series WHERE 1 = 1;")
|
||||
)
|
||||
HistoryDB.startup_done = True
|
||||
|
||||
def execute(self, command: str, args: Sequence = (), save: bool = False) -> bool:
|
||||
def execute(self, command: str, args: Sequence = ()) -> bool:
|
||||
"""Wrapper for executing SQL commands"""
|
||||
for tries in (4, 3, 2, 1, 0):
|
||||
try:
|
||||
self.cursor.execute(command, args)
|
||||
if save:
|
||||
self.connection.commit()
|
||||
return True
|
||||
except:
|
||||
error = str(sys.exc_info()[1])
|
||||
@@ -129,6 +132,7 @@ class HistoryDB:
|
||||
remove_file(HistoryDB.db_path)
|
||||
except:
|
||||
pass
|
||||
HistoryDB.startup_done = False
|
||||
self.connect()
|
||||
# Return False in case of "duplicate column" error
|
||||
# because the column addition in connect() must be terminated
|
||||
@@ -141,6 +145,7 @@ class HistoryDB:
|
||||
try:
|
||||
self.connection.rollback()
|
||||
except:
|
||||
# Can fail in case of automatic rollback
|
||||
logging.debug("Rollback Failed:", exc_info=True)
|
||||
return False
|
||||
|
||||
@@ -178,10 +183,9 @@ class HistoryDB:
|
||||
"password" TEXT,
|
||||
"duplicate_key" TEXT
|
||||
)
|
||||
""",
|
||||
save=True,
|
||||
"""
|
||||
)
|
||||
self.execute("PRAGMA user_version = 3;", save=True)
|
||||
self.execute("PRAGMA user_version = 3;")
|
||||
|
||||
def close(self):
|
||||
"""Close database connection"""
|
||||
@@ -196,9 +200,7 @@ class HistoryDB:
|
||||
"""Remove all completed jobs from the database, optional with `search` pattern"""
|
||||
search = convert_search(search)
|
||||
logging.info("Removing all completed jobs from history")
|
||||
return self.execute(
|
||||
"""DELETE FROM history WHERE name LIKE ? AND status = ?""", (search, Status.COMPLETED), save=True
|
||||
)
|
||||
return self.execute("""DELETE FROM history WHERE name LIKE ? AND status = ?""", (search, Status.COMPLETED))
|
||||
|
||||
def get_failed_paths(self, search=None):
|
||||
"""Return list of all storage paths of failed jobs (may contain non-existing or empty paths)"""
|
||||
@@ -215,9 +217,7 @@ class HistoryDB:
|
||||
"""Remove all failed jobs from the database, optional with `search` pattern"""
|
||||
search = convert_search(search)
|
||||
logging.info("Removing all failed jobs from history")
|
||||
return self.execute(
|
||||
"""DELETE FROM history WHERE name LIKE ? AND status = ?""", (search, Status.FAILED), save=True
|
||||
)
|
||||
return self.execute("""DELETE FROM history WHERE name LIKE ? AND status = ?""", (search, Status.FAILED))
|
||||
|
||||
def remove_history(self, jobs=None):
|
||||
"""Remove all jobs in the list `jobs`, empty list will remove all completed jobs"""
|
||||
@@ -228,7 +228,7 @@ class HistoryDB:
|
||||
jobs = [jobs]
|
||||
|
||||
for job in jobs:
|
||||
self.execute("""DELETE FROM history WHERE nzo_id = ?""", (job,), save=True)
|
||||
self.execute("""DELETE FROM history WHERE nzo_id = ?""", (job,))
|
||||
logging.info("[%s] Removing job %s from history", caller_name(), job)
|
||||
|
||||
def auto_history_purge(self):
|
||||
@@ -247,9 +247,7 @@ class HistoryDB:
|
||||
if days_to_keep > 0:
|
||||
logging.info("Removing completed jobs older than %s days from history", days_to_keep)
|
||||
return self.execute(
|
||||
"""DELETE FROM history WHERE status = ? AND completed < ?""",
|
||||
(Status.COMPLETED, seconds_to_keep),
|
||||
save=True,
|
||||
"""DELETE FROM history WHERE status = ? AND completed < ?""", (Status.COMPLETED, seconds_to_keep)
|
||||
)
|
||||
else:
|
||||
# How many to keep?
|
||||
@@ -261,7 +259,6 @@ class HistoryDB:
|
||||
SELECT id FROM history WHERE status = ? ORDER BY completed DESC LIMIT ?
|
||||
)""",
|
||||
(Status.COMPLETED, Status.COMPLETED, to_keep),
|
||||
save=True,
|
||||
)
|
||||
|
||||
def add_history_db(self, nzo, storage: str, postproc_time: int, script_output: str, script_line: str):
|
||||
@@ -274,7 +271,6 @@ class HistoryDB:
|
||||
downloaded, fail_message, url_info, bytes, duplicate_key, md5sum, password)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
t,
|
||||
save=True,
|
||||
)
|
||||
logging.info("Added job %s to history", nzo.final_name)
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ def decode_uu(article: Article, raw_data: bytes) -> bytes:
|
||||
"""Try to uu-decode an article. The raw_data may or may not contain headers.
|
||||
If there are headers, they will be separated from the body by at least one
|
||||
empty line. In case of no headers, the first line seems to always be the nntp
|
||||
response code (222) directly followed by the msg body."""
|
||||
response code (220/222) directly followed by the msg body."""
|
||||
if not raw_data:
|
||||
logging.debug("No data to decode")
|
||||
raise BadUu
|
||||
@@ -232,7 +232,7 @@ def decode_uu(article: Article, raw_data: bytes) -> bytes:
|
||||
uu_start = raw_data[:limit].index(b"") + 1
|
||||
except ValueError:
|
||||
# No empty line, look for a response code instead
|
||||
if raw_data[0].startswith(b"222 "):
|
||||
if raw_data[0].startswith(b"220 ") or raw_data[0].startswith(b"222 "):
|
||||
uu_start = 1
|
||||
else:
|
||||
# Invalid data?
|
||||
|
||||
@@ -80,7 +80,6 @@ class Server:
|
||||
"required",
|
||||
"optional",
|
||||
"retention",
|
||||
"send_group",
|
||||
"username",
|
||||
"password",
|
||||
"busy_threads",
|
||||
@@ -111,7 +110,6 @@ class Server:
|
||||
use_ssl,
|
||||
ssl_verify,
|
||||
ssl_ciphers,
|
||||
send_group,
|
||||
username=None,
|
||||
password=None,
|
||||
required=False,
|
||||
@@ -134,15 +132,6 @@ class Server:
|
||||
self.required: bool = required
|
||||
self.optional: bool = optional
|
||||
self.retention: int = retention
|
||||
self.send_group: bool = send_group
|
||||
|
||||
# TODO: Remove after next release
|
||||
if send_group:
|
||||
helpful_warning(
|
||||
"You have 'Send Group' enabled for %s. Could you let us know why? https://github.com/sabnzbd/sabnzbd/discussions/2715",
|
||||
self.displayname,
|
||||
)
|
||||
|
||||
self.username: Optional[str] = username
|
||||
self.password: Optional[str] = password
|
||||
|
||||
@@ -331,7 +320,6 @@ class Downloader(Thread):
|
||||
required = srv.required()
|
||||
optional = srv.optional()
|
||||
retention = int(srv.retention() * 24 * 3600) # days ==> seconds
|
||||
send_group = srv.send_group()
|
||||
create = True
|
||||
|
||||
if oldserver:
|
||||
@@ -358,7 +346,6 @@ class Downloader(Thread):
|
||||
ssl,
|
||||
ssl_verify,
|
||||
ssl_ciphers,
|
||||
send_group,
|
||||
username,
|
||||
password,
|
||||
required,
|
||||
@@ -742,7 +729,9 @@ class Downloader(Thread):
|
||||
time.sleep(0.01)
|
||||
sabnzbd.BPSMeter.update()
|
||||
|
||||
if nw.status_code != 222 and not done:
|
||||
# Response code depends on request command:
|
||||
# # 220 = ARTICLE, 222 = BODY
|
||||
if nw.status_code not in (220, 222) and not done:
|
||||
if not nw.connected or nw.status_code == 480:
|
||||
if not self.__finish_connect_nw(nw):
|
||||
return
|
||||
@@ -754,13 +743,7 @@ class Downloader(Thread):
|
||||
done = True
|
||||
logging.debug("Article <%s> is present", article.article)
|
||||
|
||||
elif nw.status_code == 211:
|
||||
logging.debug("group command ok -> %s", nw.nntp_msg)
|
||||
nw.group = nw.article.nzf.nzo.group
|
||||
nw.reset_data_buffer()
|
||||
self.__request_article(nw)
|
||||
|
||||
elif nw.status_code in (411, 423, 430):
|
||||
elif nw.status_code in (411, 423, 430, 451):
|
||||
done = True
|
||||
logging.debug(
|
||||
"Thread %s@%s: Article %s missing (error=%s)",
|
||||
@@ -783,6 +766,17 @@ class Downloader(Thread):
|
||||
nw.reset_data_buffer()
|
||||
self.__request_article(nw)
|
||||
|
||||
else:
|
||||
logging.warning(
|
||||
T("%s@%s: Received unknown status code %s for article %s"),
|
||||
nw.thrdnum,
|
||||
nw.server.host,
|
||||
nw.status_code,
|
||||
article.article,
|
||||
)
|
||||
done = True
|
||||
nw.reset_data_buffer()
|
||||
|
||||
if done:
|
||||
# Successful data, clear "bad" counter
|
||||
server.bad_cons = 0
|
||||
@@ -975,16 +969,9 @@ class Downloader(Thread):
|
||||
|
||||
def __request_article(self, nw: NewsWrapper):
|
||||
try:
|
||||
nzo = nw.article.nzf.nzo
|
||||
if nw.server.send_group and nzo.group != nw.group:
|
||||
group = nzo.group
|
||||
if sabnzbd.LOG_ALL:
|
||||
logging.debug("Thread %s@%s: GROUP <%s>", nw.thrdnum, nw.server.host, group)
|
||||
nw.send_group(group)
|
||||
else:
|
||||
if sabnzbd.LOG_ALL:
|
||||
logging.debug("Thread %s@%s: BODY %s", nw.thrdnum, nw.server.host, nw.article.article)
|
||||
nw.body()
|
||||
if sabnzbd.LOG_ALL:
|
||||
logging.debug("Thread %s@%s: BODY %s", nw.thrdnum, nw.server.host, nw.article.article)
|
||||
nw.body()
|
||||
# Mark as ready to be read
|
||||
self.add_socket(nw.nntp.fileno, nw)
|
||||
except socket.error as err:
|
||||
|
||||
@@ -50,7 +50,6 @@ from sabnzbd.misc import (
|
||||
is_lan_addr,
|
||||
is_local_addr,
|
||||
is_loopback_addr,
|
||||
helpful_warning,
|
||||
recursive_html_escape,
|
||||
is_none,
|
||||
get_cpu_name,
|
||||
@@ -81,7 +80,7 @@ from sabnzbd.constants import (
|
||||
GUESSIT_SORT_TYPES,
|
||||
VALID_NZB_FILES,
|
||||
VALID_ARCHIVES,
|
||||
DEF_TIMEOUT,
|
||||
DEF_TEST_TIMEOUT,
|
||||
)
|
||||
from sabnzbd.lang import list_languages
|
||||
from sabnzbd.api import (
|
||||
@@ -1144,7 +1143,7 @@ def handle_server(kwargs, root=None, new_svr=False):
|
||||
kwargs["connections"] = "1"
|
||||
|
||||
if kwargs.get("enable") == "1":
|
||||
if not happyeyeballs(host, int_conv(port), int_conv(kwargs.get("timeout"), default=DEF_TIMEOUT)):
|
||||
if not happyeyeballs(host, int_conv(port), int_conv(kwargs.get("timeout"), default=DEF_TEST_TIMEOUT)):
|
||||
return badParameterResponse(T('Server address "%s:%s" is not valid.') % (host, port), ajax)
|
||||
|
||||
# Default server name is just the host name
|
||||
@@ -1162,7 +1161,7 @@ def handle_server(kwargs, root=None, new_svr=False):
|
||||
if new_svr:
|
||||
server = unique_svr_name(server)
|
||||
|
||||
for kw in ("ssl", "send_group", "enable", "required", "optional"):
|
||||
for kw in ("ssl", "enable", "required", "optional"):
|
||||
if kw not in kwargs.keys():
|
||||
kwargs[kw] = None
|
||||
if svr and not new_svr:
|
||||
|
||||
@@ -482,7 +482,7 @@ def check_latest_version():
|
||||
# Fetch version info
|
||||
data = get_from_url("https://sabnzbd.org/latest.txt")
|
||||
if not data:
|
||||
logging.info("Cannot retrieve version information from GitHub.com")
|
||||
logging.info("Cannot retrieve version information from sabnzbd.org")
|
||||
logging.debug("Traceback: ", exc_info=True)
|
||||
return
|
||||
|
||||
|
||||
@@ -177,13 +177,6 @@ class NewsWrapper:
|
||||
self.nntp.sock.sendall(command)
|
||||
self.reset_data_buffer()
|
||||
|
||||
def send_group(self, group: str):
|
||||
"""Send the NNTP GROUP command"""
|
||||
self.timeout = time.time() + self.server.timeout
|
||||
command = utob("GROUP %s\r\n" % group)
|
||||
self.nntp.sock.sendall(command)
|
||||
self.reset_data_buffer()
|
||||
|
||||
def recv_chunk(self) -> Tuple[int, bool]:
|
||||
"""Receive data, return #bytes, done, skip"""
|
||||
# Resize the buffer in the extremely unlikely case that it got full
|
||||
@@ -390,7 +383,7 @@ class NNTP:
|
||||
|
||||
# Ignore if the socket was already closed, resulting in errors
|
||||
if not self.closed:
|
||||
msg = "Failed to connect: %s %s@%s:%s (%s)" % (
|
||||
msg = T("Failed to connect: %s %s@%s:%s (%s)") % (
|
||||
str(error),
|
||||
self.nw.thrdnum,
|
||||
self.nw.server.host,
|
||||
|
||||
@@ -23,6 +23,7 @@ sabnzbd.notifier - Send notifications to any notification services
|
||||
|
||||
import os.path
|
||||
import logging
|
||||
import platform
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import http.client
|
||||
@@ -34,7 +35,7 @@ import sabnzbd
|
||||
import sabnzbd.cfg
|
||||
from sabnzbd.encoding import utob
|
||||
from sabnzbd.filesystem import make_script_path
|
||||
from sabnzbd.misc import build_and_run_command
|
||||
from sabnzbd.misc import build_and_run_command, int_conv
|
||||
from sabnzbd.newsunpack import create_env
|
||||
|
||||
if sabnzbd.WIN32:
|
||||
@@ -42,11 +43,15 @@ if sabnzbd.WIN32:
|
||||
from win32comext.shell import shell
|
||||
from windows_toasts import InteractableWindowsToaster, Toast, ToastActivatedEventArgs, ToastButton
|
||||
|
||||
# Only Windows 10 and above are supported
|
||||
if int_conv(platform.release()) < 10:
|
||||
raise OSError
|
||||
|
||||
# Set a custom AUMID to display the right icon, it is written to the registry by the installer
|
||||
shell.SetCurrentProcessExplicitAppUserModelID("SABnzbd")
|
||||
_HAVE_WINDOWS_TOASTER = True
|
||||
except:
|
||||
# Only supported on Windows 10 and above
|
||||
# Sending toasts on non-supported platforms results in segfaults
|
||||
_HAVE_WINDOWS_TOASTER = False
|
||||
|
||||
try:
|
||||
@@ -210,6 +215,7 @@ def send_notify_osd(title, message):
|
||||
def send_notification_center(title: str, msg: str, notification_type: str, actions: Optional[Dict[str, str]] = None):
|
||||
"""Send message to macOS Notification Center.
|
||||
Only 1 button is possible on macOS!"""
|
||||
logging.debug("Sending macOS notification")
|
||||
try:
|
||||
subtitle = T(NOTIFICATION_TYPES.get(notification_type, "other"))
|
||||
button_text = button_action = None
|
||||
@@ -228,7 +234,7 @@ def send_notification_center(title: str, msg: str, notification_type: str, actio
|
||||
|
||||
def send_prowl(title, msg, notification_type, force=False, test=None):
|
||||
"""Send message to Prowl"""
|
||||
|
||||
logging.debug("Sending Prowl notification")
|
||||
if test:
|
||||
apikey = test.get("prowl_apikey")
|
||||
else:
|
||||
@@ -261,7 +267,7 @@ def send_prowl(title, msg, notification_type, force=False, test=None):
|
||||
|
||||
def send_pushover(title, msg, notification_type, force=False, test=None):
|
||||
"""Send message to pushover"""
|
||||
|
||||
logging.debug("Sending Pushover notification")
|
||||
if test:
|
||||
apikey = test.get("pushover_token")
|
||||
userkey = test.get("pushover_userkey")
|
||||
@@ -328,7 +334,7 @@ def do_send_pushover(body):
|
||||
|
||||
def send_pushbullet(title, msg, notification_type, force=False, test=None):
|
||||
"""Send message to Pushbullet"""
|
||||
|
||||
logging.debug("Sending Pushbullet notification")
|
||||
if test:
|
||||
apikey = test.get("pushbullet_apikey")
|
||||
device = test.get("pushbullet_device")
|
||||
@@ -363,6 +369,7 @@ def send_pushbullet(title, msg, notification_type, force=False, test=None):
|
||||
|
||||
def send_nscript(title, msg, notification_type, force=False, test=None):
|
||||
"""Run user's notification script"""
|
||||
logging.debug("Sending notification script notification")
|
||||
if test:
|
||||
script = test.get("nscript_script")
|
||||
env_params = {"notification_parameters": test.get("nscript_parameters")}
|
||||
@@ -411,6 +418,7 @@ def send_windows(title: str, msg: str, notification_type: str, actions: Optional
|
||||
if sabnzbd.WIN_SERVICE:
|
||||
return None
|
||||
|
||||
logging.debug("Sending Windows notification")
|
||||
try:
|
||||
if _HAVE_WINDOWS_TOASTER:
|
||||
notification_sender = InteractableWindowsToaster("SABnzbd", notifierAUMID="SABnzbd")
|
||||
|
||||
@@ -1884,11 +1884,15 @@ class NzbObject(TryList):
|
||||
return None, None, None
|
||||
|
||||
# Only a subset we want to apply directly to the NZO
|
||||
for attrib in ("final_name", "priority", "password", "url"):
|
||||
for attrib in ("final_name", "priority", "url"):
|
||||
# Only set if it is present and has a value
|
||||
if attribs.get(attrib):
|
||||
setattr(self, attrib, attribs[attrib])
|
||||
|
||||
# Only set password if it wasn't already set
|
||||
if not self.password and attribs.get("password"):
|
||||
self.password = attribs["password"]
|
||||
|
||||
# Rest is to be used directly in the NZO-init flow
|
||||
return attribs["cat"], attribs["pp"], attribs["script"]
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ def osx_shutdown():
|
||||
def osx_standby():
|
||||
"""Make macOS system sleep, returns after wakeup"""
|
||||
try:
|
||||
subprocess.call(["osascript", "-e", 'tell app "System Events" to sleep'])
|
||||
subprocess.call(["pmset", "sleepnow"])
|
||||
time.sleep(10)
|
||||
except:
|
||||
logging.error(T("Failed to standby system"))
|
||||
|
||||
@@ -567,8 +567,6 @@ SKIN_TEXT = {
|
||||
"button-clrServer": TT("Clear Counters"), #: Button: Clear server's byte counters
|
||||
"srv-testing": TT("Testing server details..."),
|
||||
"srv-bandwidth": TT("Bandwidth"),
|
||||
"srv-send_group": TT("Send Group"),
|
||||
"srv-explain-send_group": TT("Send group command before requesting articles."),
|
||||
"srv-notes": TT("Personal notes"),
|
||||
"srv-article-availability": TT("Article availability"),
|
||||
"srv-articles-tried": TT(
|
||||
|
||||
@@ -29,7 +29,7 @@ def generate_key(key_size=2048, output_file="key.pem"):
|
||||
private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
# encryption_algorithm=serialization.BestAvailableEncryption(b"passphrase")
|
||||
)
|
||||
)
|
||||
|
||||
@@ -241,7 +241,6 @@ kDNSServiceInterfaceIndexLocalOnly = -1
|
||||
|
||||
|
||||
class BonjourError(Exception):
|
||||
|
||||
"""
|
||||
|
||||
Exception representing an error returned by the DNS-SD library.
|
||||
@@ -321,7 +320,6 @@ _DNSServiceErrorType = ctypes.c_int32
|
||||
|
||||
|
||||
class DNSRecordRef(ctypes.c_void_p):
|
||||
|
||||
"""
|
||||
|
||||
A DNSRecordRef pointer. DO NOT CREATE INSTANCES OF THIS CLASS!
|
||||
@@ -367,7 +365,6 @@ class _DNSRecordRef_or_null(DNSRecordRef):
|
||||
|
||||
|
||||
class DNSServiceRef(DNSRecordRef):
|
||||
|
||||
"""
|
||||
|
||||
A DNSServiceRef pointer. DO NOT CREATE INSTANCES OF THIS CLASS!
|
||||
@@ -1740,7 +1737,6 @@ def DNSServiceConstructFullName(service=None, regtype=_NO_DEFAULT, domain=_NO_DE
|
||||
|
||||
|
||||
class TXTRecord(object):
|
||||
|
||||
"""
|
||||
|
||||
A mapping representing a DNS TXT record. The TXT record's
|
||||
|
||||
@@ -20,9 +20,8 @@ sabnzbd.utils.servertests - Debugging server connections. Currently only NNTP se
|
||||
"""
|
||||
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from sabnzbd.constants import DEF_TIMEOUT
|
||||
from sabnzbd.constants import DEF_TEST_TIMEOUT
|
||||
from sabnzbd.newswrapper import NewsWrapper, NNTPPermanentError
|
||||
from sabnzbd.downloader import Server, clues_login, clues_too_many
|
||||
from sabnzbd.config import get_servers
|
||||
@@ -37,7 +36,7 @@ def test_nntp_server_dict(kwargs):
|
||||
password = kwargs.get("password", "").strip()
|
||||
server = kwargs.get("server", "").strip()
|
||||
connections = int_conv(kwargs.get("connections", 0))
|
||||
timeout = int_conv(kwargs.get("timeout", DEF_TIMEOUT))
|
||||
timeout = int_conv(kwargs.get("timeout", DEF_TEST_TIMEOUT))
|
||||
ssl = int_conv(kwargs.get("ssl", 0))
|
||||
ssl_verify = int_conv(kwargs.get("ssl_verify", 1))
|
||||
ssl_ciphers = kwargs.get("ssl_ciphers", "").strip()
|
||||
@@ -56,7 +55,7 @@ def test_nntp_server_dict(kwargs):
|
||||
|
||||
if not timeout:
|
||||
# Lower value during new server testing
|
||||
timeout = 10
|
||||
timeout = DEF_TEST_TIMEOUT
|
||||
|
||||
if "*" in password and not password.strip("*"):
|
||||
# If the password is masked, try retrieving it from the config
|
||||
@@ -78,7 +77,6 @@ def test_nntp_server_dict(kwargs):
|
||||
use_ssl=ssl,
|
||||
ssl_verify=ssl_verify,
|
||||
ssl_ciphers=ssl_ciphers,
|
||||
send_group=False,
|
||||
username=username,
|
||||
password=password,
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
# You MUST use double quotes (so " and not ')
|
||||
# Do not forget to update the appdata file for every major release!
|
||||
|
||||
__version__ = "4.2.2RC1"
|
||||
__version__ = "4.2.3RC3"
|
||||
__baseline__ = "unknown"
|
||||
|
||||
@@ -769,7 +769,10 @@ class TestQueueApi(ApiTestFunctions):
|
||||
("my_scripted_script_.py", True, True),
|
||||
("유닉스.py", True, True),
|
||||
pytest.param(
|
||||
"유닉스.sh", True, True, marks=pytest.mark.skipif(sys.platform.startswith("win"), reason="Not for Windows")
|
||||
"유닉스.sh",
|
||||
True,
|
||||
True,
|
||||
marks=pytest.mark.skipif(sys.platform.startswith("win"), reason="Not for Windows"),
|
||||
),
|
||||
pytest.param(
|
||||
"لغة برمجة نصية",
|
||||
|
||||
@@ -150,11 +150,11 @@ class TestPostProc:
|
||||
"order": 0,
|
||||
"pp": None,
|
||||
"script": None,
|
||||
"dir": os.path.join(
|
||||
SAB_CACHE_DIR, ("category_dir_for_" + category + ("*" if not has_jobdir else ""))
|
||||
)
|
||||
if has_catdir
|
||||
else None,
|
||||
"dir": (
|
||||
os.path.join(SAB_CACHE_DIR, ("category_dir_for_" + category + ("*" if not has_jobdir else "")))
|
||||
if has_catdir
|
||||
else None
|
||||
),
|
||||
"newzbin": "",
|
||||
"priority": 0,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user