mirror of
https://github.com/sabnzbd/sabnzbd.git
synced 2026-02-08 14:52:26 -05:00
Compare commits
85 Commits
4.6.0Beta1
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e898f92f49 | ||
|
|
9a37306ce5 | ||
|
|
47e71912d5 | ||
|
|
b90be6e35a | ||
|
|
4e7a70c5e7 | ||
|
|
9fc2215fc8 | ||
|
|
cba63c0c3e | ||
|
|
2b62846122 | ||
|
|
cb5030d152 | ||
|
|
499e9639e9 | ||
|
|
e1ea4f1e7e | ||
|
|
e40098d0e7 | ||
|
|
5025f9ec5d | ||
|
|
26a485374c | ||
|
|
5b3a8fcd3f | ||
|
|
44447ab416 | ||
|
|
040573c75c | ||
|
|
16a6936053 | ||
|
|
e2921e7b9c | ||
|
|
e1cd1eed83 | ||
|
|
a4de704967 | ||
|
|
d9f9aa5bea | ||
|
|
f4b73cf9ec | ||
|
|
ddc84542eb | ||
|
|
9624a285f1 | ||
|
|
43a9678f07 | ||
|
|
4ee41e331c | ||
|
|
062dc9fa11 | ||
|
|
d215d4b0d7 | ||
|
|
04711886d9 | ||
|
|
a19b3750e3 | ||
|
|
eff5f663ab | ||
|
|
46c98acff3 | ||
|
|
df5fad29bc | ||
|
|
27d222943c | ||
|
|
3384beed24 | ||
|
|
bf41237135 | ||
|
|
3d4fabfbdf | ||
|
|
cf14e24036 | ||
|
|
d0c2b74181 | ||
|
|
d21a111993 | ||
|
|
3e7dcce365 | ||
|
|
5594d4d6eb | ||
|
|
605a1b30be | ||
|
|
a2cb861640 | ||
|
|
df1c0915d0 | ||
|
|
4d73c3e9c0 | ||
|
|
17dcff49b2 | ||
|
|
220186299b | ||
|
|
ae30be382b | ||
|
|
13b10fd9bb | ||
|
|
d9bb544caf | ||
|
|
bf2080068c | ||
|
|
b4e8c80bc9 | ||
|
|
33aa4f1199 | ||
|
|
ecb36442d3 | ||
|
|
0bbe34242e | ||
|
|
7c6abd9528 | ||
|
|
448c034f79 | ||
|
|
9d5cf9fc5b | ||
|
|
4f9d0fb7d4 | ||
|
|
240d5b4ff7 | ||
|
|
a2161ba89b | ||
|
|
68e193bf56 | ||
|
|
b5dda7c52d | ||
|
|
b6691003db | ||
|
|
ed655553c8 | ||
|
|
316b96c653 | ||
|
|
62401cba27 | ||
|
|
3cabf44ce3 | ||
|
|
a637d218c4 | ||
|
|
63c03b42a9 | ||
|
|
4539837fad | ||
|
|
a0cd48e3f5 | ||
|
|
ceeb7cb162 | ||
|
|
f9f4e1b028 | ||
|
|
6487944c6c | ||
|
|
239fddf39c | ||
|
|
8ada8b2fd9 | ||
|
|
b19bd65495 | ||
|
|
e3ea5fdd64 | ||
|
|
4fdb89701a | ||
|
|
9165c4f304 | ||
|
|
4152f0ba6a | ||
|
|
3eaab17739 |
2
.github/renovate.json
vendored
2
.github/renovate.json
vendored
@@ -7,7 +7,7 @@
|
||||
"schedule": [
|
||||
"before 8am on Monday"
|
||||
],
|
||||
"baseBranches": ["develop", "feature/uvicorn"],
|
||||
"baseBranches": ["develop"],
|
||||
"pip_requirements": {
|
||||
"fileMatch": [
|
||||
"requirements.txt",
|
||||
|
||||
39
.github/workflows/build_release.yml
vendored
39
.github/workflows/build_release.yml
vendored
@@ -8,8 +8,18 @@ env:
|
||||
|
||||
jobs:
|
||||
build_windows:
|
||||
name: Build Windows binary
|
||||
runs-on: windows-2022
|
||||
name: Build Windows binary (${{ matrix.architecture }})
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- architecture: x64
|
||||
runs-on: windows-2022
|
||||
- architecture: arm64
|
||||
runs-on: windows-11-arm
|
||||
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -17,15 +27,16 @@ jobs:
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.14"
|
||||
architecture: "x64"
|
||||
architecture: ${{ matrix.architecture }}
|
||||
cache: pip
|
||||
cache-dependency-path: "**/requirements.txt"
|
||||
- name: Install Python dependencies
|
||||
# Without dependencies to make sure everything is covered in the requirements.txt
|
||||
# Special cryptography is due to https://github.com/pyca/cryptography/pull/14216
|
||||
run: |
|
||||
python --version
|
||||
python -m pip install --upgrade pip wheel
|
||||
pip install --upgrade -r requirements.txt --no-dependencies
|
||||
pip install --upgrade -r requirements.txt --no-dependencies --only-binary=cryptography
|
||||
pip install --upgrade -r builder/requirements.txt --no-dependencies
|
||||
- name: Build Windows standalone binary
|
||||
id: windows_binary
|
||||
@@ -34,8 +45,8 @@ jobs:
|
||||
uses: actions/upload-artifact@v6
|
||||
id: upload-unsigned-binary
|
||||
with:
|
||||
path: "*-win64-bin.zip"
|
||||
name: Windows standalone binary
|
||||
path: "*-win*-bin.zip"
|
||||
name: Windows standalone binary (${{ matrix.architecture }})
|
||||
- name: Sign Windows standalone binary
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
@@ -52,19 +63,21 @@ jobs:
|
||||
uses: actions/upload-artifact@v6
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
with:
|
||||
name: Windows standalone binary (signed)
|
||||
name: Windows standalone binary (${{ matrix.architecture }}, signed)
|
||||
path: "signed"
|
||||
- name: Build Windows installer
|
||||
if: matrix.architecture == 'x64'
|
||||
run: python builder/package.py installer
|
||||
- name: Upload Windows installer
|
||||
if: matrix.architecture == 'x64'
|
||||
uses: actions/upload-artifact@v6
|
||||
id: upload-unsigned-installer
|
||||
with:
|
||||
path: "*-win-setup.exe"
|
||||
name: Windows installer
|
||||
name: Windows installer (${{ matrix.architecture }})
|
||||
- name: Sign Windows installer
|
||||
if: matrix.architecture == 'x64' && contains(github.ref, 'refs/tags/')
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ secrets.SIGNPATH_ORG_ID }}
|
||||
@@ -75,10 +88,10 @@ jobs:
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: "signed"
|
||||
- name: Upload Windows installer (signed)
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
if: matrix.architecture == 'x64' && contains(github.ref, 'refs/tags/')
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Windows installer (signed)
|
||||
name: Windows installer (${{ matrix.architecture }}, signed)
|
||||
path: "signed/*-win-setup.exe"
|
||||
|
||||
build_macos:
|
||||
@@ -89,7 +102,7 @@ jobs:
|
||||
# We need the official Python, because the GA ones only support newer macOS versions
|
||||
# The deployment target is picked up by the Python build tools automatically
|
||||
# If updated, make sure to also set LSMinimumSystemVersion in SABnzbd.spec
|
||||
PYTHON_VERSION: "3.14.2"
|
||||
PYTHON_VERSION: "3.14.3"
|
||||
MACOSX_DEPLOYMENT_TARGET: "10.15"
|
||||
# We need to force compile for universal2 support
|
||||
CFLAGS: -arch x86_64 -arch arm64
|
||||
@@ -167,7 +180,7 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
linux_arch: amd64
|
||||
linux_arch: x64
|
||||
- os: ubuntu-24.04-arm
|
||||
linux_arch: arm64
|
||||
|
||||
|
||||
2
.github/workflows/integration_testing.yml
vendored
2
.github/workflows/integration_testing.yml
vendored
@@ -11,11 +11,11 @@ jobs:
|
||||
- name: Black Code Formatter
|
||||
uses: lgeiger/black-action@master
|
||||
with:
|
||||
# Tools folder excluded for now due to https://github.com/psf/black/issues/4963
|
||||
args: >
|
||||
SABnzbd.py
|
||||
sabnzbd
|
||||
scripts
|
||||
tools
|
||||
builder
|
||||
builder/SABnzbd.spec
|
||||
tests
|
||||
|
||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
run: |
|
||||
python3 tools/make_mo.py
|
||||
- name: Push translatable and translated texts back to repo
|
||||
uses: stefanzweifel/git-auto-commit-action@v7.0.0
|
||||
uses: stefanzweifel/git-auto-commit-action@v7.1.0
|
||||
if: env.TX_TOKEN
|
||||
with:
|
||||
commit_message: |
|
||||
|
||||
23
README.mkd
23
README.mkd
@@ -1,27 +1,36 @@
|
||||
Release Notes - SABnzbd 4.6.0 Beta 1
|
||||
Release Notes - SABnzbd 5.0.0 Beta 1
|
||||
=========================================================
|
||||
|
||||
This is the first beta release of version 4.6.
|
||||
This is the first beta release of version 5.0.
|
||||
|
||||
## New features in 4.6.0
|
||||
Due to several fundamental changes we decided to
|
||||
not just call this 4.6 but promote it to 5.0!
|
||||
|
||||
* Added default support for NNTP Pipelining which eliminates idle waiting
|
||||
between requests, significantly improving speeds on high-latency connections.
|
||||
## New features in 5.0.0
|
||||
|
||||
* Added support for NNTP Pipelining which eliminates idle waiting between
|
||||
requests, significantly improving speeds on high-latency connections.
|
||||
Read more here: https://sabnzbd.org/wiki/advanced/nntp-pipelining
|
||||
* Dynamically increase Assembler limits on faster connections.
|
||||
* Implemented Direct Write to optimize assembly of downloaded files.
|
||||
Read more here: https://sabnzbd.org/wiki/advanced/direct-write
|
||||
* Complete redesign of article cache.
|
||||
* Improved disk speed measurement in Status window.
|
||||
* Enable `verify_xff_header` by default.
|
||||
* Reduce delays between jobs during post-processing.
|
||||
* If a download only has `.nzb` files inside, the new downloads
|
||||
will include the name of the original download.
|
||||
* No longer show tracebacks in the browser, only in the logs.
|
||||
* Dropped support for Python 3.8.
|
||||
* Windows: Added Windows ARM (portable) release.
|
||||
|
||||
## Bug fixes since 4.5.0
|
||||
|
||||
* `Check before download` could get stuck or fail to reject.
|
||||
* Windows: Tray icon disappears after Explorer restart.
|
||||
* No error was shown in case NZB upload failed.
|
||||
* Correct mobile layout if `Full Width` is enabled.
|
||||
* Aborted Direct Unpack could result in no files being unpacked.
|
||||
* Sorting of files inside jobs was inconsistent.
|
||||
* Windows: Tray icon disappears after Explorer restart.
|
||||
* macOS: Slow to start on some network setups.
|
||||
|
||||
|
||||
|
||||
12
SABnzbd.py
12
SABnzbd.py
@@ -236,9 +236,7 @@ def print_help():
|
||||
|
||||
|
||||
def print_version():
|
||||
print(
|
||||
(
|
||||
"""
|
||||
print(("""
|
||||
%s-%s
|
||||
|
||||
(C) Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
@@ -247,10 +245,7 @@ This is free software, and you are welcome to redistribute it
|
||||
under certain conditions. It is licensed under the
|
||||
GNU GENERAL PUBLIC LICENSE Version 2 or (at your option) any later version.
|
||||
|
||||
"""
|
||||
% (sabnzbd.MY_NAME, sabnzbd.__version__)
|
||||
)
|
||||
)
|
||||
""" % (sabnzbd.MY_NAME, sabnzbd.__version__)))
|
||||
|
||||
|
||||
def daemonize():
|
||||
@@ -870,7 +865,7 @@ def main():
|
||||
elif opt in ("-t", "--templates"):
|
||||
web_dir = arg
|
||||
elif opt in ("-s", "--server"):
|
||||
(web_host, web_port) = split_host(arg)
|
||||
web_host, web_port = split_host(arg)
|
||||
elif opt in ("-n", "--nobrowser"):
|
||||
autobrowser = False
|
||||
elif opt in ("-b", "--browser"):
|
||||
@@ -1280,7 +1275,6 @@ def main():
|
||||
"tools.encode.on": True,
|
||||
"tools.gzip.on": True,
|
||||
"tools.gzip.mime_types": mime_gzip,
|
||||
"request.show_tracebacks": True,
|
||||
"error_page.401": sabnzbd.panic.error_page_401,
|
||||
"error_page.404": sabnzbd.panic.error_page_404,
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
|
||||
# Constants
|
||||
@@ -43,11 +44,17 @@ RELEASE_VERSION_BASE = f"{RELEASE_VERSION_TUPLE[0]}.{RELEASE_VERSION_TUPLE[1]}.{
|
||||
RELEASE_NAME = "SABnzbd-%s" % RELEASE_VERSION
|
||||
RELEASE_TITLE = "SABnzbd %s" % RELEASE_VERSION
|
||||
RELEASE_SRC = RELEASE_NAME + "-src.tar.gz"
|
||||
RELEASE_BINARY = RELEASE_NAME + "-win64-bin.zip"
|
||||
RELEASE_INSTALLER = RELEASE_NAME + "-win-setup.exe"
|
||||
RELEASE_WIN_BIN_X64 = RELEASE_NAME + "-win64-bin.zip"
|
||||
RELEASE_WIN_BIN_ARM64 = RELEASE_NAME + "-win-arm64-bin.zip"
|
||||
RELEASE_WIN_INSTALLER = RELEASE_NAME + "-win-setup.exe"
|
||||
RELEASE_MACOS = RELEASE_NAME + "-macos.dmg"
|
||||
RELEASE_README = "README.mkd"
|
||||
|
||||
# Detect architecture
|
||||
RELEASE_WIN_BIN = RELEASE_WIN_BIN_X64
|
||||
if platform.machine() == "ARM64":
|
||||
RELEASE_WIN_BIN = RELEASE_WIN_BIN_ARM64
|
||||
|
||||
# Used in package.py and SABnzbd.spec
|
||||
EXTRA_FILES = [
|
||||
RELEASE_README,
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
import os
|
||||
from constants import RELEASE_VERSION
|
||||
|
||||
|
||||
# We need to call dmgbuild from command-line, so here we can setup how
|
||||
if __name__ == "__main__":
|
||||
# Check for DMGBuild
|
||||
|
||||
@@ -35,8 +35,8 @@ from constants import (
|
||||
VERSION_FILE,
|
||||
RELEASE_README,
|
||||
RELEASE_NAME,
|
||||
RELEASE_BINARY,
|
||||
RELEASE_INSTALLER,
|
||||
RELEASE_WIN_BIN,
|
||||
RELEASE_WIN_INSTALLER,
|
||||
ON_GITHUB_ACTIONS,
|
||||
RELEASE_THIS,
|
||||
RELEASE_SRC,
|
||||
@@ -257,7 +257,7 @@ if __name__ == "__main__":
|
||||
|
||||
# Remove any leftovers
|
||||
safe_remove(RELEASE_NAME)
|
||||
safe_remove(RELEASE_BINARY)
|
||||
safe_remove(RELEASE_WIN_BIN)
|
||||
|
||||
# Run PyInstaller and check output
|
||||
shutil.copyfile("builder/SABnzbd.spec", "SABnzbd.spec")
|
||||
@@ -275,8 +275,8 @@ if __name__ == "__main__":
|
||||
test_sab_binary("dist/SABnzbd/SABnzbd.exe")
|
||||
|
||||
# Create the archive
|
||||
run_external_command(["win/7zip/7za.exe", "a", RELEASE_BINARY, "SABnzbd"], cwd="dist")
|
||||
shutil.move(f"dist/{RELEASE_BINARY}", RELEASE_BINARY)
|
||||
run_external_command(["win/7zip/7za.exe", "a", RELEASE_WIN_BIN, "SABnzbd"], cwd="dist")
|
||||
shutil.move(f"dist/{RELEASE_WIN_BIN}", RELEASE_WIN_BIN)
|
||||
|
||||
if "installer" in sys.argv:
|
||||
# Check if we have the dist folder
|
||||
@@ -284,10 +284,10 @@ if __name__ == "__main__":
|
||||
raise FileNotFoundError("SABnzbd executable not found, run binary creation first")
|
||||
|
||||
# Check if we have a signed version
|
||||
if os.path.exists(f"signed/{RELEASE_BINARY}"):
|
||||
if os.path.exists(f"signed/{RELEASE_WIN_BIN}"):
|
||||
print("Using signed version of SABnzbd binaries")
|
||||
safe_remove("dist/SABnzbd")
|
||||
run_external_command(["win/7zip/7za.exe", "x", "-odist", f"signed/{RELEASE_BINARY}"])
|
||||
run_external_command(["win/7zip/7za.exe", "x", "-odist", f"signed/{RELEASE_WIN_BIN}"])
|
||||
|
||||
# Make sure it exists
|
||||
if not os.path.exists("dist/SABnzbd/SABnzbd.exe"):
|
||||
@@ -310,7 +310,7 @@ if __name__ == "__main__":
|
||||
"/V3",
|
||||
"/DSAB_VERSION=%s" % RELEASE_VERSION,
|
||||
"/DSAB_VERSIONKEY=%s" % ".".join(map(str, RELEASE_VERSION_TUPLE)),
|
||||
"/DSAB_FILE=%s" % RELEASE_INSTALLER,
|
||||
"/DSAB_FILE=%s" % RELEASE_WIN_INSTALLER,
|
||||
"NSIS_Installer.nsi.tmp",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -29,8 +29,9 @@ from constants import (
|
||||
RELEASE_VERSION_BASE,
|
||||
PRERELEASE,
|
||||
RELEASE_SRC,
|
||||
RELEASE_BINARY,
|
||||
RELEASE_INSTALLER,
|
||||
RELEASE_WIN_BIN_X64,
|
||||
RELEASE_WIN_BIN_ARM64,
|
||||
RELEASE_WIN_INSTALLER,
|
||||
RELEASE_MACOS,
|
||||
RELEASE_README,
|
||||
RELEASE_THIS,
|
||||
@@ -42,8 +43,9 @@ from constants import (
|
||||
# Verify we have all assets
|
||||
files_to_check = (
|
||||
RELEASE_SRC,
|
||||
RELEASE_BINARY,
|
||||
RELEASE_INSTALLER,
|
||||
RELEASE_WIN_BIN_X64,
|
||||
RELEASE_WIN_BIN_ARM64,
|
||||
RELEASE_WIN_INSTALLER,
|
||||
RELEASE_MACOS,
|
||||
RELEASE_README,
|
||||
)
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
# Basic build requirements
|
||||
# Note that not all sub-dependencies are listed, but only ones we know could cause trouble
|
||||
pyinstaller==6.17.0
|
||||
packaging==25.0
|
||||
pyinstaller-hooks-contrib==2025.10
|
||||
pyinstaller==6.18.0
|
||||
packaging==26.0
|
||||
pyinstaller-hooks-contrib==2026.0
|
||||
altgraph==0.17.5
|
||||
wrapt==2.0.1
|
||||
setuptools==80.9.0
|
||||
wrapt==2.1.1
|
||||
setuptools==80.10.2
|
||||
|
||||
# For the Windows build
|
||||
pefile==2024.8.26; sys_platform == 'win32'
|
||||
pywin32-ctypes==0.2.3; sys_platform == 'win32'
|
||||
|
||||
# For the macOS build
|
||||
dmgbuild==1.6.6; sys_platform == 'darwin'
|
||||
dmgbuild==1.6.7; sys_platform == 'darwin'
|
||||
mac-alias==2.2.3; sys_platform == 'darwin'
|
||||
macholib==1.16.4; sys_platform == 'darwin'
|
||||
ds-store==1.3.2; sys_platform == 'darwin'
|
||||
PyNaCl==1.6.1; sys_platform == 'darwin'
|
||||
PyNaCl==1.6.2; sys_platform == 'darwin'
|
||||
|
||||
39
context/Download-flow.md
Normal file
39
context/Download-flow.md
Normal file
@@ -0,0 +1,39 @@
|
||||
## Download flow (Downloader + NewsWrapper)
|
||||
|
||||
1. **Job ingestion**
|
||||
- NZBs arrive via UI/API/URL; `urlgrabber.py` fetches remote NZBs, `nzbparser.py` turns them into `NzbObject`s, and `nzbqueue.NzbQueue` stores ordered jobs with priorities and categories.
|
||||
|
||||
2. **Queue to articles**
|
||||
- When servers need work, `NzbQueue.get_articles` (called from `Server.get_article` in `downloader.py`) hands out batches of `Article`s per server, respecting retention, priority, and forced/paused items.
|
||||
|
||||
3. **Downloader setup**
|
||||
- `Downloader` thread loads server configs (`config.get_servers`), instantiates `Server` objects (per host/port/SSL/threads), and spawns `NewsWrapper` instances per configured connection.
|
||||
- A `selectors.DefaultSelector` watches all sockets; `BPSMeter` tracks throughput and speed limits; timers manage server penalties/restarts.
|
||||
|
||||
4. **Connection establishment (NewsWrapper.init_connect → NNTP.connect)**
|
||||
- `Server.request_addrinfo` resolves fastest address; `NewsWrapper` builds an `NNTP` socket, wraps SSL if needed, sets non-blocking, and registers with the selector.
|
||||
- First server greeting (200/201) is queued; `finish_connect` drives the login handshake (`AUTHINFO USER/PASS`) and handles temporary (480) or permanent (400/502) errors.
|
||||
|
||||
5. **Request scheduling & pipelining**
|
||||
- `write()` chooses the next article command (`STAT/HEAD` for precheck, `BODY` or `ARTICLE` otherwise).
|
||||
- Concurrency is limited by `server.pipelining_requests`; commands are queued and sent with `sock.sendall`, so there is no local send buffer.
|
||||
- Sockets stay registered for `EVENT_WRITE`: without write readiness events, a temporarily full kernel send buffer could stall queued commands when there is nothing to read, so WRITE interest is needed to resume sending promptly.
|
||||
|
||||
6. **Receiving data**
|
||||
- Selector events route to `process_nw_read`; `NewsWrapper.read` pulls bytes (SSL optimized via sabctools), parses NNTP responses, and calls `on_response`.
|
||||
- Successful BODY/ARTICLE (220/222) updates per-server stats; missing/500 variants toggle capability flags (BODY/STAT support).
|
||||
|
||||
7. **Decoding and caching**
|
||||
- `Downloader.decode` hands responses to `decoder.decode`, which yEnc/UU decodes, CRC-checks, and stores payloads in `ArticleCache` (memory or disk spill).
|
||||
- Articles with DMCA/bad data trigger retry on other servers until `max_art_tries` is exceeded.
|
||||
|
||||
8. **Assembly to files**
|
||||
- `Assembler` worker consumes decoded pieces, writes to the target file, updates CRC, and cleans admin markers. It guards disk space (`diskspace_check`) and schedules direct unpack or PAR2 handling when files finish.
|
||||
|
||||
9. **Queue bookkeeping**
|
||||
- `NzbQueue.register_article` records success/failure; completed files advance NZF/NZO state. If all files done, the job moves to post-processing (`PostProcessor.process`), which runs `newsunpack`, scripts, sorting, etc.
|
||||
|
||||
10. **Control & resilience**
|
||||
- Pausing/resuming (`Downloader.pause/resume`), bandwidth limiting, and sleep tuning happen in the main loop.
|
||||
- Errors/timeouts lead to `reset_nw` (close socket, return article, maybe penalize server). Optional servers can be temporarily disabled; required ones schedule resumes.
|
||||
- Forced disconnect/shutdown drains sockets, refreshes DNS, and exits cleanly.
|
||||
32
context/Repo-layout.md
Normal file
32
context/Repo-layout.md
Normal file
@@ -0,0 +1,32 @@
|
||||
## Repo layout
|
||||
|
||||
- Entry points & metadata
|
||||
- `SABnzbd.py`: starts the app.
|
||||
- `README.md` / `README.mkd`: release notes and overview.
|
||||
- `requirements.txt`: runtime deps.
|
||||
|
||||
- Core application package `sabnzbd/`
|
||||
- Download engine: `downloader.py` (main loop), `newswrapper.py` (NNTP connections), `urlgrabber.py`, `nzbqueue.py` (queue), `nzbparser.py` (parse NZB), `assembler.py` (writes decoded parts), `decoder.py` (yEnc/UU decode), `articlecache.py` (in-memory/on-disk cache).
|
||||
- Post-processing: `newsunpack.py`, `postproc.py`, `directunpacker.py`, `sorting.py`, `deobfuscate_filenames.py`.
|
||||
- Config/constants/utilities: `cfg.py`, `config.py`, `constants.py`, `misc.py`, `filesystem.py`, `encoding.py`, `lang.py`, `scheduler.py`, `notifier.py`, `emailer.py`, `rss.py`.
|
||||
- UI plumbing: `interface.py`, `skintext.py`, `version.py`, platform helpers (`macosmenu.py`, `sabtray*.py`).
|
||||
- Subpackages: `sabnzbd/nzb/` (NZB model objects), `sabnzbd/utils/` (helpers).
|
||||
|
||||
- Web interfaces & assets
|
||||
- `interfaces/Glitter`, `interfaces/Config`, `interfaces/wizard`: HTML/JS/CSS skins.
|
||||
- `icons/`: tray/web icons.
|
||||
- `locale/`, `po/`, `tools/`: translation sources and helper scripts (`make_mo.py`, etc.).
|
||||
|
||||
- Testing & samples
|
||||
- `tests/`: pytest suite plus `data/` fixtures and `test_utils/`.
|
||||
- `scripts/`: sample post-processing hooks (`Sample-PostProc.*`).
|
||||
|
||||
- Packaging/build
|
||||
- `builder/`: platform build scripts (DMG/EXE specs, `package.py`, `release.py`).
|
||||
- Platform folders `win/`, `macos/`, `linux/`, `snap/`: installer or platform-specific assets.
|
||||
- `admin/`, `builder/constants.py`, `licenses/`: release and licensing support files.
|
||||
|
||||
- Documentation
|
||||
- Documentation website source is stored in the `sabnzbd.github.io` repo.
|
||||
- This repo is most likely located 1 level up from the root folder of this repo.
|
||||
- Documentation is split per SABnzbd version, in the `wiki` folder.
|
||||
@@ -188,6 +188,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
<p>$T('explain-apprise_enable')</p>
|
||||
<p><a href="https://appriseit.com/" target="_blank">Apprise documentation</a></p>
|
||||
<p>$T('version'): ${apprise.__version__}</p>
|
||||
|
||||
$show_cat_box('apprise')
|
||||
|
||||
@@ -117,6 +117,12 @@
|
||||
<input type="checkbox" name="optional" id="optional" value="1" />
|
||||
<span class="desc">$T('explain-optional')</span>
|
||||
</div>
|
||||
<div class="field-pair advanced-settings">
|
||||
<label class="config" for="pipelining_requests">$T('srv-pipelining_requests')</label>
|
||||
<input type="number" name="pipelining_requests" id="pipelining_requests" min="1" max="20" value="1" />
|
||||
<span class="desc">$T('explain-pipelining_requests')<br>$T('readwiki')
|
||||
<a href="https://sabnzbd.org/wiki/advanced/nntp-pipelining" target="_blank">https://sabnzbd.org/wiki/advanced/nntp-pipelining</a></span>
|
||||
</div>
|
||||
<div class="field-pair advanced-settings">
|
||||
<label class="config" for="expire_date">$T('srv-expire_date')</label>
|
||||
<input type="date" name="expire_date" id="expire_date" />
|
||||
@@ -248,6 +254,12 @@
|
||||
<input type="checkbox" name="optional" id="optional$cur" value="1" <!--#if int($server['optional']) != 0 then 'checked="checked"' else ""#--> />
|
||||
<span class="desc">$T('explain-optional')</span>
|
||||
</div>
|
||||
<div class="field-pair advanced-settings">
|
||||
<label class="config" for="pipelining_requests$cur">$T('srv-pipelining_requests')</label>
|
||||
<input type="number" name="pipelining_requests" id="pipelining_requests$cur" value="$server['pipelining_requests']" min="1" max="20" required />
|
||||
<span class="desc">$T('explain-pipelining_requests')<br>$T('readwiki')
|
||||
<a href="https://sabnzbd.org/wiki/advanced/nntp-pipelining" target="_blank">https://sabnzbd.org/wiki/advanced/nntp-pipelining</a></span>
|
||||
</div>
|
||||
<div class="field-pair advanced-settings">
|
||||
<label class="config" for="expire_date$cur">$T('srv-expire_date')</label>
|
||||
<input type="date" name="expire_date" id="expire_date$cur" value="$server['expire_date']" />
|
||||
|
||||
@@ -6,8 +6,12 @@
|
||||
<span class="glyphicon glyphicon-open"></span> $T('Glitter-notification-uploading') <span class="main-notification-box-file-count"></span>
|
||||
</div>
|
||||
|
||||
<div class="main-notification-box-uploading-failed">
|
||||
<span class="glyphicon glyphicon-exclamation-sign"></span> $T('Glitter-notification-upload-failed').replace('%s', '') <span class="main-notification-box-file-count"></span>
|
||||
</div>
|
||||
|
||||
<div class="main-notification-box-queue-repair">
|
||||
<span class="glyphicon glyphicon glyphicon-wrench"></span> $T('Glitter-repairQueue')
|
||||
<span class="glyphicon glyphicon-wrench"></span> $T('Glitter-repairQueue')
|
||||
</div>
|
||||
|
||||
<div class="main-notification-box-disconnect">
|
||||
|
||||
@@ -726,6 +726,9 @@ function ViewModel() {
|
||||
$('#nzbname').val('')
|
||||
$('.btn-file em').html(glitterTranslate.chooseFile + '…')
|
||||
}
|
||||
}).fail(function(xhr, status, error) {
|
||||
// Update the uploading notification text to show error
|
||||
showNotification('.main-notification-box-uploading-failed', 0, error)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,10 @@ legend,
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.main-notification-box-uploading-failed {
|
||||
color: #F95151;
|
||||
}
|
||||
|
||||
.container,
|
||||
.modal-body,
|
||||
.modal-footer {
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
<metadata_license>MIT</metadata_license>
|
||||
<name>SABnzbd</name>
|
||||
<summary>Free and easy binary newsreader</summary>
|
||||
<branding>
|
||||
<color type="primary" scheme_preference="light">#e7e7e7</color>
|
||||
<color type="primary" scheme_preference="dark">#444444</color>
|
||||
</branding>
|
||||
<description>
|
||||
<p>
|
||||
SABnzbd is a free and Open Source web-based binary newsreader,
|
||||
@@ -17,6 +21,13 @@
|
||||
and services that help automate the download process.
|
||||
</p>
|
||||
</description>
|
||||
<keywords>
|
||||
<keyword>usenet</keyword>
|
||||
<keyword>nzb</keyword>
|
||||
<keyword>download</keyword>
|
||||
<keyword>newsreader</keyword>
|
||||
<keyword>binary</keyword>
|
||||
</keywords>
|
||||
<categories>
|
||||
<category>Network</category>
|
||||
<category>FileTransfer</category>
|
||||
@@ -24,34 +35,49 @@
|
||||
<url type="homepage">https://sabnzbd.org</url>
|
||||
<url type="bugtracker">https://github.com/sabnzbd/sabnzbd/issues</url>
|
||||
<url type="vcs-browser">https://github.com/sabnzbd/sabnzbd</url>
|
||||
<url type="contribute">https://github.com/sabnzbd/sabnzbd</url>
|
||||
<url type="translate">https://sabnzbd.org/wiki/translate</url>
|
||||
<url type="donation">https://sabnzbd.org/donate</url>
|
||||
<url type="help">https://sabnzbd.org/wiki/</url>
|
||||
<url type="faq">https://sabnzbd.org/wiki/faq</url>
|
||||
<url type="contact">https://sabnzbd.org/live-chat.html</url>
|
||||
<releases>
|
||||
<release version="4.6.0" date="2025-12-24" type="stable"/>
|
||||
<release version="4.5.5" date="2025-10-24" type="stable"/>
|
||||
<release version="4.5.4" date="2025-10-22" type="stable"/>
|
||||
<release version="4.5.3" date="2025-08-25" type="stable"/>
|
||||
<release version="4.5.2" date="2025-07-09" type="stable"/>
|
||||
<release version="4.5.1" date="2025-04-11" type="stable"/>
|
||||
<release version="4.5.0" date="2025-04-01" type="stable"/>
|
||||
<release version="4.4.1" date="2024-12-23" type="stable"/>
|
||||
<release version="4.4.0" date="2024-12-09" type="stable"/>
|
||||
<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"/>
|
||||
<release version="4.2.2" date="2024-02-01" type="stable"/>
|
||||
<release version="4.2.1" date="2024-01-05" type="stable"/>
|
||||
<release version="4.2.0" date="2024-01-03" type="stable"/>
|
||||
<release version="4.1.0" date="2023-09-26" type="stable"/>
|
||||
<release version="4.0.3" date="2023-06-16" type="stable"/>
|
||||
<release version="4.0.2" date="2023-06-09" type="stable"/>
|
||||
<release version="4.0.1" date="2023-05-01" type="stable"/>
|
||||
<release version="4.0.0" date="2023-04-28" type="stable"/>
|
||||
<release version="3.7.2" date="2023-02-05" type="stable"/>
|
||||
<release version="5.0.0" date="2026-03-01" type="stable">
|
||||
<url type="details">https://github.com/sabnzbd/sabnzbd/releases/tag/5.0.0</url>
|
||||
</release>
|
||||
<release version="4.5.5" date="2025-10-24" type="stable">
|
||||
<url type="details">https://github.com/sabnzbd/sabnzbd/releases/tag/4.5.5</url>
|
||||
</release>
|
||||
<release version="4.5.4" date="2025-10-22" type="stable">
|
||||
<url type="details">https://github.com/sabnzbd/sabnzbd/releases/tag/4.5.4</url>
|
||||
</release>
|
||||
<release version="4.5.3" date="2025-08-25" type="stable">
|
||||
<url type="details">https://github.com/sabnzbd/sabnzbd/releases/tag/4.5.3</url>
|
||||
</release>
|
||||
<release version="4.5.2" date="2025-07-09" type="stable">
|
||||
<url type="details">https://github.com/sabnzbd/sabnzbd/releases/tag/4.5.2</url>
|
||||
</release>
|
||||
<release version="4.5.1" date="2025-04-11" type="stable">
|
||||
<url type="details">https://github.com/sabnzbd/sabnzbd/releases/tag/4.5.1</url>
|
||||
</release>
|
||||
<release version="4.5.0" date="2025-04-01" type="stable">
|
||||
<url type="details">https://github.com/sabnzbd/sabnzbd/releases/tag/4.5.0</url>
|
||||
</release>
|
||||
<release version="4.4.1" date="2024-12-23" type="stable">
|
||||
<url type="details">https://github.com/sabnzbd/sabnzbd/releases/tag/4.4.1</url>
|
||||
</release>
|
||||
<release version="4.4.0" date="2024-12-09" type="stable">
|
||||
<url type="details">https://github.com/sabnzbd/sabnzbd/releases/tag/4.4.0</url>
|
||||
</release>
|
||||
<release version="4.3.3" date="2024-08-01" type="stable">
|
||||
<url type="details">https://github.com/sabnzbd/sabnzbd/releases/tag/4.3.3</url>
|
||||
</release>
|
||||
<release version="4.3.2" date="2024-05-30" type="stable">
|
||||
<url type="details">https://github.com/sabnzbd/sabnzbd/releases/tag/4.3.2</url>
|
||||
</release>
|
||||
<release version="4.3.1" date="2024-05-03" type="stable">
|
||||
<url type="details">https://github.com/sabnzbd/sabnzbd/releases/tag/4.3.1</url>
|
||||
</release>
|
||||
</releases>
|
||||
<launchable type="desktop-id">sabnzbd.desktop</launchable>
|
||||
<provides>
|
||||
@@ -74,11 +100,59 @@
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://sabnzbd.org/images/landing/screenshots/interface.png</image>
|
||||
<caption>Web interface</caption>
|
||||
<caption>Intuitive interface</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://sabnzbd.org/images/landing/screenshots/night-mode.png</image>
|
||||
<caption>Night mode</caption>
|
||||
<caption>Also comes in Night-mode</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://sabnzbd.org/images/landing/screenshots/add-nzb.png</image>
|
||||
<caption>Add NZB's or use drag-and-drop!</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://sabnzbd.org/images/landing/screenshots/phone-interface.png</image>
|
||||
<caption>Scales to any screen size</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://sabnzbd.org/images/landing/screenshots/history-details.png</image>
|
||||
<caption>Easy overview of all history details</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://sabnzbd.org/images/landing/screenshots/phone-extra.png</image>
|
||||
<caption>Every option, on every screen size</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://sabnzbd.org/images/landing/screenshots/file-lists.png</image>
|
||||
<caption>Manage a job's individual files</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://sabnzbd.org/images/landing/screenshots/set-speedlimit.png</image>
|
||||
<caption>Easy speed limiting</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://sabnzbd.org/images/landing/screenshots/set-options.png</image>
|
||||
<caption>Quickly change settings</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://sabnzbd.org/images/landing/screenshots/dashboard.png</image>
|
||||
<caption>Easy system check</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://sabnzbd.org/images/landing/screenshots/connections-overview.png</image>
|
||||
<caption>See active connections</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://sabnzbd.org/images/landing/screenshots/skin-settings.png</image>
|
||||
<caption>Customize the interface</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://sabnzbd.org/images/landing/screenshots/tabbed.png</image>
|
||||
<caption>Tabbed-mode</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://sabnzbd.org/images/landing/screenshots/set-custom-pause.png</image>
|
||||
<caption>Specify any pause duration</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://sabnzbd.org/images/landing/screenshots/config.png</image>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -4,7 +4,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\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.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\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"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Language-Team: Danish (https://app.transifex.com/sabnzbd/teams/111101/da/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: German (https://app.transifex.com/sabnzbd/teams/111101/de/)\n"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Language-Team: Spanish (https://app.transifex.com/sabnzbd/teams/111101/es/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Language-Team: Finnish (https://app.transifex.com/sabnzbd/teams/111101/fi/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Fred L <88com88@gmail.com>, 2025\n"
|
||||
"Language-Team: French (https://app.transifex.com/sabnzbd/teams/111101/fr/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Hebrew (https://app.transifex.com/sabnzbd/teams/111101/he/)\n"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\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"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Language-Team: Norwegian Bokmål (https://app.transifex.com/sabnzbd/teams/111101/nb/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Dutch (https://app.transifex.com/sabnzbd/teams/111101/nl/)\n"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Language-Team: Polish (https://app.transifex.com/sabnzbd/teams/111101/pl/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Language-Team: Portuguese (Brazil) (https://app.transifex.com/sabnzbd/teams/111101/pt_BR/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Language-Team: Romanian (https://app.transifex.com/sabnzbd/teams/111101/ro/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Language-Team: Russian (https://app.transifex.com/sabnzbd/teams/111101/ru/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Language-Team: Serbian (https://app.transifex.com/sabnzbd/teams/111101/sr/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Language-Team: Swedish (https://app.transifex.com/sabnzbd/teams/111101/sv/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: mauron, 2025\n"
|
||||
"Language-Team: Turkish (https://app.transifex.com/sabnzbd/teams/111101/tr/)\n"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/sabnzbd/teams/111101/zh_CN/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: team@sabnzbd.org\n"
|
||||
"Language-Team: SABnzbd <team@sabnzbd.org>\n"
|
||||
@@ -125,6 +125,11 @@ msgstr ""
|
||||
msgid "Current umask (%o) might deny SABnzbd access to the files and folders it creates."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Completed Download Folder %s is on FAT file system, limiting maximum file size to 4GB"
|
||||
@@ -670,6 +675,11 @@ msgstr ""
|
||||
msgid "%s is not writable with special character filenames. This can cause problems."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr ""
|
||||
@@ -884,7 +894,7 @@ msgid "Update Available!"
|
||||
msgstr ""
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr ""
|
||||
|
||||
@@ -1553,6 +1563,14 @@ msgstr ""
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr ""
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1579,14 +1597,6 @@ msgstr ""
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr ""
|
||||
@@ -3397,6 +3407,14 @@ msgstr ""
|
||||
msgid "Enable"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Request multiple articles per connection without waiting for each response first.<br />This can improve download speeds, especially on connections with higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\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"
|
||||
@@ -144,6 +144,11 @@ msgid ""
|
||||
"creates."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -726,6 +731,11 @@ msgid ""
|
||||
"problems."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr "Odmítnuto spojení z:"
|
||||
@@ -955,7 +965,7 @@ msgid "Update Available!"
|
||||
msgstr "Dostupná aktualizace!"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr "Nezdařilo se nahrát soubor: %s"
|
||||
|
||||
@@ -1644,6 +1654,14 @@ msgstr ""
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Prázdný RSS záznam nalezen (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Nekompatibilní kanál"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1670,14 +1688,6 @@ msgstr ""
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "RSS kanál %s byl prázdný"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Nekompatibilní kanál"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Prázdný RSS záznam nalezen (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "Zobrazit rozhraní"
|
||||
@@ -3587,6 +3597,17 @@ msgstr ""
|
||||
msgid "Enable"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Danish (https://app.transifex.com/sabnzbd/teams/111101/da/)\n"
|
||||
@@ -147,6 +147,11 @@ msgid ""
|
||||
msgstr ""
|
||||
"Aktuel umask (%o) kan nægte SABnzbd adgang til filer og mapper den opretter."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -764,6 +769,11 @@ msgid ""
|
||||
msgstr ""
|
||||
"%s er ikke skrivbar med filnavne med specialtegn. Dette kan give problemer."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr "Afviste forbindelse fra:"
|
||||
@@ -996,7 +1006,7 @@ msgid "Update Available!"
|
||||
msgstr "Opdatering tilgængelig!"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr "Kunne ikke uploade fil: %s"
|
||||
|
||||
@@ -1718,6 +1728,14 @@ msgstr "Fejl ved lukning af system"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr "Modtog en DBus-undtagelse %s"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Tom RSS post blev fundet (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Inkompatibel feed"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1744,14 +1762,6 @@ msgstr "Server %s bruger et upålideligt HTTPS-certifikat"
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "RSS Feed %s er tom"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Inkompatibel feed"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Tom RSS post blev fundet (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "Vis grænseflade"
|
||||
@@ -3760,6 +3770,17 @@ msgstr ""
|
||||
msgid "Enable"
|
||||
msgstr "Aktivere"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: German (https://app.transifex.com/sabnzbd/teams/111101/de/)\n"
|
||||
@@ -167,6 +167,11 @@ msgstr ""
|
||||
"Die aktuellen Zugriffseinstellungen (%o) könnte SABnzbd den Zugriff auf die "
|
||||
"erstellten Dateien und Ordner von SABnzbd verweigern."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -803,6 +808,11 @@ msgstr ""
|
||||
"Dateinamen mit Umlaute können nicht in %s gespeichert werden. Dies kann zu "
|
||||
"Problemen führen."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr "Abgelehnte Verbindung von:"
|
||||
@@ -1036,7 +1046,7 @@ msgid "Update Available!"
|
||||
msgstr "Neue Version verfügbar!"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr "Hochladen fehlgeschlagen: %s"
|
||||
|
||||
@@ -1774,6 +1784,14 @@ msgstr "Fehler beim Herunterfahren des Systems"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr "DBus-Ausnahmefehler empfangen %s "
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Leerer RSS-Feed gefunden: %s"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Inkompatibeler RSS-Feed"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1800,14 +1818,6 @@ msgstr "Der Server %s nutzt ein nicht vertrauenswürdiges HTTPS-Zertifikat"
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "RSS-Feed %s war leer"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Inkompatibeler RSS-Feed"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Leerer RSS-Feed gefunden: %s"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "Interface anzeigen"
|
||||
@@ -3866,6 +3876,17 @@ msgstr "Für unzuverlässige Server, wird bei Fehlern länger ignoriert"
|
||||
msgid "Enable"
|
||||
msgstr "Aktivieren"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Spanish (https://app.transifex.com/sabnzbd/teams/111101/es/)\n"
|
||||
@@ -156,6 +156,11 @@ msgstr ""
|
||||
"La umask actual (%o) podría denegarle acceso a SABnzbd a los archivos y "
|
||||
"carpetas que este crea."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -787,6 +792,11 @@ msgstr ""
|
||||
"%s no permite escribir nombres de archivo con caracteres especiales. Esto "
|
||||
"puede causar problemas."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr "Conexión rechazada de:"
|
||||
@@ -1020,7 +1030,7 @@ msgid "Update Available!"
|
||||
msgstr "¡Actualización Disponible!"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr "Error al subir archivo: %s"
|
||||
|
||||
@@ -1762,6 +1772,14 @@ msgstr "Error al apagarel sistema"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr "Se ha recibido una excepción DBus %s"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Entrada RSS vacía (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Canal Incorrecto"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1790,14 +1808,6 @@ msgstr "El servidor %s utiliza un certificado HTTPS no fiable"
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "El canal RSS %s estaba vacío"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Canal Incorrecto"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Entrada RSS vacía (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "Mostrar interfaz"
|
||||
@@ -3837,6 +3847,17 @@ msgstr ""
|
||||
msgid "Enable"
|
||||
msgstr "Habilitar"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\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"
|
||||
@@ -146,6 +146,11 @@ msgid ""
|
||||
"creates."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -732,6 +737,11 @@ msgid ""
|
||||
"problems."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr ""
|
||||
@@ -961,7 +971,7 @@ msgid "Update Available!"
|
||||
msgstr "Päivitys saatavilla!"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr ""
|
||||
|
||||
@@ -1671,6 +1681,14 @@ msgstr "Virhe sammutettaessa järjestelmää"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Tyhjä RSS kohde löytyi (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Puutteellinen syöte"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1697,14 +1715,6 @@ msgstr "Palvelin %s käyttää epäluotettavaa HTTPS sertifikaattia"
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "RSS syöte %s oli tyhjä"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Puutteellinen syöte"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Tyhjä RSS kohde löytyi (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "Näytä käyttöliittymä"
|
||||
@@ -3678,6 +3688,17 @@ msgstr ""
|
||||
msgid "Enable"
|
||||
msgstr "Ota käyttöön"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2025
|
||||
# Fred L <88com88@gmail.com>, 2025
|
||||
# Fred L <88com88@gmail.com>, 2026
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Fred L <88com88@gmail.com>, 2025\n"
|
||||
"Last-Translator: Fred L <88com88@gmail.com>, 2026\n"
|
||||
"Language-Team: French (https://app.transifex.com/sabnzbd/teams/111101/fr/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -157,6 +157,13 @@ msgstr ""
|
||||
"L'umask actuel (%o) pourrait refuser à SABnzbd l'accès aux fichiers et "
|
||||
"dossiers qu'il crée."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
"La version Windows ARM de SABnzbd est disponible depuis notre page "
|
||||
"Téléchargements!"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -792,6 +799,13 @@ msgstr ""
|
||||
"Le fichier %s n'est pas inscriptible à cause des caractères spéciaux dans le"
|
||||
" nom. Cela peut causer des problèmes."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
"%s ne prend pas en charge les fichiers fragmentés. Désactivation du mode "
|
||||
"d'écriture directe."
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr "Connexion refusée de:"
|
||||
@@ -1025,7 +1039,7 @@ msgid "Update Available!"
|
||||
msgstr "Mise à Jour disponible!"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr "Échec de l'upload du fichier : %s"
|
||||
|
||||
@@ -1760,6 +1774,14 @@ msgstr "Erreur lors de l'arrêt du système"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr "Exception DBus reçue %s"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Entrée vide de flux RSS trouvée (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Flux incompatible"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1787,14 +1809,6 @@ msgstr "Le serveur %s utilise un certificat de sécurité HTTPS non authentifié
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "Le flux RSS %s était vide"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Flux incompatible"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Entrée vide de flux RSS trouvée (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "Afficher l’interface"
|
||||
@@ -3852,6 +3866,20 @@ msgstr ""
|
||||
msgid "Enable"
|
||||
msgstr "Activer"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr "Articles par demande"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
" Demandez plusieurs articles par connexion sans attendre chaque réponse.<br "
|
||||
"/>Cela peut améliorer les vitesses de téléchargement, en particulier sur les"
|
||||
" connexions à latence élevée. "
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Hebrew (https://app.transifex.com/sabnzbd/teams/111101/he/)\n"
|
||||
@@ -143,6 +143,11 @@ msgstr ""
|
||||
"פקודת umask נוכחית (%o) עשויה לדחות גישה מן SABnzbd אל הקבצים והתיקיות שהוא "
|
||||
"יוצר."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -745,6 +750,11 @@ msgid ""
|
||||
"problems."
|
||||
msgstr "%s אינו בר־כתיבה עם שמות קבצים עם תו מיוחד. זה יכול לגרום לבעיות."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr "חיבור מסורב מאת:"
|
||||
@@ -973,7 +983,7 @@ msgid "Update Available!"
|
||||
msgstr "עדכון זמין!"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr "כישלון בהעלאת קובץ: %s"
|
||||
|
||||
@@ -1690,6 +1700,14 @@ msgstr "שגיאה בזמן כיבוי מערכת"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr "חריגת DBus התקבלה %s"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "כניסת RSS ריקה נמצאה (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "הזנה בלתי תואמת"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1716,14 +1734,6 @@ msgstr "השרת %s משתמש בתעודת HTTPS בלתי מהימנה"
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "הזנת RSS %s הייתה ריקה"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "הזנה בלתי תואמת"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "כניסת RSS ריקה נמצאה (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "הראה ממשק"
|
||||
@@ -3696,6 +3706,17 @@ msgstr "עבור שרתים בלתי מהימנים, ייתקל בהתעלמות
|
||||
msgid "Enable"
|
||||
msgstr "אפשר"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Italian (https://app.transifex.com/sabnzbd/teams/111101/it/)\n"
|
||||
@@ -150,6 +150,11 @@ msgstr ""
|
||||
"L'umask corrente (%o) potrebbe negare a SABnzbd l'accesso ai file e alle "
|
||||
"cartelle che crea."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -783,6 +788,11 @@ msgstr ""
|
||||
"%s non è scrivibile con nomi di file con caratteri speciali. Questo può "
|
||||
"causare problemi."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr "Connessione rifiutata da:"
|
||||
@@ -1016,7 +1026,7 @@ msgid "Update Available!"
|
||||
msgstr "Aggiornamento disponibile!"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr "Caricamento del file %s fallito"
|
||||
|
||||
@@ -1741,6 +1751,14 @@ msgstr "Errore durante lo spegnimento del sistema"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr "Ricevuta un'eccezione DBus %s"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Trovata voce RSS vuota (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Feed incompatibile"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1767,14 +1785,6 @@ msgstr "Il server %s utilizza un certificato HTTPS non attendibile"
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "Il feed RSS %s era vuoto"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Feed incompatibile"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Trovata voce RSS vuota (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "Mostra interfaccia"
|
||||
@@ -3809,6 +3819,17 @@ msgstr ""
|
||||
msgid "Enable"
|
||||
msgstr "Abilita"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\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"
|
||||
@@ -142,6 +142,11 @@ msgid ""
|
||||
"creates."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -729,6 +734,11 @@ msgid ""
|
||||
"problems."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr ""
|
||||
@@ -958,7 +968,7 @@ msgid "Update Available!"
|
||||
msgstr "Oppdatering tilgjengelig"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr ""
|
||||
|
||||
@@ -1669,6 +1679,14 @@ msgstr "Feil under avslutting av systemet"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Tom RSS post funnet (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Ukompatibel nyhetsstrøm"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1695,14 +1713,6 @@ msgstr "Server %s bruker et usikkert HTTP sertifikat"
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "RSS-kilde %s var tom"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Ukompatibel nyhetsstrøm"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Tom RSS post funnet (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "Vis grensesnitt"
|
||||
@@ -3657,6 +3667,17 @@ msgstr ""
|
||||
msgid "Enable"
|
||||
msgstr "Aktivere"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Dutch (https://app.transifex.com/sabnzbd/teams/111101/nl/)\n"
|
||||
@@ -152,6 +152,11 @@ msgstr ""
|
||||
"Huidige umask (%o) zou kunnen beletten dat SABnzbd toegang heeft tot de "
|
||||
"aangemaakte bestanden en mappen."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -786,6 +791,11 @@ msgstr ""
|
||||
"Het is niet mogelijk bestanden met speciale tekens op te slaan in %s. Dit "
|
||||
"geeft mogelijk problemen bij het verwerken van downloads."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr "Verbinding geweigerd van: "
|
||||
@@ -1019,7 +1029,7 @@ msgid "Update Available!"
|
||||
msgstr "Update beschikbaar!"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr "Kon het volgende bestand niet uploaden: %s"
|
||||
|
||||
@@ -1744,6 +1754,14 @@ msgstr "Fout bij het afsluiten van het systeem"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr "DBus foutmelding %s "
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Lege RSS-feed gevonden (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Ongeschikte RSS-feed"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1770,14 +1788,6 @@ msgstr "Server %s gebruikt een onbetrouwbaar HTTPS-certificaat"
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "RSS-feed %s is leeg"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Ongeschikte RSS-feed"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Lege RSS-feed gevonden (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "Toon webinterface"
|
||||
@@ -3809,6 +3819,17 @@ msgstr ""
|
||||
msgid "Enable"
|
||||
msgstr "Inschakelen"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\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"
|
||||
@@ -138,6 +138,11 @@ msgid ""
|
||||
"creates."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -732,6 +737,11 @@ msgid ""
|
||||
"problems."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr ""
|
||||
@@ -961,7 +971,7 @@ msgid "Update Available!"
|
||||
msgstr "Dostępna aktualizacja!"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr ""
|
||||
|
||||
@@ -1678,6 +1688,14 @@ msgstr "Wyłączenie systemu nie powiodło się"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Znaleziono pusty wpis RSS (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Niekompatybilny kanał"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1704,14 +1722,6 @@ msgstr "Serwer %s używa niezaufanego certyfikatu HTTPS"
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "Kanał RSS %s był pusty"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Niekompatybilny kanał"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Znaleziono pusty wpis RSS (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "Pokaż interfejs"
|
||||
@@ -3669,6 +3679,17 @@ msgstr ""
|
||||
msgid "Enable"
|
||||
msgstr "Włączony"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\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"
|
||||
@@ -147,6 +147,11 @@ msgstr ""
|
||||
"Mascara atual (%o) pode negar ao SABnzbd acesso aos arquivos e diretórios "
|
||||
"criados."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -744,6 +749,11 @@ msgid ""
|
||||
"problems."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr ""
|
||||
@@ -973,7 +983,7 @@ msgid "Update Available!"
|
||||
msgstr "Atualização Disponível!"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr ""
|
||||
|
||||
@@ -1688,6 +1698,14 @@ msgstr "Erro ao desligar o sistema"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Entrada RSS vazia encontrada (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Feed incompatível"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1715,14 +1733,6 @@ msgstr "Servidor %s usa um certificado HTTPS não confiável"
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "O feed RSS %s estava vazio"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Feed incompatível"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Entrada RSS vazia encontrada (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "Exibir interface"
|
||||
@@ -3680,6 +3690,17 @@ msgstr ""
|
||||
msgid "Enable"
|
||||
msgstr "Habilitar"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\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"
|
||||
@@ -147,6 +147,11 @@ msgid ""
|
||||
"creates."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -752,6 +757,11 @@ msgid ""
|
||||
"problems."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr ""
|
||||
@@ -983,7 +993,7 @@ msgid "Update Available!"
|
||||
msgstr "Actualizare Disponibilă!"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr "Eșuare la încărcarea fișierului: %s"
|
||||
|
||||
@@ -1707,6 +1717,14 @@ msgstr "Eroare la oprirea sistemului"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Valoare RSS gasită a fost goală (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Fulx RSS incompatibil"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1733,14 +1751,6 @@ msgstr "Serverul %s utilizează un certificat HTTPS nesigur"
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "Fluxul RSS %s a fost gol"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Fulx RSS incompatibil"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Valoare RSS gasită a fost goală (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "Arată interfața"
|
||||
@@ -3701,6 +3711,17 @@ msgstr ""
|
||||
msgid "Enable"
|
||||
msgstr "Activează"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2023
|
||||
# ST02, 2026
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
|
||||
"Last-Translator: ST02, 2026\n"
|
||||
"Language-Team: Russian (https://app.transifex.com/sabnzbd/teams/111101/ru/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -24,7 +25,7 @@ msgstr "Предупреждение"
|
||||
#. Notification
|
||||
#: SABnzbd.py, sabnzbd/notifier.py
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
msgstr "Ошибка"
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
@@ -88,7 +89,7 @@ msgstr ""
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid "HTTP and HTTPS ports cannot be the same"
|
||||
msgstr ""
|
||||
msgstr "HTTP и HTTPS порты не могут быть одинаковыми"
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
@@ -103,12 +104,12 @@ msgstr "HTTPS отключён, поскольку отсутствуют фай
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
msgid "Disabled HTTPS because of invalid CERT and KEY files"
|
||||
msgstr ""
|
||||
msgstr "HTTPS отключён, поскольку файлы CERT и KEY недействительны"
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid "Failed to start web-interface: "
|
||||
msgstr ""
|
||||
msgstr "Не удалось запустить веб-интерфейс:"
|
||||
|
||||
#: SABnzbd.py
|
||||
msgid "SABnzbd %s started"
|
||||
@@ -142,6 +143,11 @@ msgid ""
|
||||
"creates."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -301,7 +307,7 @@ msgstr ""
|
||||
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, encryption detected"
|
||||
msgstr ""
|
||||
msgstr "Прервано, обнаружено шифрование"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/assembler.py
|
||||
@@ -314,7 +320,7 @@ msgstr ""
|
||||
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr ""
|
||||
msgstr "Прервано, обнаружено нежелательное расширение"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/assembler.py
|
||||
@@ -343,7 +349,7 @@ msgstr ""
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Downloading resumed after quota reset"
|
||||
msgstr ""
|
||||
msgstr "Загрузка возобновилась после сброса квоты"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/interface.py
|
||||
msgid "Incorrect parameter"
|
||||
@@ -511,7 +517,7 @@ msgstr "Не удаётся прочитать наблюдаемую папку
|
||||
|
||||
#: sabnzbd/downloader.py
|
||||
msgid "Resuming"
|
||||
msgstr ""
|
||||
msgstr "Возобновление"
|
||||
|
||||
#. PP status - Priority pick list
|
||||
#: sabnzbd/downloader.py, sabnzbd/macosmenu.py, sabnzbd/sabtray.py,
|
||||
@@ -728,6 +734,11 @@ msgid ""
|
||||
"problems."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr ""
|
||||
@@ -957,7 +968,7 @@ msgid "Update Available!"
|
||||
msgstr "Доступно обновление!"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr ""
|
||||
|
||||
@@ -1671,6 +1682,14 @@ msgstr "Не удалось завершить работу системы"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Обнаружена пустая запись RSS (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Несовместимая лента"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1697,14 +1716,6 @@ msgstr ""
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "RSS-лента %s была пустой"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Несовместимая лента"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Обнаружена пустая запись RSS (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "Показать интерфейс"
|
||||
@@ -3658,6 +3669,17 @@ msgstr ""
|
||||
msgid "Enable"
|
||||
msgstr "Включить"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\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"
|
||||
@@ -140,6 +140,11 @@ msgid ""
|
||||
"creates."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -726,6 +731,11 @@ msgid ""
|
||||
"problems."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr ""
|
||||
@@ -953,7 +963,7 @@ msgid "Update Available!"
|
||||
msgstr "Нова верзија доступна!"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr ""
|
||||
|
||||
@@ -1664,6 +1674,14 @@ msgstr "Greška pri gašenju sistema"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Nađen prazan RSS unos (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Некомпатибилан Фид"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1690,14 +1708,6 @@ msgstr "Server %s koristi nepouzdan HTTPS sertifikat"
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "RSS фид %s је празан"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Некомпатибилан Фид"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Nađen prazan RSS unos (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "Pokaži interfejs"
|
||||
@@ -3644,6 +3654,17 @@ msgstr ""
|
||||
msgid "Enable"
|
||||
msgstr "Омогући"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\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"
|
||||
@@ -140,6 +140,11 @@ msgid ""
|
||||
"creates."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -726,6 +731,11 @@ msgid ""
|
||||
"problems."
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr ""
|
||||
@@ -955,7 +965,7 @@ msgid "Update Available!"
|
||||
msgstr "Uppdatering tillgänglig"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr ""
|
||||
|
||||
@@ -1670,6 +1680,14 @@ msgstr "Fel uppstod då systemet skulle stängas"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Tom RSS post hittades (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Inkompatibel feed"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1696,14 +1714,6 @@ msgstr "Server %s använder ett otillförlitlig HTTPS-certifikat"
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "RSS-flödet %s var tomt"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Inkompatibel feed"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Tom RSS post hittades (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "Visa gränssnitt"
|
||||
@@ -3656,6 +3666,17 @@ msgstr ""
|
||||
msgid "Enable"
|
||||
msgstr "Aktivera"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
#
|
||||
# Translators:
|
||||
# Taylan Tatlı, 2025
|
||||
# mauron, 2025
|
||||
# Safihre <safihre@sabnzbd.org>, 2025
|
||||
# mauron, 2026
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Last-Translator: mauron, 2026\n"
|
||||
"Language-Team: Turkish (https://app.transifex.com/sabnzbd/teams/111101/tr/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -151,6 +151,11 @@ msgstr ""
|
||||
"Güncel umask (%o), SABnzbd'nin oluşturduğu dosya ve dizinlere erişimini "
|
||||
"reddedebilir."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr "SABnzbd'nin Windows ARM sürümü İndirmeler sayfamızda mevcuttur!"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -777,6 +782,13 @@ msgid ""
|
||||
msgstr ""
|
||||
"%s özel karakterli dosya isimleri ile yazılamıyor. Bu, sorun oluşturabilir."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
"%s aralıklı dosyaları desteklememektedir. Doğrudan yazma kipi devre dışı "
|
||||
"bırakılıyor."
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr "Şuradan bağlantı reddedildi:"
|
||||
@@ -1008,7 +1020,7 @@ msgid "Update Available!"
|
||||
msgstr "Güncelleme Mevcut!"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr "Dosyanın gönderilmesi başarısız oldu: %s"
|
||||
|
||||
@@ -1733,6 +1745,14 @@ msgstr "Sistemin kapatılması esnasında hata"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr "Bir DBUS istisnası alındı %s"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Boş RSS girdisi bulundu (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Uyumsuz besleme"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1759,14 +1779,6 @@ msgstr "%s sunucusu güvenilmez bir HTTPS sertifikası kullanıyor"
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "%s RSS Beselemesi boştu"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "Uyumsuz besleme"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "Boş RSS girdisi bulundu (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "Arayüzü göster"
|
||||
@@ -3376,6 +3388,8 @@ msgstr "SFV temelli kontrolleri etkinleştir"
|
||||
msgid ""
|
||||
"If no par2 files are available, use sfv files (if present) to verify files"
|
||||
msgstr ""
|
||||
"Eğer hiçbir par2 dosyası mevcut değilse, dosyaları kontrol etmek için "
|
||||
"(mevcutsa) sfv dosyalarını kullan"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "User script can flag job as failed"
|
||||
@@ -3796,6 +3810,20 @@ msgstr ""
|
||||
msgid "Enable"
|
||||
msgstr "Etkinleştir"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr "Talep başı makale"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
"Her bir cevabı beklemeden bağlantı başına birden fazla makale talep et.<br "
|
||||
"/>Bu, indirme hızlarını bilhassa yüksek gecikmeli bağlantılarda "
|
||||
"arttırabilir."
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/sabnzbd/teams/111101/zh_CN/)\n"
|
||||
@@ -140,6 +140,11 @@ msgid ""
|
||||
"creates."
|
||||
msgstr "当前 umask (%o) 可能会拒绝 SABnzbd 访问其创建的文件和文件夹。"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid "Windows ARM version of SABnzbd is available from our Downloads page!"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/__init__.py
|
||||
msgid ""
|
||||
@@ -726,6 +731,11 @@ msgid ""
|
||||
"problems."
|
||||
msgstr "%s 不可写入带有特殊字符的文件名。这可能会导致问题。"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/filesystem.py
|
||||
msgid "%s does not support sparse files. Disabling direct write mode."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Refused connection from:"
|
||||
msgstr "拒绝来自以下的连接:"
|
||||
@@ -951,7 +961,7 @@ msgid "Update Available!"
|
||||
msgstr "有更新可用!"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/misc.py
|
||||
#: sabnzbd/misc.py, sabnzbd/skintext.py
|
||||
msgid "Failed to upload file: %s"
|
||||
msgstr "上传文件失败:%s"
|
||||
|
||||
@@ -1660,6 +1670,14 @@ msgstr "关闭系统时出错"
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr "收到 DBus 异常 %s"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "发现空的 RSS 条目 (%s)"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "feed 不兼容"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incorrect RSS feed description \"%s\""
|
||||
@@ -1686,14 +1704,6 @@ msgstr "服务器 %s 使用的 HTTPS 证书不受信任"
|
||||
msgid "RSS Feed %s was empty"
|
||||
msgstr "RSS Feed %s 为空"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Incompatible feed"
|
||||
msgstr "feed 不兼容"
|
||||
|
||||
#: sabnzbd/rss.py
|
||||
msgid "Empty RSS entry found (%s)"
|
||||
msgstr "发现空的 RSS 条目 (%s)"
|
||||
|
||||
#: sabnzbd/sabtray.py, sabnzbd/sabtraylinux.py
|
||||
msgid "Show interface"
|
||||
msgstr "显示界面"
|
||||
@@ -3605,6 +3615,17 @@ msgstr "对于不稳定的服务器,在失败后将会被忽略更长的时间
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Articles per request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Request multiple articles per connection without waiting for each response "
|
||||
"first.<br />This can improve download speeds, especially on connections with"
|
||||
" higher latency."
|
||||
msgstr ""
|
||||
|
||||
#. Button: Remove server
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Server"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: team@sabnzbd.org\n"
|
||||
"Language-Team: SABnzbd <team@sabnzbd.org>\n"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Czech (https://app.transifex.com/sabnzbd/teams/111101/cs/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Danish (https://app.transifex.com/sabnzbd/teams/111101/da/)\n"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: German (https://app.transifex.com/sabnzbd/teams/111101/de/)\n"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Spanish (https://app.transifex.com/sabnzbd/teams/111101/es/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Finnish (https://app.transifex.com/sabnzbd/teams/111101/fi/)\n"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: French (https://app.transifex.com/sabnzbd/teams/111101/fr/)\n"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Hebrew (https://app.transifex.com/sabnzbd/teams/111101/he/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Italian (https://app.transifex.com/sabnzbd/teams/111101/it/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\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.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Dutch (https://app.transifex.com/sabnzbd/teams/111101/nl/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Polish (https://app.transifex.com/sabnzbd/teams/111101/pl/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\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.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Romanian (https://app.transifex.com/sabnzbd/teams/111101/ro/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Russian (https://app.transifex.com/sabnzbd/teams/111101/ru/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Serbian (https://app.transifex.com/sabnzbd/teams/111101/sr/)\n"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Swedish (https://app.transifex.com/sabnzbd/teams/111101/sv/)\n"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Turkish (https://app.transifex.com/sabnzbd/teams/111101/tr/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"Project-Id-Version: SABnzbd-5.0.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/sabnzbd/teams/111101/zh_CN/)\n"
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
# Main requirements
|
||||
# Note that not all sub-dependencies are listed, but only ones we know could cause trouble
|
||||
apprise==1.9.6
|
||||
sabctools==9.1.0
|
||||
apprise==1.9.7
|
||||
sabctools==9.3.1
|
||||
CT3==3.4.0.post5
|
||||
cffi==2.0.0
|
||||
pycparser==2.23
|
||||
pycparser # Version-less for Python 3.9 and below
|
||||
pycparser==3.0; python_version > '3.9'
|
||||
feedparser==6.0.12
|
||||
configobj==5.0.9
|
||||
cheroot==11.1.2
|
||||
six==1.17.0
|
||||
cherrypy==18.10.0
|
||||
jaraco.functools==4.3.0
|
||||
jaraco.functools==4.4.0
|
||||
jaraco.collections==5.0.0
|
||||
jaraco.text==3.8.1 # Newer version introduces irrelevant extra dependencies
|
||||
jaraco.classes==3.4.0
|
||||
@@ -32,12 +33,13 @@ 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==46.0.3
|
||||
# Using older versions can have security implications!
|
||||
cryptography>=3.0
|
||||
|
||||
# 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.11.0
|
||||
orjson==3.11.5
|
||||
orjson==3.11.7; python_version > '3.9'
|
||||
|
||||
# Windows system integration
|
||||
pywin32==311; sys_platform == 'win32'
|
||||
@@ -61,16 +63,16 @@ requests==2.32.5
|
||||
requests-oauthlib==2.0.0
|
||||
PyYAML==6.0.3
|
||||
markdown # Version-less for Python 3.9 and below
|
||||
markdown==3.10; python_version > '3.9'
|
||||
markdown==3.10.1; python_version > '3.9'
|
||||
paho-mqtt==1.6.1 # Pinned, newer versions don't work with AppRise yet
|
||||
|
||||
# Requests Requirements
|
||||
charset_normalizer==3.4.4
|
||||
idna==3.11
|
||||
urllib3==2.6.2
|
||||
certifi==2025.11.12
|
||||
urllib3==2.6.3
|
||||
certifi==2026.1.4
|
||||
oauthlib==3.3.1
|
||||
PyJWT==2.10.1
|
||||
PyJWT==2.11.0
|
||||
blinker==1.9.0
|
||||
|
||||
# Optional support for *nix tray icon.
|
||||
|
||||
@@ -32,11 +32,12 @@ from threading import Lock, Condition
|
||||
# Determine platform flags
|
||||
##############################################################################
|
||||
|
||||
WINDOWS = MACOS = MACOSARM64 = FOUNDATION = False
|
||||
WINDOWS = WINDOWSARM64 = MACOS = MACOSARM64 = FOUNDATION = False
|
||||
KERNEL32 = LIBC = MACOSLIBC = PLATFORM = None
|
||||
|
||||
if os.name == "nt":
|
||||
WINDOWS = True
|
||||
WINDOWSARM64 = platform.uname().machine == "ARM64"
|
||||
|
||||
if platform.uname().machine not in ["AMD64", "ARM64"]:
|
||||
print("SABnzbd only supports 64-bit Windows")
|
||||
@@ -248,6 +249,7 @@ def initialize(pause_downloader=False, clean_up=False, repair=0):
|
||||
|
||||
# Set call backs for Config items
|
||||
cfg.cache_limit.callback(cfg.new_limit)
|
||||
cfg.direct_write.callback(cfg.new_direct_write)
|
||||
cfg.web_host.callback(cfg.guard_restart)
|
||||
cfg.web_port.callback(cfg.guard_restart)
|
||||
cfg.web_dir.callback(cfg.guard_restart)
|
||||
@@ -269,7 +271,6 @@ def initialize(pause_downloader=False, clean_up=False, repair=0):
|
||||
cfg.language.callback(cfg.guard_language)
|
||||
cfg.enable_https_verification.callback(cfg.guard_https_ver)
|
||||
cfg.guard_https_ver()
|
||||
cfg.pipelining_requests.callback(cfg.guard_restart)
|
||||
|
||||
# Set language files
|
||||
lang.set_locale_info("SABnzbd", DIR_LANGUAGE)
|
||||
@@ -303,6 +304,7 @@ def initialize(pause_downloader=False, clean_up=False, repair=0):
|
||||
sabnzbd.NzbQueue.read_queue(repair)
|
||||
sabnzbd.Scheduler.analyse(pause_downloader)
|
||||
sabnzbd.ArticleCache.new_limit(cfg.cache_limit.get_int())
|
||||
sabnzbd.Assembler.new_limit(sabnzbd.ArticleCache.cache_info().cache_limit)
|
||||
|
||||
logging.info("All processes started")
|
||||
sabnzbd.RESTART_REQ = False
|
||||
@@ -315,6 +317,9 @@ def start():
|
||||
logging.debug("Starting postprocessor")
|
||||
sabnzbd.PostProcessor.start()
|
||||
|
||||
logging.debug("Starting article cache")
|
||||
sabnzbd.ArticleCache.start()
|
||||
|
||||
logging.debug("Starting assembler")
|
||||
sabnzbd.Assembler.start()
|
||||
|
||||
@@ -383,6 +388,13 @@ def halt():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logging.debug("Stopping article cache")
|
||||
sabnzbd.ArticleCache.stop()
|
||||
try:
|
||||
sabnzbd.ArticleCache.join(timeout=3)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logging.debug("Stopping postprocessor")
|
||||
sabnzbd.PostProcessor.stop()
|
||||
try:
|
||||
@@ -482,6 +494,10 @@ def delayed_startup_actions():
|
||||
sabnzbd.ORG_UMASK,
|
||||
)
|
||||
|
||||
# Check if maybe we are running x64 version on ARM hardware
|
||||
if sabnzbd.WINDOWSARM64 and "AMD64" in sys.version:
|
||||
misc.helpful_warning(T("Windows ARM version of SABnzbd is available from our Downloads page!"))
|
||||
|
||||
# List the number of certificates available (can take up to 1.5 seconds)
|
||||
if cfg.log_level() > 1:
|
||||
logging.debug("Available certificates = %s", repr(ssl.create_default_context().cert_store_stats()))
|
||||
@@ -496,7 +512,7 @@ def delayed_startup_actions():
|
||||
logging.debug("Completed Download Folder %s is not on FAT", complete_dir)
|
||||
|
||||
if filesystem.directory_is_writable(sabnzbd.cfg.download_dir.get_path()):
|
||||
filesystem.check_filesystem_capabilities(sabnzbd.cfg.download_dir.get_path())
|
||||
filesystem.check_filesystem_capabilities(sabnzbd.cfg.download_dir.get_path(), is_download_dir=True)
|
||||
if filesystem.directory_is_writable(sabnzbd.cfg.complete_dir.get_path()):
|
||||
filesystem.check_filesystem_capabilities(sabnzbd.cfg.complete_dir.get_path())
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ sabnzbd.api - api
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import re
|
||||
import gc
|
||||
@@ -57,6 +58,7 @@ from sabnzbd.constants import (
|
||||
PP_LOOKUP,
|
||||
STAGES,
|
||||
DEF_NETWORKING_TEST_TIMEOUT,
|
||||
DEF_PIPELINING_REQUESTS,
|
||||
)
|
||||
import sabnzbd.config as config
|
||||
import sabnzbd.cfg as cfg
|
||||
@@ -79,6 +81,7 @@ from sabnzbd.misc import (
|
||||
clean_comma_separated_list,
|
||||
match_str,
|
||||
bool_conv,
|
||||
get_platform_description,
|
||||
)
|
||||
from sabnzbd.filesystem import diskspace, get_ext, clip_path, remove_all, list_scripts, purge_log_files, pathbrowser
|
||||
from sabnzbd.encoding import xml_name, utob
|
||||
@@ -689,9 +692,16 @@ LOG_HASH_RE = re.compile(rb"([a-zA-Z\d]{25})", re.I)
|
||||
|
||||
def _api_showlog(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""Fetch the INI and the log-data and add a message at the top"""
|
||||
log_data = b"--------------------------------\n\n"
|
||||
log_data += b"The log includes a copy of your sabnzbd.ini with\nall usernames, passwords and API-keys removed."
|
||||
log_data += b"\n\n--------------------------------\n"
|
||||
# Build header with version and environment info
|
||||
header = "--------------------------------\n"
|
||||
header += f"SABnzbd version: {sabnzbd.__version__}\n"
|
||||
header += f"Commit: {sabnzbd.__baseline__}\n"
|
||||
header += f"Python-version: {sys.version}\n"
|
||||
header += f"Platform: {get_platform_description()}\n"
|
||||
header += "--------------------------------\n\n"
|
||||
header += "The log includes a copy of your sabnzbd.ini with\nall usernames, passwords and API-keys removed."
|
||||
header += "\n\n--------------------------------\n"
|
||||
log_data = header.encode("utf-8")
|
||||
|
||||
if sabnzbd.LOGFILE and os.path.exists(sabnzbd.LOGFILE):
|
||||
with open(sabnzbd.LOGFILE, "rb") as f:
|
||||
@@ -1309,6 +1319,7 @@ def test_nntp_server_dict(kwargs: dict[str, Union[str, list[str]]]) -> tuple[boo
|
||||
ssl = int_conv(kwargs.get("ssl", 0))
|
||||
ssl_verify = int_conv(kwargs.get("ssl_verify", 3))
|
||||
ssl_ciphers = kwargs.get("ssl_ciphers", "").strip()
|
||||
pipelining_requests = int_conv(kwargs.get("pipelining_requests", DEF_PIPELINING_REQUESTS))
|
||||
|
||||
if not host:
|
||||
return False, T("The hostname is not set.")
|
||||
@@ -1345,6 +1356,7 @@ def test_nntp_server_dict(kwargs: dict[str, Union[str, list[str]]]) -> tuple[boo
|
||||
use_ssl=ssl,
|
||||
ssl_verify=ssl_verify,
|
||||
ssl_ciphers=ssl_ciphers,
|
||||
pipelining_requests=lambda: pipelining_requests,
|
||||
username=username,
|
||||
password=password,
|
||||
)
|
||||
@@ -1400,9 +1412,11 @@ def test_nntp_server_dict(kwargs: dict[str, Union[str, list[str]]]) -> tuple[boo
|
||||
|
||||
try:
|
||||
nw.init_connect()
|
||||
while not nw.connected:
|
||||
while test_server.active:
|
||||
nw.write()
|
||||
nw.read(on_response=on_response)
|
||||
if nw.ready:
|
||||
break
|
||||
|
||||
except socket.timeout:
|
||||
if port != 119 and not ssl:
|
||||
@@ -1507,10 +1521,10 @@ def build_status(calculate_performance: bool = False, skip_dashboard: bool = Fal
|
||||
info["servers"] = []
|
||||
# Servers-list could be modified during iteration, so we need a copy
|
||||
for server in sabnzbd.Downloader.servers[:]:
|
||||
activeconn = sum(nw.connected for nw in server.idle_threads.copy())
|
||||
activeconn = sum(nw.ready for nw in server.idle_threads.copy())
|
||||
serverconnections = []
|
||||
for nw in server.busy_threads.copy():
|
||||
if nw.connected:
|
||||
if nw.ready:
|
||||
activeconn += 1
|
||||
if article := nw.article:
|
||||
serverconnections.append(
|
||||
|
||||
@@ -22,26 +22,39 @@ sabnzbd.articlecache - Article cache handling
|
||||
import logging
|
||||
import threading
|
||||
import struct
|
||||
from typing import Collection
|
||||
import time
|
||||
from typing import Collection, Optional
|
||||
|
||||
import sabnzbd
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.decorators import synchronized
|
||||
from sabnzbd.constants import GIGI, ANFO, ASSEMBLER_WRITE_THRESHOLD
|
||||
from sabnzbd.nzb import Article
|
||||
from sabnzbd.constants import (
|
||||
GIGI,
|
||||
ANFO,
|
||||
ARTICLE_CACHE_NON_CONTIGUOUS_FLUSH_PERCENTAGE,
|
||||
)
|
||||
from sabnzbd.nzb import Article, NzbFile
|
||||
from sabnzbd.misc import to_units
|
||||
|
||||
# Operations on the article table are handled via try/except.
|
||||
# The counters need to be made atomic to ensure consistency.
|
||||
ARTICLE_COUNTER_LOCK = threading.RLock()
|
||||
|
||||
_SECONDS_BETWEEN_FLUSHES = 0.5
|
||||
|
||||
class ArticleCache:
|
||||
|
||||
class ArticleCache(threading.Thread):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.shutdown = False
|
||||
self.__direct_write: bool = bool(cfg.direct_write())
|
||||
self.__cache_limit_org = 0
|
||||
self.__cache_limit = 0
|
||||
self.__cache_size = 0
|
||||
self.__article_table: dict[Article, bytes] = {} # Dict of buffered articles
|
||||
|
||||
self.assembler_write_trigger: int = 1
|
||||
self.__article_table: dict[Article, bytearray] = {} # Dict of buffered articles
|
||||
self.__cache_size_cv: threading.Condition = threading.Condition(ARTICLE_COUNTER_LOCK)
|
||||
self.__last_flush: float = 0
|
||||
self.__non_contiguous_trigger: int = 0 # Force flush trigger
|
||||
|
||||
# On 32 bit we only allow the user to set 1GB
|
||||
# For 64 bit we allow up to 4GB, in case somebody wants that
|
||||
@@ -49,9 +62,62 @@ class ArticleCache:
|
||||
if sabnzbd.MACOS or sabnzbd.WINDOWS or (struct.calcsize("P") * 8) == 64:
|
||||
self.__cache_upper_limit = 4 * GIGI
|
||||
|
||||
def cache_info(self):
|
||||
return ANFO(len(self.__article_table), abs(self.__cache_size), self.__cache_limit_org)
|
||||
def change_direct_write(self, direct_write: bool) -> None:
|
||||
self.__direct_write = direct_write and self.__cache_limit > 1
|
||||
|
||||
def stop(self):
|
||||
self.shutdown = True
|
||||
with self.__cache_size_cv:
|
||||
self.__cache_size_cv.notify_all()
|
||||
|
||||
def should_flush(self) -> bool:
|
||||
"""
|
||||
Should we flush the cache?
|
||||
Only if direct write is supported and cache usage is over the upper limit.
|
||||
Or the downloader is paused and cache is not empty.
|
||||
"""
|
||||
return (
|
||||
self.__direct_write
|
||||
and self.__cache_limit
|
||||
and (
|
||||
self.__cache_size > self.__non_contiguous_trigger
|
||||
or self.__cache_size
|
||||
and sabnzbd.Downloader.no_active_jobs()
|
||||
)
|
||||
)
|
||||
|
||||
def flush_cache(self) -> None:
|
||||
"""In direct_write mode flush cache contents to file"""
|
||||
forced: set[NzbFile] = set()
|
||||
for article in self.__article_table.copy():
|
||||
if not article.can_direct_write or article.nzf in forced:
|
||||
continue
|
||||
forced.add(article.nzf)
|
||||
if time.monotonic() - self.__last_flush > 1:
|
||||
logging.debug("Forcing write of %s", article.nzf.filepath)
|
||||
sabnzbd.Assembler.process(article.nzf.nzo, article.nzf, allow_non_contiguous=True, article=article)
|
||||
self.__last_flush = time.monotonic()
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
with self.__cache_size_cv:
|
||||
self.__cache_size_cv.wait_for(
|
||||
lambda: self.shutdown or self.should_flush(),
|
||||
timeout=5.0,
|
||||
)
|
||||
if self.shutdown:
|
||||
break
|
||||
# Could be reached by timeout when paused and no further articles arrive
|
||||
with self.__cache_size_cv:
|
||||
if not self.should_flush():
|
||||
continue
|
||||
self.flush_cache()
|
||||
time.sleep(_SECONDS_BETWEEN_FLUSHES)
|
||||
|
||||
def cache_info(self):
|
||||
return ANFO(len(self.__article_table), abs(self.__cache_size), self.__cache_limit)
|
||||
|
||||
@synchronized(ARTICLE_COUNTER_LOCK)
|
||||
def new_limit(self, limit: int):
|
||||
"""Called when cache limit changes"""
|
||||
self.__cache_limit_org = limit
|
||||
@@ -59,31 +125,32 @@ class ArticleCache:
|
||||
self.__cache_limit = self.__cache_upper_limit
|
||||
else:
|
||||
self.__cache_limit = min(limit, self.__cache_upper_limit)
|
||||
|
||||
# Set assembler_write_trigger to be the equivalent of ASSEMBLER_WRITE_THRESHOLD %
|
||||
# of the total cache, assuming an article size of 750 000 bytes
|
||||
self.assembler_write_trigger = int(self.__cache_limit * ASSEMBLER_WRITE_THRESHOLD / 100 / 750_000) + 1
|
||||
|
||||
logging.debug(
|
||||
"Assembler trigger = %d",
|
||||
self.assembler_write_trigger,
|
||||
)
|
||||
self.__non_contiguous_trigger = self.__cache_limit * ARTICLE_CACHE_NON_CONTIGUOUS_FLUSH_PERCENTAGE
|
||||
if self.__cache_limit:
|
||||
logging.debug("Article cache trigger:%s", to_units(self.__non_contiguous_trigger))
|
||||
self.change_direct_write(cfg.direct_write())
|
||||
|
||||
@synchronized(ARTICLE_COUNTER_LOCK)
|
||||
def reserve_space(self, data_size: int):
|
||||
def reserve_space(self, data_size: int) -> bool:
|
||||
"""Reserve space in the cache"""
|
||||
self.__cache_size += data_size
|
||||
if (usage := self.__cache_size + data_size) > self.__cache_limit:
|
||||
return False
|
||||
self.__cache_size = usage
|
||||
self.__cache_size_cv.notify_all()
|
||||
return True
|
||||
|
||||
@synchronized(ARTICLE_COUNTER_LOCK)
|
||||
def free_reserved_space(self, data_size: int):
|
||||
"""Remove previously reserved space"""
|
||||
self.__cache_size -= data_size
|
||||
self.__cache_size_cv.notify_all()
|
||||
|
||||
@synchronized(ARTICLE_COUNTER_LOCK)
|
||||
def space_left(self) -> bool:
|
||||
"""Is there space left in the set limit?"""
|
||||
return self.__cache_size < self.__cache_limit
|
||||
|
||||
def save_article(self, article: Article, data: bytes):
|
||||
def save_article(self, article: Article, data: bytearray):
|
||||
"""Save article in cache, either memory or disk"""
|
||||
nzo = article.nzf.nzo
|
||||
# Skip if already post-processing or fully finished
|
||||
@@ -91,7 +158,8 @@ class ArticleCache:
|
||||
return
|
||||
|
||||
# Register article for bookkeeping in case the job is deleted
|
||||
nzo.saved_articles.add(article)
|
||||
with nzo.lock:
|
||||
nzo.saved_articles.add(article)
|
||||
|
||||
if article.lowest_partnum and not (article.nzf.import_finished or article.nzf.filename_checked):
|
||||
# Write the first-fetched articles to temporary file unless downloading
|
||||
@@ -100,24 +168,17 @@ class ArticleCache:
|
||||
self.__flush_article_to_disk(article, data)
|
||||
return
|
||||
|
||||
if self.__cache_limit:
|
||||
# Check if we exceed the limit
|
||||
data_size = len(data)
|
||||
self.reserve_space(data_size)
|
||||
if self.space_left():
|
||||
# Add new article to the cache
|
||||
self.__article_table[article] = data
|
||||
else:
|
||||
# Return the space and save to disk
|
||||
self.free_reserved_space(data_size)
|
||||
self.__flush_article_to_disk(article, data)
|
||||
# Check if we exceed the limit
|
||||
if self.__cache_limit and self.reserve_space(len(data)):
|
||||
# Add new article to the cache
|
||||
self.__article_table[article] = data
|
||||
else:
|
||||
# No data saved in memory, direct to disk
|
||||
self.__flush_article_to_disk(article, data)
|
||||
|
||||
def load_article(self, article: Article):
|
||||
def load_article(self, article: Article) -> Optional[bytearray]:
|
||||
"""Load the data of the article"""
|
||||
data = None
|
||||
data: Optional[bytearray] = None
|
||||
nzo = article.nzf.nzo
|
||||
|
||||
if article in self.__article_table:
|
||||
@@ -131,9 +192,10 @@ class ArticleCache:
|
||||
return data
|
||||
elif article.art_id:
|
||||
data = sabnzbd.filesystem.load_data(
|
||||
article.art_id, nzo.admin_path, remove=True, do_pickle=False, silent=True
|
||||
article.art_id, nzo.admin_path, remove=True, do_pickle=False, silent=True, mutable=True
|
||||
)
|
||||
nzo.saved_articles.discard(article)
|
||||
with nzo.lock:
|
||||
nzo.saved_articles.discard(article)
|
||||
return data
|
||||
|
||||
def flush_articles(self):
|
||||
@@ -161,10 +223,16 @@ class ArticleCache:
|
||||
elif article.art_id:
|
||||
sabnzbd.filesystem.remove_data(article.art_id, article.nzf.nzo.admin_path)
|
||||
|
||||
@staticmethod
|
||||
def __flush_article_to_disk(article: Article, data):
|
||||
def __flush_article_to_disk(self, article: Article, data: bytearray):
|
||||
# Save data, but don't complain when destination folder is missing
|
||||
# because this flush may come after completion of the NZO.
|
||||
# Direct write to destination if cache is being used
|
||||
if self.__cache_limit and self.__direct_write and sabnzbd.Assembler.assemble_article(article, data):
|
||||
with article.nzf.nzo.lock:
|
||||
article.nzf.nzo.saved_articles.discard(article)
|
||||
return
|
||||
|
||||
# Fallback to disk cache
|
||||
sabnzbd.filesystem.save_data(
|
||||
data, article.get_art_id(), article.nzf.nzo.admin_path, do_pickle=False, silent=True
|
||||
)
|
||||
|
||||
@@ -23,13 +23,16 @@ import os
|
||||
import queue
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
from threading import Thread
|
||||
import ctypes
|
||||
from typing import Optional
|
||||
from typing import Optional, NamedTuple, Union
|
||||
import rarfile
|
||||
import time
|
||||
|
||||
import sabctools
|
||||
import sabnzbd
|
||||
from sabnzbd.misc import get_all_passwords, match_str, SABRarFile
|
||||
from sabnzbd.misc import get_all_passwords, match_str, SABRarFile, to_units
|
||||
from sabnzbd.filesystem import (
|
||||
set_permissions,
|
||||
clip_path,
|
||||
@@ -39,33 +42,222 @@ from sabnzbd.filesystem import (
|
||||
has_unwanted_extension,
|
||||
get_basename,
|
||||
)
|
||||
from sabnzbd.constants import Status, GIGI
|
||||
from sabnzbd.constants import (
|
||||
Status,
|
||||
GIGI,
|
||||
ASSEMBLER_WRITE_THRESHOLD_FACTOR_APPEND,
|
||||
ASSEMBLER_WRITE_THRESHOLD_FACTOR_DIRECT_WRITE,
|
||||
ASSEMBLER_MAX_WRITE_THRESHOLD_DIRECT_WRITE,
|
||||
SOFT_ASSEMBLER_QUEUE_LIMIT,
|
||||
ASSEMBLER_DELAY_FACTOR_DIRECT_WRITE,
|
||||
ARTICLE_CACHE_NON_CONTIGUOUS_FLUSH_PERCENTAGE,
|
||||
ASSEMBLER_WRITE_INTERVAL,
|
||||
)
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.nzb import NzbFile, NzbObject
|
||||
from sabnzbd.nzb import NzbFile, NzbObject, Article
|
||||
import sabnzbd.par2file as par2file
|
||||
|
||||
|
||||
class AssemblerTask(NamedTuple):
|
||||
nzo: Optional[NzbObject] = None
|
||||
nzf: Optional[NzbFile] = None
|
||||
file_done: bool = False
|
||||
allow_non_contiguous: bool = False
|
||||
direct_write: bool = False
|
||||
|
||||
|
||||
class Assembler(Thread):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.max_queue_size: int = cfg.assembler_max_queue_size()
|
||||
self.queue: queue.Queue[tuple[Optional[NzbObject], Optional[NzbFile], Optional[bool]]] = queue.Queue()
|
||||
self.direct_write: bool = cfg.direct_write()
|
||||
self.cache_limit: int = 0
|
||||
# Contiguous bytes required to trigger append writes
|
||||
self.append_trigger: int = 1
|
||||
# Total bytes required to trigger direct-write assembles
|
||||
self.direct_write_trigger: int = 1
|
||||
self.delay_trigger: int = 1
|
||||
self.queue: queue.Queue[AssemblerTask] = queue.Queue()
|
||||
self.queued_lock = threading.Lock()
|
||||
self.queued_nzf: set[str] = set()
|
||||
self.queued_nzf_non_contiguous: set[str] = set()
|
||||
self.queued_next_time: dict[str, float] = dict()
|
||||
self.ready_bytes_lock = threading.Lock()
|
||||
self.ready_bytes: dict[str, int] = dict()
|
||||
|
||||
def stop(self):
|
||||
self.queue.put((None, None, None))
|
||||
self.queue.put(AssemblerTask())
|
||||
|
||||
def process(self, nzo: NzbObject, nzf: Optional[NzbFile] = None, file_done: Optional[bool] = None):
|
||||
self.queue.put((nzo, nzf, file_done))
|
||||
def new_limit(self, limit: int):
|
||||
"""Called when cache limit changes"""
|
||||
self.cache_limit = limit
|
||||
self.append_trigger = max(1, int(limit * ASSEMBLER_WRITE_THRESHOLD_FACTOR_APPEND))
|
||||
self.direct_write_trigger = max(
|
||||
1,
|
||||
min(
|
||||
max(1, int(limit * ASSEMBLER_WRITE_THRESHOLD_FACTOR_DIRECT_WRITE)),
|
||||
ASSEMBLER_MAX_WRITE_THRESHOLD_DIRECT_WRITE,
|
||||
),
|
||||
)
|
||||
self.calculate_delay_trigger()
|
||||
self.change_direct_write(cfg.direct_write())
|
||||
logging.debug(
|
||||
"Assembler trigger append=%s, direct=%s, delay=%s",
|
||||
to_units(self.append_trigger),
|
||||
to_units(self.direct_write_trigger),
|
||||
to_units(self.delay_trigger),
|
||||
)
|
||||
|
||||
def queue_level(self) -> float:
|
||||
return self.queue.qsize() / self.max_queue_size
|
||||
def change_direct_write(self, direct_write: bool) -> None:
|
||||
self.direct_write = direct_write and self.direct_write_trigger > 1
|
||||
self.calculate_delay_trigger()
|
||||
|
||||
def calculate_delay_trigger(self):
|
||||
"""Point at which downloader should start being delayed, recalculated when cache limit or direct write changes"""
|
||||
self.delay_trigger = int(
|
||||
max(
|
||||
(
|
||||
750_000 * self.max_queue_size * ASSEMBLER_DELAY_FACTOR_DIRECT_WRITE
|
||||
if self.direct_write
|
||||
else 750_000 * self.max_queue_size
|
||||
),
|
||||
(
|
||||
self.cache_limit * ARTICLE_CACHE_NON_CONTIGUOUS_FLUSH_PERCENTAGE
|
||||
if self.direct_write
|
||||
else min(self.append_trigger * self.max_queue_size, int(self.cache_limit * 0.5))
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def is_busy(self) -> bool:
|
||||
"""Returns True if the assembler thread has at least one NzbFile it is assembling"""
|
||||
return bool(self.queued_nzf or self.queued_nzf_non_contiguous)
|
||||
|
||||
def total_ready_bytes(self) -> int:
|
||||
with self.ready_bytes_lock:
|
||||
return sum(self.ready_bytes.values())
|
||||
|
||||
def update_ready_bytes(self, nzf: NzbFile, delta: int) -> int:
|
||||
with self.ready_bytes_lock:
|
||||
cur = self.ready_bytes.get(nzf.nzf_id, 0) + delta
|
||||
if cur <= 0:
|
||||
self.ready_bytes.pop(nzf.nzf_id, None)
|
||||
else:
|
||||
self.ready_bytes[nzf.nzf_id] = cur
|
||||
return cur
|
||||
|
||||
def clear_ready_bytes(self, *nzfs: NzbFile) -> None:
|
||||
with self.ready_bytes_lock:
|
||||
for nzf in nzfs:
|
||||
self.ready_bytes.pop(nzf.nzf_id, None)
|
||||
self.queued_next_time.pop(nzf.nzf_id, None)
|
||||
|
||||
def process(
|
||||
self,
|
||||
nzo: NzbObject = None,
|
||||
nzf: Optional[NzbFile] = None,
|
||||
file_done: bool = False,
|
||||
allow_non_contiguous: bool = False,
|
||||
article: Optional[Article] = None,
|
||||
) -> None:
|
||||
if nzf is None:
|
||||
# post-proc
|
||||
self.queue.put(AssemblerTask(nzo))
|
||||
return
|
||||
|
||||
# Track bytes pending being written for this nzf
|
||||
if self.should_track_ready_bytes(article, allow_non_contiguous):
|
||||
ready_bytes = self.update_ready_bytes(nzf, article.decoded_size)
|
||||
else:
|
||||
ready_bytes = 0
|
||||
|
||||
article_has_first_part = bool(article and article.lowest_partnum)
|
||||
if article_has_first_part:
|
||||
self.queued_next_time[nzf.nzf_id] = time.monotonic() + ASSEMBLER_WRITE_INTERVAL
|
||||
|
||||
if not self.should_queue_nzf(
|
||||
nzf,
|
||||
article_has_first_part=article_has_first_part,
|
||||
filename_checked=nzf.filename_checked,
|
||||
import_finished=nzf.import_finished,
|
||||
file_done=file_done,
|
||||
allow_non_contiguous=allow_non_contiguous,
|
||||
ready_bytes=ready_bytes,
|
||||
):
|
||||
return
|
||||
|
||||
with self.queued_lock:
|
||||
# Recheck not already in the normal queue under lock, but always enqueue when file_done
|
||||
if not file_done and nzf.nzf_id in self.queued_nzf:
|
||||
return
|
||||
if allow_non_contiguous:
|
||||
if not file_done and nzf.nzf_id in self.queued_nzf_non_contiguous:
|
||||
return
|
||||
self.queued_nzf_non_contiguous.add(nzf.nzf_id)
|
||||
else:
|
||||
self.queued_nzf.add(nzf.nzf_id)
|
||||
self.queued_next_time[nzf.nzf_id] = time.monotonic() + ASSEMBLER_WRITE_INTERVAL
|
||||
can_direct_write = self.direct_write and nzf.type == "yenc"
|
||||
self.queue.put(AssemblerTask(nzo, nzf, file_done, allow_non_contiguous, can_direct_write))
|
||||
|
||||
def should_queue_nzf(
|
||||
self,
|
||||
nzf: NzbFile,
|
||||
*,
|
||||
article_has_first_part: bool,
|
||||
filename_checked: bool,
|
||||
import_finished: bool,
|
||||
file_done: bool,
|
||||
allow_non_contiguous: bool,
|
||||
ready_bytes: int,
|
||||
) -> bool:
|
||||
# Always queue if done
|
||||
if file_done:
|
||||
return True
|
||||
if nzf.nzf_id in self.queued_nzf:
|
||||
return False
|
||||
# Always write
|
||||
if article_has_first_part and filename_checked and not import_finished:
|
||||
return True
|
||||
next_ready = (next_article := nzf.assembler_next_article) and (next_article.decoded or next_article.on_disk)
|
||||
# Trigger every 5 seconds if next article is decoded or on_disk
|
||||
if next_ready and time.monotonic() > self.queued_next_time.get(nzf.nzf_id, 0):
|
||||
return True
|
||||
# Append
|
||||
if not self.direct_write or nzf.type != "yenc":
|
||||
return nzf.contiguous_ready_bytes() >= self.append_trigger
|
||||
# Direct Write
|
||||
if allow_non_contiguous:
|
||||
return True
|
||||
# Direct Write ready bytes trigger if next is also ready
|
||||
if next_ready and ready_bytes >= self.direct_write_trigger:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def should_track_ready_bytes(article: Optional[Article], allow_non_contiguous: bool) -> bool:
|
||||
""""""
|
||||
return article and not allow_non_contiguous and article.decoded_size
|
||||
|
||||
def delay(self) -> float:
|
||||
"""Calculate how long if at all the downloader thread should sleep to allow the assembler to catch up"""
|
||||
ready_total = self.total_ready_bytes()
|
||||
# Below trigger: no delay possible
|
||||
if ready_total <= self.delay_trigger:
|
||||
return 0
|
||||
pressure = (ready_total - self.delay_trigger) / max(1.0, self.cache_limit - self.delay_trigger)
|
||||
if pressure <= SOFT_ASSEMBLER_QUEUE_LIMIT:
|
||||
return 0
|
||||
# 50-100%: 0-0.25 seconds, capped at 0.15
|
||||
sleep = min((pressure - SOFT_ASSEMBLER_QUEUE_LIMIT) / 2, 0.15)
|
||||
return max(0.001, sleep)
|
||||
|
||||
def run(self):
|
||||
while 1:
|
||||
# Set NzbObject and NzbFile objects to None so references
|
||||
# from this thread do not keep the objects alive (see #1628)
|
||||
nzo = nzf = None
|
||||
nzo, nzf, file_done = self.queue.get()
|
||||
nzo, nzf, file_done, allow_non_contiguous, direct_write = self.queue.get()
|
||||
if not nzo:
|
||||
logging.debug("Shutting down assembler")
|
||||
break
|
||||
@@ -75,11 +267,15 @@ class Assembler(Thread):
|
||||
if file_done and not sabnzbd.Downloader.paused:
|
||||
self.diskspace_check(nzo, nzf)
|
||||
|
||||
# Prepare filepath
|
||||
if filepath := nzf.prepare_filepath():
|
||||
try:
|
||||
# Prepare filepath
|
||||
if not (filepath := nzf.prepare_filepath()):
|
||||
logging.debug("Prepare filepath failed for file %s in job %s", nzf.filename, nzo.final_name)
|
||||
continue
|
||||
|
||||
try:
|
||||
logging.debug("Decoding part of %s", filepath)
|
||||
self.assemble(nzo, nzf, file_done)
|
||||
self.assemble(nzo, nzf, file_done, allow_non_contiguous, direct_write)
|
||||
|
||||
# Continue after partly written data
|
||||
if not file_done:
|
||||
@@ -122,9 +318,16 @@ class Assembler(Thread):
|
||||
except Exception:
|
||||
logging.error(T("Fatal error in Assembler"), exc_info=True)
|
||||
break
|
||||
finally:
|
||||
with self.queued_lock:
|
||||
if allow_non_contiguous:
|
||||
self.queued_nzf_non_contiguous.discard(nzf.nzf_id)
|
||||
else:
|
||||
self.queued_nzf.discard(nzf.nzf_id)
|
||||
else:
|
||||
sabnzbd.NzbQueue.remove(nzo.nzo_id, cleanup=False)
|
||||
sabnzbd.PostProcessor.process(nzo)
|
||||
self.clear_ready_bytes(*nzo.files)
|
||||
|
||||
@staticmethod
|
||||
def diskspace_check(nzo: NzbObject, nzf: NzbFile):
|
||||
@@ -162,52 +365,116 @@ class Assembler(Thread):
|
||||
sabnzbd.emailer.diskfull_mail()
|
||||
|
||||
@staticmethod
|
||||
def assemble(nzo: NzbObject, nzf: NzbFile, file_done: bool):
|
||||
def assemble(nzo: NzbObject, nzf: NzbFile, file_done: bool, allow_non_contiguous: bool, direct_write: bool) -> None:
|
||||
"""Assemble a NZF from its table of articles
|
||||
1) Partial write: write what we have
|
||||
2) Nothing written before: write all
|
||||
"""
|
||||
load_article = sabnzbd.ArticleCache.load_article
|
||||
downloader = sabnzbd.Downloader
|
||||
decodetable = nzf.decodetable
|
||||
|
||||
fd: Optional[int] = None
|
||||
skipped: bool = False # have any articles been skipped
|
||||
offset: int = 0 # sequential offset for append writes
|
||||
|
||||
try:
|
||||
# Resume assembly from where we got to previously
|
||||
for idx in range(nzf.assembler_next_index, len(decodetable)):
|
||||
article = decodetable[idx]
|
||||
|
||||
# We write large article-sized chunks, so we can safely skip the buffering of Python
|
||||
with open(nzf.filepath, "ab", buffering=0) as fout:
|
||||
for article in nzf.decodetable:
|
||||
# Break if deleted during writing
|
||||
if nzo.status is Status.DELETED:
|
||||
break
|
||||
|
||||
# allow_non_contiguous is when the cache forces the assembler to write all articles, even if it leaves gaps.
|
||||
# In most cases we can stop at the first article that has not been tried, because they are requested in order.
|
||||
# However, if we are paused then always consider the whole decodetable to ensure everything possible is written.
|
||||
if allow_non_contiguous and not article.tries and not downloader.paused:
|
||||
break
|
||||
|
||||
# Skip already written articles
|
||||
if article.on_disk:
|
||||
if fd is not None and article.decoded_size is not None:
|
||||
# Move the file descriptor forward past this article
|
||||
offset += article.decoded_size
|
||||
if not skipped:
|
||||
with nzf.lock:
|
||||
if nzf.assembler_next_index == idx:
|
||||
nzf.assembler_next_index = idx + 1
|
||||
continue
|
||||
|
||||
# Write all decoded articles
|
||||
if article.decoded:
|
||||
# Could be empty in case nzo was deleted
|
||||
if data := sabnzbd.ArticleCache.load_article(article):
|
||||
written = fout.write(data)
|
||||
|
||||
# In raw/non-buffered mode fout.write may not write everything requested:
|
||||
# https://docs.python.org/3/library/io.html?highlight=write#io.RawIOBase.write
|
||||
while written < len(data):
|
||||
written += fout.write(data[written:])
|
||||
|
||||
nzf.update_crc32(article.crc32, len(data))
|
||||
article.on_disk = True
|
||||
else:
|
||||
logging.info("No data found when trying to write %s", article)
|
||||
else:
|
||||
# stop if next piece not yet decoded
|
||||
if not article.decoded:
|
||||
# If the article was not decoded but the file
|
||||
# is done, it is just a missing piece, so keep writing
|
||||
if file_done:
|
||||
continue
|
||||
# We reach an article that was not decoded
|
||||
if allow_non_contiguous:
|
||||
skipped = True
|
||||
continue
|
||||
break
|
||||
|
||||
# Could be empty in case nzo was deleted
|
||||
data = load_article(article)
|
||||
if not data:
|
||||
if file_done:
|
||||
continue
|
||||
if allow_non_contiguous:
|
||||
skipped = True
|
||||
continue
|
||||
else:
|
||||
# We reach an article that was not decoded
|
||||
logging.info("No data found when trying to write %s", article)
|
||||
break
|
||||
|
||||
# If required open the file
|
||||
if fd is None:
|
||||
fd, offset, direct_write = Assembler.open(
|
||||
nzf, direct_write and article.can_direct_write, article.file_size
|
||||
)
|
||||
if not direct_write and allow_non_contiguous:
|
||||
# Can only be allow_non_contiguous if we wanted direct_write, file_done will always be queued separately
|
||||
break
|
||||
|
||||
if direct_write and article.can_direct_write:
|
||||
offset += Assembler.write(fd, idx, nzf, article, data)
|
||||
else:
|
||||
if direct_write and skipped and not file_done:
|
||||
# If we have already skipped an article then need to abort, unless this is the final assemble
|
||||
break
|
||||
offset += Assembler.write(fd, idx, nzf, article, data, offset)
|
||||
|
||||
finally:
|
||||
if fd is not None:
|
||||
os.close(fd)
|
||||
|
||||
# Final steps
|
||||
if file_done:
|
||||
sabnzbd.Assembler.clear_ready_bytes(nzf)
|
||||
set_permissions(nzf.filepath)
|
||||
nzf.assembled = True
|
||||
|
||||
@staticmethod
|
||||
def assemble_article(article: Article, data: bytearray) -> bool:
|
||||
"""Write a single article to disk"""
|
||||
if not article.can_direct_write:
|
||||
return False
|
||||
nzf = article.nzf
|
||||
with nzf.file_lock:
|
||||
fd, _, direct_write = Assembler.open(nzf, True, article.file_size)
|
||||
try:
|
||||
if not direct_write:
|
||||
cfg.direct_write.set(False)
|
||||
return False
|
||||
Assembler.write(fd, None, nzf, article, data)
|
||||
except FileNotFoundError:
|
||||
# nzo has probably been deleted, ArticleCache tries the fallback and handles it
|
||||
return False
|
||||
finally:
|
||||
os.close(fd)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def check_encrypted_and_unwanted(nzo: NzbObject, nzf: NzbFile):
|
||||
"""Encryption and unwanted extension detection"""
|
||||
@@ -245,6 +512,71 @@ class Assembler(Thread):
|
||||
nzo.fail_msg = T("Aborted, unwanted extension detected")
|
||||
sabnzbd.NzbQueue.end_job(nzo)
|
||||
|
||||
@staticmethod
|
||||
def write(
|
||||
fd: int, nzf_index: Optional[int], nzf: NzbFile, article: Article, data: bytearray, offset: Optional[int] = None
|
||||
) -> int:
|
||||
"""Write data at position in a file"""
|
||||
pos = article.data_begin if offset is None else offset
|
||||
written = Assembler._write(fd, nzf, data, pos)
|
||||
# In raw/non-buffered mode os.write may not write everything requested:
|
||||
# https://docs.python.org/3/library/io.html?highlight=write#io.RawIOBase.write
|
||||
if written < len(data) and (mv := memoryview(data)):
|
||||
while written < len(data):
|
||||
written += Assembler._write(fd, nzf, mv[written:], pos + written)
|
||||
|
||||
nzf.update_crc32(article.crc32, len(data))
|
||||
article.on_disk = True
|
||||
sabnzbd.Assembler.update_ready_bytes(nzf, -len(data))
|
||||
with nzf.lock:
|
||||
# assembler_next_index is the lowest index that has not yet been written sequentially from the start of the file.
|
||||
# If this was the next required index to remain sequential, it can be incremented which allows the assembler to
|
||||
# resume without rechecking articles that are already known to be on disk.
|
||||
# If nzf_index is None, determine it now.
|
||||
if nzf_index is None:
|
||||
idx = nzf.assembler_next_index
|
||||
if idx < len(nzf.decodetable) and article == nzf.decodetable[idx]:
|
||||
nzf_index = idx
|
||||
if nzf_index is not None and nzf.assembler_next_index == nzf_index:
|
||||
nzf.assembler_next_index += 1
|
||||
return written
|
||||
|
||||
@staticmethod
|
||||
def _write(fd: int, nzf: NzbFile, data: Union[bytearray, memoryview], offset: int) -> int:
|
||||
if sabnzbd.WINDOWS:
|
||||
# pwrite is not implemented on Windows so fallback to os.lseek and os.write
|
||||
# Must lock since it is possible to write from multiple threads (assembler + downloader)
|
||||
with nzf.file_lock:
|
||||
os.lseek(fd, offset, os.SEEK_SET)
|
||||
return os.write(fd, data)
|
||||
else:
|
||||
return os.pwrite(fd, data, offset)
|
||||
|
||||
@staticmethod
|
||||
def open(nzf: NzbFile, direct_write: bool, file_size: int) -> tuple[int, int, bool]:
|
||||
"""Open file for nzf
|
||||
|
||||
Use direct_write if requested, with a fallback to setting the current file position for append mode
|
||||
:returns (file_descriptor, current_offset, can_direct_write)
|
||||
"""
|
||||
with nzf.file_lock:
|
||||
# Get the current umask without changing it, to create a file with the same permissions as `with open(...)`
|
||||
os.umask(os.umask(0))
|
||||
fd = os.open(nzf.filepath, os.O_CREAT | os.O_WRONLY | getattr(os, "O_BINARY", 0), 0o666)
|
||||
offset = nzf.contiguous_offset()
|
||||
os.lseek(fd, offset, os.SEEK_SET)
|
||||
if direct_write:
|
||||
if not file_size:
|
||||
direct_write = False
|
||||
if os.fstat(fd).st_size == 0:
|
||||
try:
|
||||
sabctools.sparse(fd, file_size)
|
||||
except OSError:
|
||||
logging.debug("Sparse call failed for %s", nzf.filepath)
|
||||
cfg.direct_write.set(False)
|
||||
direct_write = False
|
||||
return fd, offset, direct_write
|
||||
|
||||
|
||||
RE_SUBS = re.compile(r"\W+sub|subs|subpack|subtitle|subtitles(?![a-z])", re.I)
|
||||
SAFE_EXTS = (".mkv", ".mp4", ".avi", ".wmv", ".mpg", ".webm")
|
||||
|
||||
@@ -25,6 +25,7 @@ import re
|
||||
import argparse
|
||||
import socket
|
||||
import ipaddress
|
||||
import threading
|
||||
from typing import Union
|
||||
|
||||
import sabnzbd
|
||||
@@ -508,6 +509,7 @@ x_frame_options = OptionBool("misc", "x_frame_options", True)
|
||||
allow_old_ssl_tls = OptionBool("misc", "allow_old_ssl_tls", False)
|
||||
enable_season_sorting = OptionBool("misc", "enable_season_sorting", True)
|
||||
verify_xff_header = OptionBool("misc", "verify_xff_header", True)
|
||||
direct_write = OptionBool("misc", "direct_write", True)
|
||||
|
||||
# Text values
|
||||
rss_odd_titles = OptionList("misc", "rss_odd_titles", ["nzbindex.nl/", "nzbindex.com/", "nzbclub.com/"])
|
||||
@@ -535,7 +537,6 @@ ssdp_broadcast_interval = OptionNumber("misc", "ssdp_broadcast_interval", 15, mi
|
||||
ext_rename_ignore = OptionList("misc", "ext_rename_ignore", validation=lower_case_ext)
|
||||
unrar_parameters = OptionStr("misc", "unrar_parameters", validation=supported_unrar_parameters)
|
||||
outgoing_nntp_ip = OptionStr("misc", "outgoing_nntp_ip")
|
||||
pipelining_requests = OptionNumber("misc", "pipelining_requests", DEF_PIPELINING_REQUESTS, minval=1, maxval=10)
|
||||
|
||||
|
||||
##############################################################################
|
||||
@@ -744,6 +745,13 @@ def new_limit():
|
||||
if sabnzbd.__INITIALIZED__:
|
||||
# Only update after full startup
|
||||
sabnzbd.ArticleCache.new_limit(cache_limit.get_int())
|
||||
sabnzbd.Assembler.new_limit(sabnzbd.ArticleCache.cache_info().cache_limit)
|
||||
|
||||
|
||||
def new_direct_write():
|
||||
"""Callback for direct write changes"""
|
||||
sabnzbd.Assembler.change_direct_write(bool(direct_write()))
|
||||
sabnzbd.ArticleCache.change_direct_write(bool(direct_write()))
|
||||
|
||||
|
||||
def guard_restart():
|
||||
|
||||
@@ -42,6 +42,7 @@ from sabnzbd.constants import (
|
||||
CONFIG_BACKUP_HTTPS,
|
||||
DEF_INI_FILE,
|
||||
DEF_SORTER_RENAME_SIZE,
|
||||
DEF_PIPELINING_REQUESTS,
|
||||
)
|
||||
from sabnzbd.decorators import synchronized
|
||||
from sabnzbd.filesystem import clip_path, real_path, create_real_path, renamer, remove_file, is_writable
|
||||
@@ -209,7 +210,8 @@ class OptionBool(Option):
|
||||
super().set(sabnzbd.misc.bool_conv(value))
|
||||
|
||||
def __call__(self) -> int:
|
||||
"""get() replacement"""
|
||||
"""Many places assume 0/1 is used for historical reasons.
|
||||
Using pure bools breaks in random places"""
|
||||
return int(self.get())
|
||||
|
||||
|
||||
@@ -444,6 +446,7 @@ class ConfigServer:
|
||||
self.enable = OptionBool(name, "enable", True, add=False)
|
||||
self.required = OptionBool(name, "required", False, add=False)
|
||||
self.optional = OptionBool(name, "optional", False, add=False)
|
||||
self.pipelining_requests = OptionNumber(name, "pipelining_requests", DEF_PIPELINING_REQUESTS, 1, 20, add=False)
|
||||
self.retention = OptionNumber(name, "retention", 0, add=False)
|
||||
self.expire_date = OptionStr(name, "expire_date", add=False)
|
||||
self.quota = OptionStr(name, "quota", add=False)
|
||||
@@ -476,6 +479,7 @@ class ConfigServer:
|
||||
"enable",
|
||||
"required",
|
||||
"optional",
|
||||
"pipelining_requests",
|
||||
"retention",
|
||||
"expire_date",
|
||||
"quota",
|
||||
@@ -511,6 +515,7 @@ class ConfigServer:
|
||||
output_dict["enable"] = self.enable()
|
||||
output_dict["required"] = self.required()
|
||||
output_dict["optional"] = self.optional()
|
||||
output_dict["pipelining_requests"] = self.pipelining_requests()
|
||||
output_dict["retention"] = self.retention()
|
||||
output_dict["expire_date"] = self.expire_date()
|
||||
output_dict["quota"] = self.quota()
|
||||
|
||||
@@ -50,7 +50,7 @@ RENAMES_FILE = "__renames__"
|
||||
ATTRIB_FILE = "SABnzbd_attrib"
|
||||
REPAIR_REQUEST = "repair-all.sab"
|
||||
|
||||
SABCTOOLS_VERSION_REQUIRED = "9.1.0"
|
||||
SABCTOOLS_VERSION_REQUIRED = "9.3.1"
|
||||
|
||||
DB_HISTORY_VERSION = 1
|
||||
DB_HISTORY_NAME = "history%s.db" % DB_HISTORY_VERSION
|
||||
@@ -100,10 +100,16 @@ CONFIG_BACKUP_HTTPS = { # "basename": "associated setting"
|
||||
DEF_MAX_ASSEMBLER_QUEUE = 12
|
||||
SOFT_ASSEMBLER_QUEUE_LIMIT = 0.5
|
||||
# Percentage of cache to use before adding file to assembler
|
||||
ASSEMBLER_WRITE_THRESHOLD = 5
|
||||
ASSEMBLER_WRITE_THRESHOLD_FACTOR_APPEND = 0.05
|
||||
ASSEMBLER_WRITE_THRESHOLD_FACTOR_DIRECT_WRITE = 0.75
|
||||
ASSEMBLER_MAX_WRITE_THRESHOLD_DIRECT_WRITE = int(1 * GIGI)
|
||||
ASSEMBLER_DELAY_FACTOR_DIRECT_WRITE = 1.5
|
||||
ASSEMBLER_WRITE_INTERVAL = 5.0
|
||||
NNTP_BUFFER_SIZE = int(256 * KIBI)
|
||||
NTTP_MAX_BUFFER_SIZE = int(10 * MEBI)
|
||||
DEF_PIPELINING_REQUESTS = 2
|
||||
DEF_PIPELINING_REQUESTS = 1
|
||||
# Article cache capacity factor to force a non-contiguous flush to disk
|
||||
ARTICLE_CACHE_NON_CONTIGUOUS_FLUSH_PERCENTAGE = 0.9
|
||||
|
||||
REPAIR_PRIORITY = 3
|
||||
FORCE_PRIORITY = 2
|
||||
|
||||
@@ -114,6 +114,12 @@ class HistoryDB:
|
||||
_ = self.execute("PRAGMA user_version = 5;") and self.execute(
|
||||
"ALTER TABLE history ADD COLUMN time_added INTEGER;"
|
||||
)
|
||||
if version < 6:
|
||||
_ = (
|
||||
self.execute("PRAGMA user_version = 6;")
|
||||
and self.execute("CREATE UNIQUE INDEX idx_history_nzo_id ON history(nzo_id);")
|
||||
and self.execute("CREATE INDEX idx_history_archive_completed ON history(archive, completed DESC);")
|
||||
)
|
||||
|
||||
HistoryDB.startup_done = True
|
||||
|
||||
@@ -160,8 +166,7 @@ class HistoryDB:
|
||||
|
||||
def create_history_db(self):
|
||||
"""Create a new (empty) database file"""
|
||||
self.execute(
|
||||
"""
|
||||
self.execute("""
|
||||
CREATE TABLE history (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"completed" INTEGER NOT NULL,
|
||||
@@ -194,9 +199,10 @@ class HistoryDB:
|
||||
"archive" INTEGER,
|
||||
"time_added" INTEGER
|
||||
)
|
||||
"""
|
||||
)
|
||||
self.execute("PRAGMA user_version = 5;")
|
||||
""")
|
||||
self.execute("PRAGMA user_version = 6;")
|
||||
self.execute("CREATE UNIQUE INDEX idx_history_nzo_id ON history(nzo_id);")
|
||||
self.execute("CREATE INDEX idx_history_archive_completed ON history(archive, completed DESC);")
|
||||
|
||||
def close(self):
|
||||
"""Close database connection"""
|
||||
@@ -369,33 +375,34 @@ class HistoryDB:
|
||||
|
||||
def have_duplicate_key(self, duplicate_key: str) -> bool:
|
||||
"""Check whether History contains this duplicate key"""
|
||||
total = 0
|
||||
if self.execute(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM History
|
||||
WHERE
|
||||
duplicate_key = ? AND
|
||||
STATUS != ?""",
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM history
|
||||
WHERE duplicate_key = ? AND status != ?
|
||||
) as found
|
||||
""",
|
||||
(duplicate_key, Status.FAILED),
|
||||
):
|
||||
total = self.cursor.fetchone()["COUNT(*)"]
|
||||
return total > 0
|
||||
return bool(self.cursor.fetchone()["found"])
|
||||
return False
|
||||
|
||||
def have_name_or_md5sum(self, name: str, md5sum: str) -> bool:
|
||||
"""Check whether this name or md5sum is already in History"""
|
||||
total = 0
|
||||
if self.execute(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM History
|
||||
WHERE
|
||||
( LOWER(name) = LOWER(?) OR md5sum = ? ) AND
|
||||
STATUS != ?""",
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM history
|
||||
WHERE (name = ? COLLATE NOCASE OR md5sum = ?)
|
||||
AND status != ?
|
||||
) as found
|
||||
""",
|
||||
(name, md5sum, Status.FAILED),
|
||||
):
|
||||
total = self.cursor.fetchone()["COUNT(*)"]
|
||||
return total > 0
|
||||
return bool(self.cursor.fetchone()["found"])
|
||||
return False
|
||||
|
||||
def get_history_size(self) -> tuple[int, int, int]:
|
||||
"""Returns the total size of the history and
|
||||
|
||||
@@ -163,6 +163,7 @@ def decode_yenc(article: Article, response: sabctools.NNTPResponse) -> bytearray
|
||||
article.file_size = response.file_size
|
||||
article.data_begin = response.part_begin
|
||||
article.data_size = response.part_size
|
||||
article.decoded_size = response.bytes_decoded
|
||||
|
||||
nzf = article.nzf
|
||||
# Assume it is yenc
|
||||
@@ -198,6 +199,7 @@ def decode_uu(article: Article, response: sabctools.NNTPResponse) -> bytearray:
|
||||
raise BadData(response.data)
|
||||
|
||||
decoded_data = response.data
|
||||
article.decoded_size = response.bytes_decoded
|
||||
nzf = article.nzf
|
||||
nzf.type = "uu"
|
||||
|
||||
|
||||
@@ -20,10 +20,9 @@
|
||||
##############################################################################
|
||||
import time
|
||||
import functools
|
||||
from typing import Union, Callable
|
||||
from typing import Union, Callable, Any
|
||||
from threading import Lock, RLock, Condition
|
||||
|
||||
|
||||
# All operations that modify the queue need to happen in a lock
|
||||
# Also used when importing NZBs to prevent IO-race conditions
|
||||
# Names of wrapper-functions should be the same in misc.caller_name
|
||||
@@ -35,15 +34,21 @@ DOWNLOADER_CV = Condition(NZBQUEUE_LOCK)
|
||||
DOWNLOADER_LOCK = RLock()
|
||||
|
||||
|
||||
def synchronized(lock: Union[Lock, RLock]):
|
||||
def synchronized(lock: Union[Lock, RLock, Condition, None] = None):
|
||||
def wrap(func: Callable):
|
||||
def call_func(*args, **kw):
|
||||
# Using the try/finally approach is 25% faster compared to using "with lock"
|
||||
# Either use the supplied lock or the object-specific one
|
||||
# Because it's a variable in the upper function, we cannot use it directly
|
||||
lock_obj = lock
|
||||
if not lock_obj:
|
||||
lock_obj = getattr(args[0], "lock")
|
||||
|
||||
# Using try/finally is ~25% faster than "with lock"
|
||||
try:
|
||||
lock.acquire()
|
||||
lock_obj.acquire()
|
||||
return func(*args, **kw)
|
||||
finally:
|
||||
lock.release()
|
||||
lock_obj.release()
|
||||
|
||||
return call_func
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ files to the job-name in the queue if the filename looks obfuscated
|
||||
Based on work by P1nGu1n
|
||||
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -28,7 +28,7 @@ import sys
|
||||
import ssl
|
||||
import time
|
||||
from datetime import date
|
||||
from typing import Optional, Union, Deque
|
||||
from typing import Optional, Union, Deque, Callable
|
||||
|
||||
import sabctools
|
||||
|
||||
@@ -37,10 +37,8 @@ from sabnzbd.decorators import synchronized, NzbQueueLocker, DOWNLOADER_CV, DOWN
|
||||
from sabnzbd.newswrapper import NewsWrapper, NNTPPermanentError
|
||||
import sabnzbd.config as config
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.misc import from_units, helpful_warning, int_conv, MultiAddQueue
|
||||
from sabnzbd.misc import from_units, helpful_warning, int_conv, MultiAddQueue, to_units
|
||||
from sabnzbd.get_addrinfo import get_fastest_addrinfo, AddrInfo
|
||||
from sabnzbd.constants import SOFT_ASSEMBLER_QUEUE_LIMIT
|
||||
|
||||
|
||||
# Timeout penalty in minutes for each cause
|
||||
_PENALTY_UNKNOWN = 3 # Unknown cause
|
||||
@@ -85,6 +83,7 @@ class Server:
|
||||
"retention",
|
||||
"username",
|
||||
"password",
|
||||
"pipelining_requests",
|
||||
"busy_threads",
|
||||
"next_busy_threads_check",
|
||||
"idle_threads",
|
||||
@@ -113,6 +112,7 @@ class Server:
|
||||
use_ssl,
|
||||
ssl_verify,
|
||||
ssl_ciphers,
|
||||
pipelining_requests,
|
||||
username=None,
|
||||
password=None,
|
||||
required=False,
|
||||
@@ -137,6 +137,7 @@ class Server:
|
||||
self.retention: int = retention
|
||||
self.username: Optional[str] = username
|
||||
self.password: Optional[str] = password
|
||||
self.pipelining_requests: Callable[[], int] = pipelining_requests
|
||||
|
||||
self.busy_threads: set[NewsWrapper] = set()
|
||||
self.next_busy_threads_check: float = 0
|
||||
@@ -170,7 +171,6 @@ class Server:
|
||||
def stop(self):
|
||||
"""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()
|
||||
@@ -188,8 +188,12 @@ class Server:
|
||||
if self.article_queue:
|
||||
article = self.article_queue[0] if peek else self.article_queue.popleft()
|
||||
# Mark expired articles as tried on this server
|
||||
if not peek and self.retention and article.nzf.nzo.avg_stamp < time.time() - self.retention:
|
||||
sabnzbd.Downloader.decode(article)
|
||||
if self.retention and article.nzf.nzo.avg_stamp < time.time() - self.retention:
|
||||
if not peek:
|
||||
sabnzbd.Downloader.decode(article)
|
||||
# sabnzbd.NzbQueue.get_articles stops after each nzo with articles.
|
||||
# As a result, if one article is out of retention, all remaining
|
||||
# entries in article_queue will also be out of retention.
|
||||
while self.article_queue:
|
||||
sabnzbd.Downloader.decode(self.article_queue.pop())
|
||||
else:
|
||||
@@ -296,7 +300,12 @@ class Downloader(Thread):
|
||||
|
||||
self.force_disconnect: bool = False
|
||||
|
||||
self.selector: selectors.DefaultSelector = selectors.DefaultSelector()
|
||||
# macOS/BSD will default to KqueueSelector, it's very efficient but produces separate events for READ and WRITE.
|
||||
# Which causes problems when two receive threads are both trying to use the connection while it is resetting.
|
||||
if selectors.DefaultSelector is getattr(selectors, "KqueueSelector", None):
|
||||
self.selector: selectors.BaseSelector = selectors.PollSelector()
|
||||
else:
|
||||
self.selector: selectors.BaseSelector = selectors.DefaultSelector()
|
||||
|
||||
self.servers: list[Server] = []
|
||||
self.timers: dict[str, list[float]] = {}
|
||||
@@ -325,6 +334,7 @@ class Downloader(Thread):
|
||||
ssl = srv.ssl()
|
||||
ssl_verify = srv.ssl_verify()
|
||||
ssl_ciphers = srv.ssl_ciphers()
|
||||
pipelining_requests = srv.pipelining_requests
|
||||
username = srv.username()
|
||||
password = srv.password()
|
||||
required = srv.required()
|
||||
@@ -355,6 +365,7 @@ class Downloader(Thread):
|
||||
ssl,
|
||||
ssl_verify,
|
||||
ssl_ciphers,
|
||||
pipelining_requests,
|
||||
username,
|
||||
password,
|
||||
required,
|
||||
@@ -370,6 +381,8 @@ class Downloader(Thread):
|
||||
def add_socket(self, nw: NewsWrapper):
|
||||
"""Add a socket to be watched for read or write availability"""
|
||||
if nw.nntp:
|
||||
nw.server.idle_threads.discard(nw)
|
||||
nw.server.busy_threads.add(nw)
|
||||
try:
|
||||
self.selector.register(nw.nntp.fileno, selectors.EVENT_READ | selectors.EVENT_WRITE, nw)
|
||||
nw.selector_events = selectors.EVENT_READ | selectors.EVENT_WRITE
|
||||
@@ -379,7 +392,7 @@ class Downloader(Thread):
|
||||
@synchronized(DOWNLOADER_LOCK)
|
||||
def modify_socket(self, nw: NewsWrapper, events: int):
|
||||
"""Modify the events socket are watched for"""
|
||||
if nw.nntp and nw.selector_events != events:
|
||||
if nw.nntp and nw.selector_events != events and not nw.blocking:
|
||||
try:
|
||||
self.selector.modify(nw.nntp.fileno, events, nw)
|
||||
nw.selector_events = events
|
||||
@@ -390,6 +403,9 @@ class Downloader(Thread):
|
||||
def remove_socket(self, nw: NewsWrapper):
|
||||
"""Remove a socket to be watched"""
|
||||
if nw.nntp:
|
||||
nw.server.busy_threads.discard(nw)
|
||||
nw.server.idle_threads.add(nw)
|
||||
nw.timeout = None
|
||||
try:
|
||||
self.selector.unregister(nw.nntp.fileno)
|
||||
nw.selector_events = 0
|
||||
@@ -644,12 +660,12 @@ class Downloader(Thread):
|
||||
if not server.get_article(peek=True):
|
||||
break
|
||||
|
||||
server.idle_threads.remove(nw)
|
||||
server.busy_threads.add(nw)
|
||||
|
||||
if nw.connected:
|
||||
# Assign a request immediately if NewsWrapper is ready, if we wait until the socket is
|
||||
# selected all idle connections will be activated when there may only be one request
|
||||
nw.prepare_request()
|
||||
self.add_socket(nw)
|
||||
else:
|
||||
elif not nw.nntp:
|
||||
try:
|
||||
logging.info("%s@%s: Initiating connection", nw.thrdnum, server.host)
|
||||
nw.init_connect()
|
||||
@@ -744,24 +760,43 @@ class Downloader(Thread):
|
||||
# Drop stale items
|
||||
if nw.generation != generation:
|
||||
return
|
||||
if event & selectors.EVENT_READ:
|
||||
|
||||
# Read on EVENT_READ, or on EVENT_WRITE if TLS needs a write to complete a read
|
||||
if (event & selectors.EVENT_READ) or (event & selectors.EVENT_WRITE and nw.tls_wants_write):
|
||||
self.process_nw_read(nw, generation)
|
||||
# If read caused a reset, don't proceed to write
|
||||
if nw.generation != generation:
|
||||
return
|
||||
if event & selectors.EVENT_WRITE:
|
||||
# The read may have removed the socket, so prevent calling prepare_request again
|
||||
if not (nw.selector_events & selectors.EVENT_WRITE):
|
||||
return
|
||||
|
||||
# Only attempt app-level writes if TLS is not blocked
|
||||
if (event & selectors.EVENT_WRITE) and not nw.tls_wants_write:
|
||||
nw.write()
|
||||
|
||||
def process_nw_read(self, nw: NewsWrapper, generation: int) -> None:
|
||||
bytes_received: int = 0
|
||||
bytes_pending: int = 0
|
||||
|
||||
while nw.decoder and nw.generation == generation:
|
||||
while (
|
||||
nw.connected
|
||||
and nw.generation == generation
|
||||
and not self.force_disconnect
|
||||
and not self.shutdown
|
||||
and not (nw.timeout and time.time() > nw.timeout)
|
||||
):
|
||||
try:
|
||||
n, bytes_pending = nw.read(nbytes=bytes_pending, generation=generation)
|
||||
bytes_received += n
|
||||
nw.tls_wants_write = False
|
||||
except ssl.SSLWantReadError:
|
||||
return
|
||||
except ssl.SSLWantWriteError:
|
||||
# TLS needs to write handshake/key-update data before we can continue reading
|
||||
nw.tls_wants_write = True
|
||||
self.modify_socket(nw, selectors.EVENT_READ | selectors.EVENT_WRITE)
|
||||
return
|
||||
except (ConnectionError, ConnectionAbortedError):
|
||||
# The ConnectionAbortedError is also thrown by sabctools in case of fatal SSL-layer problems
|
||||
self.reset_nw(nw, "Server closed connection", wait=False)
|
||||
@@ -791,33 +826,38 @@ class Downloader(Thread):
|
||||
and sabnzbd.BPSMeter.bps + sabnzbd.BPSMeter.sum_cached_amount > self.bandwidth_limit
|
||||
):
|
||||
sabnzbd.BPSMeter.update()
|
||||
while sabnzbd.BPSMeter.bps > self.bandwidth_limit:
|
||||
while self.bandwidth_limit and sabnzbd.BPSMeter.bps > self.bandwidth_limit:
|
||||
time.sleep(0.01)
|
||||
sabnzbd.BPSMeter.update()
|
||||
|
||||
def check_assembler_levels(self):
|
||||
"""Check the Assembler queue to see if we need to delay, depending on queue size"""
|
||||
if (assembler_level := sabnzbd.Assembler.queue_level()) > SOFT_ASSEMBLER_QUEUE_LIMIT:
|
||||
time.sleep(min((assembler_level - SOFT_ASSEMBLER_QUEUE_LIMIT) / 4, 0.15))
|
||||
sabnzbd.BPSMeter.delayed_assembler += 1
|
||||
logged_counter = 0
|
||||
if not sabnzbd.Assembler.is_busy() or (delay := sabnzbd.Assembler.delay()) <= 0:
|
||||
return
|
||||
time.sleep(delay)
|
||||
sabnzbd.BPSMeter.delayed_assembler += 1
|
||||
start_time = time.monotonic()
|
||||
deadline = start_time + 5
|
||||
next_log = start_time + 1.0
|
||||
logged_counter = 0
|
||||
|
||||
while not self.shutdown and sabnzbd.Assembler.queue_level() >= 1:
|
||||
# Only log/update once every second, to not waste any CPU-cycles
|
||||
if not logged_counter % 10:
|
||||
# Make sure the BPS-meter is updated
|
||||
sabnzbd.BPSMeter.update()
|
||||
|
||||
# Update who is delaying us
|
||||
logging.debug(
|
||||
"Delayed - %d seconds - Assembler queue: %d",
|
||||
logged_counter / 10,
|
||||
sabnzbd.Assembler.queue.qsize(),
|
||||
)
|
||||
|
||||
# Wait and update the queue sizes
|
||||
time.sleep(0.1)
|
||||
while not self.shutdown and sabnzbd.Assembler.is_busy() and time.monotonic() < deadline:
|
||||
if (delay := sabnzbd.Assembler.delay()) <= 0:
|
||||
break
|
||||
# Sleep for the current delay (but cap to remaining time)
|
||||
sleep_time = max(0.001, min(delay, deadline - time.monotonic()))
|
||||
time.sleep(sleep_time)
|
||||
# Make sure the BPS-meter is updated
|
||||
sabnzbd.BPSMeter.update()
|
||||
# Only log/update once every second
|
||||
if time.monotonic() >= next_log:
|
||||
logged_counter += 1
|
||||
logging.debug(
|
||||
"Delayed - %d seconds - Assembler queue: %s",
|
||||
logged_counter,
|
||||
to_units(sabnzbd.Assembler.total_ready_bytes()),
|
||||
)
|
||||
next_log += 1.0
|
||||
|
||||
@synchronized(DOWNLOADER_LOCK)
|
||||
def finish_connect_nw(self, nw: NewsWrapper, response: sabctools.NNTPResponse) -> bool:
|
||||
@@ -920,13 +960,6 @@ class Downloader(Thread):
|
||||
elif reset_msg:
|
||||
logging.debug("Thread %s@%s: %s", nw.thrdnum, nw.server.host, reset_msg)
|
||||
|
||||
# Make sure this NewsWrapper is in the idle threads
|
||||
nw.server.busy_threads.discard(nw)
|
||||
nw.server.idle_threads.add(nw)
|
||||
|
||||
# Make sure it is not in the readable sockets
|
||||
self.remove_socket(nw)
|
||||
|
||||
# Discard the article request which failed
|
||||
nw.discard(article, count_article_try=count_article_try, retry_article=retry_article)
|
||||
|
||||
|
||||
@@ -255,8 +255,7 @@ def diskfull_mail():
|
||||
"""Send email about disk full, no templates"""
|
||||
if cfg.email_full():
|
||||
return send_email(
|
||||
T(
|
||||
"""To: %s
|
||||
T("""To: %s
|
||||
From: %s
|
||||
Date: %s
|
||||
Subject: SABnzbd reports Disk Full
|
||||
@@ -266,9 +265,7 @@ Hi,
|
||||
SABnzbd has stopped downloading, because the disk is almost full.
|
||||
Please make room and resume SABnzbd manually.
|
||||
|
||||
"""
|
||||
)
|
||||
% (cfg.email_to.get_string(), cfg.email_from(), get_email_date()),
|
||||
""") % (cfg.email_to.get_string(), cfg.email_from(), get_email_date()),
|
||||
cfg.email_to(),
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"""
|
||||
sabnzbd.misc - filesystem operations
|
||||
"""
|
||||
|
||||
import gzip
|
||||
import os
|
||||
import pickle
|
||||
@@ -42,6 +43,7 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import sabctools
|
||||
import sabnzbd
|
||||
from sabnzbd.decorators import synchronized, conditional_cache
|
||||
from sabnzbd.constants import (
|
||||
@@ -56,7 +58,6 @@ from sabnzbd.constants import (
|
||||
from sabnzbd.encoding import correct_unknown_encoding, utob, limit_encoded_length
|
||||
import rarfile
|
||||
|
||||
|
||||
# For Windows: determine executable extensions
|
||||
if os.name == "nt":
|
||||
PATHEXT = os.environ.get("PATHEXT", "").lower().split(";")
|
||||
@@ -1081,7 +1082,14 @@ def save_data(data: Any, _id: str, path: str, do_pickle: bool = True, silent: bo
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
def load_data(data_id: str, path: str, remove: bool = True, do_pickle: bool = True, silent: bool = False) -> Any:
|
||||
def load_data(
|
||||
data_id: str,
|
||||
path: str,
|
||||
remove: bool = True,
|
||||
do_pickle: bool = True,
|
||||
silent: bool = False,
|
||||
mutable: bool = False,
|
||||
) -> Any:
|
||||
"""Read data from disk file"""
|
||||
path = os.path.join(path, data_id)
|
||||
|
||||
@@ -1100,6 +1108,9 @@ def load_data(data_id: str, path: str, remove: bool = True, do_pickle: bool = Tr
|
||||
except UnicodeDecodeError:
|
||||
# Could be Python 2 data that we can load using old encoding
|
||||
data = pickle.load(data_file, encoding="latin1")
|
||||
elif mutable:
|
||||
data = bytearray(os.fstat(data_file.fileno()).st_size)
|
||||
data_file.readinto(data)
|
||||
else:
|
||||
data = data_file.read()
|
||||
|
||||
@@ -1222,7 +1233,7 @@ def directory_is_writable(test_dir: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def check_filesystem_capabilities(test_dir: str) -> bool:
|
||||
def check_filesystem_capabilities(test_dir: str, is_download_dir: bool = False) -> bool:
|
||||
"""Checks if we can write long and unicode filenames to the given directory.
|
||||
If not on Windows, also check for special chars like slashes and :
|
||||
Returns True if all OK, otherwise False"""
|
||||
@@ -1250,9 +1261,24 @@ def check_filesystem_capabilities(test_dir: str) -> bool:
|
||||
)
|
||||
allgood = False
|
||||
|
||||
# sparse files allow efficient use of empty space in files
|
||||
if is_download_dir and not check_sparse_and_disable(test_dir):
|
||||
# Writing to correct file offsets will be disabled, and it won't be possible to flush the article cache
|
||||
# directly to the destination file
|
||||
sabnzbd.misc.helpful_warning(T("%s does not support sparse files. Disabling direct write mode."), test_dir)
|
||||
allgood = False
|
||||
|
||||
return allgood
|
||||
|
||||
|
||||
def check_sparse_and_disable(test_dir: str) -> bool:
|
||||
"""Check if sparse files are supported, otherwise disable direct write mode"""
|
||||
if sabnzbd.cfg.direct_write() and not is_sparse_supported(test_dir):
|
||||
sabnzbd.cfg.direct_write.set(False)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_win_drives() -> list[str]:
|
||||
"""Return list of detected drives, adapted from:
|
||||
http://stackoverflow.com/questions/827371/is-there-a-way-to-list-all-the-available-drive-letters-in-python/827490
|
||||
@@ -1378,43 +1404,33 @@ def create_work_name(name: str) -> str:
|
||||
return name.strip()
|
||||
|
||||
|
||||
def nzf_cmp_name(nzf1, nzf2):
|
||||
"""Comparison function for sorting NZB files.
|
||||
The comparison will sort .par2 files to the top of the queue followed by .rar files,
|
||||
they will then be sorted by name.
|
||||
def is_sparse(path: str) -> bool:
|
||||
"""Check if a path is a sparse file"""
|
||||
info = os.stat(path)
|
||||
if sabnzbd.WINDOWS:
|
||||
return bool(info.st_file_attributes & stat.FILE_ATTRIBUTE_SPARSE_FILE)
|
||||
|
||||
Note: nzf1 and nzf2 should be NzbFile objects, but we can't import that here
|
||||
to avoid circular dependencies.
|
||||
"""
|
||||
nzf1_name = nzf1.filename.lower()
|
||||
nzf2_name = nzf2.filename.lower()
|
||||
# Linux and macOS
|
||||
if info.st_blocks * 512 < info.st_size:
|
||||
return True
|
||||
|
||||
# Determine vol-pars
|
||||
is_par1 = ".vol" in nzf1_name and ".par2" in nzf1_name
|
||||
is_par2 = ".vol" in nzf2_name and ".par2" in nzf2_name
|
||||
# Filesystem with SEEK_HOLE (ZFS)
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
pos = f.seek(0, os.SEEK_HOLE)
|
||||
return pos < info.st_size
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
|
||||
# mini-par2 in front
|
||||
if not is_par1 and nzf1_name.endswith(".par2"):
|
||||
return -1
|
||||
if not is_par2 and nzf2_name.endswith(".par2"):
|
||||
return 1
|
||||
return False
|
||||
|
||||
# vol-pars go to the back
|
||||
if is_par1 and not is_par2:
|
||||
return 1
|
||||
if is_par2 and not is_par1:
|
||||
return -1
|
||||
|
||||
# Prioritize .rar files above any other type of file (other than vol-par)
|
||||
m1 = RAR_RE.search(nzf1_name)
|
||||
m2 = RAR_RE.search(nzf2_name)
|
||||
if m1 and not (is_par2 or m2):
|
||||
return -1
|
||||
elif m2 and not (is_par1 or m1):
|
||||
return 1
|
||||
# Force .rar to come before 'r00'
|
||||
if m1 and m1.group(1) == ".rar":
|
||||
nzf1_name = nzf1_name.replace(".rar", ".r//")
|
||||
if m2 and m2.group(1) == ".rar":
|
||||
nzf2_name = nzf2_name.replace(".rar", ".r//")
|
||||
return sabnzbd.misc.cmp(nzf1_name, nzf2_name)
|
||||
def is_sparse_supported(check_dir: str) -> bool:
|
||||
"""Check if a directory supports sparse files"""
|
||||
sparse_file = tempfile.NamedTemporaryFile(dir=check_dir, delete=False)
|
||||
try:
|
||||
sabctools.sparse(sparse_file.fileno(), 64)
|
||||
sparse_file.close()
|
||||
return is_sparse(sparse_file.name)
|
||||
finally:
|
||||
os.remove(sparse_file.name)
|
||||
|
||||
@@ -895,6 +895,7 @@ SPECIAL_BOOL_LIST = (
|
||||
"allow_old_ssl_tls",
|
||||
"enable_season_sorting",
|
||||
"verify_xff_header",
|
||||
"direct_write",
|
||||
)
|
||||
SPECIAL_VALUE_LIST = (
|
||||
"downloader_sleep_time",
|
||||
@@ -913,7 +914,6 @@ SPECIAL_VALUE_LIST = (
|
||||
"ssdp_broadcast_interval",
|
||||
"unrar_parameters",
|
||||
"outgoing_nntp_ip",
|
||||
"pipelining_requests",
|
||||
)
|
||||
SPECIAL_LIST_LIST = (
|
||||
"rss_odd_titles",
|
||||
@@ -1270,7 +1270,7 @@ class ConfigRss:
|
||||
active_feed,
|
||||
download=self.__refresh_download,
|
||||
force=self.__refresh_force,
|
||||
ignoreFirst=self.__refresh_ignore,
|
||||
ignore_first=self.__refresh_ignore,
|
||||
readout=readout,
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -62,6 +62,7 @@ from sabnzbd.filesystem import userxbit, make_script_path, remove_file, strip_ex
|
||||
if sabnzbd.WINDOWS:
|
||||
try:
|
||||
import winreg
|
||||
import win32api
|
||||
import win32process
|
||||
import win32con
|
||||
|
||||
@@ -717,15 +718,7 @@ def get_cache_limit() -> str:
|
||||
"""
|
||||
# Calculate, if possible
|
||||
try:
|
||||
if sabnzbd.WINDOWS:
|
||||
# Windows
|
||||
mem_bytes = get_windows_memory()
|
||||
elif sabnzbd.MACOS:
|
||||
# macOS
|
||||
mem_bytes = get_macos_memory()
|
||||
else:
|
||||
# Linux
|
||||
mem_bytes = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
|
||||
mem_bytes = get_memory()
|
||||
|
||||
# Use 1/4th of available memory
|
||||
mem_bytes = mem_bytes / 4
|
||||
@@ -748,36 +741,27 @@ def get_cache_limit() -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def get_windows_memory() -> int:
|
||||
"""Use ctypes to extract available memory"""
|
||||
|
||||
class MEMORYSTATUSEX(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dwLength", ctypes.c_ulong),
|
||||
("dwMemoryLoad", ctypes.c_ulong),
|
||||
("ullTotalPhys", ctypes.c_ulonglong),
|
||||
("ullAvailPhys", ctypes.c_ulonglong),
|
||||
("ullTotalPageFile", ctypes.c_ulonglong),
|
||||
("ullAvailPageFile", ctypes.c_ulonglong),
|
||||
("ullTotalVirtual", ctypes.c_ulonglong),
|
||||
("ullAvailVirtual", ctypes.c_ulonglong),
|
||||
("sullAvailExtendedVirtual", ctypes.c_ulonglong),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
# have to initialize this to the size of MEMORYSTATUSEX
|
||||
self.dwLength = ctypes.sizeof(self)
|
||||
super(MEMORYSTATUSEX, self).__init__()
|
||||
|
||||
stat = MEMORYSTATUSEX()
|
||||
ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat))
|
||||
return stat.ullTotalPhys
|
||||
|
||||
|
||||
def get_macos_memory() -> float:
|
||||
"""Use system-call to extract total memory on macOS"""
|
||||
system_output = run_command(["sysctl", "hw.memsize"])
|
||||
return float(system_output.split()[1])
|
||||
def get_memory() -> int:
|
||||
try:
|
||||
if sabnzbd.WINDOWS:
|
||||
# Use win32api to get total physical memory
|
||||
mem_info = win32api.GlobalMemoryStatusEx()
|
||||
return mem_info["TotalPhys"]
|
||||
elif sabnzbd.MACOS:
|
||||
# Use system-call to extract total memory on macOS
|
||||
system_output = run_command(["sysctl", "-n", "hw.memsize"]).strip()
|
||||
else:
|
||||
try:
|
||||
with open("/proc/meminfo") as f:
|
||||
for line in f:
|
||||
if line.startswith("MemTotal:"):
|
||||
return int(line.split()[1]) * 1024
|
||||
except Exception:
|
||||
pass
|
||||
return os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
@conditional_cache(cache_time=3600)
|
||||
|
||||
@@ -70,7 +70,6 @@ from sabnzbd.nzb import NzbObject
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.constants import Status
|
||||
|
||||
|
||||
# Regex globals
|
||||
RAR_V3_RE = re.compile(r"\.(?P<ext>part\d*)$", re.I)
|
||||
RAR_EXTRACTFROM_RE = re.compile(r"^Extracting\sfrom\s(.+)")
|
||||
@@ -118,7 +117,14 @@ def find_programs(curdir: str):
|
||||
sabnzbd.newsunpack.SEVENZIP_COMMAND = check(curdir, "macos/7zip/7zz")
|
||||
|
||||
if sabnzbd.WINDOWS:
|
||||
sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, "win/par2/par2.exe")
|
||||
if sabnzbd.WINDOWSARM64:
|
||||
# ARM64 version of par2
|
||||
sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, "win/par2/arm64/par2.exe")
|
||||
else:
|
||||
# Regular x64 version
|
||||
sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, "win/par2/par2.exe")
|
||||
|
||||
# UnRAR has no arm64 version, so we skip it also for 7zip
|
||||
sabnzbd.newsunpack.RAR_COMMAND = check(curdir, "win/unrar/UnRAR.exe")
|
||||
sabnzbd.newsunpack.SEVENZIP_COMMAND = check(curdir, "win/7zip/7za.exe")
|
||||
else:
|
||||
|
||||
@@ -23,6 +23,7 @@ import errno
|
||||
import socket
|
||||
import threading
|
||||
from collections import deque
|
||||
from contextlib import suppress
|
||||
from selectors import EVENT_READ, EVENT_WRITE
|
||||
from threading import Thread
|
||||
import time
|
||||
@@ -60,9 +61,9 @@ class NewsWrapper:
|
||||
"blocking",
|
||||
"timeout",
|
||||
"decoder",
|
||||
"send_buffer",
|
||||
"nntp",
|
||||
"connected",
|
||||
"ready",
|
||||
"user_sent",
|
||||
"pass_sent",
|
||||
"group",
|
||||
@@ -75,6 +76,7 @@ class NewsWrapper:
|
||||
"selector_events",
|
||||
"lock",
|
||||
"generation",
|
||||
"tls_wants_write",
|
||||
)
|
||||
|
||||
def __init__(self, server: "sabnzbd.downloader.Server", thrdnum: int, block: bool = False, generation: int = 0):
|
||||
@@ -82,15 +84,17 @@ class NewsWrapper:
|
||||
self.thrdnum: int = thrdnum
|
||||
self.blocking: bool = block
|
||||
self.generation: int = generation
|
||||
if getattr(self, "lock", None) is None:
|
||||
self.lock: threading.Lock = threading.Lock()
|
||||
|
||||
self.timeout: Optional[float] = None
|
||||
|
||||
self.decoder: Optional[sabctools.Decoder] = None
|
||||
self.send_buffer = b""
|
||||
|
||||
self.nntp: Optional[NNTP] = None
|
||||
|
||||
self.connected: bool = False
|
||||
self.connected: bool = False # TCP/TLS handshake complete
|
||||
self.ready: bool = False # Auth complete, can serve requests
|
||||
self.user_sent: bool = False
|
||||
self.pass_sent: bool = False
|
||||
self.user_ok: bool = False
|
||||
@@ -101,11 +105,11 @@ class NewsWrapper:
|
||||
# Command queue and concurrency
|
||||
self.next_request: Optional[tuple[bytes, Optional["sabnzbd.nzb.Article"]]] = None
|
||||
self.concurrent_requests: threading.BoundedSemaphore = threading.BoundedSemaphore(
|
||||
sabnzbd.cfg.pipelining_requests()
|
||||
self.server.pipelining_requests()
|
||||
)
|
||||
self._response_queue: deque[Optional[sabnzbd.nzb.Article]] = deque()
|
||||
self.selector_events = 0
|
||||
self.lock: threading.Lock = threading.Lock()
|
||||
self.tls_wants_write: bool = False
|
||||
|
||||
@property
|
||||
def article(self) -> Optional["sabnzbd.nzb.Article"]:
|
||||
@@ -133,7 +137,7 @@ class NewsWrapper:
|
||||
def finish_connect(self, code: int, message: str) -> None:
|
||||
"""Perform login options"""
|
||||
if not (self.server.username or self.server.password or self.force_login):
|
||||
self.connected = True
|
||||
self.ready = True
|
||||
self.user_sent = True
|
||||
self.user_ok = True
|
||||
self.pass_sent = True
|
||||
@@ -141,7 +145,7 @@ class NewsWrapper:
|
||||
|
||||
if code == 480:
|
||||
self.force_login = True
|
||||
self.connected = False
|
||||
self.ready = False
|
||||
self.user_sent = False
|
||||
self.user_ok = False
|
||||
self.pass_sent = False
|
||||
@@ -161,7 +165,7 @@ class NewsWrapper:
|
||||
self.user_ok = True
|
||||
self.pass_sent = True
|
||||
self.pass_ok = True
|
||||
self.connected = True
|
||||
self.ready = True
|
||||
|
||||
if self.user_ok and not self.pass_sent:
|
||||
command = utob("authinfo pass %s\r\n" % self.server.password)
|
||||
@@ -172,7 +176,7 @@ class NewsWrapper:
|
||||
# Assume that login failed (code 481 or other)
|
||||
raise NNTPPermanentError(message, code)
|
||||
else:
|
||||
self.connected = True
|
||||
self.ready = True
|
||||
|
||||
self.timeout = time.time() + self.server.timeout
|
||||
|
||||
@@ -201,7 +205,6 @@ class NewsWrapper:
|
||||
def on_response(self, response: sabctools.NNTPResponse, article: Optional["sabnzbd.nzb.Article"]) -> None:
|
||||
"""A response to a NNTP request is received"""
|
||||
self.concurrent_requests.release()
|
||||
sabnzbd.Downloader.modify_socket(self, EVENT_READ | EVENT_WRITE)
|
||||
server = self.server
|
||||
article_done = response.status_code in (220, 222) and article
|
||||
|
||||
@@ -214,11 +217,11 @@ class NewsWrapper:
|
||||
# Response code depends on request command:
|
||||
# 220 = ARTICLE, 222 = BODY
|
||||
if not article_done:
|
||||
if not self.connected or not article or response.status_code in (281, 381, 480, 481, 482):
|
||||
if not self.ready or not article or response.status_code in (281, 381, 480, 481, 482):
|
||||
self.discard(article, count_article_try=False)
|
||||
if not sabnzbd.Downloader.finish_connect_nw(self, response):
|
||||
return
|
||||
if self.connected:
|
||||
if self.ready:
|
||||
logging.info("Connecting %s@%s finished", self.thrdnum, server.host)
|
||||
|
||||
elif response.status_code == 223:
|
||||
@@ -237,15 +240,9 @@ class NewsWrapper:
|
||||
|
||||
elif response.status_code == 500:
|
||||
if article.nzf.nzo.precheck:
|
||||
# Did we try "STAT" already?
|
||||
if not server.have_stat:
|
||||
# Hopless server, just discard
|
||||
logging.info("Server %s does not support STAT or HEAD, precheck not possible", server.host)
|
||||
article_done = True
|
||||
else:
|
||||
# Assume "STAT" command is not supported
|
||||
server.have_stat = False
|
||||
logging.debug("Server %s does not support STAT, trying HEAD", server.host)
|
||||
# Assume "STAT" command is not supported
|
||||
server.have_stat = False
|
||||
logging.debug("Server %s does not support STAT", server.host)
|
||||
else:
|
||||
# Assume "BODY" command is not supported
|
||||
server.have_body = False
|
||||
@@ -296,7 +293,7 @@ class NewsWrapper:
|
||||
generation = self.generation
|
||||
|
||||
# NewsWrapper is being reset
|
||||
if not self.decoder:
|
||||
if self.decoder is None:
|
||||
return 0, None
|
||||
|
||||
# Receive data into the decoder pre-allocated buffer
|
||||
@@ -314,17 +311,31 @@ class NewsWrapper:
|
||||
self.timeout = time.time() + self.server.timeout
|
||||
|
||||
self.decoder.process(bytes_recv)
|
||||
for response in self.decoder:
|
||||
if self.generation != generation:
|
||||
break
|
||||
with self.lock:
|
||||
# Re-check under lock to avoid racing with hard_reset
|
||||
if self.generation != generation or not self._response_queue:
|
||||
break
|
||||
article = self._response_queue.popleft()
|
||||
if on_response:
|
||||
on_response(response.status_code, response.message)
|
||||
self.on_response(response, article)
|
||||
if self.decoder:
|
||||
for response in self.decoder:
|
||||
with self.lock:
|
||||
# Check generation under lock to avoid racing with hard_reset
|
||||
if self.generation != generation or not self._response_queue:
|
||||
break
|
||||
article = self._response_queue.popleft()
|
||||
if on_response:
|
||||
on_response(response.status_code, response.message)
|
||||
self.on_response(response, article)
|
||||
|
||||
# After each response this socket may need to be made available to write the next request,
|
||||
# or removed from socket monitoring to prevent hot looping.
|
||||
if self.prepare_request():
|
||||
# There is either a next_request or an inflight request
|
||||
# If there is a next_request to send, ensure the socket is registered for write events
|
||||
# Checks before calling modify_socket to prevent locks on the hot path
|
||||
if self.next_request and self.selector_events != EVENT_READ | EVENT_WRITE:
|
||||
sabnzbd.Downloader.modify_socket(self, EVENT_READ | EVENT_WRITE)
|
||||
else:
|
||||
# Only remove the socket if it's not SSL or has no pending data, otherwise the recursive call may
|
||||
# call prepare_request again and find a request, but the socket would have already been removed.
|
||||
if not self.server.ssl or not self.nntp or not self.nntp.sock.pending():
|
||||
# No further work for this socket
|
||||
sabnzbd.Downloader.remove_socket(self)
|
||||
|
||||
# The SSL-layer might still contain data even though the socket does not. Another Downloader-loop would
|
||||
# not identify this socket anymore as it is not returned by select(). So, we have to forcefully trigger
|
||||
@@ -333,86 +344,101 @@ class NewsWrapper:
|
||||
return bytes_recv, pending
|
||||
return bytes_recv, None
|
||||
|
||||
def prepare_request(self) -> bool:
|
||||
"""Queue an article request if appropriate."""
|
||||
server = self.server
|
||||
|
||||
# Do not pipeline requests until authentication is completed (connected)
|
||||
if self.ready or not self._response_queue:
|
||||
server_ready = (
|
||||
server.active
|
||||
and not server.restart
|
||||
and not (
|
||||
sabnzbd.Downloader.no_active_jobs()
|
||||
or sabnzbd.Downloader.shutdown
|
||||
or sabnzbd.Downloader.paused_for_postproc
|
||||
)
|
||||
)
|
||||
|
||||
if server_ready:
|
||||
# Queue the next article if none exists
|
||||
if not self.next_request and (article := server.get_article()):
|
||||
self.next_request = self.body(article)
|
||||
return True
|
||||
else:
|
||||
# Server not ready, discard any queued next_request
|
||||
if self.next_request and self.next_request[1]:
|
||||
self.discard(self.next_request[1], count_article_try=False, retry_article=True)
|
||||
self.next_request = None
|
||||
|
||||
# Return True if there is work queued or in flight
|
||||
return bool(self.next_request or self._response_queue)
|
||||
|
||||
def write(self):
|
||||
"""Send data to server"""
|
||||
server = self.server
|
||||
|
||||
try:
|
||||
# First, try to flush any remaining data
|
||||
if self.send_buffer:
|
||||
sent = self.nntp.sock.send(self.send_buffer)
|
||||
self.send_buffer = self.send_buffer[sent:]
|
||||
if self.send_buffer:
|
||||
# Still unsent data, wait for next EVENT_WRITE
|
||||
# Flush any buffered data
|
||||
if self.nntp.write_buffer:
|
||||
sent = self.nntp.sock.send(self.nntp.write_buffer)
|
||||
self.nntp.write_buffer = self.nntp.write_buffer[sent:]
|
||||
# If buffer still has data, wait for next write opportunity
|
||||
if self.nntp.write_buffer:
|
||||
return
|
||||
|
||||
if self.connected:
|
||||
if (
|
||||
server.active
|
||||
and not server.restart
|
||||
and not (
|
||||
sabnzbd.Downloader.paused
|
||||
or sabnzbd.Downloader.shutdown
|
||||
or sabnzbd.Downloader.paused_for_postproc
|
||||
)
|
||||
):
|
||||
# Prepare the next request
|
||||
if not self.next_request and (article := server.get_article()):
|
||||
self.next_request = self.body(article)
|
||||
elif self.next_request and self.next_request[1]:
|
||||
# Discard the next request
|
||||
self.discard(self.next_request[1], count_article_try=False, retry_article=True)
|
||||
self.next_request = None
|
||||
# If available, try to send new command
|
||||
if self.prepare_request():
|
||||
# Nothing to send but already requests in-flight
|
||||
if not self.next_request:
|
||||
sabnzbd.Downloader.modify_socket(self, EVENT_READ)
|
||||
return
|
||||
|
||||
# If no pending buffer, try to send new command
|
||||
if not self.send_buffer and self.next_request:
|
||||
if self.concurrent_requests.acquire(blocking=False):
|
||||
command, article = self.next_request
|
||||
self.next_request = None
|
||||
if article:
|
||||
nzo = article.nzf.nzo
|
||||
if nzo.removed_from_queue or nzo.status is Status.PAUSED and nzo.priority is not FORCE_PRIORITY:
|
||||
self.discard(article, count_article_try=False, retry_article=True)
|
||||
self.concurrent_requests.release()
|
||||
self.next_request = None
|
||||
return
|
||||
self._response_queue.append(article)
|
||||
|
||||
if sabnzbd.LOG_ALL:
|
||||
logging.debug("Thread %s@%s: %s", self.thrdnum, server.host, command)
|
||||
try:
|
||||
sent = self.nntp.sock.send(command)
|
||||
if sent < len(command):
|
||||
# Partial send, store remainder
|
||||
self.send_buffer = command[sent:]
|
||||
except (BlockingIOError, ssl.SSLWantWriteError):
|
||||
# Can't send now, store full command
|
||||
self.send_buffer = command
|
||||
|
||||
# Non-blocking send - buffer any unsent data
|
||||
sent = self.nntp.sock.send(command)
|
||||
if sent < len(command):
|
||||
logging.debug("%s@%s: Partial send", self.thrdnum, server.host)
|
||||
self.nntp.write_buffer = command[sent:]
|
||||
|
||||
self._response_queue.append(article)
|
||||
self.next_request = None
|
||||
else:
|
||||
# Concurrency limit reached
|
||||
# Concurrency limit reached; wait until a response is read to prevent hot looping on EVENT_WRITE
|
||||
sabnzbd.Downloader.modify_socket(self, EVENT_READ)
|
||||
else:
|
||||
# Is it safe to shut down this socket?
|
||||
if (
|
||||
not self.send_buffer
|
||||
and not self.next_request
|
||||
and not self._response_queue
|
||||
and (not server.active or server.restart or not self.timeout or time.time() > self.timeout)
|
||||
):
|
||||
# Make socket available again
|
||||
server.busy_threads.discard(self)
|
||||
server.idle_threads.add(self)
|
||||
sabnzbd.Downloader.remove_socket(self)
|
||||
|
||||
except (BlockingIOError, ssl.SSLWantWriteError):
|
||||
# Socket not currently writable — just try again later
|
||||
return
|
||||
# No further work for this socket
|
||||
sabnzbd.Downloader.remove_socket(self)
|
||||
except ssl.SSLWantWriteError:
|
||||
# Socket not ready for writing, keep buffer and wait for next write event
|
||||
pass
|
||||
except ssl.SSLWantReadError:
|
||||
# SSL renegotiation needs read first
|
||||
sabnzbd.Downloader.modify_socket(self, EVENT_READ)
|
||||
except BlockingIOError:
|
||||
# Socket not ready for writing, keep buffer and wait for next write event
|
||||
pass
|
||||
except socket.error as err:
|
||||
logging.info("Looks like server closed connection: %s", err)
|
||||
sabnzbd.Downloader.reset_nw(self, "Server broke off connection", warn=True)
|
||||
logging.info("Looks like server closed connection: %s, type: %s", err, type(err))
|
||||
sabnzbd.Downloader.reset_nw(self, "Server broke off connection", warn=True, wait=False)
|
||||
except Exception:
|
||||
logging.error(T("Suspect error in downloader"))
|
||||
logging.info("Traceback: ", exc_info=True)
|
||||
sabnzbd.Downloader.reset_nw(self, "Server broke off connection", warn=True)
|
||||
|
||||
@synchronized(DOWNLOADER_LOCK)
|
||||
def hard_reset(self, wait: bool = True):
|
||||
"""Destroy and restart"""
|
||||
with self.lock:
|
||||
@@ -427,11 +453,11 @@ class NewsWrapper:
|
||||
if article := self._response_queue.popleft():
|
||||
self.discard(article, count_article_try=False, retry_article=True)
|
||||
|
||||
if self.nntp:
|
||||
self.nntp.close(send_quit=self.connected)
|
||||
self.nntp = None
|
||||
if self.nntp:
|
||||
sabnzbd.Downloader.remove_socket(self)
|
||||
self.nntp.close(send_quit=self.ready)
|
||||
self.nntp = None
|
||||
|
||||
with self.lock:
|
||||
# Reset all variables (including the NNTP connection) and increment the generation counter
|
||||
self.__init__(self.server, self.thrdnum, generation=self.generation + 1)
|
||||
|
||||
@@ -470,13 +496,13 @@ class NewsWrapper:
|
||||
self.server.host,
|
||||
self.server.port,
|
||||
self.thrdnum,
|
||||
self.connected,
|
||||
self.ready,
|
||||
)
|
||||
|
||||
|
||||
class NNTP:
|
||||
# Pre-define attributes to save memory
|
||||
__slots__ = ("nw", "addrinfo", "error_msg", "sock", "fileno", "closed")
|
||||
__slots__ = ("nw", "addrinfo", "error_msg", "sock", "fileno", "closed", "write_buffer")
|
||||
|
||||
def __init__(self, nw: NewsWrapper, addrinfo: AddrInfo):
|
||||
self.nw: NewsWrapper = nw
|
||||
@@ -487,6 +513,9 @@ class NNTP:
|
||||
# Prevent closing this socket until it's done connecting
|
||||
self.closed = False
|
||||
|
||||
# Buffer for non-blocking writes
|
||||
self.write_buffer: bytes = b""
|
||||
|
||||
# Create SSL-context if it is needed and not created yet
|
||||
if self.nw.server.ssl and not self.nw.server.ssl_context:
|
||||
# Setup the SSL socket
|
||||
@@ -586,6 +615,7 @@ class NNTP:
|
||||
# Locked, so it can't interleave with any of the Downloader "__nw" actions
|
||||
with DOWNLOADER_LOCK:
|
||||
if not self.closed:
|
||||
self.nw.connected = True
|
||||
sabnzbd.Downloader.add_socket(self.nw)
|
||||
except OSError as e:
|
||||
self.error(e)
|
||||
@@ -643,6 +673,8 @@ class NNTP:
|
||||
else:
|
||||
logging.warning(msg)
|
||||
self.nw.server.warning = msg
|
||||
# No reset-warning needed, above logging is sufficient
|
||||
sabnzbd.Downloader.reset_nw(self.nw)
|
||||
|
||||
@synchronized(DOWNLOADER_LOCK)
|
||||
def close(self, send_quit: bool):
|
||||
@@ -650,10 +682,12 @@ class NNTP:
|
||||
Locked to match connect(), even though most likely the caller already holds the same lock."""
|
||||
# Set status first, so any calls in connect/error are handled correctly
|
||||
self.closed = True
|
||||
self.write_buffer = b""
|
||||
try:
|
||||
if send_quit:
|
||||
self.sock.sendall(b"QUIT\r\n")
|
||||
time.sleep(0.01)
|
||||
with suppress(socket.error):
|
||||
self.sock.sendall(b"QUIT\r\n")
|
||||
time.sleep(0.01)
|
||||
self.sock.close()
|
||||
except Exception as e:
|
||||
logging.info("%s@%s: Failed to close socket (error=%s)", self.nw.thrdnum, self.nw.server.host, str(e))
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
sabnzbd.notifier - Send notifications to any notification services
|
||||
"""
|
||||
|
||||
|
||||
import sys
|
||||
import os.path
|
||||
import logging
|
||||
|
||||
@@ -20,7 +20,7 @@ sabnzbd.nzb - NZB-related classes and functionality
|
||||
"""
|
||||
|
||||
# Article-related classes
|
||||
from sabnzbd.nzb.article import Article, ArticleSaver, TryList, TRYLIST_LOCK
|
||||
from sabnzbd.nzb.article import Article, ArticleSaver, TryList
|
||||
|
||||
# File-related classes
|
||||
from sabnzbd.nzb.file import NzbFile, NzbFileSaver, SkippedNzbFile
|
||||
@@ -30,7 +30,6 @@ from sabnzbd.nzb.object import (
|
||||
NzbObject,
|
||||
NzbObjectSaver,
|
||||
NzoAttributeSaver,
|
||||
NZO_LOCK,
|
||||
NzbEmpty,
|
||||
NzbRejected,
|
||||
NzbPreQueueRejected,
|
||||
@@ -42,7 +41,6 @@ __all__ = [
|
||||
"Article",
|
||||
"ArticleSaver",
|
||||
"TryList",
|
||||
"TRYLIST_LOCK",
|
||||
# File
|
||||
"NzbFile",
|
||||
"NzbFileSaver",
|
||||
@@ -51,7 +49,6 @@ __all__ = [
|
||||
"NzbObject",
|
||||
"NzbObjectSaver",
|
||||
"NzoAttributeSaver",
|
||||
"NZO_LOCK",
|
||||
"NzbEmpty",
|
||||
"NzbRejected",
|
||||
"NzbPreQueueRejected",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"""
|
||||
sabnzbd.article - Article and TryList classes for NZB downloading
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from typing import Optional
|
||||
@@ -27,13 +28,10 @@ from sabnzbd.downloader import Server
|
||||
from sabnzbd.filesystem import get_new_id
|
||||
from sabnzbd.decorators import synchronized
|
||||
|
||||
|
||||
##############################################################################
|
||||
# Trylist
|
||||
##############################################################################
|
||||
|
||||
TRYLIST_LOCK = threading.RLock()
|
||||
|
||||
|
||||
class TryList:
|
||||
"""TryList keeps track of which servers have been tried for a specific article"""
|
||||
@@ -45,32 +43,32 @@ class TryList:
|
||||
# Sets are faster than lists
|
||||
self.try_list: set[Server] = set()
|
||||
|
||||
@synchronized()
|
||||
def server_in_try_list(self, server: Server) -> bool:
|
||||
"""Return whether specified server has been tried"""
|
||||
with TRYLIST_LOCK:
|
||||
return server in self.try_list
|
||||
return server in self.try_list
|
||||
|
||||
@synchronized()
|
||||
def all_servers_in_try_list(self, all_servers: set[Server]) -> bool:
|
||||
"""Check if all servers have been tried"""
|
||||
with TRYLIST_LOCK:
|
||||
return all_servers.issubset(self.try_list)
|
||||
return all_servers.issubset(self.try_list)
|
||||
|
||||
@synchronized()
|
||||
def add_to_try_list(self, server: Server):
|
||||
"""Register server as having been tried already"""
|
||||
with TRYLIST_LOCK:
|
||||
# Sets cannot contain duplicate items
|
||||
self.try_list.add(server)
|
||||
# Sets cannot contain duplicate items
|
||||
self.try_list.add(server)
|
||||
|
||||
@synchronized()
|
||||
def remove_from_try_list(self, server: Server):
|
||||
"""Remove server from list of tried servers"""
|
||||
with TRYLIST_LOCK:
|
||||
# Discard does not require the item to be present
|
||||
self.try_list.discard(server)
|
||||
# Discard does not require the item to be present
|
||||
self.try_list.discard(server)
|
||||
|
||||
@synchronized()
|
||||
def reset_try_list(self):
|
||||
"""Clean the list"""
|
||||
with TRYLIST_LOCK:
|
||||
self.try_list = set()
|
||||
self.try_list = set()
|
||||
|
||||
def __getstate__(self):
|
||||
"""Save the servers"""
|
||||
@@ -98,6 +96,7 @@ ArticleSaver = (
|
||||
"on_disk",
|
||||
"nzf",
|
||||
"crc32",
|
||||
"decoded_size",
|
||||
)
|
||||
|
||||
|
||||
@@ -105,7 +104,7 @@ class Article(TryList):
|
||||
"""Representation of one article"""
|
||||
|
||||
# Pre-define attributes to save memory
|
||||
__slots__ = ArticleSaver + ("fetcher", "fetcher_priority", "tries")
|
||||
__slots__ = ArticleSaver + ("fetcher", "fetcher_priority", "tries", "lock")
|
||||
|
||||
def __init__(self, article, article_bytes, nzf):
|
||||
super().__init__()
|
||||
@@ -120,11 +119,14 @@ class Article(TryList):
|
||||
self.file_size: Optional[int] = None
|
||||
self.data_begin: Optional[int] = None
|
||||
self.data_size: Optional[int] = None
|
||||
self.decoded_size: Optional[int] = None # Size of the decoded article
|
||||
self.on_disk: bool = False
|
||||
self.crc32: Optional[int] = None
|
||||
self.nzf = nzf # NzbFile reference
|
||||
self.nzf: "sabnzbd.nzb.NzbFile" = nzf # NzbFile reference
|
||||
# Share NzbFile lock for file-wide atomicity of try-list ops
|
||||
self.lock: threading.RLock = nzf.lock
|
||||
|
||||
@synchronized(TRYLIST_LOCK)
|
||||
@synchronized()
|
||||
def reset_try_list(self):
|
||||
"""In addition to resetting the try list, also reset fetcher so all servers
|
||||
are tried again. Locked so fetcher setting changes are also protected."""
|
||||
@@ -132,16 +134,17 @@ class Article(TryList):
|
||||
self.fetcher_priority = 0
|
||||
super().reset_try_list()
|
||||
|
||||
@synchronized(TRYLIST_LOCK)
|
||||
def allow_new_fetcher(self, remove_fetcher_from_try_list: bool = True):
|
||||
"""Let article get new fetcher and reset try lists of file and job.
|
||||
Locked so all resets are performed at once"""
|
||||
if remove_fetcher_from_try_list:
|
||||
self.remove_from_try_list(self.fetcher)
|
||||
self.fetcher = None
|
||||
self.tries = 0
|
||||
self.nzf.reset_try_list()
|
||||
self.nzf.nzo.reset_try_list()
|
||||
Locked so all resets are performed at once.
|
||||
Must acquire nzo lock first, then nzf lock (which is self.lock) to prevent deadlock."""
|
||||
with self.nzf.nzo.lock, self.lock:
|
||||
if remove_fetcher_from_try_list:
|
||||
self.remove_from_try_list(self.fetcher)
|
||||
self.fetcher = None
|
||||
self.tries = 0
|
||||
self.nzf.reset_try_list()
|
||||
self.nzf.nzo.reset_try_list()
|
||||
|
||||
def get_article(self, server: Server, servers: list[Server]):
|
||||
"""Return article when appropriate for specified server"""
|
||||
@@ -189,6 +192,14 @@ class Article(TryList):
|
||||
logging.info("Article %s unavailable on all servers, discarding", self.article)
|
||||
return False
|
||||
|
||||
@property
|
||||
def can_direct_write(self) -> bool:
|
||||
return bool(
|
||||
self.data_size # decoder sets data_size to 0 when offsets or file_size are outside allowed range
|
||||
and self.nzf.type == "yenc"
|
||||
and self.nzf.prepare_filepath()
|
||||
)
|
||||
|
||||
def __getstate__(self):
|
||||
"""Save to pickle file, selecting attributes"""
|
||||
dict_ = {}
|
||||
@@ -205,6 +216,7 @@ class Article(TryList):
|
||||
except KeyError:
|
||||
# Handle new attributes
|
||||
setattr(self, item, None)
|
||||
self.lock = threading.RLock()
|
||||
super().__setstate__(dict_.get("try_list", []))
|
||||
self.fetcher = None
|
||||
self.fetcher_priority = 0
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user