mirror of
https://github.com/sabnzbd/sabnzbd.git
synced 2025-12-25 08:38:05 -05:00
Compare commits
111 Commits
4.3.2
...
bugfix/nor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cbae891d0 | ||
|
|
fb0ef21768 | ||
|
|
277679ef53 | ||
|
|
e7e47bbcb0 | ||
|
|
65ffb5ca81 | ||
|
|
6cf308e441 | ||
|
|
870fa40c91 | ||
|
|
39d9eaec2a | ||
|
|
6fd4d0882c | ||
|
|
32591f7c46 | ||
|
|
6b47d1126d | ||
|
|
53df39dd12 | ||
|
|
b5d33fc17c | ||
|
|
8a517b668e | ||
|
|
67135ba4c8 | ||
|
|
c9efda1889 | ||
|
|
250869c242 | ||
|
|
844650e6be | ||
|
|
6685c72894 | ||
|
|
154a5e4989 | ||
|
|
93d302c9d7 | ||
|
|
f664df7f05 | ||
|
|
8fbf50292b | ||
|
|
f3fed43022 | ||
|
|
2d323ba18c | ||
|
|
1ec30a56e1 | ||
|
|
b98f3a07dd | ||
|
|
46170ffb3d | ||
|
|
5e8b41be5a | ||
|
|
47a2d5387d | ||
|
|
1e61239933 | ||
|
|
aedbf35be8 | ||
|
|
cf9540842b | ||
|
|
9205b9161b | ||
|
|
07b64b4abb | ||
|
|
c56145e424 | ||
|
|
ef11aba166 | ||
|
|
fcf03e9a59 | ||
|
|
6662065bb1 | ||
|
|
4973672892 | ||
|
|
efa73a52e1 | ||
|
|
82098a6228 | ||
|
|
07250aa355 | ||
|
|
46caa8b33f | ||
|
|
b0564c1bab | ||
|
|
3c8a85ff35 | ||
|
|
656c329912 | ||
|
|
983253908c | ||
|
|
cef0eeb25b | ||
|
|
a9eace759f | ||
|
|
ad0e7bf5df | ||
|
|
bea348232a | ||
|
|
1519dbc554 | ||
|
|
297455cd35 | ||
|
|
56b68024db | ||
|
|
09aa09a55b | ||
|
|
f1d134fe2e | ||
|
|
621d586c2f | ||
|
|
4966f9c753 | ||
|
|
059d82f6f0 | ||
|
|
bca41db6b7 | ||
|
|
613ba0b05f | ||
|
|
5f3b03ed87 | ||
|
|
f6fe801000 | ||
|
|
8ff34660d8 | ||
|
|
0c1b8dd60a | ||
|
|
8e8ee7a3ab | ||
|
|
9145a90e33 | ||
|
|
02b4a116dd | ||
|
|
e504b288a2 | ||
|
|
5128f788f0 | ||
|
|
044fe7a26a | ||
|
|
4ed2565101 | ||
|
|
abbd77bac4 | ||
|
|
38c9a52e1d | ||
|
|
f89114ca7e | ||
|
|
773d567eed | ||
|
|
ee717b679e | ||
|
|
f50810fb58 | ||
|
|
08b1b20b34 | ||
|
|
edca79af83 | ||
|
|
dd5dcd0ec9 | ||
|
|
820824e443 | ||
|
|
4c2dfdee43 | ||
|
|
ece4437c3a | ||
|
|
74daa15ce4 | ||
|
|
4f81bc8a26 | ||
|
|
e77d15f75e | ||
|
|
8668852574 | ||
|
|
7e944f393e | ||
|
|
1646fbfd17 | ||
|
|
72b0521325 | ||
|
|
8aa53fd43f | ||
|
|
aa67edb2d9 | ||
|
|
0054b17f41 | ||
|
|
2af2cc7370 | ||
|
|
5aa7aafebb | ||
|
|
3bd0f7c1e0 | ||
|
|
9c8d21f6db | ||
|
|
4947effeb7 | ||
|
|
b8fd9e6e31 | ||
|
|
2a02c93e4b | ||
|
|
a0ef520e06 | ||
|
|
a9eb32eba6 | ||
|
|
592ef0e645 | ||
|
|
cce53ee058 | ||
|
|
93755aa6d8 | ||
|
|
b1d42c7c22 | ||
|
|
8286b7b830 | ||
|
|
fbaa3c0420 | ||
|
|
ba6c30cf24 |
@@ -40,3 +40,4 @@ f06891926661986fff52d6eb4b4cb120c71972d1
|
||||
9bcbcaefdfecc85aedfd8e2f8aaa1ca7f959404e
|
||||
433dcab02b29f7bd3827e237434034deecc1b549
|
||||
9f6a9f991222efccc87b45a701086c95629c67b6
|
||||
f89114ca7e1b20bf8e645ecd0b52b707ec857aa9
|
||||
|
||||
1
.github/renovate.json
vendored
1
.github/renovate.json
vendored
@@ -15,6 +15,7 @@
|
||||
"builder/release-requirements.txt"
|
||||
]
|
||||
},
|
||||
"ignorePaths": [],
|
||||
"ignoreDeps": [
|
||||
"jaraco.text",
|
||||
"jaraco.context",
|
||||
|
||||
10
.github/workflows/build_release.yml
vendored
10
.github/workflows/build_release.yml
vendored
@@ -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:
|
||||
|
||||
10
.github/workflows/integration_testing.yml
vendored
10
.github/workflows/integration_testing.yml
vendored
@@ -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
|
||||
|
||||
47
README.mkd
47
README.mkd
@@ -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
|
||||
|
||||
|
||||
182
SABnzbd.py
182
SABnzbd.py
@@ -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(
|
||||
|
||||
@@ -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"},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
PyGithub==2.3.0
|
||||
PyGithub==2.4.0
|
||||
praw==7.7.1
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -704,6 +704,7 @@ function ViewModel() {
|
||||
data.append("apikey", apiKey);
|
||||
|
||||
// Add this one
|
||||
debugger
|
||||
$.ajax({
|
||||
url: "./api",
|
||||
type: "POST",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
BIN
osx/7zip/7zz
BIN
osx/7zip/7zz
Binary file not shown.
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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í"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "פריקה ישירה"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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ă"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user