Compare commits

...

111 Commits

Author SHA1 Message Date
Safihre
9cbae891d0 WIP on release313 2024-10-16 10:37:53 +02:00
Safihre
fb0ef21768 The tests folder was ignored by default by Renovate 2024-10-15 11:55:26 +02:00
Safihre
277679ef53 Add Python 3.13 to CI tests 2024-10-15 10:10:24 +02:00
Safihre
e7e47bbcb0 Do not compare articles just based on article-ID
Turns out that there are NZBs that contain duplicate article-ID's within 1 file. This causes all "article in nzf.article" comparisons to return the wrong comparison.
2024-10-15 09:44:04 +02:00
Safihre
65ffb5ca81 All resets of try lists should be locked fully 2024-10-15 09:43:55 +02:00
SABnzbd Automation
6cf308e441 Update translatable texts
[skip ci]
2024-10-14 01:47:23 +00:00
renovate[bot]
870fa40c91 Update all dependencies 2024-10-14 01:46:42 +00:00
Safihre
39d9eaec2a Create new history database in case of no such table error 2024-10-08 21:06:59 +02:00
SABnzbd Automation
6fd4d0882c Update translatable texts
[skip ci]
2024-10-08 07:39:00 +00:00
Safihre
32591f7c46 Update text files for 4.4.0Alpha2 2024-10-08 09:38:01 +02:00
SABnzbd Automation
6b47d1126d Update translatable texts
[skip ci]
2024-10-07 00:23:20 +00:00
renovate[bot]
53df39dd12 Update dependency pywin32 to v307 2024-10-07 00:22:35 +00:00
Safihre
b5d33fc17c Mount interface again on both / and url_base
Closes #2936
2024-10-05 20:58:45 +02:00
SABnzbd Automation
8a517b668e Update translatable texts
[skip ci]
2024-10-01 06:27:56 +00:00
thezoggy
67135ba4c8 Update 7zip and multipar (#2942)
* Update 7zip to 24.08

* Update multipar to 1.3.3.3
2024-10-01 08:27:12 +02:00
renovate[bot]
c9efda1889 Update all dependencies 2024-09-30 00:44:59 +00:00
Safihre
250869c242 Force Selenium browser_version to 127
Closes #2932
2024-09-26 22:41:20 +02:00
SABnzbd Automation
844650e6be Update translatable texts
[skip ci]
2024-09-26 20:27:25 +00:00
Safihre
6685c72894 Saving Specials page would result in restart even for no changes
Relates to #2932
2024-09-26 22:26:39 +02:00
renovate[bot]
154a5e4989 Update all dependencies (#2937)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 07:34:11 +02:00
SABnzbd Automation
93d302c9d7 Update translatable texts
[skip ci]
2024-09-20 08:11:43 +00:00
Safihre
f664df7f05 Update text files for 4.4.1Alpha1 2024-09-20 10:10:55 +02:00
thezoggy
8fbf50292b Add additional server priority colors (#2934) 2024-09-17 20:27:22 +02:00
Safihre
f3fed43022 Remove logging line for scheduled history purge 2024-09-17 15:06:56 +02:00
Safihre
2d323ba18c Update to Python 3.12.6 and drop macOS <10.13 support
Closes #2784
2024-09-16 13:59:38 +02:00
Safihre
1ec30a56e1 Improve logging of server address retrieval 2024-09-16 13:58:57 +02:00
renovate[bot]
b98f3a07dd Update all dependencies (#2933)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-16 13:58:12 +02:00
Safihre
46170ffb3d Reset article queue only when really stopping the server
Closes #2866
2024-09-10 20:25:56 +03:00
renovate[bot]
5e8b41be5a Update all dependencies (#2930)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-10 20:13:14 +03:00
SABnzbd Automation
47a2d5387d Update translatable texts
[skip ci]
2024-09-10 07:57:26 +00:00
Safihre
1e61239933 Add Bootstrap tooltips to any elements with title 2024-09-10 10:55:06 +03:00
Safihre
aedbf35be8 Show file being moved during Moving phase 2024-09-07 11:10:58 +03:00
Safihre
cf9540842b Config restart would always try to build URL instead of using current
#2835
2024-09-07 10:51:11 +03:00
Safihre
9205b9161b Update Linux test runners 2024-09-05 12:43:31 +03:00
Safihre
07b64b4abb Update tests for change in webpage mounting 2024-09-04 12:09:07 +03:00
Safihre
c56145e424 Only mount webpages on /, no longer on /sabnzbd
Closes #2929
2024-09-04 11:39:38 +03:00
Safihre
ef11aba166 Add basic system to do trigger one-time config conversions 2024-09-04 11:39:37 +03:00
SABnzbd Automation
fcf03e9a59 Update translatable texts
[skip ci]
2024-09-03 09:06:42 +00:00
renovate[bot]
6662065bb1 Update all dependencies (#2928)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 12:05:52 +03:00
SABnzbd Automation
4973672892 Update translatable texts
[skip ci]
2024-08-30 12:47:50 +00:00
bt90
efa73a52e1 Add doctype to login page (#2927) 2024-08-30 14:47:02 +02:00
SABnzbd Automation
82098a6228 Update translatable texts
[skip ci]
2024-08-26 02:47:15 +00:00
renovate[bot]
07250aa355 Update all dependencies 2024-08-26 02:46:28 +00:00
Safihre
46caa8b33f Add Docker to CPU label 2024-08-19 20:21:19 +02:00
Safihre
b0564c1bab Small style fixes for the Config 2024-08-19 11:42:12 +02:00
SABnzbd Automation
3c8a85ff35 Update translatable texts
[skip ci]
2024-08-19 09:28:34 +00:00
Safihre
656c329912 Update version to 4.4.0-develop 2024-08-19 11:27:46 +02:00
SABnzbd Automation
983253908c Update translatable texts
[skip ci]
2024-08-19 01:20:50 +00:00
renovate[bot]
cef0eeb25b Update all dependencies 2024-08-19 01:20:10 +00:00
Safihre
a9eace759f increase_bad_articles_counter should be called before register_article 2024-08-16 22:23:14 +02:00
Safihre
ad0e7bf5df Prevent excessive newswrapper data buffer size
Closes #2895
2024-08-16 17:25:57 +02:00
Safihre
bea348232a Do not use article_queue when resetting newswrapper
Relates to #2866
2024-08-16 16:50:28 +02:00
jcfp
1519dbc554 Remove pyfakefs workarounds and put its new apply_umask option to good use (#2922) 2024-08-16 15:44:52 +02:00
SABnzbd Automation
297455cd35 Update translatable texts
[skip ci]
2024-08-12 02:04:47 +00:00
renovate[bot]
56b68024db Update all dependencies 2024-08-12 02:04:06 +00:00
SABnzbd Automation
09aa09a55b Update translatable texts
[skip ci]
2024-08-09 17:46:39 +00:00
Sander
f1d134fe2e Deobfuscate subtitles (#2903)
* deobfuscate_subtitles

* deobfuscate_subtitles: unit test aka pytest

* deobfuscate_subtitles: unit test aka pytest

* deobfuscate_subtitles: no reanem is first part of filename is the samen

* deobfuscate_subtitles: no reanem is first part of filename is the samen

* deobfuscate_subtitles: no reanem is first part of filename is the samen

* deobfuscate_subtitles: more structured unit test method

* deobfuscate_subtitles: back to basic testing method

* deobfuscate_subtitles: cleanup

* deobfuscate_subtitles: cleanup

* deobfuscate_filenames.test_first_file_is_much_bigger() improved

* deobfuscate_subtitles(): checks on biggest file and srt files. input can be directory or filelist.

* rename to clearly_one_biggest_file()

* WIP on develop

* accept work by safihre

* do nothing when not one_file_is_biggest

* a lot of cleanup, also with help of the walrus

* a lot of cleanup, also with help of the walrus

* fix typo's in test_deobfuscate_filenames.py

* Update sabnzbd/postproc.py

Co-authored-by: Safihre <safihre@sabnzbd.org>

* handle review comments

* handle review comments

* remove import glob

* remove special underscore support. Add srt deob info into GUI-history

---------

Co-authored-by: sander <san.d.erjonkers+github@gmail.com>
Co-authored-by: Safihre <safihre@sabnzbd.org>
2024-08-09 19:45:57 +02:00
Safihre
621d586c2f Reduce timeouts during happyeyeballs and others 2024-08-09 10:01:21 +02:00
Safihre
4966f9c753 Update sabctools to 8.2.5 2024-08-09 09:42:57 +02:00
SABnzbd Automation
059d82f6f0 Update translatable texts
[skip ci]
2024-08-05 00:18:07 +00:00
renovate[bot]
bca41db6b7 Update all dependencies 2024-08-05 00:17:22 +00:00
renovate[bot]
613ba0b05f Update dependency setuptools to v72 2024-07-29 03:45:46 +00:00
renovate[bot]
5f3b03ed87 Update all dependencies 2024-07-29 02:12:43 +00:00
SABnzbd Automation
f6fe801000 Update translatable texts
[skip ci]
2024-07-28 16:31:44 +00:00
bt90
8ff34660d8 Add autocomplete attributes (#2916) 2024-07-28 10:31:03 -06:00
renovate[bot]
0c1b8dd60a Update all dependencies 2024-07-24 23:08:07 +00:00
SABnzbd Automation
8e8ee7a3ab Update translatable texts
[skip ci]
2024-07-24 22:11:38 +00:00
Safihre
9145a90e33 Update test_cert_gen for new cryptography default 2024-07-24 16:10:58 -06:00
Safihre
02b4a116dd Update AppData for 4.3.3 2024-07-16 00:06:21 +02:00
SABnzbd Automation
e504b288a2 Update translatable texts
[skip ci]
2024-07-15 22:05:36 +00:00
Safihre
5128f788f0 Update text files for 4.3.3Beta2 2024-07-16 00:02:42 +02:00
Sander
044fe7a26a fix when no connection (for example IPv6-test on IPv4-only connection) (#2908)
* fix when no connection (for example IPv6-test on IPv4-only connection)

* fix when no connection (for example IPv6-test on IPv4-only connection)

* make black happy ... hopefully

* make black happy ... hopefully ... linelength 120
2024-07-15 07:27:43 +02:00
renovate[bot]
4ed2565101 Update all dependencies 2024-07-15 03:56:19 +00:00
SABnzbd Automation
abbd77bac4 Update translatable texts
[skip ci]
2024-07-14 21:24:11 +00:00
Safihre
38c9a52e1d Add changes to web_host and web_port to ignored revs 2024-07-14 23:23:26 +02:00
Safihre
f89114ca7e Rename cherryhost and cherryport to web_host and web_port 2024-07-14 23:23:01 +02:00
Safihre
773d567eed Skip propagation delay calculation if delay is set to 0
Since OptionNumber doesn't allow empty values
2024-07-14 23:19:05 +02:00
Safihre
ee717b679e Set limits to number-input types that were missing them 2024-07-13 19:39:53 +02:00
Safihre
f50810fb58 Remove article from TryList when resetting article Queue
When we reset the Server's Article-queue, we should retry that article again on that server.
2024-07-10 20:48:06 +02:00
jcfp
08b1b20b34 fix filesystem permission test for pyfakefs 5.4.0+ (#2905) 2024-07-10 16:16:18 +02:00
SABnzbd Automation
edca79af83 Update translatable texts
[skip ci]
2024-07-08 13:11:04 +00:00
Safihre
dd5dcd0ec9 Update text files for 4.3.3Beta1 2024-07-08 15:10:22 +02:00
Safihre
820824e443 Disable failing test until #2883 is fixed 2024-07-08 13:47:01 +02:00
Safihre
4c2dfdee43 Correctly handle the difference between no category and Default
Closes #2902
2024-07-08 13:47:00 +02:00
Safihre
ece4437c3a General changes to sanitization functions 2024-07-08 13:46:59 +02:00
renovate[bot]
74daa15ce4 Update dependency certifi to v2024.7.4 (#2904)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-08 08:04:01 +02:00
renovate[bot]
4f81bc8a26 Update all dependencies (#2901)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-02 22:10:07 +02:00
jcfp
e77d15f75e don't bother looking up a public address if the local ipv6 is link-local (#2899) 2024-06-29 10:16:12 +02:00
Sander
8668852574 make WIN64 also true if on ARM64 (#2893)
Co-authored-by: sander <san.d.erjonkers+github@gmail.com>
2024-06-25 07:03:18 +02:00
renovate[bot]
7e944f393e Update winrt dependencies to v2.1.0 (#2891)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-24 15:29:58 +02:00
renovate[bot]
1646fbfd17 Update all dependencies (#2890)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-24 06:52:59 +02:00
Safihre
72b0521325 Correct crash in Notifications page
Closes #2887
2024-06-19 21:11:03 +02:00
renovate[bot]
8aa53fd43f Update all dependencies (#2882)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-17 14:22:03 +02:00
Safihre
aa67edb2d9 Fix macOS CI run by pinning older Python 3.12.3 version 2024-06-17 09:25:51 +02:00
SABnzbd Automation
0054b17f41 Update translatable texts
[skip ci]
2024-06-17 07:03:09 +00:00
Safihre
2af2cc7370 Add file version information to Windows installer
#2870
2024-06-17 09:02:29 +02:00
renovate[bot]
5aa7aafebb Update all dependencies 2024-06-10 01:14:35 +00:00
Safihre
3bd0f7c1e0 Experiment to improve idle job detection
No longer rely on just length of arrays, but use fast set-comparisons.
2024-06-09 22:23:55 +02:00
Safihre
9c8d21f6db Remove warning about AppRise
Closes #2875
2024-06-09 21:22:52 +02:00
SABnzbd Automation
4947effeb7 Update translatable texts
[skip ci]
2024-06-09 19:02:22 +00:00
Safihre
b8fd9e6e31 Introduce bool_conv and fix ambiguous JSON retry value in history queue 2024-06-09 21:01:32 +02:00
jcfp
2a02c93e4b Fix checkdir argument handling (#2873)
* fix checkdir argument handling

* housekeeping

* shut up black
2024-06-03 22:03:20 +02:00
SABnzbd Automation
a0ef520e06 Update translatable texts
[skip ci]
2024-06-03 02:05:42 +00:00
renovate[bot]
a9eb32eba6 Update all dependencies 2024-06-03 02:05:01 +00:00
SABnzbd Automation
592ef0e645 Update translatable texts
[skip ci]
2024-05-31 14:13:53 +00:00
Safihre
cce53ee058 Set version to 4.3.2 2024-05-31 16:13:02 +02:00
Safihre
93755aa6d8 Update sabctools to 8.3.3 2024-05-29 16:55:12 +02:00
SABnzbd Automation
b1d42c7c22 Update translatable texts
[skip ci]
2024-05-27 20:00:41 +00:00
Safihre
8286b7b830 Add pkg_resources.extern to hidden imports 2024-05-27 19:59:56 +00:00
renovate[bot]
fbaa3c0420 Update all dependencies 2024-05-27 19:59:56 +00:00
SABnzbd Automation
ba6c30cf24 Update translatable texts
[skip ci]
2024-05-23 08:24:29 +00:00
128 changed files with 1220 additions and 820 deletions

View File

@@ -40,3 +40,4 @@ f06891926661986fff52d6eb4b4cb120c71972d1
9bcbcaefdfecc85aedfd8e2f8aaa1ca7f959404e
433dcab02b29f7bd3827e237434034deecc1b549
9f6a9f991222efccc87b45a701086c95629c67b6
f89114ca7e1b20bf8e645ecd0b52b707ec857aa9

View File

@@ -15,6 +15,7 @@
"builder/release-requirements.txt"
]
},
"ignorePaths": [],
"ignoreDeps": [
"jaraco.text",
"jaraco.context",

View File

@@ -69,8 +69,8 @@ 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.3"
MACOSX_DEPLOYMENT_TARGET: "10.9"
PYTHON_VERSION: "3.12.6"
MACOSX_DEPLOYMENT_TARGET: "10.13"
# We need to force compile for universal2 support
CFLAGS: -arch x86_64 -arch arm64
ARCHFLAGS: -arch x86_64 -arch arm64
@@ -95,14 +95,12 @@ jobs:
- name: Install Python
run: sudo installer -pkg ~/python.pkg -target /
- name: Install Python dependencies
# We have to manually take a few steps:
# 1. We need to build the PyInstaller bootloader:
# https://github.com/pyinstaller/pyinstaller/issues/6235
# We have to manually compile some modules as they don't automatically fetch universal2 binaries
run: |
python3 --version
pip3 install --upgrade pip wheel
pip3 install --upgrade -r requirements.txt --no-binary cffi,CT3,PyYAML,charset_normalizer --no-dependencies
PYINSTALLER_COMPILE_BOOTLOADER=1 pip3 install --upgrade -r builder/requirements.txt --no-binary pyinstaller --no-dependencies
pip3 install --upgrade -r builder/requirements.txt --no-dependencies
- name: Import macOS codesign certificates
# Taken from https://github.com/Apple-Actions/import-codesign-certs/pull/27 (comments)
env:

View File

@@ -31,18 +31,18 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-architecture: ["x64"]
name: ["Linux"]
os: [ubuntu-20.04]
os: [ubuntu-latest]
include:
- name: macOS
os: macos-latest
python-version: "3.12"
python-version: "3.13"
python-architecture: "x64"
- name: Windows
os: windows-latest
python-version: "3.12"
python-version: "3.13"
python-architecture: "x64"
- name: Windows (32bit)
os: windows-latest
@@ -60,7 +60,7 @@ jobs:
cache-dependency-path: "**/requirements.txt"
- name: Install system dependencies
if: runner.os == 'Linux'
run: sudo apt-get install unrar p7zip-full par2
run: sudo apt-get install unrar 7zip par2
- name: Install Python dependencies
run: |
python --version

View File

@@ -1,48 +1,17 @@
Release Notes - SABnzbd 4.3.2 Release Candidate 2
Release Notes - SABnzbd 4.4.0 Alpha 2
=========================================================
This is the second bug fix release of SABnzbd 4.3.0.
This is the first test release of SABnzbd 4.4.0.
## Bug fixes and changes since 4.3.1
## New features since 4.3.0
* Added Special option `disable_archive` for jobs to always be permanently deleted.
* Specific AppRise notifications could fail to send.
* Update of the article decoder core (`rapidyenc`).
* Windows: After some time the interface would no longer load.
* Windows: Custom shortcuts would be removed by the installer.
* Windows/macOS: Updated Unrar to 7.01 and 7zip to 24.05.
* Subtitle files will be deobfuscated if required.
* macOS: Dropped support for macOS 10.12 and below.
## Key changes since 4.2.0
## Bug fixes since 4.3.0
* **Archive:**
* When jobs are removed from the History, they are moved to the Archive.
* Keep your History clean and still be able to reliably use Duplicate Detection.
* **Apprise Integrated:**
* Send notifications using Apprise to almost any notification service.
* Supported notifications: https://github.com/caronc/apprise/wiki
* Notification Script `SABnzbd-notify.py` is no longer needed.
* **Added IPv6-staging option:**
* Enable `ipv6_staging` in Config - Specials to get additional IPv6 features:
* Add IPv6 hostnames during address selection.
* Internet Bandwidth is measured separately for IPv4 and IPv6.
* **Other:**
* The `text` output format is removed from the API, `json` is the default.
* Handling of multiple inputs to several API methods was improved.
* File browser dialog is available to select file paths in the Config.
* Users will be warned if they configure an Indexer as a Server.
* Added `SAB_API_KEY` and `SAB_API_URL` to script environment variables.
* Windows/macOS: Updated Python to 3.12.3, Multipar to v1.3.3.2,
Unrar to 7.00 and 7zip to 24.03.
## Bug fixes since 4.2.0
* Incorrect warnings of unknown status codes could be thrown.
* Watched Folder would not work if Socks5 proxy was active.
* Prevent crash on invalid Server Expiration Date.
* Windows: Installer could create duplicate shortcuts.
* Toggling of Servers could result in jobs being stuck at 99%.
* Config restart would always determine redirect URL instead of using current.
## Upgrade notices

View File

@@ -65,7 +65,7 @@ import sabnzbd
import sabnzbd.lang
import sabnzbd.interface
from sabnzbd.constants import (
DEF_TIMEOUT,
DEF_NETWORKING_TIMEOUT,
DEF_LOG_ERRFILE,
DEF_MAIN_TMPL,
DEF_STD_WEB_DIR,
@@ -170,7 +170,8 @@ class GUIHandler(logging.Handler):
# This prevents endless looping if the notification service itself throws an error/warning
# We don't check based on message content, because if it includes a timestamp it's not unique
if not any(
stored_warning["origin"] == warning["origin"] and stored_warning["time"] + DEF_TIMEOUT > time.time()
stored_warning["origin"] == warning["origin"]
and stored_warning["time"] + DEF_NETWORKING_TIMEOUT > time.time()
for stored_warning in self.store
):
if record.levelno == logging.WARNING:
@@ -297,14 +298,14 @@ def daemonize():
os.dup2(f.fileno(), sys.stderr.fileno())
def abort_and_show_error(browserhost, cherryport, err=""):
def abort_and_show_error(browserhost, web_port, err=""):
"""Abort program because of CherryPy troubles"""
logging.error(T("Failed to start web-interface") + " : " + str(err))
if not sabnzbd.DAEMON:
if "49" in err:
panic_host(browserhost, cherryport)
panic_host(browserhost, web_port)
else:
panic_port(browserhost, cherryport)
panic_port(browserhost, web_port)
sabnzbd.halt()
exit_sab(2)
@@ -530,19 +531,19 @@ def check_resolve(host):
return True
def get_webhost(cherryhost, cherryport, https_port):
def get_webhost(web_host, web_port, https_port):
"""Determine the webhost address and port,
return (host, port, browserhost)
"""
if cherryhost == "0.0.0.0" and not check_resolve("127.0.0.1"):
cherryhost = ""
elif cherryhost == "::" and not check_resolve("::1"):
cherryhost = ""
if web_host == "0.0.0.0" and not check_resolve("127.0.0.1"):
web_host = ""
elif web_host == "::" and not check_resolve("::1"):
web_host = ""
if cherryhost is None:
cherryhost = sabnzbd.cfg.cherryhost()
if web_host is None:
web_host = sabnzbd.cfg.web_host()
else:
sabnzbd.cfg.cherryhost.set(cherryhost)
sabnzbd.cfg.web_host.set(web_host)
# Get IP address, but discard APIPA/IPV6
# If only APIPA's or IPV6 are found, fall back to localhost
@@ -554,10 +555,10 @@ def get_webhost(cherryhost, cherryport, https_port):
# Hostname does not resolve
try:
# Valid user defined name?
info = socket.getaddrinfo(cherryhost, None)
info = socket.getaddrinfo(web_host, None)
except socket.error:
if not is_localhost(cherryhost):
cherryhost = "0.0.0.0"
if not is_localhost(web_host):
web_host = "0.0.0.0"
try:
info = socket.getaddrinfo(localhost, None)
except socket.error:
@@ -574,75 +575,75 @@ def get_webhost(cherryhost, cherryport, https_port):
hostip = ip
# A blank host will use the local ip address
if cherryhost == "":
if web_host == "":
if ipv6 and ipv4:
# To protect Firefox users, use numeric IP
cherryhost = hostip
web_host = hostip
browserhost = hostip
else:
cherryhost = socket.gethostname()
browserhost = cherryhost
web_host = socket.gethostname()
browserhost = web_host
# 0.0.0.0 will listen on all ipv4 interfaces (no ipv6 addresses)
elif cherryhost == "0.0.0.0":
elif web_host == "0.0.0.0":
# Just take the gamble for this
cherryhost = "0.0.0.0"
web_host = "0.0.0.0"
browserhost = localhost
# :: will listen on all ipv6 interfaces (no ipv4 addresses)
elif cherryhost in ("::", "[::]"):
cherryhost = cherryhost.strip("[").strip("]")
elif web_host in ("::", "[::]"):
web_host = web_host.strip("[").strip("]")
# Assume '::1' == 'localhost'
browserhost = localhost
# IPV6 address
elif "[" in cherryhost or ":" in cherryhost:
browserhost = cherryhost
elif "[" in web_host or ":" in web_host:
browserhost = web_host
# IPV6 numeric address
elif cherryhost.replace(".", "").isdigit():
elif web_host.replace(".", "").isdigit():
# IPV4 numerical
browserhost = cherryhost
browserhost = web_host
elif cherryhost == localhost:
cherryhost = localhost
elif web_host == localhost:
web_host = localhost
browserhost = localhost
else:
# If on APIPA, use numerical IP, to help FireFoxers
if ipv6 and ipv4:
cherryhost = hostip
browserhost = cherryhost
web_host = hostip
browserhost = web_host
# Some systems don't like brackets in numerical ipv6
if sabnzbd.MACOS:
cherryhost = cherryhost.strip("[]")
web_host = web_host.strip("[]")
else:
try:
socket.getaddrinfo(cherryhost, None)
socket.getaddrinfo(web_host, None)
except socket.error:
cherryhost = cherryhost.strip("[]")
web_host = web_host.strip("[]")
if ipv6 and ipv4 and cherryhost == "" and sabnzbd.WIN32:
if ipv6 and ipv4 and web_host == "" and sabnzbd.WIN32:
helpful_warning(T("Please be aware the 0.0.0.0 hostname will need an IPv6 address for external access"))
if cherryhost == "localhost" and not sabnzbd.WIN32 and not sabnzbd.MACOS:
if web_host == "localhost" and not sabnzbd.WIN32 and not sabnzbd.MACOS:
# On the Ubuntu family, localhost leads to problems for CherryPy
ips = ip_extract()
if "127.0.0.1" in ips and "::1" in ips:
cherryhost = "127.0.0.1"
web_host = "127.0.0.1"
if ips[0] != "127.0.0.1":
browserhost = "127.0.0.1"
# This is to please Chrome on macOS
if cherryhost == "localhost" and sabnzbd.MACOS:
cherryhost = "127.0.0.1"
if web_host == "localhost" and sabnzbd.MACOS:
web_host = "127.0.0.1"
browserhost = "localhost"
if cherryport is None:
cherryport = sabnzbd.cfg.cherryport.get_int()
if web_port is None:
web_port = sabnzbd.cfg.web_port.get_int()
else:
sabnzbd.cfg.cherryport.set(str(cherryport))
sabnzbd.cfg.web_port.set(str(web_port))
if https_port is None:
https_port = sabnzbd.cfg.https_port.get_int()
@@ -651,12 +652,12 @@ def get_webhost(cherryhost, cherryport, https_port):
# if the https port was specified, assume they want HTTPS enabling also
sabnzbd.cfg.enable_https.set(True)
if cherryport == https_port and sabnzbd.cfg.enable_https():
if web_port == https_port and sabnzbd.cfg.enable_https():
sabnzbd.cfg.enable_https.set(False)
# Should have a translated message, but that's not available yet
logging.error(T("HTTP and HTTPS ports cannot be the same"))
return cherryhost, cherryport, browserhost, https_port
return web_host, web_port, browserhost, https_port
def attach_server(host, port, cert=None, key=None, chain=None):
@@ -841,8 +842,8 @@ def main():
fork = False
pause = False
inifile = None
cherryhost = None
cherryport = None
web_host = None
web_port = None
https_port = None
cherrypylogging = None
clean_up = False
@@ -880,14 +881,11 @@ def main():
elif opt in ("-t", "--templates"):
web_dir = arg
elif opt in ("-s", "--server"):
(cherryhost, cherryport) = split_host(arg)
(web_host, web_port) = split_host(arg)
elif opt in ("-n", "--nobrowser"):
autobrowser = False
elif opt in ("-b", "--browser"):
try:
autobrowser = bool(int(arg))
except ValueError:
autobrowser = True
autobrowser = sabnzbd.misc.bool_conv(arg)
elif opt == "--autorestarted":
autorestarted = True
elif opt in ("-c", "--clean"):
@@ -1006,24 +1004,24 @@ def main():
sabnzbd.cfg.ipv6_hosting.set(ipv6_hosting)
# Determine web host address
cherryhost, cherryport, browserhost, https_port = get_webhost(cherryhost, cherryport, https_port)
web_host, web_port, browserhost, https_port = get_webhost(web_host, web_port, https_port)
enable_https = sabnzbd.cfg.enable_https()
# When this is a daemon, just check and bail out if port in use
if sabnzbd.DAEMON:
if enable_https and https_port:
try:
portend.free(cherryhost, https_port, timeout=0.05)
portend.free(web_host, https_port, timeout=0.05)
except IOError:
abort_and_show_error(browserhost, cherryport)
abort_and_show_error(browserhost, web_port)
except:
abort_and_show_error(browserhost, cherryport, "49")
abort_and_show_error(browserhost, web_port, "49")
try:
portend.free(cherryhost, cherryport, timeout=0.05)
portend.free(web_host, web_port, timeout=0.05)
except IOError:
abort_and_show_error(browserhost, cherryport)
abort_and_show_error(browserhost, web_port)
except:
abort_and_show_error(browserhost, cherryport, "49")
abort_and_show_error(browserhost, web_port, "49")
# Windows instance is reachable through registry
url = None
@@ -1034,7 +1032,7 @@ def main():
# SSL
if enable_https:
port = https_port or cherryport
port = https_port or web_port
try:
portend.free(browserhost, port, timeout=0.05)
except IOError as error:
@@ -1046,7 +1044,7 @@ def main():
if new_instance or not check_for_sabnzbd(url, upload_nzbs, autobrowser):
# Bail out if we have fixed our ports after first start-up
if sabnzbd.cfg.fixed_ports():
abort_and_show_error(browserhost, cherryport)
abort_and_show_error(browserhost, web_port)
# Find free port to bind
newport = find_free_port(browserhost, port)
if newport > 0:
@@ -1056,34 +1054,34 @@ def main():
sabnzbd.cfg.https_port.set(newport)
else:
# In case HTTPS == HTTP port
cherryport = newport
sabnzbd.cfg.cherryport.set(newport)
web_port = newport
sabnzbd.cfg.web_port.set(newport)
except:
# Something else wrong, probably badly specified host
abort_and_show_error(browserhost, cherryport, "49")
abort_and_show_error(browserhost, web_port, "49")
# NonSSL check if there's no HTTPS or we only use 1 port
if not (enable_https and not https_port):
try:
portend.free(browserhost, cherryport, timeout=0.05)
portend.free(browserhost, web_port, timeout=0.05)
except IOError as error:
if str(error) == "Port not bound.":
pass
else:
if not url:
url = "http://%s:%s%s/api?" % (browserhost, cherryport, sabnzbd.cfg.url_base())
url = "http://%s:%s%s/api?" % (browserhost, web_port, sabnzbd.cfg.url_base())
if new_instance or not check_for_sabnzbd(url, upload_nzbs, autobrowser):
# Bail out if we have fixed our ports after first start-up
if sabnzbd.cfg.fixed_ports():
abort_and_show_error(browserhost, cherryport)
abort_and_show_error(browserhost, web_port)
# Find free port to bind
port = find_free_port(browserhost, cherryport)
port = find_free_port(browserhost, web_port)
if port > 0:
sabnzbd.cfg.cherryport.set(port)
cherryport = port
sabnzbd.cfg.web_port.set(port)
web_port = port
except:
# Something else wrong, probably badly specified host
abort_and_show_error(browserhost, cherryport, "49")
abort_and_show_error(browserhost, web_port, "49")
# We found a port, now we never check again
sabnzbd.cfg.fixed_ports.set(True)
@@ -1277,29 +1275,29 @@ def main():
# Starting of the webserver
# Determine if this system has multiple definitions for 'localhost'
hosts = all_localhosts()
multilocal = len(hosts) > 1 and cherryhost in ("localhost", "0.0.0.0")
multilocal = len(hosts) > 1 and web_host in ("localhost", "0.0.0.0")
# For 0.0.0.0 CherryPy will always pick IPv4, so make sure the secondary localhost is IPv6
if multilocal and cherryhost == "0.0.0.0" and hosts[1] == "127.0.0.1":
if multilocal and web_host == "0.0.0.0" and hosts[1] == "127.0.0.1":
hosts[1] = "::1"
# The Windows binary requires numeric localhost as primary address
if cherryhost == "localhost":
cherryhost = hosts[0]
if web_host == "localhost":
web_host = hosts[0]
if enable_https:
if https_port:
# Extra HTTP port for primary localhost
attach_server(cherryhost, cherryport)
attach_server(web_host, web_port)
if multilocal:
# Extra HTTP port for secondary localhost
attach_server(hosts[1], cherryport)
attach_server(hosts[1], web_port)
# Extra HTTPS port for secondary localhost
attach_server(hosts[1], https_port, https_cert, https_key, https_chain)
cherryport = https_port
web_port = https_port
elif multilocal:
# Extra HTTPS port for secondary localhost
attach_server(hosts[1], cherryport, https_cert, https_key, https_chain)
attach_server(hosts[1], web_port, https_cert, https_key, https_chain)
cherrypy.config.update(
{
@@ -1311,7 +1309,7 @@ def main():
)
elif multilocal:
# Extra HTTP port for secondary localhost
attach_server(hosts[1], cherryport)
attach_server(hosts[1], web_port)
if no_login:
sabnzbd.cfg.username.set("")
@@ -1334,8 +1332,8 @@ def main():
cherrypy.config.update(
{
"server.environment": "production",
"server.socket_host": cherryhost,
"server.socket_port": cherryport,
"server.socket_host": web_host,
"server.socket_port": web_port,
"server.shutdown_timeout": 0,
"engine.autoreload.on": False,
"tools.encode.on": True,
@@ -1403,7 +1401,7 @@ def main():
# Set authentication for CherryPy
sabnzbd.interface.set_auth(cherrypy.config)
logging.info("Starting web-interface on %s:%s", cherryhost, cherryport)
logging.info("Starting web-interface on %s:%s", web_host, web_port)
sabnzbd.cfg.log_level.callback(guard_loglevel)
@@ -1413,7 +1411,7 @@ def main():
# Since the webserver is started by cherrypy in a separate thread, we can't really catch any
# start-up errors. This try/except only catches very few errors, the rest is only shown in the console.
logging.error(T("Failed to start web-interface: "), exc_info=True)
abort_and_show_error(browserhost, cherryport)
abort_and_show_error(browserhost, web_port)
# Create a record of the active cert/key/chain files, for use with config.create_config_backup()
if enable_https:
@@ -1423,16 +1421,16 @@ def main():
# Set URL for browser
if enable_https:
sabnzbd.BROWSER_URL = "https://%s:%s%s" % (browserhost, cherryport, sabnzbd.cfg.url_base())
sabnzbd.BROWSER_URL = "https://%s:%s%s" % (browserhost, web_port, sabnzbd.cfg.url_base())
else:
sabnzbd.BROWSER_URL = "http://%s:%s%s" % (browserhost, cherryport, sabnzbd.cfg.url_base())
sabnzbd.BROWSER_URL = "http://%s:%s%s" % (browserhost, web_port, sabnzbd.cfg.url_base())
if sabnzbd.WIN32:
# Write URL for uploads and version check directly to registry
set_connection_info(f"{sabnzbd.BROWSER_URL}/api?apikey={sabnzbd.cfg.api_key()}")
if pid_path or pid_file:
sabnzbd.pid_file(pid_path, pid_file, cherryport)
sabnzbd.pid_file(pid_path, pid_file, web_port)
# Stop here in case of fatal errors
if sabnzbd.NO_DOWNLOADING:
@@ -1460,11 +1458,11 @@ def main():
autorestarted = False
# Start SSDP and Bonjour if SABnzbd isn't listening on localhost only
if sabnzbd.cfg.enable_broadcast() and not is_localhost(cherryhost):
if sabnzbd.cfg.enable_broadcast() and not is_localhost(web_host):
# Try to find a LAN IP address for SSDP/Bonjour
if is_lan_addr(cherryhost):
if is_lan_addr(web_host):
# A specific listening address was configured, use that
external_host = cherryhost
external_host = web_host
else:
# Fall back to the IPv4 address of the LAN interface
external_host = local_ipv4()
@@ -1478,13 +1476,13 @@ def main():
(not sabnzbd.cfg.local_ranges()) or any(ip_in_subnet(external_host, r) for r in sabnzbd.cfg.local_ranges())
):
# Start Bonjour and SSDP
sabnzbd.zconfig.set_bonjour(external_host, cherryport)
sabnzbd.zconfig.set_bonjour(external_host, web_port)
# Set URL for browser for external hosts
ssdp_url = "%s://%s:%s%s" % (
("https" if enable_https else "http"),
external_host,
cherryport,
web_port,
sabnzbd.cfg.url_base(),
)
ssdp.start_ssdp(

View File

@@ -1,6 +1,5 @@
# -*- mode: python -*-
import os
import re
import sys
from PyInstaller.building.api import EXE, COLLECT, PYZ
@@ -8,13 +7,13 @@ from PyInstaller.building.build_main import Analysis
from PyInstaller.building.osx import BUNDLE
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
from builder.constants import EXTRA_FILES, EXTRA_FOLDERS, RELEASE_VERSION
from builder.constants import EXTRA_FILES, EXTRA_FOLDERS, RELEASE_VERSION, RELEASE_VERSION_TUPLE
# Add extra files in the PyInstaller-spec
extra_pyinstaller_files = []
# Add hidden imports
extra_hiddenimports = ["Cheetah.DummyTransaction", "cheroot.ssl.builtin", "certifi"]
extra_hiddenimports = ["Cheetah.DummyTransaction", "cheroot.ssl.builtin", "certifi", "pkg_resources.extern"]
extra_hiddenimports.extend(collect_submodules("apprise"))
extra_hiddenimports.extend(collect_submodules("babelfish.converters"))
extra_hiddenimports.extend(collect_submodules("guessit.data"))
@@ -45,16 +44,12 @@ else:
EXTRA_FOLDERS += ["win/multipar/", "win/par2/", "win/unrar/", "win/7zip/"]
EXTRA_FILES += ["portable.cmd"]
# Parse the version info
version_regexed = re.search(r"(\d+)\.(\d+)\.(\d+)([a-zA-Z]*)(\d*)", RELEASE_VERSION)
version_tuple = (int(version_regexed.group(1)), int(version_regexed.group(2)), int(version_regexed.group(3)), 0)
# Detailed instructions are in the PyInstaller documentation
# We don't include the alpha/beta/rc in the counters
version_info = VSVersionInfo(
ffi=FixedFileInfo(
filevers=version_tuple,
prodvers=version_tuple,
filevers=RELEASE_VERSION_TUPLE,
prodvers=RELEASE_VERSION_TUPLE,
mask=0x3F,
flags=0x0,
OS=0x40004,
@@ -170,7 +165,7 @@ if sys.platform == "darwin":
"NSPersistentStoreTypeKey": "Binary",
}
],
"LSMinimumSystemVersion": "10.9",
"LSMinimumSystemVersion": "10.13",
"LSEnvironment": {"LANG": "en_US.UTF-8", "LC_ALL": "en_US.UTF-8"},
}

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import os
import re
# Constants
VERSION_FILE = "sabnzbd/version.py"
@@ -33,6 +34,10 @@ RELEASE_VERSION = __version__
# Pre-releases are longer than 6 characters (e.g. 3.1.0Beta1 vs 3.1.0, but also 3.0.11)
PRERELEASE = len(RELEASE_VERSION) > 5
# Parse the version info for Windows file properties information
version_regexed = re.search(r"(\d+)\.(\d+)\.(\d+)([a-zA-Z]*)(\d*)", RELEASE_VERSION)
RELEASE_VERSION_TUPLE = (int(version_regexed.group(1)), int(version_regexed.group(2)), int(version_regexed.group(3)), 0)
# Define release name
RELEASE_NAME = "SABnzbd-%s" % RELEASE_VERSION
RELEASE_TITLE = "SABnzbd %s" % RELEASE_VERSION

View File

@@ -32,6 +32,7 @@ from typing import List
from constants import (
RELEASE_VERSION,
RELEASE_VERSION_TUPLE,
VERSION_FILE,
RELEASE_README,
RELEASE_NAME,
@@ -258,8 +259,8 @@ if __name__ == "__main__":
[
"makensis.exe",
"/V3",
"/DSAB_PRODUCT=%s" % RELEASE_NAME,
"/DSAB_VERSION=%s" % RELEASE_VERSION,
"/DSAB_VERSIONKEY=%s" % ".".join(map(str, RELEASE_VERSION_TUPLE)),
"/DSAB_FILE=%s" % RELEASE_INSTALLER,
"NSIS_Installer.nsi.tmp",
]

View File

@@ -1,2 +1,2 @@
PyGithub==2.3.0
PyGithub==2.4.0
praw==7.7.1

View File

@@ -1,26 +1,26 @@
# Basic build requirements
# Note that not all sub-dependencies are listed, but only ones we know could cause trouble
pyinstaller==5.13.2
packaging==24.0
pyinstaller-hooks-contrib==2024.6
packaging==24.1
pyinstaller-hooks-contrib==2024.8
altgraph==0.17.4
wrapt==1.16.0
setuptools==69.5.1
setuptools==75.1.0
# Required on 32bit Windows, exclude it based on Python-version
importlib_metadata==7.1.0; python_version < '3.10'
importlib_resources==6.4.0; python_version < '3.10'
zipp==3.18.2; python_version < '3.10'
importlib_metadata==8.5.0; python_version < '3.10'
importlib_resources==6.4.5; python_version < '3.10'
zipp==3.20.2; python_version < '3.10'
# orjson does not support 32bit Windows, also exclude based on Python-version
orjson==3.10.3; python_version > '3.8'
orjson==3.10.7; python_version > '3.8'
# For the Windows build
pefile==2023.2.7; sys_platform == 'win32'
pywin32-ctypes==0.2.2; sys_platform == 'win32'
pefile==2024.8.26; sys_platform == 'win32'
pywin32-ctypes==0.2.3; sys_platform == 'win32'
# For the macOS build
dmgbuild==1.6.1; sys_platform == 'darwin'
dmgbuild==1.6.2; sys_platform == 'darwin'
mac-alias==2.2.2; sys_platform == 'darwin'
macholib==1.16.3; sys_platform == 'darwin'
ds-store==1.3.1; sys_platform == 'darwin'

View File

@@ -68,7 +68,18 @@ Unicode true
;------------------------------------------------------------------
; Define names of the product
Name "${SAB_PRODUCT}"
Name "SABnzbd ${SAB_VERSION}"
VIProductVersion "${SAB_VERSIONKEY}"
VIFileVersion "${SAB_VERSIONKEY}"
VIAddVersionKey "Comments" "SABnzbd ${SAB_VERSION}"
VIAddVersionKey "CompanyName" "The SABnzbd-Team"
VIAddVersionKey "FileDescription" "SABnzbd ${SAB_VERSION}"
VIAddVersionKey "FileVersion" "${SAB_VERSION}"
VIAddVersionKey "LegalCopyright" "The SABnzbd-Team"
VIAddVersionKey "ProductName" "SABnzbd ${SAB_VERSION}"
VIAddVersionKey "ProductVersion" "${SAB_VERSION}"
OutFile "${SAB_FILE}"
InstallDir "$PROGRAMFILES\SABnzbd"

View File

@@ -92,7 +92,7 @@
<span class="icon-bar"></span>
</button>
<a class="navbar-logo navbar-logo-small" href="${root}" title="$T('Home')">
<a class="navbar-logo navbar-logo-small" href="${root}" title="$T('Home')" data-placement="bottom">
#include $webdir + "/staticcfg/images/logo-small.svg"#
</a>
</div>

View File

@@ -26,7 +26,7 @@
</div>
<div class="field-pair">
<label class="config" for="port">$T('opt-port')</label>
<input type="number" name="port" id="port" value="$port" size="8" data-original="$port" />
<input type="number" name="port" id="port" value="$port" size="8" data-original="$port" min="0" max="65535" />
<span class="desc">$T('explain-port')</span>
</div>
<div class="field-pair">
@@ -69,7 +69,7 @@
</div>
<div class="field-pair advanced-settings">
<label class="config" for="https_port">$T('opt-https_port')</label>
<input type="number" name="https_port" id="https_port" value="$https_port" size="8" data-original="$https_port" />
<input type="number" name="https_port" id="https_port" value="$https_port" size="8" data-original="$https_port" min="0" max="65535" />
<span class="desc">$T('explain-https_port')</span>
</div>
<div class="field-pair advanced-settings">

View File

@@ -10,13 +10,13 @@
<label class="config wide" for="${section_label}_prio_$type">
$T($notify_types[$type]).replace('/', ' / ')
</label>
<input type="checkbox" name="${section_label}_prio_$type" id="${section_label}_prio_$type" value="1" <!--#if int($getVar($section_label + '_prio_' + $type)) > 0 then 'checked="checked"' else ""#--> />
<input type="checkbox" name="${section_label}_prio_$type" id="${section_label}_prio_$type" value="1" <!--#if $getVar($section_label + '_prio_' + $type) then 'checked="checked"' else ""#--> />
</div>
<!--#end for#-->
<!--#end def#-->
<!--#def show_cat_box($section_label)#-->
<div class="col2-cats" <!--#if int($getVar($section_label + '_enable')) > 0 then '' else 'style="display:none"'#-->>
<div class="col2-cats" <!--#if $getVar($section_label + '_enable') then '' else 'style="display:none"'#-->>
<hr>
<b>$T('affectedCat')</b><br/>
<select name="${section_label}_cats" multiple="multiple" class="multiple_cats" size="$len($categories)">
@@ -58,12 +58,12 @@
</div>
<div class="field-pair">
<label class="config" for="email_full">$T('opt-email_full')</label>
<input type="checkbox" name="email_full" id="email_full" value="1" <!--#if int($email_full) > 0 then 'checked="checked"' else ""#--> />
<input type="checkbox" name="email_full" id="email_full" value="1" <!--#if $email_full then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-email_full')</span>
</div>
<div class="field-pair">
<label class="config" for="email_rss">$T('opt-email_rss')</label>
<input type="checkbox" name="email_rss" id="email_rss" value="1" <!--#if int($email_rss) > 0 then 'checked="checked"' else ""#--> />
<input type="checkbox" name="email_rss" id="email_rss" value="1" <!--#if $email_rss then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-email_rss')</span>
</div>
<div class="field-pair">
@@ -107,12 +107,12 @@
<h3>$T('section-NC')</h3>
<table>
<tr>
<td><input type="checkbox" name="ncenter_enable" id="ncenter_enable" value="1" <!--#if int($ncenter_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><input type="checkbox" name="ncenter_enable" id="ncenter_enable" value="1" <!--#if $ncenter_enable then 'checked="checked"' else ""#--> /></td>
<td><label for="ncenter_enable"> $T('opt-ncenter_enable')</label></td>
</tr>
</table>
</div>
<div class="col1" <!--#if int($ncenter_enable) > 0 then '' else 'style="display:none"'#-->>
<div class="col1" <!--#if $ncenter_enable then '' else 'style="display:none"'#-->>
<fieldset>
$show_notify_checkboxes('ncenter')
<div class="field-pair no-field-pair-bg">
@@ -132,13 +132,13 @@
<h3>$T('section-AC')</h3>
<table>
<tr>
<td><input type="checkbox" name="acenter_enable" id="acenter_enable" value="1" <!--#if int($acenter_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><input type="checkbox" name="acenter_enable" id="acenter_enable" value="1" <!--#if $acenter_enable then 'checked="checked"' else ""#--> /></td>
<td><label for="acenter_enable"> $T('opt-acenter_enable')</label></td>
</tr>
</table>
$show_cat_box('acenter')
</div>
<div class="col1" <!--#if int($acenter_enable) > 0 then '' else 'style="display:none"'#-->>
<div class="col1" <!--#if $acenter_enable then '' else 'style="display:none"'#-->>
<fieldset>
$show_notify_checkboxes('acenter')
<div class="field-pair no-field-pair-bg">
@@ -158,13 +158,13 @@
<h3>$T('section-OSD') <a href="$help_uri#toc4" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<table>
<tr>
<td><input type="checkbox" name="ntfosd_enable" id="ntfosd_enable" value="1" <!--#if int($ntfosd_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><input type="checkbox" name="ntfosd_enable" id="ntfosd_enable" value="1" <!--#if $ntfosd_enable then 'checked="checked"' else ""#--> /></td>
<td><label for="ntfosd_enable"> $T('opt-ntfosd_enable')</label></td>
</tr>
</table>
$show_cat_box('ntfosd')
</div>
<div class="col1" <!--#if int($ntfosd_enable) > 0 then '' else 'style="display:none"'#-->>
<div class="col1" <!--#if $ntfosd_enable then '' else 'style="display:none"'#-->>
<fieldset>
$show_notify_checkboxes('ntfosd')
<div class="field-pair no-field-pair-bg">
@@ -183,7 +183,7 @@
<h3>Apprise <a href="$help_uri#apprise" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<table>
<tr>
<td><input type="checkbox" name="apprise_enable" id="apprise_enable" value="1" <!--#if int($apprise_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><input type="checkbox" name="apprise_enable" id="apprise_enable" value="1" <!--#if $apprise_enable then 'checked="checked"' else ""#--> /></td>
<td><label for="apprise_enable"> $T('opt-apprise_enable')</label></td>
</tr>
</table>
@@ -192,7 +192,7 @@
$show_cat_box('apprise')
</div>
<div class="col1" <!--#if int($apprise_enable) > 0 then '' else 'style="display:none"'#-->>
<div class="col1" <!--#if $apprise_enable then '' else 'style="display:none"'#-->>
<fieldset>
<div class="field-pair">
<label class="config" for="apprise_urls">$T('opt-apprise_urls')</label>
@@ -208,7 +208,7 @@
<label class="config" for="${section_label}_target_${type}">
$T($notify_types[$type]).replace('/', ' / ')
</label>
<input type="checkbox" name="${section_label}_target_${type}_enable" id="${section_label}_target_${type}_enable" value="1" <!--#if int($getVar($section_label + '_target_' + $type + '_enable')) > 0 then 'checked="checked"' else ""#--> />
<input type="checkbox" name="${section_label}_target_${type}_enable" id="${section_label}_target_${type}_enable" value="1" <!--#if $getVar($section_label + '_target_' + $type + '_enable') then 'checked="checked"' else ""#--> />
<input type="text" name="${section_label}_target_${type}" id="${section_label}_target_${type}" value="$getVar($section_label + '_target_' + $type)" placeholder="$T('opt-apprise_urls')" />
</div>
<!--#end for#-->
@@ -228,14 +228,14 @@
<h3>$T('section-NScript') <a href="$help_uri#nscript" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<table>
<tr>
<td><input type="checkbox" name="nscript_enable" id="nscript_enable" value="1" <!--#if int($nscript_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><input type="checkbox" name="nscript_enable" id="nscript_enable" value="1" <!--#if $nscript_enable then 'checked="checked"' else ""#--> /></td>
<td><label for="nscript_enable"> $T('opt-nscript_enable')</label></td>
</tr>
</table>
<em>$T('explain-nscript_enable')</em><br><a href="$help_uri#nscript" target="_blank">$T('readwiki')</a>
$show_cat_box('nscript')
</div>
<div class="col1" <!--#if int($nscript_enable) > 0 then '' else 'style="display:none"'#-->>
<div class="col1" <!--#if $nscript_enable then '' else 'style="display:none"'#-->>
<fieldset>
<div class="field-pair">
<label class="config" for="nscript_script">$T('opt-nscript_script')</label>
@@ -267,14 +267,14 @@
<h3>$T('section-Prowl')</h3>
<table>
<tr>
<td><input type="checkbox" name="prowl_enable" id="prowl_enable" value="1" <!--#if int($prowl_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><input type="checkbox" name="prowl_enable" id="prowl_enable" value="1" <!--#if $prowl_enable then 'checked="checked"' else ""#--> /></td>
<td><label for="prowl_enable"> $T('opt-prowl_enable')</label></td>
</tr>
</table>
<em>$T('explain-prowl_enable')</em>
$show_cat_box('prowl')
</div>
<div class="col1" <!--#if int($prowl_enable) > 0 then '' else 'style="display:none"'#-->>
<div class="col1" <!--#if $prowl_enable then '' else 'style="display:none"'#-->>
<fieldset>
<div class="field-pair">
<label class="config" for="prowl_apikey">$T('opt-prowl_apikey')</label>
@@ -313,14 +313,14 @@
<h3>$T('section-Pushover')</h3>
<table>
<tr>
<td><input type="checkbox" name="pushover_enable" id="pushover_enable" value="1" <!--#if int($pushover_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><input type="checkbox" name="pushover_enable" id="pushover_enable" value="1" <!--#if $pushover_enable then 'checked="checked"' else ""#--> /></td>
<td><label for="pushover_enable"> $T('opt-pushover_enable')</label></td>
</tr>
</table>
<em>$T('explain-pushover_enable')</em>
$show_cat_box('pushover')
</div>
<div class="col1" <!--#if int($pushover_enable) > 0 then '' else 'style="display:none"'#-->>
<div class="col1" <!--#if $pushover_enable then '' else 'style="display:none"'#-->>
<fieldset>
<div class="field-pair">
<label class="config" for="pushover_token">$T('opt-pushover_token')</label>
@@ -378,14 +378,14 @@
<h3>$T('section-Pushbullet')</h3>
<table>
<tr>
<td><input type="checkbox" name="pushbullet_enable" id="pushbullet_enable" value="1" <!--#if int($pushbullet_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><input type="checkbox" name="pushbullet_enable" id="pushbullet_enable" value="1" <!--#if $pushbullet_enable then 'checked="checked"' else ""#--> /></td>
<td><label for="pushbullet_enable"> $T('opt-pushbullet_enable')</label></td>
</tr>
</table>
<em>$T('explain-pushbullet_enable')</em>
$show_cat_box('pushbullet')
</div>
<div class="col1" <!--#if int($pushbullet_enable) > 0 then '' else 'style="display:none"'#-->>
<div class="col1" <!--#if $pushbullet_enable then '' else 'style="display:none"'#-->>
<fieldset>
<div class="field-pair">
<label class="config" for="pushbullet_apikey">$T('opt-pushbullet_apikey')</label>

View File

@@ -28,7 +28,7 @@
</label>
<div class="advanced-buttonSeperator"></div>
<div class="chart-selector-container" title="$T('selectedDates')">
<div class="chart-selector-container" title="$T('selectedDates')" data-placement="bottom">
<span class="glyphicon glyphicon-signal"></span>
<!--#set today = datetime.date.today()#-->
<input type="date" name="chart-start" id="chart-start" value="<!--#echo (today-datetime.timedelta(days=30)).strftime('%Y-%m-%d')#-->"> -
@@ -59,7 +59,7 @@
</div>
<div class="field-pair advanced-settings">
<label class="config" for="port">$T('srv-port')</label>
<input type="number" name="port" id="port" size="8" value="563" min="0" />
<input type="number" name="port" id="port" size="8" value="563" min="0" max="65535" />
</div>
<div class="field-pair">
<label class="config" for="ssl">$T('srv-ssl')</label>
@@ -142,7 +142,7 @@
</div>
</div>
<!--#set $prio_colors = ["#59cc33", "#3366cc","#7f33cc", "#cc33a6", "#cc3333"] #-->
<!--#set $prio_colors = ["#59cc33", "#26a69a", "#3366cc", "#7f33cc", "#cc33a6", "#f39c12", "#cc3333", "#8d6e63"] #-->
<!--#set $cur_prio_color = -1 #-->
<!--#set $last_prio = -1 #-->
<!--#for $cur, $server in enumerate($servers) #-->
@@ -185,7 +185,7 @@
</div>
<div class="field-pair advanced-settings">
<label class="config" for="port$cur">$T('srv-port')</label>
<input type="number" name="port" id="port$cur" value="$server['port']" size="8" min="0" required />
<input type="number" name="port" id="port$cur" value="$server['port']" size="8" min="0" max="65535" required />
</div>
<div class="field-pair">
<label class="config" for="ssl$cur">$T('srv-ssl')</label>

View File

@@ -71,7 +71,7 @@
<div class="field-pair">
<label class="config" for="field_sort_string_$cur">$T('sortString')</label>
<input type="text" name="sort_string" id="field_sort_string_$cur" value="$slot.sort_string" required="required" />
<button type="button" title="$T('sort-legenda')" class="btn btn-default patternKey" onclick="jQuery('#pattern_explainer_$cur').toggle(); window.scrollBy(0, 500);">
<button type="button" class="btn btn-default patternKey" onclick="jQuery('#pattern_explainer_$cur').toggle(); window.scrollBy(0, 500);">
<span class="glyphicon glyphicon-list-alt" aria-hidden="true"></span> $T('sort-legenda')
</button>
</div>

View File

@@ -69,7 +69,7 @@
</div>
<div class="field-pair">
<label class="config" for="propagation_delay">$T('opt-propagation_delay')</label>
<input type="number" name="propagation_delay" id="propagation_delay" value="$propagation_delay" /> <i>$T('minutes')</i>
<input type="number" name="propagation_delay" id="propagation_delay" value="$propagation_delay" min="0" /> <i>$T('minutes')</i>
<span class="desc">$T('explain-propagation_delay')</span>
</div>
<div class="field-pair advanced-settings">

View File

@@ -1,3 +1,4 @@
<!DOCTYPE HTML>
<html lang="$active_lang">
<head>
<title>SABnzbd - $T('login')</title>
@@ -36,8 +37,8 @@
<div class="alert alert-danger" role="alert">$error</div>
<!--#end if#-->
<input type="text" class="form-control" name="username" placeholder="$T('srv-username')" required autofocus>
<input type="password" class="form-control" name="password" placeholder="$T('srv-password')" required>
<input type="text" class="form-control" name="username" placeholder="$T('srv-username')" autocomplete="username" required autofocus>
<input type="password" class="form-control" name="password" placeholder="$T('srv-password')" autocomplete="current-password" required>
<button class="btn btn-default"><span class="glyphicon glyphicon-circle-arrow-right"></span> $T('login') </button>
@@ -62,4 +63,4 @@
} catch(err) { }
</script>
</body>
</html>
</html>

View File

@@ -208,7 +208,7 @@ ul.tabs a,
#subscriptions,
.RSS form[action="add_rss_feed"] tr:nth-child(even),
.Config .table {
border: 1px solid #555555 !important;
border: 1px solid #555555;
}
.Categories form:first-of-type tr:last-of-type,

View File

@@ -19,6 +19,7 @@ body {
float: left;
overflow: visible;
border: 1px solid #dfdede;
border-bottom: none !important;
background-color: #FFF;
width: 100%
}
@@ -1222,7 +1223,6 @@ input[type="checkbox"] {
}
.value-and-select select {
min-width: 30px;
margin-top: 1px;
}
.dotOne, .dotTwo, .dotThree {

View File

@@ -262,9 +262,10 @@ function do_restart() {
// Show overlay
$('.main-restarting').show()
// What template
// Check if we need redirect
// Uses == on purpose, because val() returns string and data() returns int!
var switchedHTTPS = ($('#enable_https').is(':checked') === ($('#enable_https').data('original') === undefined))
var portsUnchanged = ($('#port').val() === $('#port').data('original')) && ($('#https_port').val() === $('#https_port').data('original'))
var portsUnchanged = ($('#port').val() == $('#port').data('original')) && ($('#https_port').val() == $('#https_port').data('original'))
// Are we on settings page or did nothing change?
if(!$('body').hasClass('General') || (!switchedHTTPS && portsUnchanged)) {
@@ -489,6 +490,9 @@ $(document).ready(function () {
addRowColor()
}
addRowColor()
// Add tooltips
jQuery('[title]').tooltip()
});
/*

View File

@@ -135,7 +135,7 @@
<div class="col-sm-6 col-dot-overflow" data-bind="visible: hasPerformanceInfo">
<span data-bind="text: statusInfo.pystone"></span>
<a href="#" class="diskspeed-button" data-bind="click: loadStatusInfo" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest') (~10 $T('seconds'))"><span class="glyphicon glyphicon-repeat"></span></a>
<small title="$cpumodel $cpusimd" data-tooltip="true">$cpumodel $cpusimd</small>
<small title="$cpumodel $cpusimd $docker" data-tooltip="true">$cpumodel $cpusimd $docker</small>
</div>
<div class="col-sm-6 col-loading" data-bind="visible: !hasPerformanceInfo()">$T('Glitter-loading')<span class="loader-dot-one">.</span><span class="loader-dot-two">.</span><span class="loader-dot-three">.</span></div>
</div>
@@ -528,7 +528,7 @@
<div class="form-group">
<label class="col-sm-4 control-label">$T('category')</label>
<div class="col-sm-6">
<select name="Category" class="form-control" data-bind="options: queue.categoriesList, optionsValue: 'catValue', optionsText: 'catText'"></select>
<select name="Category" class="form-control" data-bind="options: queue.categoriesList, optionsValue: 'catValue', optionsText: 'catText', optionsCaption: ''"></select>
<span class="glyphicon glyphicon-tag"></span>
</div>
</div>
@@ -537,7 +537,7 @@
<div class="col-sm-6">
<!-- This list is different from the one during download! -->
<select name="Priority" class="form-control">
<option value="-100">$T('default')</option>
<option value=""></option>
<option value="2">$T('pr-force')</option>
<option value="1">$T('pr-high')</option>
<option value="0">$T('pr-normal')</option>
@@ -550,14 +550,14 @@
<div class="form-group">
<label class="col-sm-4 control-label">$T('swtag-pp')</label>
<div class="col-sm-6">
<select name="Processing" class="form-control" data-bind="options: queue.processingOptions, optionsValue: 'value', optionsText: 'name', optionsCaption: '$T('default')'"></select>
<select name="Processing" class="form-control" data-bind="options: queue.processingOptions, optionsValue: 'value', optionsText: 'name', optionsCaption: ''"></select>
<span class="glyphicon glyphicon-check"></span>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">$T('eoq-scripts')</label>
<div class="col-sm-6">
<select name="Post-processing" class="form-control" data-bind="options: queue.scriptsList, optionsCaption: '$T('default')', optionsValue: 'scriptValue', optionsText: 'scriptText', enable: (queue.scriptsList().length > 1)"></select>
<select name="Post-processing" class="form-control" data-bind="options: queue.scriptsList, optionsCaption: '', optionsValue: 'scriptValue', optionsText: 'scriptText', enable: (queue.scriptsList().length > 1)"></select>
<span class="glyphicon glyphicon-flash"></span>
</div>
</div>

View File

@@ -704,6 +704,7 @@ function ViewModel() {
data.append("apikey", apiKey);
// Add this one
debugger
$.ajax({
url: "./api",
type: "POST",

View File

@@ -57,13 +57,13 @@
<div class="form-group">
<label for="port" class="col-sm-4 control-label">$T('srv-port')</label>
<div class="col-sm-8">
<input type="number" class="form-control" name="port" id="port" value="<!--#if $port then $port else '563' #-->" />
<input type="number" class="form-control" name="port" id="port" value="<!--#if $port then $port else '563' #-->" min="0" max="65535" />
</div>
</div>
<div class="form-group">
<label for="connections" class="col-sm-4 control-label">$T('srv-connections')</label>
<div class="col-sm-8">
<input type="number" class="form-control" name="connections" id="connections" value="<!--#if $connections then $connections else '8'#-->" data-toggle="tooltip" data-placement="right" title="$T('wizard-server-con-explain') $T('wizard-server-con-eg')" />
<input type="number" class="form-control" name="connections" id="connections" value="<!--#if $connections then $connections else '8'#-->" min="1" max="500" data-toggle="tooltip" data-placement="right" title="$T('wizard-server-con-explain') $T('wizard-server-con-eg')" />
</div>
</div>
<div class="form-group">

View File

@@ -30,6 +30,7 @@
<url type="faq">https://sabnzbd.org/wiki/faq</url>
<url type="contact">https://sabnzbd.org/live-chat.html</url>
<releases>
<release version="4.3.3" date="2024-08-01" type="stable"/>
<release version="4.3.2" date="2024-05-30" type="stable"/>
<release version="4.3.1" date="2024-05-03" type="stable"/>
<release version="4.3.0" date="2024-05-01" type="stable"/>

View File

Binary file not shown.

View File

@@ -4,7 +4,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.2RC1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: team@sabnzbd.org\n"
"Language-Team: SABnzbd <team@sabnzbd.org>\n"

View File

@@ -3,7 +3,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Language-Team: Czech (https://app.transifex.com/sabnzbd/teams/111101/cs/)\n"
"MIME-Version: 1.0\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Danish (https://app.transifex.com/sabnzbd/teams/111101/da/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: German (https://app.transifex.com/sabnzbd/teams/111101/de/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Spanish (https://app.transifex.com/sabnzbd/teams/111101/es/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Finnish (https://app.transifex.com/sabnzbd/teams/111101/fi/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: French (https://app.transifex.com/sabnzbd/teams/111101/fr/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: ION, 2020\n"
"Language-Team: Hebrew (https://app.transifex.com/sabnzbd/teams/111101/he/)\n"

View File

@@ -3,7 +3,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Language-Team: Italian (https://app.transifex.com/sabnzbd/teams/111101/it/)\n"
"MIME-Version: 1.0\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Norwegian Bokmål (https://app.transifex.com/sabnzbd/teams/111101/nb/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Dutch (https://app.transifex.com/sabnzbd/teams/111101/nl/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Polish (https://app.transifex.com/sabnzbd/teams/111101/pl/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Portuguese (Brazil) (https://app.transifex.com/sabnzbd/teams/111101/pt_BR/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Romanian (https://app.transifex.com/sabnzbd/teams/111101/ro/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Russian (https://app.transifex.com/sabnzbd/teams/111101/ru/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Serbian (https://app.transifex.com/sabnzbd/teams/111101/sr/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Swedish (https://app.transifex.com/sabnzbd/teams/111101/sv/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Chinese (China) (https://app.transifex.com/sabnzbd/teams/111101/zh_CN/)\n"

View File

@@ -4,7 +4,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.2RC1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: team@sabnzbd.org\n"
"Language-Team: SABnzbd <team@sabnzbd.org>\n"
@@ -399,6 +399,10 @@ msgstr ""
msgid "Deobfuscate renamed %d file(s)"
msgstr ""
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr ""
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr ""

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Czech (https://app.transifex.com/sabnzbd/teams/111101/cs/)\n"
@@ -434,6 +434,10 @@ msgstr ""
msgid "Deobfuscate renamed %d file(s)"
msgstr ""
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr ""
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr "Přímé rozbalení"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Danish (https://app.transifex.com/sabnzbd/teams/111101/da/)\n"
@@ -437,6 +437,10 @@ msgstr ""
msgid "Deobfuscate renamed %d file(s)"
msgstr ""
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr ""
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr ""

View File

@@ -14,12 +14,13 @@
# HandyDandy04, 2024
# Safihre <safihre@sabnzbd.org>, 2024
# Gjelbrim Haskaj, 2024
# Stefan Rodriguez Galeano, 2024
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Gjelbrim Haskaj, 2024\n"
"Last-Translator: Stefan Rodriguez Galeano, 2024\n"
"Language-Team: German (https://app.transifex.com/sabnzbd/teams/111101/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -257,7 +258,7 @@ msgstr "Die Verbindung konnte nicht überprüft werden. (%s)"
#: sabnzbd/api.py
msgid "Resolving address"
msgstr "Adresse wird aufgelöst"
msgstr "Adresse wird aufgelöst"
#. No value, used in dropdown menus
#: sabnzbd/api.py, sabnzbd/skintext.py
@@ -477,6 +478,10 @@ msgstr "Entschleiern korrigierte die Erweiterung von %d Datei(en)"
msgid "Deobfuscate renamed %d file(s)"
msgstr "Entschleiern hat %dDatei(en) umbenannt"
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr "Umbenannte Untertiteldatei(en)%d verschleiern"
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr "Direkt entpacken"
@@ -557,7 +562,7 @@ msgstr "Schwerer Fehler im Downloader"
#. Warning message
#: sabnzbd/downloader.py
msgid "%s@%s: Received unknown status code %s for article %s"
msgstr ""
msgstr "%s@%s:Unbekannter Statuscode%s für Artikel erhalten %s"
#: sabnzbd/downloader.py
msgid "Too many connections to server %s [%s]"
@@ -1231,21 +1236,22 @@ msgstr "Prowl-Nachricht konnte nicht versendet werden"
#. Warning message
#: sabnzbd/notifier.py
msgid "Failed to send Apprise message - no URLs defined"
msgstr ""
msgstr "Übertragung der Info-Nachricht fehlgeschlagen - keine URLs definiert"
#. Warning message
#: sabnzbd/notifier.py
msgid "One or more Apprise URLs could not be loaded."
msgstr ""
msgstr "Eine oder mehrere Informations-URLs konnten nicht geladen werden."
#: sabnzbd/notifier.py
msgid "Failed to send one or more Apprise Notifications"
msgstr ""
"Eine oder mehrere Info-Benachrichtigungen konnten nicht gesendet werden"
#. Warning message
#: sabnzbd/notifier.py
msgid "Failed to send Apprise message"
msgstr ""
msgstr "Info-Nachricht konnte nicht gesendet werden"
#. Error message
#: sabnzbd/notifier.py
@@ -2430,7 +2436,7 @@ msgstr "Alle Elemente in der Warteschlange löschen?"
#. Delete confirmation popup
#: sabnzbd/skintext.py
msgid "Are you sure you want to remove these jobs?"
msgstr ""
msgstr "Sind Sie sicher, dass Sie diese Aufträge entfernen wollen?"
#. Queue page button
#: sabnzbd/skintext.py
@@ -2455,7 +2461,7 @@ msgstr "NZBs und Dateien löschen"
#. Checkbox if job should be added to Archive
#: sabnzbd/skintext.py
msgid "Permanently delete (skip archive)"
msgstr ""
msgstr "erhaft löschen (Archiv überspringen)"
#. Caption for missing articles in Queue
#: sabnzbd/skintext.py
@@ -2478,7 +2484,7 @@ msgstr "Kontingent jetzt zurücksetzen"
#: sabnzbd/skintext.py
msgid "Archive"
msgstr ""
msgstr "Archiv"
#. Button/link hiding History job details
#: sabnzbd/skintext.py
@@ -2503,7 +2509,7 @@ msgstr "Alle anzeigen"
#. Button showing all archived jobs
#: sabnzbd/skintext.py
msgid "Show Archive"
msgstr ""
msgstr "Zeige Archiv"
#. History table header - Size of the download quota
#: sabnzbd/skintext.py
@@ -2796,11 +2802,11 @@ msgstr "Port, auf dem SABnzbd auf Anfragen warten soll."
#: sabnzbd/skintext.py
msgid "Web Interface Theme"
msgstr ""
msgstr "Benutzeroberfläche"
#: sabnzbd/skintext.py
msgid "Choose a theme."
msgstr ""
msgstr "Wählen Sie ein Theme."
#: sabnzbd/skintext.py
msgid "SABnzbd Username"
@@ -2971,28 +2977,36 @@ msgstr "Alle Aufträge behalten"
msgid ""
"Move jobs to the archive if the history exceeds specified number of jobs"
msgstr ""
"Verschieben von Aufträgen in das Archiv, wenn der Verlauf die angegebene "
"Anzahl von Aufträgen überschreitet."
#: sabnzbd/skintext.py
msgid ""
"Delete jobs if the history and archive exceeds specified number of jobs"
msgstr ""
"Löschen von Aufträgen, wenn der Verlauf und das Archiv die angegebene Anzahl"
" von Aufträgen überschreiten"
#: sabnzbd/skintext.py
msgid "Move jobs to the archive after specified number of days"
msgstr ""
"Verschieben von Aufträgen in das Archiv nach einer bestimmten Anzahl von "
"Tagen"
#: sabnzbd/skintext.py
msgid ""
"Delete jobs from the history and archive after specified number of days"
msgstr ""
"Löschen von Aufträgen aus der Historie und dem Archiv nach einer bestimmten "
"Anzahl von Tagen"
#: sabnzbd/skintext.py
msgid "Move all completed jobs to archive"
msgstr ""
msgstr "Alle abgeschlossenen Aufträge ins Archiv verschieben"
#: sabnzbd/skintext.py
msgid "Delete all completed jobs"
msgstr ""
msgstr "Alle abgeschlossenen Aufträge löschen"
#: sabnzbd/skintext.py
msgid "Jobs"
@@ -3281,7 +3295,7 @@ msgstr ""
#: sabnzbd/skintext.py
msgid "Purge Logs"
msgstr ""
msgstr "Protokolle bereinigen"
#: sabnzbd/skintext.py
msgid ".nzb Backup Folder"
@@ -3406,7 +3420,7 @@ msgstr "Aufgabe abgebrochen (verschoben in die Historie)"
#: sabnzbd/skintext.py
msgid "Abort post-processing"
msgstr ""
msgstr "Nachbearbeitung abbrechen"
#: sabnzbd/skintext.py
msgid "Action when unwanted extension detected"
@@ -3479,7 +3493,7 @@ msgstr ""
#: sabnzbd/skintext.py
msgid "On queue finish script"
msgstr ""
msgstr "Skript zur Beendigung der Warteschlange"
#: sabnzbd/skintext.py
msgid "Executed after the queue finishes downloading."
@@ -4280,11 +4294,13 @@ msgstr "Geräte, welche die Benachrichtigungen empfangen sollen"
#. Apprise settings
#: sabnzbd/skintext.py
msgid "Enable Apprise notifications"
msgstr ""
msgstr "Aktivieren Sie Info-Benachrichtigungen"
#: sabnzbd/skintext.py
msgid "Send notifications using Apprise to almost any notification service"
msgstr ""
"Senden Sie Benachrichtigungen mit Anfragen an fast jeden "
"Benachrichtigungsdienst"
#. Apprise settings
#: sabnzbd/skintext.py
@@ -4295,11 +4311,15 @@ msgstr ""
#: sabnzbd/skintext.py
msgid "Use a comma and/or space to identify more than one URL."
msgstr ""
"Verwenden Sie ein Komma und/oder ein Leerzeichen, um mehr als eine URL zu "
"kennzeichnen."
#: sabnzbd/skintext.py
msgid ""
"Override the default URLs for specific notification types below, if desired."
msgstr ""
"Falls gewünscht, können Sie die Standard-URLs für bestimmte "
"Benachrichtigungstypen unten überschreiben."
#. Header for Notification Script notification section
#: sabnzbd/skintext.py
@@ -4798,6 +4818,9 @@ msgid ""
"When you Retry a job, 'Duplicate Detection' and 'Abort jobs that cannot be "
"completed' are disabled."
msgstr ""
"Wenn Sie einen Auftrag wiederholen, sind die Funktionen „Erkennung von "
"Duplikaten“ und „Abbruch von Aufträgen, die nicht abgeschlossen werden "
"können“ deaktiviert."
#: sabnzbd/skintext.py
msgid "View Script Log"
@@ -5032,7 +5055,7 @@ msgstr ""
#. Error message
#: sabnzbd/sorting.py
msgid "Failed to rename %s to %s"
msgstr "Fehler beim umbennenen von %s nach %s"
msgstr "Fehler beim Umbenennen von %s nach %s"
#. Error message
#: sabnzbd/sorting.py

View File

@@ -8,7 +8,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Spanish (https://app.transifex.com/sabnzbd/teams/111101/es/)\n"
@@ -455,6 +455,10 @@ msgstr ""
msgid "Deobfuscate renamed %d file(s)"
msgstr ""
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr ""
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr "Descomprimir directamente"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Finnish (https://app.transifex.com/sabnzbd/teams/111101/fi/)\n"
@@ -433,6 +433,10 @@ msgstr ""
msgid "Deobfuscate renamed %d file(s)"
msgstr ""
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr ""
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr ""

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.2RC1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Fred L <88com88@gmail.com>, 2024\n"
"Language-Team: French (https://app.transifex.com/sabnzbd/teams/111101/fr/)\n"
@@ -469,6 +469,10 @@ msgstr "La désobfuscation a corrigé l'extension de %d fichier(s)"
msgid "Deobfuscate renamed %d file(s)"
msgstr "La désobfuscation a renommé %d fichier(s)"
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr "Désobfusquer le(s) fichier(s) de sous-titres renommé(s) %d"
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr "Décompression Directe"

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: ION, 2024\n"
"Language-Team: Hebrew (https://app.transifex.com/sabnzbd/teams/111101/he/)\n"
@@ -437,6 +437,10 @@ msgstr "אי־האפלה תיקנה את הסיומת של %d קבצים"
msgid "Deobfuscate renamed %d file(s)"
msgstr "אי־האפלה שינתה שם של %d קבצים"
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr ""
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr "פריקה ישירה"

View File

@@ -3,7 +3,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Language-Team: Italian (https://app.transifex.com/sabnzbd/teams/111101/it/)\n"
"MIME-Version: 1.0\n"
@@ -420,6 +420,10 @@ msgstr ""
msgid "Deobfuscate renamed %d file(s)"
msgstr ""
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr ""
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr ""

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Norwegian Bokmål (https://app.transifex.com/sabnzbd/teams/111101/nb/)\n"
@@ -431,6 +431,10 @@ msgstr ""
msgid "Deobfuscate renamed %d file(s)"
msgstr ""
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr ""
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr ""

View File

@@ -8,7 +8,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2024\n"
"Language-Team: Dutch (https://app.transifex.com/sabnzbd/teams/111101/nl/)\n"
@@ -463,6 +463,10 @@ msgstr "Extensie van %d bestand(en) gecorrigeerd"
msgid "Deobfuscate renamed %d file(s)"
msgstr "Bestandsnamen van %d bestand(en) aangepast."
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr ""
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr "Direct Uitpakken"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Polish (https://app.transifex.com/sabnzbd/teams/111101/pl/)\n"
@@ -430,6 +430,10 @@ msgstr ""
msgid "Deobfuscate renamed %d file(s)"
msgstr ""
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr ""
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr ""

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Portuguese (Brazil) (https://app.transifex.com/sabnzbd/teams/111101/pt_BR/)\n"
@@ -444,6 +444,10 @@ msgstr ""
msgid "Deobfuscate renamed %d file(s)"
msgstr ""
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr ""
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr ""

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Romanian (https://app.transifex.com/sabnzbd/teams/111101/ro/)\n"
@@ -447,6 +447,10 @@ msgstr ""
msgid "Deobfuscate renamed %d file(s)"
msgstr ""
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr ""
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr "Dezarhivare directă"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Russian (https://app.transifex.com/sabnzbd/teams/111101/ru/)\n"
@@ -430,6 +430,10 @@ msgstr ""
msgid "Deobfuscate renamed %d file(s)"
msgstr ""
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr ""
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr ""

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Serbian (https://app.transifex.com/sabnzbd/teams/111101/sr/)\n"
@@ -427,6 +427,10 @@ msgstr ""
msgid "Deobfuscate renamed %d file(s)"
msgstr ""
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr ""
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr ""

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Swedish (https://app.transifex.com/sabnzbd/teams/111101/sv/)\n"
@@ -428,6 +428,10 @@ msgstr ""
msgid "Deobfuscate renamed %d file(s)"
msgstr ""
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr ""
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr ""

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Kangwei Li <lkw20010211@gmail.com>, 2023\n"
"Language-Team: Chinese (China) (https://app.transifex.com/sabnzbd/teams/111101/zh_CN/)\n"
@@ -425,6 +425,10 @@ msgstr ""
msgid "Deobfuscate renamed %d file(s)"
msgstr ""
#: sabnzbd/deobfuscate_filenames.py
msgid "Deobfuscate renamed %d subtitle file(s)"
msgstr ""
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
msgid "Direct Unpack"
msgstr ""

View File

@@ -4,7 +4,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.2RC1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha2\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: team@sabnzbd.org\n"
"Language-Team: SABnzbd <team@sabnzbd.org>\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Pavel C <quoing_transifex@mess.cz>, 2022\n"
"Language-Team: Czech (https://app.transifex.com/sabnzbd/teams/111101/cs/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Danish (https://app.transifex.com/sabnzbd/teams/111101/da/)\n"

View File

@@ -9,7 +9,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Lorenz B, 2024\n"
"Language-Team: German (https://app.transifex.com/sabnzbd/teams/111101/de/)\n"

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Ester Molla Aragones <moarages@gmail.com>, 2020\n"
"Language-Team: Spanish (https://app.transifex.com/sabnzbd/teams/111101/es/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Finnish (https://app.transifex.com/sabnzbd/teams/111101/fi/)\n"

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Fred L <88com88@gmail.com>, 2024\n"
"Language-Team: French (https://app.transifex.com/sabnzbd/teams/111101/fr/)\n"

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: ION, 2024\n"
"Language-Team: Hebrew (https://app.transifex.com/sabnzbd/teams/111101/he/)\n"

View File

@@ -3,7 +3,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Language-Team: Italian (https://app.transifex.com/sabnzbd/teams/111101/it/)\n"
"MIME-Version: 1.0\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Norwegian Bokmål (https://app.transifex.com/sabnzbd/teams/111101/nb/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2024\n"
"Language-Team: Dutch (https://app.transifex.com/sabnzbd/teams/111101/nl/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Polish (https://app.transifex.com/sabnzbd/teams/111101/pl/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Portuguese (Brazil) (https://app.transifex.com/sabnzbd/teams/111101/pt_BR/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Romanian (https://app.transifex.com/sabnzbd/teams/111101/ro/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Russian (https://app.transifex.com/sabnzbd/teams/111101/ru/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Serbian (https://app.transifex.com/sabnzbd/teams/111101/sr/)\n"

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Petter Ramme, 2024\n"
"Language-Team: Swedish (https://app.transifex.com/sabnzbd/teams/111101/sv/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.3.1\n"
"Project-Id-Version: SABnzbd-4.4.0Alpha1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Chinese (China) (https://app.transifex.com/sabnzbd/teams/111101/zh_CN/)\n"

View File

@@ -1,72 +1,73 @@
# Main requirements
# Note that not all sub-dependencies are listed, but only ones we know could cause trouble
apprise==1.8.0
sabctools==8.2.0
apprise==1.9.0
sabctools==8.2.5
CT3==3.3.3.post1
cffi==1.16.0
cffi==1.17.1
pycparser==2.22
feedparser==6.0.11
configobj==5.0.8
configobj==5.0.9
cheroot==10.0.1
six==1.16.0
cherrypy==18.9.0
jaraco.functools==4.0.1
cherrypy==18.10.0
jaraco.functools==4.1.0
jaraco.collections==5.0.0
jaraco.text==3.8.1 # Newer version introduces irrelevant extra dependencies
jaraco.classes==3.4.0
jaraco.context==4.3.0
more-itertools==10.2.0
more-itertools==10.5.0
zc.lockfile==3.0.post1
python-dateutil==2.9.0.post0
tempora==5.5.1
pytz==2024.1
tempora==5.7.0
pytz==2024.2
sgmllib3k==1.0.0
portend==3.2.0
chardet==5.2.0
pyunormalize==16.0.0
PySocks==1.7.1
puremagic==1.23
puremagic==1.28
guessit==3.8.0
babelfish==0.6.1
rebulk==3.2.0
# Recent cryptography versions require Rust. If you run into issues compiling this
# SABnzbd will also work with older pre-Rust versions such as cryptography==3.3.2
cryptography==42.0.7
cryptography==43.0.1
# We recommend using "orjson" as it is 2x as fast as "ujson". However, it requires
# Rust so SABnzbd works just as well with "ujson" or the Python built in "json" module
ujson==5.10.0
# Windows system integration
pywin32==306; sys_platform == 'win32'
windows-toasts==1.1.1; sys_platform == 'win32' and python_version > '3.8'
winrt-runtime==2.0.1; sys_platform == 'win32' and python_version > '3.8'
winrt-Windows.Data.Xml.Dom==2.0.1; sys_platform == 'win32' and python_version > '3.8'
winrt-Windows.Foundation==2.0.1; sys_platform == 'win32' and python_version > '3.8'
winrt-Windows.Foundation.Collections==2.0.1; sys_platform == 'win32' and python_version > '3.8'
winrt-Windows.UI.Notifications==2.0.1; sys_platform == 'win32' and python_version > '3.8'
pywin32==308; sys_platform == 'win32'
windows-toasts==1.3.0; sys_platform == 'win32' and python_version > '3.8'
winrt-runtime==2.2.0; sys_platform == 'win32' and python_version > '3.8'
winrt-Windows.Data.Xml.Dom==2.2.0; sys_platform == 'win32' and python_version > '3.8'
winrt-Windows.Foundation==2.2.0; sys_platform == 'win32' and python_version > '3.8'
winrt-Windows.Foundation.Collections==2.2.0; sys_platform == 'win32' and python_version > '3.8'
winrt-Windows.UI.Notifications==2.2.0; sys_platform == 'win32' and python_version > '3.8'
# macOS system calls
pyobjc-core==10.2; sys_platform == 'darwin'
pyobjc-framework-Cocoa==10.2; sys_platform == 'darwin'
pyobjc-core==10.3.1; sys_platform == 'darwin'
pyobjc-framework-Cocoa==10.3.1; sys_platform == 'darwin'
# Linux notifications
notify2==0.3.1; sys_platform != 'win32' and sys_platform != 'darwin'
# Apprise Requirements
requests==2.31.0
requests==2.32.3
requests-oauthlib==2.0.0
PyYAML==6.0.1
markdown==3.6
PyYAML==6.0.2
markdown==3.7
paho-mqtt==1.6.1 # Pinned, newer versions don't work with AppRise yet
# Requests Requirements
charset_normalizer==3.3.2
idna==3.7
urllib3==2.2.1
certifi==2024.2.2
charset_normalizer==3.4.0
idna==3.10
urllib3==2.2.3
certifi==2024.8.30
oauthlib==3.2.2
PyJWT==2.8.0
PyJWT==2.9.0
blinker==1.8.2
# Optional support for *nix tray icon.

View File

@@ -37,7 +37,7 @@ KERNEL32 = LIBC = MACOSLIBC = None
if os.name == "nt":
WIN32 = True
WIN64 = platform.uname().machine == "AMD64"
WIN64 = platform.uname().machine in ["AMD64", "ARM64"] # includes emulation of X86_64 on Windows ARM64
from sabnzbd.utils.apireg import del_connection_info
try:
@@ -245,10 +245,11 @@ def initialize(pause_downloader=False, clean_up=False, repair=0):
# Set call backs for Config items
cfg.cache_limit.callback(cfg.new_limit)
cfg.cherryhost.callback(cfg.guard_restart)
cfg.cherryport.callback(cfg.guard_restart)
cfg.web_host.callback(cfg.guard_restart)
cfg.web_port.callback(cfg.guard_restart)
cfg.web_dir.callback(cfg.guard_restart)
cfg.web_color.callback(cfg.guard_restart)
cfg.url_base.callback(trigger_restart)
cfg.username.callback(cfg.guard_restart)
cfg.password.callback(cfg.guard_restart)
cfg.log_dir.callback(cfg.guard_restart)
@@ -274,30 +275,8 @@ def initialize(pause_downloader=False, clean_up=False, repair=0):
# Set end-of-queue action
sabnzbd.misc.change_queue_complete_action(cfg.queue_complete(), new=False)
# Convert auto-sort
if cfg.auto_sort() == "0":
cfg.auto_sort.set("")
elif cfg.auto_sort() == "1":
cfg.auto_sort.set("avg_age asc")
# Convert old series/date/movie sorters
if not cfg.sorters_converted():
misc.convert_sorter_settings()
cfg.sorters_converted.set(True)
# Convert duplicate settings
if cfg.no_series_dupes():
cfg.no_smart_dupes.set(cfg.no_series_dupes())
cfg.no_series_dupes.set(0)
# Convert history retention setting
if cfg.history_retention():
misc.convert_history_retention()
cfg.history_retention.set("")
# Add hostname to the whitelist
if not cfg.host_whitelist():
cfg.host_whitelist.set(socket.gethostname())
# Do any config conversions
cfg.config_conversions()
# Do repair if requested
if misc.check_repair_request():
@@ -319,10 +298,6 @@ def initialize(pause_downloader=False, clean_up=False, repair=0):
# Run startup tasks
sabnzbd.NzbQueue.read_queue(repair)
sabnzbd.Scheduler.analyse(pause_downloader)
# Set cache limit for new users
if not cfg.cache_limit():
cfg.cache_limit.set(misc.get_cache_limit())
sabnzbd.ArticleCache.new_limit(cfg.cache_limit.get_int())
logging.info("All processes started")

View File

@@ -54,7 +54,7 @@ from sabnzbd.constants import (
AddNzbFileResult,
PP_LOOKUP,
STAGES,
DEF_TEST_TIMEOUT,
DEF_NETWORKING_TEST_TIMEOUT,
)
import sabnzbd.config as config
import sabnzbd.cfg as cfg
@@ -76,6 +76,7 @@ from sabnzbd.misc import (
change_queue_complete_action,
clean_comma_separated_list,
match_str,
bool_conv,
)
from sabnzbd.filesystem import diskspace, get_ext, clip_path, remove_all, list_scripts, purge_log_files, pathbrowser
from sabnzbd.encoding import xml_name, utob
@@ -180,7 +181,7 @@ def _api_queue_delete(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> b
removed = sabnzbd.NzbQueue.remove_all(kwargs.get("search"))
return report(keyword="", data={"status": bool(removed), "nzo_ids": removed})
elif items := clean_comma_separated_list(value):
delete_all_data = int_conv(kwargs.get("del_files"))
delete_all_data = bool_conv(kwargs.get("del_files"))
removed = sabnzbd.NzbQueue.remove_multiple(items, delete_all_data=delete_all_data)
return report(keyword="", data={"status": bool(removed), "nzo_ids": removed})
else:
@@ -422,8 +423,8 @@ def _api_change_opts(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> byt
def _api_fullstatus(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
"""API: full history status"""
status = build_status(
calculate_performance=int_conv(kwargs.get("calculate_performance", 0)),
skip_dashboard=int_conv(kwargs.get("skip_dashboard", 1)),
calculate_performance=bool_conv(kwargs.get("calculate_performance")),
skip_dashboard=bool_conv(kwargs.get("skip_dashboard")),
)
return report(keyword="status", data=status)
@@ -487,7 +488,7 @@ def _api_history(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
search = kwargs.get("search")
categories = clean_comma_separated_list(kwargs.get("cat") or kwargs.get("category"))
statuses = clean_comma_separated_list(kwargs.get("status"))
failed_only = int_conv(kwargs.get("failed_only"))
failed_only = bool_conv(kwargs.get("failed_only"))
nzo_ids = clean_comma_separated_list(kwargs.get("nzo_ids"))
archive = True
@@ -498,7 +499,7 @@ def _api_history(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
archive = False
special = value.lower()
del_files = bool(int_conv(kwargs.get("del_files")))
del_files = bool_conv(kwargs.get("del_files"))
if special in ("all", "failed", "completed"):
history_db = sabnzbd.get_db_connection()
if special in ("all", "failed"):
@@ -868,9 +869,9 @@ def _api_undefined(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes
def _api_browse(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
"""Return tree of local path"""
compact = bool(int_conv(kwargs.get("compact")))
show_files = bool(int_conv(kwargs.get("show_files")))
show_hidden = bool(int_conv(kwargs.get("show_hidden_folders")))
compact = bool_conv(kwargs.get("compact"))
show_files = bool_conv(kwargs.get("show_files"))
show_hidden = bool_conv(kwargs.get("show_hidden_folders"))
if compact:
# Used for typeahead
@@ -1267,7 +1268,7 @@ def test_nntp_server_dict(kwargs: Dict[str, Union[str, List[str]]]) -> Tuple[boo
password = kwargs.get("password", "").strip()
server = kwargs.get("server", "").strip()
connections = int_conv(kwargs.get("connections", 0))
timeout = int_conv(kwargs.get("timeout", DEF_TEST_TIMEOUT))
timeout = int_conv(kwargs.get("timeout", DEF_NETWORKING_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()
@@ -1286,7 +1287,7 @@ def test_nntp_server_dict(kwargs: Dict[str, Union[str, List[str]]]) -> Tuple[boo
if not timeout:
# Lower value during new server testing
timeout = DEF_TEST_TIMEOUT
timeout = DEF_NETWORKING_TEST_TIMEOUT
if "*" in password and not password.strip("*"):
# If the password is masked, try retrieving it from the config
@@ -1318,7 +1319,7 @@ def test_nntp_server_dict(kwargs: Dict[str, Union[str, List[str]]]) -> Tuple[boo
if not test_server.addrinfo:
# Try if we can connect on port 80 (so web server), forcing a short timeout
test_server.port = 80
test_server.timeout = DEF_TEST_TIMEOUT
test_server.timeout = DEF_NETWORKING_TEST_TIMEOUT
test_server.request_addrinfo_blocking()
if test_server.addrinfo:
return False, T(
@@ -1384,7 +1385,7 @@ def test_nntp_server_dict(kwargs: Dict[str, Union[str, List[str]]]) -> Tuple[boo
return return_status
def build_status(calculate_performance: int = False, skip_dashboard: int = False) -> Dict[str, Any]:
def build_status(calculate_performance: bool = False, skip_dashboard: bool = False) -> Dict[str, Any]:
# build up header full of basic information
info = build_header(trans_functions=False)

View File

@@ -115,9 +115,11 @@ def validate_single_tag(value: List[str]) -> Tuple[None, List[str]]:
return None, value
def validate_strip_right_slash(value: str) -> Tuple[None, str]:
"""Strips the right slash"""
if value:
def validate_url_base(value: str) -> Tuple[None, str]:
"""Strips the right slash and adds starting slash, if not present"""
if value and isinstance(value, str):
if not value.startswith("/"):
value = "/" + value
return None, value.rstrip("/")
return None, value
@@ -277,6 +279,9 @@ def validate_default_if_empty(root: str, value: str, default: str) -> Tuple[None
# Special settings
##############################################################################
# Increase everytime we do a configuration conversion
config_conversion_version = OptionNumber("misc", "config_conversion_version", default_val=0)
# This should be here so it's initialized first when the config is read
helpful_warnings = OptionBool("misc", "helpful_warnings", True)
@@ -309,8 +314,8 @@ version_check = OptionNumber("misc", "check_new_rel", 1)
autobrowser = OptionBool("misc", "auto_browser", True)
language = OptionStr("misc", "language", "en")
enable_https_verification = OptionBool("misc", "enable_https_verification", True)
cherryhost = OptionStr("misc", "host", DEF_HOST, validation=validate_host)
cherryport = OptionStr("misc", "port", DEF_PORT)
web_host = OptionStr("misc", "host", DEF_HOST, validation=validate_host)
web_port = OptionStr("misc", "port", DEF_PORT)
https_port = OptionStr("misc", "https_port")
username = OptionStr("misc", "username")
password = OptionPassword("misc", "password")
@@ -486,7 +491,7 @@ wait_ext_drive = OptionNumber("misc", "wait_ext_drive", 5, minval=1, maxval=60)
max_foldername_length = OptionNumber("misc", "max_foldername_length", DEF_FOLDER_MAX, minval=20, maxval=65000)
marker_file = OptionStr("misc", "nomedia_marker")
ipv6_servers = OptionBool("misc", "ipv6_servers", True)
url_base = OptionStr("misc", "url_base", "/sabnzbd", validation=validate_strip_right_slash)
url_base = OptionStr("misc", "url_base", "", validation=validate_url_base)
host_whitelist = OptionList("misc", "host_whitelist", validation=all_lowercase)
local_ranges = OptionList("misc", "local_ranges", protect=True)
max_url_retries = OptionNumber("misc", "max_url_retries", 10, minval=1)
@@ -691,7 +696,9 @@ def set_root_folders2():
##############################################################################
def new_limit():
"""Callback for article cache changes"""
sabnzbd.ArticleCache.new_limit(cache_limit.get_int())
if sabnzbd.__INITIALIZED__:
# Only update after full startup
sabnzbd.ArticleCache.new_limit(cache_limit.get_int())
def guard_restart():
@@ -732,3 +739,52 @@ def guard_language():
def guard_https_ver():
"""Callback for change of https verification"""
sabnzbd.misc.set_https_verification(enable_https_verification())
##############################################################################
# Conversions
##############################################################################
def config_conversions():
"""Update sections of the config, only once"""
# Basic old conversions
if config_conversion_version() < 1:
logging.info("Config conversion set 1")
# Convert auto-sort
if auto_sort() == "0":
auto_sort.set("")
elif auto_sort() == "1":
auto_sort.set("avg_age asc")
# Convert old series/date/movie sorters
if not sorters_converted():
sabnzbd.misc.convert_sorter_settings()
sorters_converted.set(True)
# Convert duplicate settings
if no_series_dupes():
no_smart_dupes.set(no_series_dupes())
no_series_dupes.set(0)
# Convert history retention setting
if history_retention():
sabnzbd.misc.convert_history_retention()
history_retention.set("")
# Add hostname to the whitelist
if not host_whitelist():
host_whitelist.set(socket.gethostname())
# Set cache limit for new users
if not cache_limit():
cache_limit.set(sabnzbd.misc.get_cache_limit())
# Done
config_conversion_version.set(1)
# url_base conversion
if config_conversion_version() < 2:
logging.info("Config conversion set 2")
# We did not end up applying this conversion, so we skip this conversion_version
pass

View File

@@ -120,7 +120,8 @@ class Option:
"""Set new value, no validation"""
global CFG_MODIFIED
if value is not None:
if isinstance(value, list) or isinstance(value, dict) or value != self.__value:
# Use get() to make sure we use default if nothing was set yet
if isinstance(value, list) or isinstance(value, dict) or value != self.get():
self.__value = value
CFG_MODIFIED = True
if self.__callback:
@@ -205,7 +206,7 @@ class OptionBool(Option):
def set(self, value: Any):
# Store the value as integer, easier to parse when reading the config.
super().set(sabnzbd.misc.int_conv(value))
super().set(sabnzbd.misc.bool_conv(value))
def __call__(self) -> int:
"""get() replacement"""

View File

@@ -49,7 +49,7 @@ RENAMES_FILE = "__renames__"
ATTRIB_FILE = "SABnzbd_attrib"
REPAIR_REQUEST = "repair-all.sab"
SABCTOOLS_VERSION_REQUIRED = "8.2.0"
SABCTOOLS_VERSION_REQUIRED = "8.2.5"
DB_HISTORY_VERSION = 1
DB_HISTORY_NAME = "history%s.db" % DB_HISTORY_VERSION
@@ -74,8 +74,9 @@ DEF_LOG_ERRFILE = "sabnzbd.error.log"
DEF_LOG_CHERRY = "cherrypy.log"
DEF_ARTICLE_CACHE_DEFAULT = "500M"
DEF_ARTICLE_CACHE_MAX = "1G"
DEF_TIMEOUT = 60
DEF_TEST_TIMEOUT = 5
DEF_NETWORKING_TIMEOUT = 60
DEF_NETWORKING_TEST_TIMEOUT = 5
DEF_NETWORKING_SHORT_TIMEOUT = 3
DEF_SCANRATE = 5
DEF_HTTPS_CERT_FILE = "server.cert"
DEF_HTTPS_KEY_FILE = "server.key"
@@ -100,6 +101,7 @@ SOFT_QUEUE_LIMIT = 0.5
# Percentage of cache to use before adding file to assembler
ASSEMBLER_WRITE_THRESHOLD = 5
NNTP_BUFFER_SIZE = int(800 * KIBI)
NTTP_MAX_BUFFER_SIZE = int(10 * MEBI)
REPAIR_PRIORITY = 3
FORCE_PRIORITY = 2

View File

@@ -35,7 +35,7 @@ from sabnzbd.constants import DB_HISTORY_NAME, STAGES, Status, PP_LOOKUP
from sabnzbd.bpsmeter import this_week, this_month
from sabnzbd.decorators import synchronized
from sabnzbd.encoding import ubtou, utob
from sabnzbd.misc import int_conv, caller_name, opts_to_pp, to_units
from sabnzbd.misc import caller_name, opts_to_pp, to_units, bool_conv, match_str
from sabnzbd.filesystem import remove_file, clip_path
DB_LOCK = threading.Lock()
@@ -129,7 +129,7 @@ class HistoryDB:
logging.error(T("Cannot write to History database, check access rights!"))
# Report back success, because there's no recovery possible
return True
elif "not a database" in error or "malformed" in error or "duplicate column name" in error:
elif match_str(error, ("not a database", "malformed", "no such table", "duplicate column name")):
logging.error(T("Damaged History database, created empty replacement"))
logging.info("Traceback: ", exc_info=True)
self.close()
@@ -141,7 +141,7 @@ class HistoryDB:
self.connect()
# Return False in case of "duplicate column" error
# because the column addition in connect() must be terminated
return "duplicate column name" not in error
return True
else:
logging.error(T("SQL Command Failed, see log"))
logging.info("SQL: %s", command)
@@ -596,7 +596,7 @@ def unpack_history_info(item: sqlite3.Row) -> Dict[str, Any]:
item["archive"] = bool(item["archive"])
# Retry and retry for failed URL-fetch
item["retry"] = int_conv(item["status"] == Status.FAILED and item["path"] and os.path.exists(item["path"]))
item["retry"] = bool_conv(item["status"] == Status.FAILED and item["path"] and os.path.exists(item["path"]))
if item["report"] == "future":
item["retry"] = True
@@ -604,6 +604,5 @@ def unpack_history_info(item: sqlite3.Row) -> Dict[str, Any]:
def scheduled_history_purge():
logging.info("Scheduled history purge")
with HistoryDB() as history_db:
history_db.auto_history_purge()

View File

@@ -92,7 +92,7 @@ def decode(article: Article, data_view: memoryview):
sabnzbd.Downloader.pause()
# This article should be fetched again
sabnzbd.NzbQueue.reset_try_lists(article)
article.allow_new_fetcher()
return
except BadData as error:

View File

@@ -27,14 +27,13 @@ files to the job-name in the queue if the filename looks obfuscated
Based on work by P1nGu1n
"""
import hashlib
import logging
import os
import re
import sabnzbd
from sabnzbd.filesystem import get_unique_filename, renamer, get_ext, get_basename
from sabnzbd.filesystem import get_unique_filename, renamer, get_ext, get_basename, listdir_normalized
from sabnzbd.par2file import is_parfile, parse_par2_file
import sabnzbd.utils.file_extension as file_extension
from sabnzbd.misc import match_str
@@ -60,7 +59,7 @@ def decode_par2(parfile: str) -> List[str]:
# Parse all files in the folder
dirname = os.path.dirname(parfile)
new_files = [] # list of new files generated
for fn in os.listdir(dirname):
for fn in listdir_normalized(dirname):
filepath = os.path.join(dirname, fn)
# Only check files
if os.path.isfile(filepath):
@@ -169,21 +168,29 @@ def is_probably_obfuscated(myinputfilename: str) -> bool:
return True # default is obfuscated
def first_file_is_much_bigger(filelist):
# returns True if first file is much bigger than second file
# Note: input parameter filelist must ordered on size!
def get_biggest_file(filelist: List[str]) -> str:
"""Returns biggest file if that file is much bigger than the other files
If only one file exists, return that. If no file, return None
Note: the files in filelist must exist, because their sizes on disk are checked"""
# sort from big to small
filelist = sorted(filelist, key=os.path.getsize)[::-1] # reversed, so big to small. Format [start:stop:step]
try:
factor = os.path.getsize(filelist[0]) / os.path.getsize(filelist[1])
if factor > 3:
return True
return filelist[0]
else:
return False
except:
# no second file at all
return True
if len(filelist) == 1:
# the only file, so biggest
return filelist[0]
else:
# no existing file(s)
return None
def deobfuscate(nzo, filelist: List[str], usefulname: str):
def deobfuscate(nzo, filelist: List[str], usefulname: str) -> List[str]:
"""
For files in filelist:
1. if a file has no meaningful extension, add it (for example ".txt" or ".png")
@@ -225,69 +232,60 @@ def deobfuscate(nzo, filelist: List[str], usefulname: str):
nzo: sabnzbd.nzbstuff.NzbObject
# to be sure, only keep really existing files and remove any duplicates:
filelist = set(f for f in filelist if os.path.isfile(f))
filtered_filelist = list(set(f for f in filelist if os.path.isfile(f)))
# Do not deobfuscate/rename anything if there is a typical DVD or Bluray directory:
ignored_movie_folders_with_dir_sep = tuple(os.path.sep + f + os.path.sep for f in IGNORED_MOVIE_FOLDERS)
match_ignored_movie_folders = [f for f in filelist if match_str(f, ignored_movie_folders_with_dir_sep)]
match_ignored_movie_folders = [f for f in filtered_filelist if match_str(f, ignored_movie_folders_with_dir_sep)]
if match_ignored_movie_folders:
logging.info(
"Skipping deobfuscation because of DVD/Bluray directory name(s), like: %s",
str(match_ignored_movie_folders)[:200],
)
nzo.set_unpack_info("Deobfuscate", T("Deobfuscate skipped due to DVD/Bluray directories"))
return
return filtered_filelist
# If needed, add a useful extension (by looking at file contents)
# Example: if 'kjladsflkjadf.adsflkjads' is probably a PNG, rename to 'kjladsflkjadf.adsflkjads.png'
newlist = []
new_filelist = []
nr_ext_renamed = 0
for file in filelist:
for file in filtered_filelist:
if file_extension.has_popular_extension(file):
# common extension, like .doc or .iso, so assume OK and change nothing
logging.debug("Extension of %s looks common", file)
newlist.append(file)
new_filelist.append(file)
else:
# uncommon (so: obfuscated) extension
new_extension_to_add = file_extension.what_is_most_likely_extension(file)
if new_extension_to_add:
if new_extension_to_add := file_extension.what_is_most_likely_extension(file):
new_name = get_unique_filename("%s%s" % (file, new_extension_to_add))
logging.info("Deobfuscate renaming (adding extension) %s to %s", file, new_name)
renamer(file, new_name)
newlist.append(new_name)
# Use output of renamer, just in case it's somehow modified by sanitization
new_filelist.append(renamer(file, new_name))
nr_ext_renamed += 1
else:
# no new extension found
newlist.append(file)
new_filelist.append(file)
if nr_ext_renamed:
nzo.set_unpack_info("Deobfuscate", T("Deobfuscate corrected the extension of %d file(s)") % nr_ext_renamed)
filelist = newlist
filtered_filelist = new_filelist
logging.debug("Trying to see if there are qualifying files to be deobfuscated")
nr_files_renamed = 0
# We pick the biggest file ... probably the most important file
# so sort filelist on size:
filelist = sorted(filelist, key=os.path.getsize, reverse=True)
if filelist:
biggest_file = filelist[0]
else:
biggest_file = None
if not biggest_file or not os.path.isfile(biggest_file):
logging.debug("Trying to see if there are qualifying files to be deobfuscated")
if not (biggest_file := get_biggest_file(filtered_filelist)) or not os.path.isfile(biggest_file):
# no file found, which is weird
logging.info("No file given, or not found (%s)", biggest_file)
return
logging.info("No biggest file found, or not found (%s)", biggest_file)
return filtered_filelist
logging.debug("Deobfuscate inspecting biggest file%s", biggest_file)
if not first_file_is_much_bigger(filelist):
logging.debug("%s excluded from deobfuscation because it is not much bigger than other file(s)", biggest_file)
return
if get_ext(biggest_file) in EXCLUDED_FILE_EXTS:
logging.debug("%s excluded from deobfuscation because of excluded extension", biggest_file)
return
return filtered_filelist
if not is_probably_obfuscated(biggest_file):
logging.debug("%s excluded from deobfuscation because filename does not look obfuscated", biggest_file)
return
return filtered_filelist
# if we get here, the biggest_file is relatively big, has no excluded extension, and is obfuscated
# Rename the biggest_file and make sure the new filename is unique
@@ -295,20 +293,89 @@ def deobfuscate(nzo, filelist: List[str], usefulname: str):
# construct new_name: <path><usefulname><extension>
new_name = get_unique_filename("%s%s" % (os.path.join(path, usefulname), get_ext(biggest_file)))
logging.info("Deobfuscate renaming %s to %s", biggest_file, new_name)
renamer(biggest_file, new_name)
filtered_filelist.remove(biggest_file)
filtered_filelist.append(renamer(biggest_file, new_name))
nr_files_renamed += 1
# Now find other files with the same basename in filelist, and rename them in the same way:
basedirfile = get_basename(biggest_file) # something like "/home/this/myiso"
for otherfile in filelist:
for otherfile in filtered_filelist[:]:
if otherfile.startswith(basedirfile) and os.path.isfile(otherfile):
# yes, same basedirfile, only different ending
remaining_ending = otherfile.replace(basedirfile, "") # might be long ext, like ".dut.srt" or "-sample.iso"
new_name = get_unique_filename("%s%s" % (os.path.join(path, usefulname), remaining_ending))
logging.info("Deobfuscate renaming %s to %s", otherfile, new_name)
# Rename and make sure the new filename is unique
renamer(otherfile, new_name)
filtered_filelist.remove(otherfile)
filtered_filelist.append(renamer(otherfile, new_name))
nr_files_renamed += 1
if nr_files_renamed:
nzo.set_unpack_info("Deobfuscate", T("Deobfuscate renamed %d file(s)") % nr_files_renamed)
return filtered_filelist
def without_extension(fullpathfilename: str) -> str:
"""Returns full file path, without extension
So '/some/dir/somefile.bin' results in '/some/dir/somefile'"""
return os.path.splitext(fullpathfilename)[0]
def deobfuscate_subtitles(nzo, filelist: List[str]):
"""
input:
nzo, so we can update result via set_unpack_info()
filelist must be a List of existing filenames
Find .srt subtitle files, and rename to match the biggest file (if there is a clearly biggest file)
Some_Big_File_2024.avi # biggest file
Some_Big_File_2024.srt # no renaming wanted
Some_Big_File_2024.ger.srt # no renaming wanted
14_English.srt # to be renamed
dut.srt # to be renamed
Something.else.txt # no renaming wanted, because no .srt
will result in
Some_Big_File_2024.avi
Some_Big_File_2024.srt
Some_Big_File_2024.ger.srt
Some_Big_File_2024.14_English.srt # renamed by prepending base name
Some_Big_File_2024.dut.srt # renamed by prepending base name
Something.else.txt
"""
# Can't be imported directly due to circular import
nzo: sabnzbd.nzbstuff.NzbObject
# find .srt files
if not (srt_files := [f for f in filelist if f.endswith(".srt")]):
logging.debug("No .srt files found, so nothing to do")
return None
# check there is a clearly biggest file
if not (biggest_file := get_biggest_file(filelist)):
logging.debug("No clearly biggest file found, so no subtitle renaming feasible")
return None
biggest_file_without_ext = without_extension(biggest_file) # get full path base name of biggest file
logging.debug(f"Using as base filename: {biggest_file_without_ext}")
# handle srt files one by one
nr_files_renamed = 0
for srt_file in srt_files:
if without_extension(srt_file).startswith(biggest_file_without_ext):
# already the same start as the biggest file, so skip
continue
# not the same start, so rename the srt file
nr_files_renamed += 1
filename_only = os.path.basename(srt_file) # like "14_English.srt", so without path
# now put that name after the base name of the biggestfile:
new_full_name = f"{biggest_file_without_ext}.{filename_only}" # put (renamed) srt behind that
unique_filename = get_unique_filename(new_full_name) # make sure it's really unique
renamer(srt_file, unique_filename) # ... and rename actual file on disk
if nr_files_renamed > 0:
# and put it into history to be shown in GUI
nzo.set_unpack_info("Deobfuscate", T("Deobfuscate renamed %d subtitle file(s)") % nr_files_renamed)

View File

@@ -165,11 +165,12 @@ class Server:
self.reset_article_queue()
def stop(self):
"""Remove all connections from server"""
"""Remove all connections and cached articles from server"""
for nw in self.idle_threads:
sabnzbd.Downloader.remove_socket(nw)
nw.hard_reset()
self.idle_threads = set()
self.reset_article_queue()
@synchronized(DOWNLOADER_LOCK)
def get_article(self):
@@ -195,10 +196,13 @@ class Server:
self.next_article_search = time.time() + _SERVER_CHECK_DELAY
return None
@synchronized(DOWNLOADER_LOCK)
def reset_article_queue(self):
logging.debug("Resetting article queue for %s", self)
"""Reset articles queued for the Server. Locked to prevent
articles getting stuck in the Server when enabled/disabled"""
logging.debug("Resetting article queue for %s (%s)", self, self.article_queue)
for article in self.article_queue:
sabnzbd.NzbQueue.reset_try_lists(article, remove_fetcher_from_trylist=False)
article.allow_new_fetcher()
self.article_queue = []
def request_addrinfo(self):
@@ -211,7 +215,7 @@ class Server:
def request_addrinfo_blocking(self):
"""Blocking attempt to run getaddrinfo() and Happy Eyeballs for specified server"""
logging.debug("Retrieving server address information for %s", self.host)
logging.debug("Retrieving server address information for %s", self)
# Disable IPV6 if desired
family = socket.AF_UNSPEC
@@ -229,7 +233,7 @@ class Server:
sabnzbd.Downloader.wakeup()
def __repr__(self):
return "<Server: %s:%s>" % (self.host, self.port)
return "<Server: id=%s, host=%s:%s>" % (self.id, self.host, self.port)
class Downloader(Thread):
@@ -329,7 +333,6 @@ class Downloader(Thread):
create = False
server.newid = newserver
server.restart = True
server.reset_article_queue()
self.server_restarts += 1
break
@@ -510,8 +513,8 @@ class Downloader(Thread):
# Handle broken articles directly
if not data_view:
if not article.search_new_server():
sabnzbd.NzbQueue.register_article(article, success=False)
article.nzf.nzo.increase_bad_articles_counter("missing_articles")
sabnzbd.NzbQueue.register_article(article, success=False)
return
# Decode and send to article cache
@@ -704,9 +707,14 @@ class Downloader(Thread):
except ssl.SSLWantReadError:
return
except (ConnectionError, ConnectionAbortedError):
# The ConnectionAbortedError is thrown by sabctools in case of fatal SSL-layer problems
# The ConnectionAbortedError is also thrown by sabctools in case of fatal SSL-layer problems
self.__reset_nw(nw, "Server closed connection", wait=False)
return
except BufferError:
# The BufferError is thrown when exceeding maximum buffer size
# Make sure to discard the article
self.__reset_nw(nw, "Maximum data buffer size exceeded", wait=False, retry_article=False)
return
article = nw.article
server = nw.server
@@ -960,14 +968,9 @@ class Downloader(Thread):
self.decode(nw.article)
nw.article.tries = 0
else:
# Retry again with the same server
logging.debug(
"Re-adding article %s from %s to server %s",
nw.article.article,
nw.article.nzf.filename,
nw.article.fetcher,
)
nw.article.fetcher.article_queue.append(nw.article)
# Allow all servers again for this article
# Do not use the article_queue, as the server could already have been disabled when we get here!
nw.article.allow_new_fetcher()
# Reset connection object
nw.hard_reset(wait)

View File

@@ -20,6 +20,7 @@ sabnzbd.encoding - Unicode/byte translation functions
"""
import locale
import pyunormalize
import chardet
from xml.sax.saxutils import escape
from typing import AnyStr
@@ -27,6 +28,11 @@ from typing import AnyStr
CODEPAGE = locale.getpreferredencoding()
def normalize_utf8(inputstring: str) -> str:
"""Make sure we return an utf8 normalized version"""
return pyunormalize.NFC(inputstring)
def utob(str_in: AnyStr) -> bytes:
"""Shorthand for converting UTF-8 string to bytes"""
if isinstance(str_in, bytes):
@@ -37,22 +43,19 @@ def utob(str_in: AnyStr) -> bytes:
def ubtou(str_in: AnyStr) -> str:
"""Shorthand for converting unicode bytes to UTF-8 string"""
if isinstance(str_in, str):
return str_in
return str_in.decode("utf-8")
return normalize_utf8(str_in)
return normalize_utf8(str_in.decode("utf-8"))
def platform_btou(str_in: AnyStr) -> str:
"""Return Unicode string, if not already Unicode, decode with locale encoding.
NOTE: Used for POpen because universal_newlines/text parameter doesn't
always work! We cannot use encoding-parameter because it's Python 3.7+
"""
"""Return Unicode string, if not already Unicode, decode with locale encoding"""
if isinstance(str_in, bytes):
try:
return ubtou(str_in)
except UnicodeDecodeError:
return str_in.decode(CODEPAGE, errors="replace").replace("?", "!")
return normalize_utf8(str_in.decode(CODEPAGE, errors="replace").replace("?", "!"))
else:
return str_in
return normalize_utf8(str_in)
def correct_unknown_encoding(str_or_bytes_in: AnyStr) -> str:
@@ -71,10 +74,10 @@ def correct_unknown_encoding(str_or_bytes_in: AnyStr) -> str:
except UnicodeDecodeError:
try:
# Try using 8-bit ASCII, if came from Windows
return str_or_bytes_in.decode("ISO-8859-1")
return normalize_utf8(str_or_bytes_in.decode("ISO-8859-1"))
except ValueError:
# Last resort we use the slow chardet package
return str_or_bytes_in.decode(chardet.detect(str_or_bytes_in)["encoding"])
return normalize_utf8(str_or_bytes_in.decode(chardet.detect(str_or_bytes_in)["encoding"]))
def correct_cherrypy_encoding(inputstring: str) -> str:

View File

@@ -46,7 +46,7 @@ except ImportError:
import sabnzbd
from sabnzbd.decorators import synchronized, cache_maintainer
from sabnzbd.constants import FUTURE_Q_FOLDER, JOB_ADMIN, GIGI, DEF_FILE_MAX, IGNORED_FILES_AND_FOLDERS, DEF_LOG_FILE
from sabnzbd.encoding import correct_unknown_encoding, utob, ubtou
from sabnzbd.encoding import correct_unknown_encoding, utob, ubtou, normalize_utf8
from sabnzbd.utils import rarfile
@@ -561,7 +561,7 @@ def globber(path: str, pattern: str = "*") -> List[str]:
"""Return matching base file/folder names in folder `path`"""
# Cannot use glob.glob() because it doesn't support Windows long name notation
if os.path.exists(path):
return [f for f in os.listdir(path) if safe_fnmatch(f, pattern)]
return [f for f in listdir_normalized(path) if safe_fnmatch(f, pattern)]
return []
@@ -569,7 +569,8 @@ def globber_full(path: str, pattern: str = "*") -> List[str]:
"""Return matching full file/folder names in folder `path`"""
# Cannot use glob.glob() because it doesn't support Windows long name notation
if os.path.exists(path):
return [os.path.join(path, f) for f in os.listdir(path) if safe_fnmatch(f, pattern)]
path = normalize_utf8(path)
return [os.path.join(path, f) for f in listdir_normalized(path) if safe_fnmatch(f, pattern)]
return []
@@ -581,7 +582,7 @@ def fix_unix_encoding(folder: str):
if not sabnzbd.WIN32 and not sabnzbd.MACOS:
for root, dirs, files in os.walk(folder):
for name in files:
new_name = correct_unknown_encoding(name)
new_name = normalize_utf8(correct_unknown_encoding(name))
if name != new_name:
try:
renamer(os.path.join(root, name), os.path.join(root, new_name))
@@ -804,6 +805,12 @@ def get_unique_filename(path: str) -> str:
return path
def listdir_normalized(input_dir: str) -> List[str]:
"""On macOS, the OS returns un-normalized results.
Always use the same normalization on all platforms"""
return [normalize_utf8(path) for path in os.listdir(input_dir)]
@synchronized(DIR_LOCK)
def listdir_full(input_dir: str, recursive: bool = True) -> List[str]:
"""List all files in dirs and sub-dirs"""
@@ -812,7 +819,7 @@ def listdir_full(input_dir: str, recursive: bool = True) -> List[str]:
for file in files:
# Ignore special folders and resources files created by macOS
if not sabnzbd.misc.match_str(root, IGNORED_FILES_AND_FOLDERS) and not file.startswith("._"):
filelist.append(os.path.join(root, file))
filelist.append(normalize_utf8(os.path.join(root, file)))
if not recursive:
break
return filelist
@@ -1386,7 +1393,7 @@ def pathbrowser(path: str, show_hidden: bool = False, show_files: bool = False)
# List all files and folders
file_list = []
for filename in os.listdir(path):
for filename in listdir_normalized(path):
fpath = os.path.join(path, filename)
isdir = os.path.isdir(fpath)

View File

@@ -25,7 +25,7 @@ import socket
import time
import urllib.error
import urllib.request
from typing import Callable
from typing import Callable, Optional
import socks
@@ -33,9 +33,10 @@ import sabnzbd
import sabnzbd.cfg
from sabnzbd.encoding import ubtou
from sabnzbd.happyeyeballs import happyeyeballs, family_type
from sabnzbd.constants import DEF_NETWORKING_SHORT_TIMEOUT
def timeout(max_timeout: float):
def timeout(max_timeout: int):
"""Timeout decorator, parameter in seconds."""
def timeout_decorator(item: Callable) -> Callable:
@@ -56,29 +57,29 @@ def timeout(max_timeout: float):
return timeout_decorator
@timeout(3.0)
@timeout(DEF_NETWORKING_SHORT_TIMEOUT)
def addresslookup(myhost):
return socket.getaddrinfo(myhost, 80)
@timeout(3.0)
@timeout(DEF_NETWORKING_SHORT_TIMEOUT)
def addresslookup4(myhost):
return socket.getaddrinfo(myhost, 80, socket.AF_INET)
@timeout(3.0)
@timeout(DEF_NETWORKING_SHORT_TIMEOUT)
def addresslookup6(myhost):
return socket.getaddrinfo(myhost, 80, socket.AF_INET6)
def active_socks5_proxy():
def active_socks5_proxy() -> Optional[str]:
"""Return the active proxy"""
if socket.socket == socks.socksocket:
return "%s:%s" % socks.socksocket.default_proxy[1:3]
return None
def dnslookup():
def dnslookup() -> bool:
"""Perform a basic DNS lookup"""
start = time.time()
try:
@@ -90,7 +91,7 @@ def dnslookup():
return result
def local_ipv4():
def local_ipv4() -> Optional[str]:
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s_ipv4:
# Option: use 100.64.1.1 (IANA-Reserved IPv4 Prefix for Shared Address Space)
@@ -103,12 +104,17 @@ def local_ipv4():
return ipv4
def public_ip(family=socket.AF_UNSPEC):
def public_ip(family: int = socket.AF_UNSPEC) -> Optional[str]:
"""
Reports the client's public IP address (IPv4 or IPv6, if specified by family), as reported by selftest host
"""
start = time.time()
if resolvehostaddress := happyeyeballs(sabnzbd.cfg.selftest_host(), port=443, family=family):
if resolvehostaddress := happyeyeballs(
sabnzbd.cfg.selftest_host(),
port=443,
timeout=DEF_NETWORKING_SHORT_TIMEOUT,
family=family,
):
resolvehostip = resolvehostaddress.ipaddress
else:
logging.debug("Error resolving my IP address: resolvehost not found")
@@ -126,7 +132,7 @@ def public_ip(family=socket.AF_UNSPEC):
req = urllib.request.Request(resolveurl)
req.add_header("Host", sabnzbd.cfg.selftest_host())
req.add_header("User-Agent", "SABnzbd/%s" % sabnzbd.__version__)
with urllib.request.urlopen(req, timeout=2) as open_req:
with urllib.request.urlopen(req, timeout=DEF_NETWORKING_SHORT_TIMEOUT) as open_req:
client_ip = ubtou(open_req.read().strip())
# Make sure it's a valid IPv4 or IPv6 address
@@ -146,11 +152,11 @@ def public_ip(family=socket.AF_UNSPEC):
return client_ip
def public_ipv4():
def public_ipv4() -> Optional[str]:
return public_ip(family=socket.AF_INET)
def local_ipv6():
def local_ipv6() -> Optional[str]:
"""
return IPv6 address on local LAN interface. So a first check if there is IPv6 connectivity
"""
@@ -167,8 +173,8 @@ def local_ipv6():
return ipv6_address
def public_ipv6():
if local_address := local_ipv6():
def public_ipv6() -> Optional[str]:
if (local_address := local_ipv6()) and not sabnzbd.misc.ip_in_subnet(local_address, "fe80::/10"):
if public_address := public_ip(family=socket.AF_INET6):
return public_address
elif not sabnzbd.misc.is_lan_addr(local_address):

View File

@@ -34,7 +34,7 @@ from typing import Tuple, Union, Optional
from more_itertools import roundrobin
import sabnzbd.cfg as cfg
from sabnzbd.constants import DEF_TIMEOUT
from sabnzbd.constants import DEF_NETWORKING_TIMEOUT
from sabnzbd.decorators import cache_maintainer
# How long to delay between connection attempts? The RFC suggests 250ms, but this is
@@ -118,7 +118,12 @@ def do_socket_connect(result_queue: queue.Queue, addrinfo: AddrInfo, timeout: in
@cache_maintainer(clear_time=10)
@functools.lru_cache(maxsize=None)
def happyeyeballs(host: str, port: int, timeout: int = DEF_TIMEOUT, family=socket.AF_UNSPEC) -> Optional[AddrInfo]:
def happyeyeballs(
host: str,
port: int,
timeout: int = DEF_NETWORKING_TIMEOUT,
family=socket.AF_UNSPEC,
) -> Optional[AddrInfo]:
"""Return the fastest result of getaddrinfo() based on RFC 6555/8305 (Happy Eyeballs),
including IPv6 addresses if desired. Returns None in case no addresses were returned
by getaddrinfo or if no connection could be made to any of the addresses.

View File

@@ -80,7 +80,7 @@ from sabnzbd.constants import (
GUESSIT_SORT_TYPES,
VALID_NZB_FILES,
VALID_ARCHIVES,
DEF_TEST_TIMEOUT,
DEF_NETWORKING_TEST_TIMEOUT,
)
from sabnzbd.lang import list_languages
from sabnzbd.api import (
@@ -395,9 +395,8 @@ def Raiser(root: str = "", **kwargs):
if kwargs:
root = "%s?%s" % (root, urllib.parse.urlencode(kwargs))
# Optionally add the leading /sabnzbd/ (or what the user set)
if not root.startswith(cfg.url_base()):
root = cherrypy.request.script_name + root
# Add the leading /sabnzbd/ (or what the user set)
root = cfg.url_base() + root
# Log the redirect
if cfg.api_logging():
@@ -434,6 +433,7 @@ class MainPage:
info["cpumodel"] = get_cpu_name()
info["cpusimd"] = sabnzbd.decoder.SABCTOOLS_SIMD
info["docker"] = "Docker" if sabnzbd.DOCKER else ""
# Have logout only with HTML and if inet=5, only when we are external
info["have_logout"] = (
@@ -577,7 +577,7 @@ class Wizard:
def get_access_info():
"""Build up a list of url's that sabnzbd can be accessed from"""
# Access_url is used to provide the user a link to SABnzbd depending on the host
cherryhost = cfg.cherryhost()
web_host = cfg.web_host()
host = socket.gethostname().lower()
logging.info("hostname is", host)
socks = [host]
@@ -587,7 +587,7 @@ def get_access_info():
except:
addresses = []
if cherryhost == "0.0.0.0":
if web_host == "0.0.0.0":
# Grab a list of all ips for the hostname
for addr in addresses:
address = addr[4][0]
@@ -595,7 +595,7 @@ def get_access_info():
if ":" not in address and address not in socks:
socks.append(address)
socks.insert(0, "localhost")
elif cherryhost == "::":
elif web_host == "::":
# Grab a list of all ips for the hostname
for addr in addresses:
address = addr[4][0]
@@ -605,8 +605,8 @@ def get_access_info():
if address not in socks:
socks.append(address)
socks.insert(0, "localhost")
elif cherryhost:
socks = [cherryhost]
elif web_host:
socks = [web_host]
# Add the current requested URL as the base
access_url = urllib.parse.urljoin(cherrypy.request.base, cfg.url_base())
@@ -617,9 +617,9 @@ def get_access_info():
if cfg.enable_https() and cfg.https_port():
url = "https://%s:%s%s" % (sock, cfg.https_port(), cfg.url_base())
elif cfg.enable_https():
url = "https://%s:%s%s" % (sock, cfg.cherryport(), cfg.url_base())
url = "https://%s:%s%s" % (sock, cfg.web_port(), cfg.url_base())
else:
url = "http://%s:%s%s" % (sock, cfg.cherryport(), cfg.url_base())
url = "http://%s:%s%s" % (sock, cfg.web_port(), cfg.url_base())
urls.append(url)
# Return a unique list
@@ -641,7 +641,7 @@ class LoginPage:
# Check if there's even a username/password set
if check_login():
raise Raiser(cherrypy.request.script_name + "/")
raise Raiser("/")
# Check login info
if kwargs.get("username") == cfg.username() and kwargs.get("password") == cfg.password():
@@ -650,7 +650,7 @@ class LoginPage:
# Log the success
logging.info("Successful login from %s", cherrypy.request.remote_label)
# Redirect
raise Raiser(cherrypy.request.script_name + "/")
raise Raiser("/")
elif kwargs.get("username") or kwargs.get("password"):
info["error"] = T("Authentication failed, check username/password.")
# Warn about the potential security problem
@@ -1144,7 +1144,9 @@ 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_TEST_TIMEOUT)):
if not happyeyeballs(
host, int_conv(port), int_conv(kwargs.get("timeout"), default=DEF_NETWORKING_TEST_TIMEOUT)
):
return badParameterResponse(T('Server address "%s:%s" is not valid.') % (host, port), ajax)
# Default server name is just the host name
@@ -2156,8 +2158,10 @@ class ConfigNotify:
for section in NOTIFY_OPTIONS:
for option in NOTIFY_OPTIONS[section]:
# Use get_string to make sure lists are displayed correctly
conf[option] = config.get_config(section, option).get_string()
conf[option] = config.get_config(section, option)()
# Use get_string to make sure lists are displayed correctly
conf["email_to"] = cfg.email_to.get_string()
return template_filtered_response(
file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_notify.tmpl"),

View File

@@ -30,6 +30,7 @@ from typing import Dict
import sabctools
import sabnzbd
from sabnzbd.constants import DEF_NETWORKING_SHORT_TIMEOUT
from sabnzbd.happyeyeballs import happyeyeballs, family_type
TEST_HOSTNAME = "sabnzbd.org"
@@ -37,7 +38,6 @@ TEST_PORT = 443
TEST_FILE = "/tests/internetspeed/100MB.bin"
TEST_FILE_SIZE = 100 * 1024 * 1024
TEST_REQUEST = f"GET {TEST_FILE} HTTP/1.1\nHost: {TEST_HOSTNAME}\nUser-Agent: SABnzbd/{sabnzbd.__version__}\n\n"
SOCKET_TIMEOUT = 3
BUFFER_SIZE = 5 * 1024 * 1024 # Each connection will allocate its own buffer, so mind the memory usage!
NR_CONNECTIONS = 5
@@ -75,33 +75,45 @@ def internetspeed_worker(secure_sock: ssl.SSLSocket, socket_speed: Dict[ssl.SSLS
pass
def internetspeed_interal(test_time_limit: int = TIME_LIMIT, family: int = socket.AF_UNSPEC) -> float:
def internetspeed_interal(family: int = socket.AF_UNSPEC) -> float:
"""Measure internet speed from a test-download using our optimized SSL-code"""
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
socket_speed = {}
try:
addrinfo = happyeyeballs(TEST_HOSTNAME, TEST_PORT, SOCKET_TIMEOUT, family)
if not (addrinfo := happyeyeballs(TEST_HOSTNAME, TEST_PORT, DEF_NETWORKING_SHORT_TIMEOUT, family)):
# no addrinfo from happyeyeballs, so no connection was possible
return 0.0 # no speed at all
for _ in range(NR_CONNECTIONS):
sock = socket.socket(addrinfo.family, addrinfo.type)
sock.settimeout(SOCKET_TIMEOUT)
sock.settimeout(DEF_NETWORKING_SHORT_TIMEOUT)
sock.connect(addrinfo.sockaddr)
secure_sock = context.wrap_socket(sock, server_hostname=TEST_HOSTNAME)
secure_sock.setblocking(False)
socket_speed[secure_sock] = 0
for secure_sock in socket_speed:
threading.Thread(target=internetspeed_worker, args=(secure_sock, socket_speed), daemon=True).start()
threading.Thread(
target=internetspeed_worker,
args=(secure_sock, socket_speed),
daemon=True,
).start()
except Exception:
logging.info("Internet Bandwidth connection failure", exc_info=True)
return 0.0
# We let the workers finish
time.sleep(test_time_limit + 0.5)
time.sleep(TIME_LIMIT + 0.5)
speed = sum(socket_speed.values()) / 1024 / 1024
logging.debug("Internet Bandwidth (%s) = %.2f MB/s - %.2f Mbps", family_type(family), speed, speed * 8.05)
logging.debug(
"Internet Bandwidth (%s) = %.2f MB/s - %.2f Mbps",
family_type(family),
speed,
speed * 8.05,
)
return speed

View File

@@ -167,10 +167,9 @@ def calc_age(date: datetime.datetime, trans: bool = False) -> str:
def safe_lower(txt: Any) -> str:
"""Return lowercased string. Return '' for None"""
if txt:
if txt := str_conv(txt):
return txt.lower()
else:
return ""
return ""
def is_none(inp: Any) -> bool:
@@ -219,17 +218,18 @@ def cat_pp_script_sanitizer(
script: Optional[str] = None,
) -> Tuple[Optional[Union[int, str]], Optional[str], Optional[str]]:
"""Basic sanitizer from outside input to a bit more predictable values"""
# * and Default are valid values
if safe_lower(cat) in ("", "none"):
cat = None
# Cannot use "not pp" because pp can also be 0
if pp in ("", "-1"):
if safe_lower(pp) in ("", "-1", "none"):
pp = None
# Check for valid script is performed in NzbObject init
if not script or script.lower() == "default":
if not script or safe_lower(script) == "default":
script = None
if not cat or cat.lower() in ("default", "*"):
cat = None
return cat, pp, script
@@ -281,7 +281,7 @@ def cat_to_opts(cat, pp=None, script=None, priority=None) -> Tuple[str, int, str
def pp_to_opts(pp: Optional[int]) -> Tuple[bool, bool, bool]:
"""Convert numeric processing options to (repair, unpack, delete)"""
# Convert the pp to an int
pp = sabnzbd.interface.int_conv(pp)
pp = int_conv(pp)
if pp == 0:
return False, False, False
if pp == 1:
@@ -882,7 +882,18 @@ def format_time_string(seconds: float) -> str:
return " ".join(completestr)
def int_conv(value: Any, default: Any = 0) -> int:
def str_conv(value: Any, default: str = "") -> str:
"""Safe conversion to str (None will be converted to empty string)
Returns empty string or requested default value"""
if value is None:
return default
try:
return str(value)
except:
return default
def int_conv(value: Any, default: int = 0) -> int:
"""Safe conversion to int (can handle None)
Returns 0 or requested default value"""
try:
@@ -891,6 +902,12 @@ def int_conv(value: Any, default: Any = 0) -> int:
return default
def bool_conv(value: Any) -> bool:
"""Safe conversion to bool (can handle None)
Returns False in case of None or non-convertable value"""
return bool(int_conv(value))
def create_https_certificates(ssl_cert, ssl_key):
"""Create self-signed HTTPS certificates and store in paths 'ssl_cert' and 'ssl_key'"""
try:

View File

@@ -63,6 +63,7 @@ from sabnzbd.filesystem import (
SEVENMULTI_RE,
is_size,
get_basename,
listdir_normalized,
)
from sabnzbd.nzbstuff import NzbObject
import sabnzbd.cfg as cfg
@@ -1020,7 +1021,7 @@ def par2_repair(nzo: NzbObject, setname: str) -> Tuple[bool, bool]:
return False, True
parfile = os.path.join(nzo.download_path, parfile_nzf.filename)
old_dir_content = os.listdir(nzo.download_path)
old_dir_content = listdir_normalized(nzo.download_path)
used_joinables = ()
joinables = ()
used_for_repair = ()
@@ -1080,7 +1081,7 @@ def par2_repair(nzo: NzbObject, setname: str) -> Tuple[bool, bool]:
try:
if cfg.enable_par_cleanup():
deletables = []
new_dir_content = os.listdir(nzo.download_path)
new_dir_content = listdir_normalized(nzo.download_path)
# If Multipar or par2cmdline repairs a broken part of a joinable, it doesn't list it as such.
# So we need to manually add all joinables of the set to the list of used joinables.

View File

@@ -30,14 +30,14 @@ from typing import Optional, Tuple, Union
import sabnzbd
import sabnzbd.cfg
from sabnzbd.constants import DEF_TIMEOUT, NNTP_BUFFER_SIZE
from sabnzbd.constants import DEF_NETWORKING_TIMEOUT, NNTP_BUFFER_SIZE, NTTP_MAX_BUFFER_SIZE
from sabnzbd.encoding import utob, ubtou
from sabnzbd.happyeyeballs import AddrInfo
from sabnzbd.decorators import synchronized, DOWNLOADER_LOCK
from sabnzbd.misc import int_conv
# Set pre-defined socket timeout
socket.setdefaulttimeout(DEF_TIMEOUT)
socket.setdefaulttimeout(DEF_NETWORKING_TIMEOUT)
class NNTPPermanentError(Exception):
@@ -230,6 +230,10 @@ class NewsWrapper:
def increase_data_buffer(self):
"""Resize the buffer in the extremely unlikely case that it overflows"""
# Sanity check before we go any further
if len(self.data) > NTTP_MAX_BUFFER_SIZE:
raise BufferError("Maximum data buffer size exceeded")
# Input needs to be integer, floats don't work
new_buffer = sabctools.bytearray_malloc(len(self.data) + NNTP_BUFFER_SIZE // 2)
new_buffer[: len(self.data)] = self.data

Some files were not shown because too many files have changed in this diff Show More