mirror of
https://github.com/sabnzbd/sabnzbd.git
synced 2026-02-02 11:52:37 -05:00
Compare commits
83 Commits
4.6.0Alpha
...
5.0.0Beta1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
578bfd083d | ||
|
|
dd464456e4 | ||
|
|
e7a0255359 | ||
|
|
2e1281d9e8 | ||
|
|
efecefdd3b | ||
|
|
a91e718ef5 | ||
|
|
b420975267 | ||
|
|
c4211df8dc |
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",
|
||||
|
||||
58
.github/workflows/build_release.yml
vendored
58
.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,7 +27,7 @@ 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
|
||||
@@ -31,11 +41,11 @@ jobs:
|
||||
id: windows_binary
|
||||
run: python builder/package.py binary
|
||||
- name: Upload Windows standalone binary (unsigned)
|
||||
uses: actions/upload-artifact@v5
|
||||
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/')
|
||||
@@ -49,22 +59,24 @@ jobs:
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: "signed"
|
||||
- name: Upload Windows standalone binary (signed)
|
||||
uses: actions/upload-artifact@v5
|
||||
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
|
||||
uses: actions/upload-artifact@v5
|
||||
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 +87,10 @@ jobs:
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: "signed"
|
||||
- name: Upload Windows installer (signed)
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
uses: actions/upload-artifact@v5
|
||||
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:
|
||||
@@ -105,7 +117,7 @@ jobs:
|
||||
cache-dependency-path: "**/requirements.txt"
|
||||
- name: Cache Python download
|
||||
id: cache-python-download
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/python.pkg
|
||||
key: cache-macOS-Python-${{ env.PYTHON_VERSION }}
|
||||
@@ -140,7 +152,7 @@ jobs:
|
||||
# Run this on macOS so the line endings are correct by default
|
||||
run: python builder/package.py source
|
||||
- name: Upload source distribution
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
path: "*-src.tar.gz"
|
||||
name: Source distribution
|
||||
@@ -153,7 +165,7 @@ jobs:
|
||||
python3 builder/package.py app
|
||||
python3 builder/make_dmg.py
|
||||
- name: Upload macOS binary
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
path: "*-macos.dmg"
|
||||
name: macOS binary
|
||||
@@ -167,14 +179,14 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
linux_arch: amd64
|
||||
linux_arch: x64
|
||||
- os: ubuntu-24.04-arm
|
||||
linux_arch: arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Cache par2cmdline-turbo tarball
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
id: cache-par2cmdline
|
||||
# Clearing the cache in case of new version requires manual clearing in GitHub!
|
||||
with:
|
||||
@@ -196,7 +208,7 @@ jobs:
|
||||
timeout 10s snap run sabnzbd --help || true
|
||||
sudo snap remove sabnzbd
|
||||
- name: Upload snap
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Snap package (${{ matrix.linux_arch }})
|
||||
path: ${{ steps.snapcraft.outputs.snap }}
|
||||
@@ -223,15 +235,15 @@ jobs:
|
||||
cache: pip
|
||||
cache-dependency-path: "builder/release-requirements.txt"
|
||||
- name: Download Source distribution artifact
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: Source distribution
|
||||
- name: Download macOS artifact
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: macOS binary
|
||||
- name: Download Windows artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: ${{ (contains(github.ref, 'refs/tags/')) && '*signed*' || '*Windows*' }}
|
||||
merge-multiple: true
|
||||
|
||||
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/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
if: github.repository_owner == 'sabnzbd'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
- uses: dessant/lock-threads@v6
|
||||
with:
|
||||
log-output: true
|
||||
issue-inactive-days: 60
|
||||
|
||||
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 Alpha 2
|
||||
Release Notes - SABnzbd 5.0.0 Beta 1
|
||||
=========================================================
|
||||
|
||||
This is the second test 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
|
||||
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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -279,7 +284,7 @@ msgstr ""
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr ""
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -1300,103 +1310,18 @@ msgstr ""
|
||||
msgid "NZB added to queue"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr ""
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr ""
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr ""
|
||||
@@ -1638,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\""
|
||||
@@ -1664,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 ""
|
||||
@@ -3482,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"
|
||||
@@ -4189,6 +4122,11 @@ msgstr ""
|
||||
msgid "Filename"
|
||||
msgstr ""
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr ""
|
||||
@@ -4598,6 +4536,10 @@ msgstr ""
|
||||
msgid "Server could not complete request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr ""
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
143
po/main/cs.po
143
po/main/cs.po
@@ -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 ""
|
||||
@@ -316,7 +321,7 @@ msgstr ""
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "Neočekávaná přípona v rar souboru %s"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "Přerušeno, nalezena neočekávaná připona"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1388,103 +1398,18 @@ msgstr "Nelze nahrát %s, detekován porušený soubor"
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB přidáno do fronty"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "Ignoruji duplikátní NZB \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr "Nezdařilo se duplikovat NZB \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr "Duplikátní NZB"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Prázdný NZB soubor %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr "Nechtěná přípona v souboru %s (%s)"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "Zrušeno, nelze dokončit"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "Chyba při importu %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "DUPLIKÁT"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "ŠIFROVANÉ"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "PŘÍLIŠ VELKÝ"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "NEKOMPLETNÍ"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "NECHTĚNÝ"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "ČEKÁNÍ %s s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr "PROPAGUJI %s min"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "Staženo do %s s průměrnou rychlostí %s B/s"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Stáří"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "Pozastavuji duplikátní NZB \"%s\""
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "Problém s"
|
||||
@@ -1729,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\""
|
||||
@@ -1755,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í"
|
||||
@@ -3672,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"
|
||||
@@ -4413,6 +4349,11 @@ msgstr ""
|
||||
msgid "Filename"
|
||||
msgstr ""
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Stáří"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr ""
|
||||
@@ -4832,6 +4773,10 @@ msgstr ""
|
||||
msgid "Server could not complete request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Prázdný NZB soubor %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
143
po/main/da.po
143
po/main/da.po
@@ -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 ""
|
||||
@@ -331,7 +336,7 @@ msgstr "I \"%s\" uønsket extension i RAR fil. Uønsket fil er \"%s\" "
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "Uønsket extension i rar fil %s"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "Afbrudt, uønsket extension fundet"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1432,103 +1442,18 @@ msgstr "Downloadnings fejl %s, ødelagt fil fundet"
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB tilføjet i køen"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "Ignorerer identiske NZB \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr "Fejler dublet NZB \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr "Dublet NZB"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr "Ødelagt NZB fil %s, springer over (årsag=%s)"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Tom NZB fil %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr "Før-kø script job markeret som mislykkedet"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr "Uønsket filtype i fil %s (%s)"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "Afbrudt, kan ikke afsluttes"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "Det lykkedes ikke at importere %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "DUPLIKERE"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr "ALTERNATIV"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "KRYPTEREDE"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "FOR STOR"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "UFULDSTÆNDIG"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "UØNSKET"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "VENT %s sekunder"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr "PROPAGATING %s min"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "Hentede i %s med et gennemsnit på %sB/s"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Alder"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s artikler misdannede"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "%s artikler manglede"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "%s artikler havde ikke-matchende dubletter"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "Pause duplikeret NZB \"%s\""
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "Problem med"
|
||||
@@ -1803,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\""
|
||||
@@ -1829,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"
|
||||
@@ -3845,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"
|
||||
@@ -4620,6 +4556,11 @@ msgstr "Slet"
|
||||
msgid "Filename"
|
||||
msgstr "Filnavn"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Alder"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "Ledig diskplads"
|
||||
@@ -5053,6 +4994,10 @@ msgstr "Fil ikke på server"
|
||||
msgid "Server could not complete request"
|
||||
msgstr "Serveren kunne ikke fuldføre anmodningen"
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Tom NZB fil %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
146
po/main/de.po
146
po/main/de.po
@@ -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 ""
|
||||
@@ -357,7 +362,7 @@ msgstr "Unerwünschter Typ \"%s\" in RAR Datei. Unerwünschte Datei ist %s "
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "Unerwünschter Dateityp im RAR-Archiv %s"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "Abgebrochen, unerwünschte Dateieindung gefunden"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1479,106 +1489,18 @@ msgstr "Fehler beim Laden von %s. Beschädigte Datei gefunden."
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB zur Warteschlange hinzugefügt"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "Doppelte NZB \"%s\" wird ignoriert"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr "kopieren der NZB \"%s\" fehlgeschlagen"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr "Doppelte NZB"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr "Ungültige NZB-Datei %s wird übersprungen (Fehler: %s)"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Leere NZB-Datei %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr ""
|
||||
"Das Vorwarteschlangen (pre-queue) Skript hat die Downloadaufgabe als "
|
||||
"gescheitert markiert"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr "Ungewollte Dateiendung in der Datei %s (%s)"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "Abgebrochen, kann nicht fertiggestellt werden"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "Fehler beim Importieren von %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "DUPLIKAT"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr "ALTERNATIVE"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "VERSCHLÜSSELT"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "ZU GROSS"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "UNVOLLSTÄNDIG"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "UNERWÜNSCHT"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "WARTE %s Sek"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr "AUSBREITUNG %s min"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr ""
|
||||
"Heruntergeladen in %s mit einer Durchschnittsgeschwindigkeit von %sB/s"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Alter"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s Artikel hatten ein ungültiges Format"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "%s Artikel fehlten"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "%s Artikel hatten nicht übereinstimmende Duplikate"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "Doppelt vorhandene NZB \"%s\" angehalten"
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "Problem mit"
|
||||
@@ -1862,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\""
|
||||
@@ -1888,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"
|
||||
@@ -3954,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"
|
||||
@@ -4737,6 +4670,11 @@ msgstr "Löschen"
|
||||
msgid "Filename"
|
||||
msgstr "Dateiname"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Alter"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "Freier Speicherplatz"
|
||||
@@ -5171,6 +5109,10 @@ msgstr "Datei nicht auf dem Server"
|
||||
msgid "Server could not complete request"
|
||||
msgstr "Server konnte nicht vollständig antworten"
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Leere NZB-Datei %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
145
po/main/es.po
145
po/main/es.po
@@ -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 ""
|
||||
@@ -342,7 +347,7 @@ msgstr ""
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "Se ha encontrado una extensión desconocida en el fichero rar %s"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "Se interrumpió la acción porque se detectó una extensión no deseada"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1471,105 +1481,18 @@ msgstr "Error al cargar %s, archivo corrupto"
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB añadido a la cola"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "Ignorando NZB Duplicado \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr "Fallo al duplicar NZB \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr "Duplicar NZB"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr "Fichero NBZ inválido: %s, omitiendo (razón=%s)"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Fichero NZB vacío: %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr ""
|
||||
"La secuencia de comandos de la cola preestablecida ha marcado la tarea como "
|
||||
"fallida"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr "Extensión no deseada en el archivo %s (%s)"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "Abortado, No puede ser completado"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "Error importando %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "DUPLICADO"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr "ALTERNATIVO"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "ENCRIPTADO"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "DEMASIADO GRANDE"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "INCOMPLETO"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "NO DESEADO"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "ESPERAR %s seg"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr "PROPAGANDO %s min"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "Descargado en %s a una media de %sB/s"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Edad"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s artículos estaban mal formados."
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "%s artículos no encontrados"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "%s artículos contenían duplicados inconexos"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "Pausando NZB duplicados \"%s\""
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "Problema con"
|
||||
@@ -1849,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\""
|
||||
@@ -1877,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"
|
||||
@@ -3924,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"
|
||||
@@ -4706,6 +4640,11 @@ msgstr "Eliminar"
|
||||
msgid "Filename"
|
||||
msgstr "Nombre de archivo"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Edad"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "Espacio libre"
|
||||
@@ -5142,6 +5081,10 @@ msgstr "El fichero no se encuentra en el servidor"
|
||||
msgid "Server could not complete request"
|
||||
msgstr "El servidor no ha podido completar la solicitud"
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Fichero NZB vacío: %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
143
po/main/fi.po
143
po/main/fi.po
@@ -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 ""
|
||||
@@ -315,7 +320,7 @@ msgstr ""
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "Ei toivottu tiedostopääte on rar arkistossa %s"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "Peruutettu, ei toivottu tiedostopääte havaittu"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -1387,103 +1397,18 @@ msgstr "Virhe ladattaessa %s, korruptoitunut tiedosto havaittu"
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB lisätty jonoon"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "Ohitetaan kaksoiskappale NZB \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Tyhjä NZB tiedosto %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "Peruutettu, ei voi valmistua"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "Virhe tuotaessa %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "KAKSOISKAPPALE"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "SALATTU"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "LIIAN SUURI"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "KESKENERÄINEN"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "EI TOIVOTTU"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "ODOTA %s sekuntia"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr "LEVITETÄÄN %s min"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "Ladattiin ajassa %s keskilatausnopeudella %sB/s"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Ikä"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s artikkelia oli väärin muotoiltuja"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "%s artikkelia puuttui"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "%s artikkelissa oli ei-vastaavia kaksoiskappaleita"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "Keskeytetään kaksoiskappale NZB \"%s\""
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "Ongelma"
|
||||
@@ -1756,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\""
|
||||
@@ -1782,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ä"
|
||||
@@ -3763,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"
|
||||
@@ -4519,6 +4455,11 @@ msgstr "Poista"
|
||||
msgid "Filename"
|
||||
msgstr "Tiedostonimi"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Ikä"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "Vapaa tila"
|
||||
@@ -4949,6 +4890,10 @@ msgstr "Tiedostoa ei ole palvelimella"
|
||||
msgid "Server could not complete request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Tyhjä NZB tiedosto %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
156
po/main/fr.po
156
po/main/fr.po
@@ -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 ""
|
||||
@@ -347,7 +354,7 @@ msgstr ""
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "L'extension indésirable est dans le fichier rar %s"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "Interrompu, extension indésirable détectée"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1466,103 +1480,18 @@ msgstr "Erreur lors du chargement de %s, fichier corrompu détecté"
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB ajouté à la file d'attente"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "Doublon NZB ignoré \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr "Échec de duplication du NZB \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr "Dupliquer NZB"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr "Fichier NZB %s invalide, sera ignoré (erreur : %s)"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Fichier NZB %s vide"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr "Le script de pré-file d'attente a marqué la tâche comme échouée"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr "Extension non souhaitée dans le fichier %s (%s)"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "Interrompu, ne peut être achevé"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "Erreur lors de l'importation de %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "DOUBLON"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr "ALTERNATIVE"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "CHIFFRÉ"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "TROP VOLUMINEUX"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "INCOMPLET"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "INDÉSIRABLE"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "PATIENTER %s sec"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr "PROPAGATION %s min"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "Téléchargé en %s à %sB/s de moyenne"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Âge"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s articles malformés"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "%s articles manquants"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "%s articles avec doublons sans correspondance"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "Mise en pause du doublon NZB \"%s\""
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "Problème avec"
|
||||
@@ -1845,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\""
|
||||
@@ -1872,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"
|
||||
@@ -3505,6 +3434,8 @@ msgstr "Activer les contrôles SFV"
|
||||
msgid ""
|
||||
"If no par2 files are available, use sfv files (if present) to verify files"
|
||||
msgstr ""
|
||||
"Si aucun fichier par2 n'est disponible, utiliser les fichiers sfv (si "
|
||||
"présents) pour vérifier les fichiers"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "User script can flag job as failed"
|
||||
@@ -3935,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"
|
||||
@@ -4720,6 +4665,11 @@ msgstr "Supprimer"
|
||||
msgid "Filename"
|
||||
msgstr "Nom de fichier"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Âge"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "Espace libre"
|
||||
@@ -5158,6 +5108,10 @@ msgstr "Fichier introuvable sur le serveur"
|
||||
msgid "Server could not complete request"
|
||||
msgstr "Le serveur n'a pas pu terminer la requête"
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Fichier NZB %s vide"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
143
po/main/he.po
143
po/main/he.po
@@ -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 ""
|
||||
@@ -320,7 +325,7 @@ msgstr "בעבודה \"%s\" יש סיומת בלתי רצויה בתוך קוב
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "סיומת בלתי רצויה בקובץ rar %s"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "בוטל, סיומת בלתי רצויה התגלתה"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1404,103 +1414,18 @@ msgstr "שגיאה בטעינת %s, קובץ פגום התגלה"
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB התווסף לתור"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "מתעלם מן NZB כפול \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr "מכשיל NZB כפול \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr "NZB כפול"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr "קובץ NZB בלתי תקף %s, מדלג (שגיאה: %s)"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "קובץ NZB ריק %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr "תסריט קדם־תור סומן כנכשל"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr "סיומת בלתי רצויה בקובץ %s (%s)"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "בוטל, לא יכול להיות שלם"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "שגיאה ביבוא %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "כפול"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr "חלופה"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "מוצפן"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "גדול מדי"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "בלתי שלם"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "בלתי רצוי"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "המתן %s שניות"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr "מפיץ %s דקות"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "ירד תוך %s בממוצע של %s ב/ש"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "גיל"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s מאמרים עוותו"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "%s מאמרים היו חסרים"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "אל %s מאמרים יש כפילויות בלתי תואמות"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "משהה NZB כפול \"%s\""
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "בעיה עם"
|
||||
@@ -1775,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\""
|
||||
@@ -1801,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 "הראה ממשק"
|
||||
@@ -3781,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"
|
||||
@@ -4546,6 +4482,11 @@ msgstr "מחק"
|
||||
msgid "Filename"
|
||||
msgstr "שם קובץ"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "גיל"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "שטח פנוי"
|
||||
@@ -4979,6 +4920,10 @@ msgstr "קובץ לא על השרת"
|
||||
msgid "Server could not complete request"
|
||||
msgstr "השרת לא היה יכול להשלים בקשה"
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "קובץ NZB ריק %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
143
po/main/it.po
143
po/main/it.po
@@ -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 ""
|
||||
@@ -340,7 +345,7 @@ msgstr ""
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "L'estensione non desiderata è nel file rar %s"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "Annullato, rilevata estensione non desiderata"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1454,103 +1464,18 @@ msgstr "Errore durante il caricamento di %s, rilevato file corrotto"
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB aggiunto alla coda"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "Ignorando NZB duplicato \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr "Fallimento NZB duplicato \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr "NZB duplicato"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr "File NZB non valido %s, saltato (errore: %s)"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "File NZB vuoto %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr "Lo script pre-coda ha contrassegnato il processo come fallito"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr "Estensione non desiderata nel file %s (%s)"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "Annullato, non può essere completato"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "Errore durante l'importazione di %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "DUPLICATO"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr "ALTERNATIVO"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "CRITTOGRAFATO"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "TROPPO GRANDE"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "INCOMPLETO"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "NON DESIDERATO"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "ATTENDI %s sec"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr "PROPAGAZIONE %s min"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "Scaricato in %s a una media di %sB/s"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Età"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s articoli erano malformati"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "%s articoli erano mancanti"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "%s articoli avevano duplicati non corrispondenti"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "Messa in pausa NZB duplicato \"%s\""
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "Problema con"
|
||||
@@ -1826,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\""
|
||||
@@ -1852,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"
|
||||
@@ -3894,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"
|
||||
@@ -4675,6 +4611,11 @@ msgstr "Elimina"
|
||||
msgid "Filename"
|
||||
msgstr "Nome file"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Età"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "Spazio libero"
|
||||
@@ -5111,6 +5052,10 @@ msgstr "File non presente sul server"
|
||||
msgid "Server could not complete request"
|
||||
msgstr "Il server non ha potuto completare la richiesta"
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "File NZB vuoto %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
143
po/main/nb.po
143
po/main/nb.po
@@ -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 ""
|
||||
@@ -313,7 +318,7 @@ msgstr ""
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "Uønsket forlenging finnes i rar fil %s"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "Avbryt, uønsket forlenging oppdaget"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -1385,103 +1395,18 @@ msgstr "Lastingsfeil %s, feilaktig fil oppdaget"
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB er lagt til i køen"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "Ignorerer duplikatfil \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Tom NZB-fil %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "Avbrutt, kan ikke fullføres"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "Kunne ikke importere %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "DUPLIKAT"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "KRYPTERT"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "FOR STOR"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "UFULLSTENDIG"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "UØNSKET"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "VENT %s sek"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "Hentet filer på %s med gjenomsnitts hastighet på %sB/s"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Tid"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s artikler var korrupte"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "%s artikler manglet"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "%s artikler hadde ulike duplikater"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "Stanser duplikatfil \"%s\""
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "Problem med"
|
||||
@@ -1754,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\""
|
||||
@@ -1780,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"
|
||||
@@ -3742,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"
|
||||
@@ -4493,6 +4429,11 @@ msgstr "Fjern"
|
||||
msgid "Filename"
|
||||
msgstr "Filnavn"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Tid"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "Ledig plass"
|
||||
@@ -4921,6 +4862,10 @@ msgstr ""
|
||||
msgid "Server could not complete request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Tom NZB-fil %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
143
po/main/nl.po
143
po/main/nl.po
@@ -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 ""
|
||||
@@ -337,7 +342,7 @@ msgstr "Ongewenste extensie ontdekt in \"%s\". Het ongewenste bestand is \"%s\"
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "De ongewenste extensie zit in RAR-bestand %s"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "Afgebroken, ongewenste extensie ontdekt"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1457,103 +1467,18 @@ msgstr "Fout bij inladen van %s, corrupt bestand gevonden"
|
||||
msgid "NZB added to queue"
|
||||
msgstr "Download aan wachtrij toegevoegd"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "Dubbele download \"%s\" overgeslagen"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr "Download '%s' geweigerd omdat het een dubbele is"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr "Dubbele download"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr "Corrupte NZB %s wordt overgeslagen (foutmelding: %s)"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "NZB-bestand %s is leeg"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr "Wachtrij filter script heeft de download afgekeurd"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr "Ongewenste extensie gevonden in %s (%s) "
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "Afgebroken, kan niet voltooid worden"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "Fout bij importeren van %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "DUBBEL"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr "ALTERNATIEF"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "VERSLEUTELD"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "TE GROOT"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "ONVOLLEDIG"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "ONGEWENST"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "WACHT %s sec"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr "VERSPREIDINGSWACHTTIJD %s min"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "Gedownload in %s met een gemiddelde snelheid van %sB/s"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Leeftijd"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s artikelen zijn misvormd"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "%s artikelen ontbreken"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "%s artikelen hadden afwijkende duplicaten"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "Dubbele download \"%s\" gepauzeerd"
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "Probleem met"
|
||||
@@ -1829,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\""
|
||||
@@ -1855,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"
|
||||
@@ -3894,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"
|
||||
@@ -4673,6 +4609,11 @@ msgstr "Verwijder"
|
||||
msgid "Filename"
|
||||
msgstr "Bestandsnaam"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Leeftijd"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "Vrije ruimte"
|
||||
@@ -5107,6 +5048,10 @@ msgstr "Bestand bestaat niet op de server"
|
||||
msgid "Server could not complete request"
|
||||
msgstr "De server kon de opdracht niet uitvoeren"
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "NZB-bestand %s is leeg"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
143
po/main/pl.po
143
po/main/pl.po
@@ -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 ""
|
||||
@@ -312,7 +317,7 @@ msgstr ""
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "Niepożądane rozszerzenie w pliku RAR %s"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "Przerwano, wykryto niepożądane rozszerzenie"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -1390,103 +1400,18 @@ msgstr "Błąd ładowania %s, wykryto uszkodzony plik"
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB dodany do kolejki"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "Ignoruję zduplikowany NZB \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Pusty plik NZB %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "Przerwano, nie można ukończyć"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "Błąd importu %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "DUPLIKAT"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "ZASZYFROWANY"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "ZA DUŻY"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "NIEKOMPLETNY"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "NIEPOŻĄDANY"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "CZEKAM %s s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "Pobrano w %s ze średnią %sB/s"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Wiek"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s artykułów było uszkodzonych"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "Brakowało %s artykułów"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "%s artykułów posiadało niepasujące duplikaty"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "Wstrzymuję zduplikowany NZB \"%s\""
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "Problem z"
|
||||
@@ -1763,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\""
|
||||
@@ -1789,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"
|
||||
@@ -3754,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"
|
||||
@@ -4505,6 +4441,11 @@ msgstr "Usuń"
|
||||
msgid "Filename"
|
||||
msgstr "Nazwa pliku"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Wiek"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "Wolne miejsce"
|
||||
@@ -4931,6 +4872,10 @@ msgstr ""
|
||||
msgid "Server could not complete request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Pusty plik NZB %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
143
po/main/pt_BR.po
143
po/main/pt_BR.po
@@ -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 ""
|
||||
@@ -324,7 +329,7 @@ msgstr ""
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "A extensão indesejada está no arquivo rar %s"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "Cancelado, extensão indesejada detectada"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -1399,103 +1409,18 @@ msgstr "Erro ao carregar %s. Arquivo corrompido detectado"
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB adicionado à fila"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "Ignorando NZB duplicado \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Arquivo NZB %s vazio"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "Cancelado, não é possível concluir"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "Erro ao importar %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "DUPLICADO"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "CRIPTOGRAFADO"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "MUITO GRANDE"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "INCOMPLETO"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "INDESEJADO"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "Espere %s segundo(s)"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "Baixado em %s a uma média de %sB/s"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Idade"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s artigos estavam malformados"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "%s artigos estavam faltando"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "%s artigos tinham duplicatas não-correspondentes"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "Pausando NZB duplicado \"%s\""
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "Problema com"
|
||||
@@ -1773,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\""
|
||||
@@ -1800,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"
|
||||
@@ -3765,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"
|
||||
@@ -4516,6 +4452,11 @@ msgstr "Eliminar"
|
||||
msgid "Filename"
|
||||
msgstr "Nome do arquivo"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Idade"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "Espaço Disponível"
|
||||
@@ -4942,6 +4883,10 @@ msgstr ""
|
||||
msgid "Server could not complete request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Arquivo NZB %s vazio"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
143
po/main/ro.po
143
po/main/ro.po
@@ -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 ""
|
||||
@@ -326,7 +331,7 @@ msgstr "Extensie nedorită în fișierul RAR al „%s”. Fișierul nedorit este
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "Extensii fișier nedorite în fișierul rar %s"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "Oprit, extensii nedorite detectate"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1417,103 +1427,18 @@ msgstr "Eroare încărcare %s, fişier corupt detectat"
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB adăugat în coadă"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "Ignorăm duplicat NZB \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr "Eșuare duplicat NZB „%s”"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr "NZB duplicat"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Fişier NZB gol %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr "Scriptul pre-coadă a marcat sarcina ca nereușită"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr "Extensie nedorită în fișierul %s (%s)"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "Anulat nu poate fi finalizat"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "Eroare importare %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "DUPLICAT"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "ENCRIPTAT"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "PREA MARE"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "INCOMPLET"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "NEDORIT"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "AŞTEAPTĂ %s sec"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr "SE PROPAGHEAZĂ %s min"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "Descărcat în %s cu o medie de %sB/s"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Vârsta"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s articolele au fost incorecte"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "%s articolele au fost lipsă"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "%s articolele au avut duplicate diferite"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "Întrerupem duplicat NZB \"%s\""
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "Problemă cu"
|
||||
@@ -1792,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\""
|
||||
@@ -1818,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"
|
||||
@@ -3786,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"
|
||||
@@ -4536,6 +4472,11 @@ msgstr "Şterge"
|
||||
msgid "Filename"
|
||||
msgstr "Nume de fișier"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Vârsta"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "Spațiu liber"
|
||||
@@ -4964,6 +4905,10 @@ msgstr "Fișierul nu este pe server"
|
||||
msgid "Server could not complete request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Fişier NZB gol %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
160
po/main/ru.po
160
po/main/ru.po
@@ -3,12 +3,13 @@
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2023
|
||||
# ST02, 2026
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.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
|
||||
@@ -312,9 +318,9 @@ msgstr ""
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: 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 ""
|
||||
|
||||
@@ -1385,103 +1396,18 @@ msgstr "Ошибка загрузки %s: обнаружен повреждён
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB-файл добавлен в очередь"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "Пропущен повторяющийся NZB-файл «%s»"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Пустой NZB-файл %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr ""
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "Ошибка импорта %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "ПОВТОР"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "ЗАШИФРОВАН"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "СЛИШКОМ БОЛЬШОЙ"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "НЕПОЛНЫЙ"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "ОЖИДАНИЕ %s с"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "Загружено за %s со средней скоростью %sБ/с"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Возраст"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s статей с ошибками"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "%s статей отсутствует"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "%s статей содержат несовпадающие повторы"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "Приостановлен повторяющийся NZB-файл «%s»"
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "Проблема с"
|
||||
@@ -1756,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\""
|
||||
@@ -1782,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 "Показать интерфейс"
|
||||
@@ -3743,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"
|
||||
@@ -4500,6 +4437,11 @@ msgstr "Удалить"
|
||||
msgid "Filename"
|
||||
msgstr "Название файла"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Возраст"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "свободно на диске"
|
||||
@@ -4927,6 +4869,10 @@ msgstr ""
|
||||
msgid "Server could not complete request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Пустой NZB-файл %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
143
po/main/sr.po
143
po/main/sr.po
@@ -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 ""
|
||||
@@ -309,7 +314,7 @@ msgstr ""
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "Neželjena ekstenzija je u rar datoteci %s"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "Prekinuto, detektovana neželjena ekstenzija"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -1380,103 +1390,18 @@ msgstr "Грешка учитавање %s, покварена датотека
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB додат у ред"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "Игнорисање дуплог NZB-а \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Празан NZB %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "Поништено, не може да се заврши"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "Грешка увоза %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "ДУПЛИКАТ"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "ШИФРИРАНО"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "ПРЕВЕЛИКО"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "НЕПОТПУНО"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "NEŽELJENI"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "Чекање %s сек"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "Преузето за %s на просек од %sБ/с"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Старост"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s артикла нису добро формирани"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "%s артикла недостају"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "%s артикла нису дупликате"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "Паузирам због дуплог NZB-а \"%s\""
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "Проблем са"
|
||||
@@ -1749,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\""
|
||||
@@ -1775,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"
|
||||
@@ -3729,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"
|
||||
@@ -4478,6 +4414,11 @@ msgstr "Обриши"
|
||||
msgid "Filename"
|
||||
msgstr "Име датотеке"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Старост"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "Слободан простор"
|
||||
@@ -4904,6 +4845,10 @@ msgstr ""
|
||||
msgid "Server could not complete request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Празан NZB %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
143
po/main/sv.po
143
po/main/sv.po
@@ -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 ""
|
||||
@@ -309,7 +314,7 @@ msgstr ""
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "Oönskad filändelse i RAR-fil %s"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "Avbruten, oönskad filändelse detekterad"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -1384,103 +1394,18 @@ msgstr "Laddningsfel %s, felaktig fil detekterad"
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB tillagd i kön"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "Ignorerar dubblett för NZB \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "NZB filen %s är tom"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "Avbrutet, kan inte slutföras"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "Det gick inte att importera %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "DUBLETT"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "KRYPTERAT"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "FÖR STOR"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "INKOMPLETT"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "OÖNSKAD"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "VÄNTA %s SEKUNDER"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "Hämtade i %s vid ett genomsnitt på %sB/s"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Ålder"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s artiklar var felaktiga"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "%s artiklar saknades"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "%s artiklar hade icke-matchande dubletter"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "Pausar dubblett för NZB \"%s\""
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "Problem med"
|
||||
@@ -1755,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\""
|
||||
@@ -1781,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"
|
||||
@@ -3741,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"
|
||||
@@ -4491,6 +4427,11 @@ msgstr "Ta bort"
|
||||
msgid "Filename"
|
||||
msgstr "Filnamn"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Ålder"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "Ledigt diskutrymme"
|
||||
@@ -4918,6 +4859,10 @@ msgstr ""
|
||||
msgid "Server could not complete request"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "NZB filen %s är tom"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
154
po/main/tr.po
154
po/main/tr.po
@@ -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 ""
|
||||
@@ -342,7 +347,7 @@ msgstr ""
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "İstenmeyen uzantı %s rar dosyasındadır"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "İptal edildi, istenmeyen uzantı tespit edildi"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1445,103 +1457,18 @@ msgstr "%s yüklenirken hata, bozuk dosya tespit edildi"
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB kuyruğa ilave edildi"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "Yinelenmiş NZB \"%s\" dikkate alınmıyor"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr "\"%s\" NSB dosyasının yinelenmesi başarısız"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr "Yinelenmiş NZB"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr "Geçersiz NZB dosyası %s, atlanıyor (hata: %s)"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Boş NZB dosyası %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr "Kuyruk öncesi betiği işi başarısız oldu olarak işaretlemiş"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr "%s (%s) dosyasında İstenmeyen Uzantı"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "İptal edildi, tamamlanamıyor"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "%s unsurunun içe aktarılmasında hata"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "YİNELENMİŞ"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr "ALTERNATİF"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "ŞİFRELENMİŞ"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "ÇOK BÜYÜK"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "TAMAMLANMAMIŞ"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "İSTENMEYEN"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "%s saniye BEKLEYİN"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr "YAYINLANIYOR %s dakika"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "%s içinde ortalama %sB/s hızında indirildi"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Yaş"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s makale yanlış şekillendirilmişti"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "%s makale eksikti"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "%s makale eşleşmeyen yinelenmişler bulunduruyordu"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "Yinelenmiş NZB \"%s\" duraklatılıyor"
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "Şununla sorun"
|
||||
@@ -1818,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\""
|
||||
@@ -1844,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"
|
||||
@@ -3461,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"
|
||||
@@ -3881,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"
|
||||
@@ -4661,6 +4604,11 @@ msgstr "Sil"
|
||||
msgid "Filename"
|
||||
msgstr "Dosya ismi"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "Yaş"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "Boş alan"
|
||||
@@ -5100,6 +5048,10 @@ msgstr "Dosya sunucuda yok"
|
||||
msgid "Server could not complete request"
|
||||
msgstr "Sunucu talebi tamamlayamadı"
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "Boş NZB dosyası %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
143
po/main/zh_CN.po
143
po/main/zh_CN.po
@@ -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 ""
|
||||
@@ -310,7 +315,7 @@ msgstr "RAR 文件“%s”中出现不需要的扩展名。不需要的文件名
|
||||
msgid "Unwanted extension is in rar file %s"
|
||||
msgstr "rar 文件中出现不需要的扩展名 %s"
|
||||
|
||||
#: sabnzbd/assembler.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/assembler.py
|
||||
msgid "Aborted, unwanted extension detected"
|
||||
msgstr "已中止,侦测到不需要的扩展名"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1376,103 +1386,18 @@ msgstr "无法加载 %s,侦测到损坏文件"
|
||||
msgid "NZB added to queue"
|
||||
msgstr "NZB 已添加到队列"
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Ignoring duplicate NZB \"%s\""
|
||||
msgstr "正在忽略重复 NZB \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Failing duplicate NZB \"%s\""
|
||||
msgstr "失败于重复的 NZB 文件 \"%s\""
|
||||
|
||||
#: sabnzbd/nzbqueue.py, sabnzbd/nzbstuff.py
|
||||
#: sabnzbd/nzbqueue.py
|
||||
msgid "Duplicate NZB"
|
||||
msgstr "重复的 NZB 文件"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Invalid NZB file %s, skipping (error: %s)"
|
||||
msgstr "无效的 NZB 文件 %s,跳过(错误:%s)"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "空 NZB 文件 %s"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pre-queue script marked job as failed"
|
||||
msgstr "预队列脚本将任务标记为失败的"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr "文件 %s 中有不需要的扩展名 (%s)"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
msgstr "已中止,无法完成"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Error importing %s"
|
||||
msgstr "导入 %s 出错"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "DUPLICATE"
|
||||
msgstr "*重复*"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr "备选"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
msgstr "*加密*"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "TOO LARGE"
|
||||
msgstr "*太大*"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "INCOMPLETE"
|
||||
msgstr "*不完整*"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "UNWANTED"
|
||||
msgstr "不需要"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "WAIT %s sec"
|
||||
msgstr "*等待* %s 秒"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "PROPAGATING %s min"
|
||||
msgstr "传播延迟生效,等待 %s 分钟"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Downloaded in %s at an average of %sB/s"
|
||||
msgstr "已下载,耗时 %s,平均速度 %sB/s"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/nzbstuff.py, sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "发布时间"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were malformed"
|
||||
msgstr "%s 篇文章损坏"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles were missing"
|
||||
msgstr "%s 篇文章缺失"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "%s articles had non-matching duplicates"
|
||||
msgstr "%s 篇文章存在未匹配的重复"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Pausing duplicate NZB \"%s\""
|
||||
msgstr "正在暂停重复 NZB \"%s\""
|
||||
|
||||
#: sabnzbd/panic.py
|
||||
msgid "Problem with"
|
||||
msgstr "问题"
|
||||
@@ -1745,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\""
|
||||
@@ -1771,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 "显示界面"
|
||||
@@ -3690,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"
|
||||
@@ -4442,6 +4378,11 @@ msgstr "删除"
|
||||
msgid "Filename"
|
||||
msgstr "文件名"
|
||||
|
||||
#. Job details page, file age column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Age"
|
||||
msgstr "发布时间"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Free Space"
|
||||
msgstr "剩余空间"
|
||||
@@ -4865,6 +4806,10 @@ msgstr "服务器上无此文件"
|
||||
msgid "Server could not complete request"
|
||||
msgstr "服务器无法完成请求"
|
||||
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "Empty NZB file %s"
|
||||
msgstr "空 NZB 文件 %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/urlgrabber.py
|
||||
msgid "URLGRABBER CRASHED"
|
||||
|
||||
@@ -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
|
||||
@@ -61,14 +62,14 @@ 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.0
|
||||
certifi==2025.11.12
|
||||
urllib3==2.6.3
|
||||
certifi==2026.1.4
|
||||
oauthlib==3.3.1
|
||||
PyJWT==2.10.1
|
||||
blinker==1.9.0
|
||||
|
||||
@@ -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")
|
||||
@@ -82,15 +83,15 @@ from sabnzbd.version import __version__, __baseline__
|
||||
import sabnzbd.misc as misc
|
||||
import sabnzbd.filesystem as filesystem
|
||||
import sabnzbd.powersup as powersup
|
||||
import sabnzbd.rss as rss
|
||||
import sabnzbd.emailer as emailer
|
||||
import sabnzbd.encoding as encoding
|
||||
import sabnzbd.config as config
|
||||
import sabnzbd.cfg as cfg
|
||||
import sabnzbd.database
|
||||
import sabnzbd.lang as lang
|
||||
import sabnzbd.nzb
|
||||
import sabnzbd.nzbparser as nzbparser
|
||||
import sabnzbd.nzbstuff
|
||||
import sabnzbd.rss as rss
|
||||
import sabnzbd.emailer as emailer
|
||||
import sabnzbd.getipaddress
|
||||
import sabnzbd.newsunpack
|
||||
import sabnzbd.par2file
|
||||
@@ -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,13 +81,14 @@ 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
|
||||
from sabnzbd.getipaddress import local_ipv4, public_ipv4, public_ipv6, dnslookup, active_socks5_proxy
|
||||
from sabnzbd.database import HistoryDB
|
||||
from sabnzbd.lang import is_rtl
|
||||
from sabnzbd.nzbstuff import NzbObject
|
||||
from sabnzbd.nzb import TryList, NzbObject
|
||||
from sabnzbd.newswrapper import NewsWrapper, NNTPPermanentError
|
||||
import sabnzbd.emailer
|
||||
import sabnzbd.sorting
|
||||
@@ -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:
|
||||
@@ -1006,7 +1016,7 @@ def _api_gc_stats(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
# Collect before we check
|
||||
gc.collect()
|
||||
# We cannot create any lists/dicts, as they would create a reference
|
||||
return report(data=[str(obj) for obj in gc.get_objects() if isinstance(obj, sabnzbd.nzbstuff.TryList)])
|
||||
return report(data=[str(obj) for obj in gc.get_objects() if isinstance(obj, TryList)])
|
||||
|
||||
|
||||
##############################################################################
|
||||
@@ -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.nzbstuff 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.nzbstuff import NzbObject, NzbFile
|
||||
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,115 @@ 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:
|
||||
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 +511,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
|
||||
@@ -498,9 +505,14 @@ def convert_search(search: str) -> str:
|
||||
return search
|
||||
|
||||
|
||||
def build_history_info(nzo, workdir_complete: str, postproc_time: int, script_output: str, script_line: str):
|
||||
def build_history_info(
|
||||
nzo: "sabnzbd.nzb.NzbObject",
|
||||
workdir_complete: str,
|
||||
postproc_time: int,
|
||||
script_output: str,
|
||||
script_line: str,
|
||||
):
|
||||
"""Collects all the information needed for the database"""
|
||||
nzo: sabnzbd.nzbstuff.NzbObject
|
||||
completed = int(time.time())
|
||||
pp = PP_LOOKUP.get(opts_to_pp(nzo.repair, nzo.unpack, nzo.delete), "X")
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ from typing import Optional
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.constants import SABCTOOLS_VERSION_REQUIRED
|
||||
from sabnzbd.nzbstuff import Article
|
||||
from sabnzbd.nzb import Article
|
||||
from sabnzbd.misc import match_str
|
||||
|
||||
# Check for correct SABCTools version
|
||||
@@ -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
|
||||
@@ -189,7 +190,7 @@ def get_biggest_file(filelist: list[str]) -> str:
|
||||
return None
|
||||
|
||||
|
||||
def deobfuscate(nzo, filelist: list[str], usefulname: str) -> list[str]:
|
||||
def deobfuscate(nzo: "sabnzbd.nzb.NzbObject", filelist: list[str], usefulname: str) -> list[str]:
|
||||
"""
|
||||
For files in filelist:
|
||||
1. if a file has no meaningful extension, add it (for example ".txt" or ".png")
|
||||
@@ -227,9 +228,6 @@ def deobfuscate(nzo, filelist: list[str], usefulname: str) -> list[str]:
|
||||
|
||||
"""
|
||||
|
||||
# Can't be imported directly due to circular import
|
||||
nzo: sabnzbd.nzbstuff.NzbObject
|
||||
|
||||
# to be sure, only keep really existing files and remove any duplicates:
|
||||
filtered_filelist = list(set(f for f in filelist if os.path.isfile(f)))
|
||||
|
||||
@@ -320,7 +318,7 @@ def without_extension(fullpathfilename: str) -> str:
|
||||
return os.path.splitext(fullpathfilename)[0]
|
||||
|
||||
|
||||
def deobfuscate_subtitles(nzo, filelist: list[str]):
|
||||
def deobfuscate_subtitles(nzo: "sabnzbd.nzb.NzbObject", filelist: list[str]):
|
||||
"""
|
||||
input:
|
||||
nzo, so we can update result via set_unpack_info()
|
||||
@@ -345,10 +343,6 @@ def deobfuscate_subtitles(nzo, filelist: list[str]):
|
||||
Something.else.txt
|
||||
|
||||
"""
|
||||
|
||||
# Can't be imported directly due to circular import
|
||||
nzo: sabnzbd.nzbstuff.NzbObject
|
||||
|
||||
# find .srt files
|
||||
if not (srt_files := [f for f in filelist if f.endswith(".srt")]):
|
||||
logging.debug("No .srt files found, so nothing to do")
|
||||
|
||||
@@ -31,7 +31,7 @@ import sabnzbd
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.misc import int_conv, format_time_string, build_and_run_command
|
||||
from sabnzbd.filesystem import remove_all, real_path, remove_file, get_basename, clip_path
|
||||
from sabnzbd.nzbstuff import NzbObject, NzbFile
|
||||
from sabnzbd.nzb import NzbFile, NzbObject
|
||||
from sabnzbd.encoding import platform_btou
|
||||
from sabnzbd.decorators import synchronized
|
||||
from sabnzbd.newsunpack import RAR_EXTRACTFROM_RE, RAR_EXTRACTED_RE, rar_volumelist, add_time_left
|
||||
|
||||
@@ -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
|
||||
@@ -151,7 +152,7 @@ class Server:
|
||||
self.request: bool = False # True if a getaddrinfo() request is pending
|
||||
self.have_body: bool = True # Assume server has "BODY", until proven otherwise
|
||||
self.have_stat: bool = True # Assume server has "STAT", until proven otherwise
|
||||
self.article_queue: Deque[sabnzbd.nzbstuff.Article] = deque()
|
||||
self.article_queue: Deque[sabnzbd.nzb.Article] = deque()
|
||||
|
||||
# Skip during server testing
|
||||
if threads:
|
||||
@@ -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()
|
||||
@@ -296,7 +296,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 +330,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 +361,7 @@ class Downloader(Thread):
|
||||
ssl,
|
||||
ssl_verify,
|
||||
ssl_ciphers,
|
||||
pipelining_requests,
|
||||
username,
|
||||
password,
|
||||
required,
|
||||
@@ -370,6 +377,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 +388,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 +399,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
|
||||
@@ -540,7 +552,7 @@ class Downloader(Thread):
|
||||
server.addrinfo = None
|
||||
|
||||
@staticmethod
|
||||
def decode(article: "sabnzbd.nzbstuff.Article", response: Optional[sabctools.NNTPResponse] = None):
|
||||
def decode(article: "sabnzbd.nzb.Article", response: Optional[sabctools.NNTPResponse] = None):
|
||||
"""Decode article"""
|
||||
# Need a better way of draining requests
|
||||
if article.nzf.nzo.removed_from_queue:
|
||||
@@ -644,12 +656,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()
|
||||
@@ -694,7 +706,8 @@ class Downloader(Thread):
|
||||
if self.selector.get_map():
|
||||
if events := self.selector.select(timeout=1.0):
|
||||
for key, ev in events:
|
||||
process_nw_queue.put((key.data, ev))
|
||||
nw = key.data
|
||||
process_nw_queue.put((nw, ev, nw.generation))
|
||||
else:
|
||||
events = []
|
||||
BPSMeter.reset()
|
||||
@@ -738,23 +751,48 @@ class Downloader(Thread):
|
||||
logging.error(T("Fatal error in Downloader"), exc_info=True)
|
||||
self.pause()
|
||||
|
||||
def process_nw(self, nw: NewsWrapper, event: int):
|
||||
def process_nw(self, nw: NewsWrapper, event: int, generation: int):
|
||||
"""Receive data from a NewsWrapper and handle the response"""
|
||||
if event & selectors.EVENT_READ:
|
||||
self.process_nw_read(nw)
|
||||
if event & selectors.EVENT_WRITE:
|
||||
# Drop stale items
|
||||
if nw.generation != generation:
|
||||
return
|
||||
|
||||
# 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
|
||||
# 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) -> None:
|
||||
def process_nw_read(self, nw: NewsWrapper, generation: int) -> None:
|
||||
bytes_received: int = 0
|
||||
bytes_pending: int = 0
|
||||
|
||||
while True:
|
||||
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)
|
||||
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)
|
||||
@@ -768,6 +806,10 @@ class Downloader(Thread):
|
||||
if not bytes_pending:
|
||||
break
|
||||
|
||||
# Ignore metrics for reset connections
|
||||
if nw.generation != generation:
|
||||
return
|
||||
|
||||
server = nw.server
|
||||
|
||||
with DOWNLOADER_LOCK:
|
||||
@@ -780,33 +822,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:
|
||||
@@ -900,7 +947,7 @@ class Downloader(Thread):
|
||||
wait: bool = True,
|
||||
count_article_try: bool = True,
|
||||
retry_article: bool = True,
|
||||
article: Optional["sabnzbd.nzbstuff.Article"] = None,
|
||||
article: Optional["sabnzbd.nzb.Article"] = None,
|
||||
):
|
||||
# Some warnings are errors, and not added as server.warning
|
||||
if warn and reset_msg:
|
||||
@@ -909,13 +956,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
|
||||
@@ -1367,3 +1393,44 @@ def pathbrowser(path: str, show_hidden: bool = False, show_files: bool = False)
|
||||
)
|
||||
|
||||
return file_list
|
||||
|
||||
|
||||
def create_work_name(name: str) -> str:
|
||||
"""Remove ".nzb" and ".par(2)" and sanitize, skip URL's"""
|
||||
if name.find("://") < 0:
|
||||
# Invalid charters need to be removed before and after (see unit-tests)
|
||||
return sanitize_foldername(strip_extensions(sanitize_foldername(name)))
|
||||
else:
|
||||
return name.strip()
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# Linux and macOS
|
||||
if info.st_blocks * 512 < info.st_size:
|
||||
return True
|
||||
|
||||
# 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
|
||||
|
||||
return False
|
||||
|
||||
|
||||
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:
|
||||
|
||||
128
sabnzbd/misc.py
128
sabnzbd/misc.py
@@ -57,11 +57,12 @@ import sabnzbd.config as config
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.decorators import conditional_cache
|
||||
from sabnzbd.encoding import ubtou, platform_btou
|
||||
from sabnzbd.filesystem import userxbit, make_script_path, remove_file
|
||||
from sabnzbd.filesystem import userxbit, make_script_path, remove_file, strip_extensions
|
||||
|
||||
if sabnzbd.WINDOWS:
|
||||
try:
|
||||
import winreg
|
||||
import win32api
|
||||
import win32process
|
||||
import win32con
|
||||
|
||||
@@ -85,6 +86,10 @@ RE_SAMPLE = re.compile(r"((^|[\W_])(sample|proof))", re.I) # something-sample o
|
||||
RE_IP4 = re.compile(r"inet\s+(addr:\s*)?(\d+\.\d+\.\d+\.\d+)")
|
||||
RE_IP6 = re.compile(r"inet6\s+(addr:\s*)?([0-9a-f:]+)", re.I)
|
||||
|
||||
# Name patterns for NZB parsing
|
||||
RE_SUBJECT_FILENAME_QUOTES = re.compile(r'"([^"]*)"')
|
||||
RE_SUBJECT_BASIC_FILENAME = re.compile(r"\b([\w\-+()' .,]+(?:\[[\w\-/+()' .,]*][\w\-+()' .,]*)*\.[A-Za-z0-9]{2,4})\b")
|
||||
|
||||
# Check if strings are defined for AM and PM
|
||||
HAVE_AMPM = bool(time.strftime("%p"))
|
||||
|
||||
@@ -713,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
|
||||
@@ -744,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)
|
||||
@@ -1591,6 +1579,66 @@ def convert_history_retention():
|
||||
cfg.history_retention_option.set("all-delete")
|
||||
|
||||
|
||||
def scan_password(name: str) -> tuple[str, Optional[str]]:
|
||||
"""Get password (if any) from the title"""
|
||||
if "http://" in name or "https://" in name:
|
||||
return name, None
|
||||
|
||||
# Strip any unwanted usenet-related extensions
|
||||
name = strip_extensions(name)
|
||||
|
||||
# Identify any braces
|
||||
braces = name[1:].find("{{")
|
||||
if braces < 0:
|
||||
braces = len(name)
|
||||
else:
|
||||
braces += 1
|
||||
slash = name.find("/")
|
||||
|
||||
# Look for name/password, but make sure that '/' comes before any {{
|
||||
if 0 < slash < braces and "password=" not in name:
|
||||
# Is it maybe in 'name / password' notation?
|
||||
if slash == name.find(" / ") + 1 and name[: slash - 1].strip(". "):
|
||||
# Remove the extra space after name and before password
|
||||
return name[: slash - 1].strip(". "), name[slash + 2 :]
|
||||
if name[:slash].strip(". "):
|
||||
return name[:slash].strip(". "), name[slash + 1 :]
|
||||
|
||||
# Look for "name password=password"
|
||||
pw = name.find("password=")
|
||||
if pw > 0 and name[:pw].strip(". "):
|
||||
return name[:pw].strip(". "), name[pw + 9 :]
|
||||
|
||||
# Look for name{{password}}
|
||||
if braces < len(name):
|
||||
closing_braces = name.rfind("}}")
|
||||
if closing_braces > braces and name[:braces].strip(". "):
|
||||
return name[:braces].strip(". "), name[braces + 2 : closing_braces]
|
||||
|
||||
# Look again for name/password
|
||||
if slash > 0 and name[:slash].strip(". "):
|
||||
return name[:slash].strip(". "), name[slash + 1 :]
|
||||
|
||||
# No password found
|
||||
return name, None
|
||||
|
||||
|
||||
def subject_name_extractor(subject: str) -> str:
|
||||
"""Try to extract a file name from a subject line, return `subject` if in doubt"""
|
||||
# Filename nicely wrapped in quotes
|
||||
for name in re.findall(RE_SUBJECT_FILENAME_QUOTES, subject):
|
||||
if name := name.strip(' "'):
|
||||
return name
|
||||
|
||||
# Found nothing? Try a basic filename-like search
|
||||
for name in re.findall(RE_SUBJECT_BASIC_FILENAME, subject):
|
||||
if name := name.strip():
|
||||
return name
|
||||
|
||||
# Return the subject
|
||||
return subject
|
||||
|
||||
|
||||
##
|
||||
## SABnzbd patched rarfile classes
|
||||
## Patch for https://github.com/markokr/rarfile/issues/56#issuecomment-711146569
|
||||
|
||||
@@ -66,11 +66,10 @@ from sabnzbd.filesystem import (
|
||||
get_basename,
|
||||
create_all_dirs,
|
||||
)
|
||||
from sabnzbd.nzbstuff import NzbObject
|
||||
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",
|
||||
@@ -74,21 +75,26 @@ class NewsWrapper:
|
||||
"_response_queue",
|
||||
"selector_events",
|
||||
"lock",
|
||||
"generation",
|
||||
"tls_wants_write",
|
||||
)
|
||||
|
||||
def __init__(self, server, thrdnum, block=False):
|
||||
def __init__(self, server: "sabnzbd.downloader.Server", thrdnum: int, block: bool = False, generation: int = 0):
|
||||
self.server: sabnzbd.downloader.Server = server
|
||||
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
|
||||
@@ -97,16 +103,16 @@ class NewsWrapper:
|
||||
self.group: Optional[str] = None
|
||||
|
||||
# Command queue and concurrency
|
||||
self.next_request: Optional[tuple[bytes, Optional["sabnzbd.nzbstuff.Article"]]] = None
|
||||
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.nzbstuff.Article]] = deque()
|
||||
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.nzbstuff.Article"]:
|
||||
def article(self) -> Optional["sabnzbd.nzb.Article"]:
|
||||
"""The article currently being downloaded"""
|
||||
with self.lock:
|
||||
if self._response_queue:
|
||||
@@ -131,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
|
||||
@@ -139,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
|
||||
@@ -159,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)
|
||||
@@ -170,19 +176,19 @@ 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
|
||||
|
||||
def queue_command(
|
||||
self,
|
||||
command: bytes,
|
||||
article: Optional["sabnzbd.nzbstuff.Article"] = None,
|
||||
article: Optional["sabnzbd.nzb.Article"] = None,
|
||||
) -> None:
|
||||
"""Add a command to the command queue"""
|
||||
self.next_request = command, article
|
||||
|
||||
def body(self, article: "sabnzbd.nzbstuff.Article") -> tuple[bytes, "sabnzbd.nzbstuff.Article"]:
|
||||
def body(self, article: "sabnzbd.nzb.Article") -> tuple[bytes, "sabnzbd.nzb.Article"]:
|
||||
"""Request the body of the article"""
|
||||
self.timeout = time.time() + self.server.timeout
|
||||
if article.nzf.nzo.precheck:
|
||||
@@ -196,10 +202,9 @@ class NewsWrapper:
|
||||
command = utob("ARTICLE <%s>\r\n" % article.article)
|
||||
return command, article
|
||||
|
||||
def on_response(self, response: sabctools.NNTPResponse, article: Optional["sabnzbd.nzbstuff.Article"]) -> None:
|
||||
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
|
||||
|
||||
@@ -212,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:
|
||||
@@ -235,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
|
||||
@@ -282,12 +281,21 @@ class NewsWrapper:
|
||||
self,
|
||||
nbytes: int = 0,
|
||||
on_response: Optional[Callable[[int, str], None]] = None,
|
||||
generation: Optional[int] = None,
|
||||
) -> Tuple[int, Optional[int]]:
|
||||
"""Receive data, return #bytes, #pendingbytes
|
||||
:param nbytes: maximum number of bytes to read
|
||||
:param on_response: callback for each complete response received
|
||||
:param generation: expected reset generation
|
||||
:return: #bytes, #pendingbytes
|
||||
"""
|
||||
if generation is None:
|
||||
generation = self.generation
|
||||
|
||||
# NewsWrapper is being reset
|
||||
if self.decoder is None:
|
||||
return 0, None
|
||||
|
||||
# Receive data into the decoder pre-allocated buffer
|
||||
if not nbytes and self.nntp.nw.server.ssl and not self.nntp.nw.blocking and sabctools.openssl_linked:
|
||||
# Use patched version when downloading
|
||||
@@ -303,12 +311,31 @@ class NewsWrapper:
|
||||
self.timeout = time.time() + self.server.timeout
|
||||
|
||||
self.decoder.process(bytes_recv)
|
||||
for response in self.decoder:
|
||||
with self.lock:
|
||||
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
|
||||
@@ -317,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 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:
|
||||
@@ -411,12 +453,13 @@ 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
|
||||
|
||||
# Reset all variables (including the NNTP connection)
|
||||
self.__init__(self.server, self.thrdnum)
|
||||
# Reset all variables (including the NNTP connection) and increment the generation counter
|
||||
self.__init__(self.server, self.thrdnum, generation=self.generation + 1)
|
||||
|
||||
# Wait before re-using this newswrapper
|
||||
if wait:
|
||||
@@ -428,7 +471,7 @@ class NewsWrapper:
|
||||
|
||||
def discard(
|
||||
self,
|
||||
article: Optional["sabnzbd.nzbstuff.Article"],
|
||||
article: Optional["sabnzbd.nzb.Article"],
|
||||
count_article_try: bool = True,
|
||||
retry_article: bool = True,
|
||||
) -> None:
|
||||
@@ -453,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
|
||||
@@ -470,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
|
||||
@@ -569,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)
|
||||
@@ -626,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):
|
||||
@@ -633,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
|
||||
|
||||
56
sabnzbd/nzb/__init__.py
Normal file
56
sabnzbd/nzb/__init__.py
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/python3 -OO
|
||||
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
"""
|
||||
sabnzbd.nzb - NZB-related classes and functionality
|
||||
"""
|
||||
|
||||
# Article-related classes
|
||||
from sabnzbd.nzb.article import Article, ArticleSaver, TryList
|
||||
|
||||
# File-related classes
|
||||
from sabnzbd.nzb.file import NzbFile, NzbFileSaver, SkippedNzbFile
|
||||
|
||||
# Object-related classes
|
||||
from sabnzbd.nzb.object import (
|
||||
NzbObject,
|
||||
NzbObjectSaver,
|
||||
NzoAttributeSaver,
|
||||
NzbEmpty,
|
||||
NzbRejected,
|
||||
NzbPreQueueRejected,
|
||||
NzbRejectToHistory,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Article
|
||||
"Article",
|
||||
"ArticleSaver",
|
||||
"TryList",
|
||||
# File
|
||||
"NzbFile",
|
||||
"NzbFileSaver",
|
||||
"SkippedNzbFile",
|
||||
# Object
|
||||
"NzbObject",
|
||||
"NzbObjectSaver",
|
||||
"NzoAttributeSaver",
|
||||
"NzbEmpty",
|
||||
"NzbRejected",
|
||||
"NzbPreQueueRejected",
|
||||
"NzbRejectToHistory",
|
||||
]
|
||||
226
sabnzbd/nzb/article.py
Normal file
226
sabnzbd/nzb/article.py
Normal file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/python3 -OO
|
||||
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
"""
|
||||
sabnzbd.article - Article and TryList classes for NZB downloading
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from typing import Optional
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.downloader import Server
|
||||
from sabnzbd.filesystem import get_new_id
|
||||
from sabnzbd.decorators import synchronized
|
||||
|
||||
##############################################################################
|
||||
# Trylist
|
||||
##############################################################################
|
||||
|
||||
|
||||
class TryList:
|
||||
"""TryList keeps track of which servers have been tried for a specific article"""
|
||||
|
||||
# Pre-define attributes to save memory
|
||||
__slots__ = ("try_list",)
|
||||
|
||||
def __init__(self):
|
||||
# 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"""
|
||||
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"""
|
||||
return all_servers.issubset(self.try_list)
|
||||
|
||||
@synchronized()
|
||||
def add_to_try_list(self, server: Server):
|
||||
"""Register server as having been tried already"""
|
||||
# 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"""
|
||||
# Discard does not require the item to be present
|
||||
self.try_list.discard(server)
|
||||
|
||||
@synchronized()
|
||||
def reset_try_list(self):
|
||||
"""Clean the list"""
|
||||
self.try_list = set()
|
||||
|
||||
def __getstate__(self):
|
||||
"""Save the servers"""
|
||||
return set(server.id for server in self.try_list)
|
||||
|
||||
def __setstate__(self, servers_ids: list[str]):
|
||||
self.try_list = set()
|
||||
for server in sabnzbd.Downloader.servers:
|
||||
if server.id in servers_ids:
|
||||
self.add_to_try_list(server)
|
||||
|
||||
|
||||
##############################################################################
|
||||
# Article
|
||||
##############################################################################
|
||||
ArticleSaver = (
|
||||
"article",
|
||||
"art_id",
|
||||
"bytes",
|
||||
"lowest_partnum",
|
||||
"decoded",
|
||||
"file_size",
|
||||
"data_begin",
|
||||
"data_size",
|
||||
"on_disk",
|
||||
"nzf",
|
||||
"crc32",
|
||||
"decoded_size",
|
||||
)
|
||||
|
||||
|
||||
class Article(TryList):
|
||||
"""Representation of one article"""
|
||||
|
||||
# Pre-define attributes to save memory
|
||||
__slots__ = ArticleSaver + ("fetcher", "fetcher_priority", "tries", "lock")
|
||||
|
||||
def __init__(self, article, article_bytes, nzf):
|
||||
super().__init__()
|
||||
self.article: str = article
|
||||
self.art_id: Optional[str] = None
|
||||
self.bytes: int = article_bytes
|
||||
self.lowest_partnum: bool = False
|
||||
self.fetcher: Optional[Server] = None
|
||||
self.fetcher_priority: int = 0
|
||||
self.tries: int = 0 # Try count
|
||||
self.decoded: bool = False
|
||||
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: "sabnzbd.nzb.NzbFile" = nzf # NzbFile reference
|
||||
# Share NzbFile lock for file-wide atomicity of try-list ops
|
||||
self.lock: threading.RLock = nzf.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."""
|
||||
self.fetcher = None
|
||||
self.fetcher_priority = 0
|
||||
super().reset_try_list()
|
||||
|
||||
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.
|
||||
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"""
|
||||
if self.fetcher or self.server_in_try_list(server):
|
||||
return None
|
||||
|
||||
if server.priority > self.fetcher_priority:
|
||||
# Check for higher priority server, taking advantage of servers list being sorted by priority
|
||||
for server_check in servers:
|
||||
if server_check.priority < server.priority:
|
||||
if server_check.active and not self.server_in_try_list(server_check):
|
||||
# There is a higher priority server, so set article priority and return
|
||||
self.fetcher_priority = server_check.priority
|
||||
return None
|
||||
else:
|
||||
# All servers with a higher priority have been checked
|
||||
break
|
||||
|
||||
# If no higher priority servers, use this server
|
||||
self.fetcher_priority = server.priority
|
||||
self.fetcher = server
|
||||
self.tries += 1
|
||||
return self
|
||||
|
||||
def get_art_id(self):
|
||||
"""Return unique article storage name, create if needed"""
|
||||
if not self.art_id:
|
||||
self.art_id = get_new_id("article", self.nzf.nzo.admin_path)
|
||||
return self.art_id
|
||||
|
||||
def search_new_server(self):
|
||||
"""Search for a new server for this article"""
|
||||
# Since we need a new server, this one can be listed as failed
|
||||
sabnzbd.BPSMeter.register_server_article_failed(self.fetcher.id)
|
||||
self.add_to_try_list(self.fetcher)
|
||||
# Servers-list could be modified during iteration, so we need a copy
|
||||
for server in sabnzbd.Downloader.servers[:]:
|
||||
if server.active and not self.server_in_try_list(server):
|
||||
if server.priority >= self.fetcher.priority:
|
||||
self.tries = 0
|
||||
# Allow all servers for this nzo and nzf again (but not this fetcher for this article)
|
||||
self.allow_new_fetcher(remove_fetcher_from_try_list=False)
|
||||
return True
|
||||
|
||||
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_ = {}
|
||||
for item in ArticleSaver:
|
||||
dict_[item] = getattr(self, item)
|
||||
dict_["try_list"] = super().__getstate__()
|
||||
return dict_
|
||||
|
||||
def __setstate__(self, dict_):
|
||||
"""Load from pickle file, selecting attributes"""
|
||||
for item in ArticleSaver:
|
||||
try:
|
||||
setattr(self, item, dict_[item])
|
||||
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
|
||||
self.tries = 0
|
||||
|
||||
def __repr__(self):
|
||||
return "<Article: article=%s, bytes=%s, art_id=%s>" % (self.article, self.bytes, self.art_id)
|
||||
387
sabnzbd/nzb/file.py
Normal file
387
sabnzbd/nzb/file.py
Normal file
@@ -0,0 +1,387 @@
|
||||
#!/usr/bin/python3 -OO
|
||||
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
"""
|
||||
sabnzbd.nzb.file - NzbFile class for representing files in NZB downloads
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from typing import Optional, Any
|
||||
|
||||
import sabctools
|
||||
from sabnzbd.nzb.article import TryList, Article
|
||||
from sabnzbd.downloader import Server
|
||||
from sabnzbd.filesystem import (
|
||||
sanitize_filename,
|
||||
get_unique_filename,
|
||||
get_filename,
|
||||
remove_file,
|
||||
get_new_id,
|
||||
save_data,
|
||||
load_data,
|
||||
RAR_RE,
|
||||
)
|
||||
from sabnzbd.misc import int_conv, subject_name_extractor
|
||||
from sabnzbd.decorators import synchronized
|
||||
|
||||
|
||||
##############################################################################
|
||||
# NzbFile
|
||||
##############################################################################
|
||||
class SkippedNzbFile(Exception):
|
||||
pass
|
||||
|
||||
|
||||
NzbFileSaver = (
|
||||
"date",
|
||||
"filename",
|
||||
"filename_checked",
|
||||
"filepath",
|
||||
"type",
|
||||
"is_par2",
|
||||
"vol",
|
||||
"blocks",
|
||||
"setname",
|
||||
"articles",
|
||||
"decodetable",
|
||||
"bytes",
|
||||
"bytes_left",
|
||||
"nzo",
|
||||
"nzf_id",
|
||||
"deleted",
|
||||
"import_finished",
|
||||
"crc32",
|
||||
"assembled",
|
||||
"md5of16k",
|
||||
)
|
||||
|
||||
|
||||
class NzbFile(TryList):
|
||||
"""Representation of one file consisting of multiple articles"""
|
||||
|
||||
# Pre-define attributes to save memory
|
||||
__slots__ = NzbFileSaver + ("lock", "file_lock", "assembler_next_index")
|
||||
|
||||
def __init__(self, date, subject, raw_article_db, file_bytes, nzo):
|
||||
"""Setup object"""
|
||||
super().__init__()
|
||||
self.lock: threading.RLock = threading.RLock()
|
||||
self.file_lock: threading.RLock = threading.RLock()
|
||||
|
||||
self.date: datetime.datetime = date
|
||||
self.type: Optional[str] = None
|
||||
self.filename: str = sanitize_filename(subject_name_extractor(subject))
|
||||
self.filename_checked = False
|
||||
self.filepath: Optional[str] = None
|
||||
|
||||
# Identifiers for par2 files
|
||||
self.is_par2: bool = False
|
||||
self.vol: Optional[int] = None
|
||||
self.blocks: Optional[int] = None
|
||||
self.setname: Optional[str] = None
|
||||
|
||||
# Articles are removed from "articles" after being fetched
|
||||
self.articles: dict[Article, Article] = {}
|
||||
self.decodetable: list[Article] = []
|
||||
|
||||
self.bytes: int = file_bytes
|
||||
self.bytes_left: int = file_bytes
|
||||
|
||||
self.nzo = nzo # NzbObject reference
|
||||
self.deleted = False
|
||||
self.import_finished = False
|
||||
|
||||
self.crc32: Optional[int] = 0
|
||||
self.assembled: bool = False
|
||||
self.md5of16k: Optional[bytes] = None
|
||||
self.assembler_next_index: int = 0
|
||||
|
||||
# Add first article to decodetable, this way we can check
|
||||
# if this is maybe a duplicate nzf
|
||||
if raw_article_db:
|
||||
first_article = self.add_article(raw_article_db.pop(0))
|
||||
first_article.lowest_partnum = True
|
||||
|
||||
if self in nzo.files:
|
||||
logging.info("File %s occurred twice in NZB, skipping", self.filename)
|
||||
raise SkippedNzbFile
|
||||
|
||||
# Create file on disk, which can fail in case of disk errors
|
||||
self.nzf_id: str = get_new_id("nzf", nzo.admin_path)
|
||||
if not self.nzf_id:
|
||||
# Error already shown to user from get_new_id
|
||||
raise SkippedNzbFile
|
||||
|
||||
# Any articles left?
|
||||
if raw_article_db:
|
||||
# Save the rest
|
||||
save_data(raw_article_db, self.nzf_id, nzo.admin_path)
|
||||
else:
|
||||
# All imported
|
||||
self.import_finished = True
|
||||
|
||||
@property
|
||||
@synchronized()
|
||||
def assembler_next_article(self) -> Optional[Article]:
|
||||
if (next_index := self.assembler_next_index) < len(self.decodetable):
|
||||
return self.decodetable[next_index]
|
||||
return None
|
||||
|
||||
def finish_import(self):
|
||||
"""Load the article objects from disk"""
|
||||
logging.debug("Finishing import on %s", self.filename)
|
||||
if raw_article_db := load_data(self.nzf_id, self.nzo.admin_path, remove=False):
|
||||
for raw_article in raw_article_db:
|
||||
self.add_article(raw_article)
|
||||
|
||||
# Make sure we have labeled the lowest part number
|
||||
# Also when DirectUnpack is disabled we need to know
|
||||
self.decodetable[0].lowest_partnum = True
|
||||
|
||||
# Mark safe to continue
|
||||
self.import_finished = True
|
||||
|
||||
@synchronized()
|
||||
def add_article(self, article_info):
|
||||
"""Add article to object database and return article object"""
|
||||
article = Article(article_info[0], article_info[1], self)
|
||||
self.articles[article] = article
|
||||
self.decodetable.append(article)
|
||||
return article
|
||||
|
||||
@synchronized()
|
||||
def remove_article(self, article: Article, success: bool) -> int:
|
||||
"""Handle completed article, possibly end of file"""
|
||||
if self.articles.pop(article, None) is not None:
|
||||
if success:
|
||||
self.bytes_left -= article.bytes
|
||||
return len(self.articles)
|
||||
|
||||
def set_par2(self, setname, vol, blocks):
|
||||
"""Designate this file as a par2 file"""
|
||||
self.is_par2 = True
|
||||
self.setname = setname
|
||||
self.vol = vol
|
||||
self.blocks = int_conv(blocks)
|
||||
|
||||
@synchronized()
|
||||
def update_crc32(self, crc32: Optional[int], length: int) -> None:
|
||||
if self.crc32 is None or crc32 is None:
|
||||
self.crc32 = None
|
||||
else:
|
||||
self.crc32 = sabctools.crc32_combine(self.crc32, crc32, length)
|
||||
|
||||
@synchronized()
|
||||
def get_articles(self, server: Server, servers: list[Server], fetch_limit: int):
|
||||
"""Get next articles to be downloaded"""
|
||||
articles = server.article_queue
|
||||
for article in self.articles:
|
||||
if article := article.get_article(server, servers):
|
||||
articles.append(article)
|
||||
if len(articles) >= fetch_limit:
|
||||
return
|
||||
self.add_to_try_list(server)
|
||||
|
||||
@synchronized()
|
||||
def reset_all_try_lists(self):
|
||||
"""Reset all try lists. Locked so reset is performed
|
||||
for all items at the same time without chance of another
|
||||
thread changing any of the items while we are resetting"""
|
||||
for art in self.articles:
|
||||
art.reset_try_list()
|
||||
self.reset_try_list()
|
||||
|
||||
def first_article_processed(self) -> bool:
|
||||
"""Check if the first article has been processed.
|
||||
This ensures we have attempted to extract md5of16k and filename information
|
||||
before creating the filepath.
|
||||
"""
|
||||
# The first article of decodetable is always the lowest
|
||||
first_article = self.decodetable[0]
|
||||
# If it's still in nzo.first_articles, it hasn't been processed yet
|
||||
return first_article not in self.nzo.first_articles
|
||||
|
||||
def prepare_filepath(self):
|
||||
"""Do all checks before making the final path"""
|
||||
if not self.filepath:
|
||||
# Wait for the first article to be processed so we can get md5of16k
|
||||
# and proper filename before creating the filepath
|
||||
if not self.first_article_processed():
|
||||
return None
|
||||
|
||||
self.nzo.verify_nzf_filename(self)
|
||||
filename = sanitize_filename(self.filename)
|
||||
self.filepath = get_unique_filename(os.path.join(self.nzo.download_path, filename))
|
||||
self.filename = get_filename(self.filepath)
|
||||
return self.filepath
|
||||
|
||||
@property
|
||||
def completed(self):
|
||||
"""Is this file completed?"""
|
||||
if not self.import_finished:
|
||||
return False
|
||||
with self.lock:
|
||||
return not self.articles
|
||||
|
||||
def remove_admin(self):
|
||||
"""Remove article database from disk (sabnzbd_nzf_<id>)"""
|
||||
try:
|
||||
logging.debug("Removing article database for %s", self.nzf_id)
|
||||
remove_file(os.path.join(self.nzo.admin_path, self.nzf_id))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@synchronized()
|
||||
def contiguous_offset(self) -> int:
|
||||
"""The next file offset to write to continue sequentially.
|
||||
|
||||
Note: there could be non-sequential direct writes already beyond this point
|
||||
"""
|
||||
# If last written article has valid yenc headers
|
||||
if self.assembler_next_index:
|
||||
article = self.decodetable[self.assembler_next_index - 1]
|
||||
if article.on_disk and article.data_size:
|
||||
return article.data_begin + article.data_size
|
||||
|
||||
# Fallback to summing decoded size
|
||||
offset = 0
|
||||
for article in self.decodetable[: self.assembler_next_index]:
|
||||
if not article.on_disk:
|
||||
break
|
||||
if article.data_size:
|
||||
offset = article.data_begin + article.decoded_size
|
||||
elif article.decoded_size is not None:
|
||||
# queues from <= 4.5.5 do not have this attribute
|
||||
offset += article.decoded_size
|
||||
elif os.path.exists(self.filepath):
|
||||
# fallback for <= 4.5.5 because files were always opened in append mode, so use the file size
|
||||
return os.path.getsize(self.filepath)
|
||||
return offset
|
||||
|
||||
@synchronized()
|
||||
def contiguous_ready_bytes(self) -> int:
|
||||
"""How many bytes from assembler_next_index onward are ready to write to file contiguously?"""
|
||||
bytes_ready: int = 0
|
||||
for article in self.decodetable[self.assembler_next_index :]:
|
||||
if not article.decoded:
|
||||
break
|
||||
if article.on_disk:
|
||||
continue
|
||||
if article.decoded_size is None:
|
||||
break
|
||||
bytes_ready += article.decoded_size
|
||||
return bytes_ready
|
||||
|
||||
def sort_key(self) -> tuple[Any, ...]:
|
||||
"""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.
|
||||
"""
|
||||
name = self.filename.lower()
|
||||
base, ext = os.path.splitext(name)
|
||||
|
||||
is_par2 = ext == ".par2"
|
||||
is_vol_par2 = is_par2 and ".vol" in base
|
||||
is_mini_par2 = is_par2 and not is_vol_par2
|
||||
|
||||
m = RAR_RE.search(name)
|
||||
is_rar = bool(m)
|
||||
is_main_rar = is_rar and m.group(1) == "rar"
|
||||
|
||||
# Initially group by mini-par2, other files, vol-par2
|
||||
if is_mini_par2:
|
||||
tier = 0
|
||||
elif is_vol_par2:
|
||||
tier = 2
|
||||
else:
|
||||
tier = 1
|
||||
|
||||
if tier == 1:
|
||||
if is_rar and m:
|
||||
# strip matched RAR suffix including leading dot (.part01.rar, .rar, .r00, ...)
|
||||
group_base = name[: m.start()]
|
||||
local_group = 0
|
||||
type_rank = 0 if is_main_rar else 1
|
||||
else:
|
||||
# nfo, sfv, sample.mkv, etc.
|
||||
group_base = base
|
||||
local_group = 1
|
||||
type_rank = 0
|
||||
else:
|
||||
# mini/vol par2 ignore the group base
|
||||
group_base = ""
|
||||
local_group = 0
|
||||
type_rank = 0
|
||||
|
||||
return tier, group_base, local_group, type_rank, name
|
||||
|
||||
def __getstate__(self):
|
||||
"""Save to pickle file, selecting attributes"""
|
||||
dict_ = {}
|
||||
for item in NzbFileSaver:
|
||||
dict_[item] = getattr(self, item)
|
||||
dict_["try_list"] = super().__getstate__()
|
||||
return dict_
|
||||
|
||||
def __setstate__(self, dict_):
|
||||
"""Load from pickle file, selecting attributes"""
|
||||
for item in NzbFileSaver:
|
||||
try:
|
||||
setattr(self, item, dict_[item])
|
||||
except KeyError:
|
||||
# Handle new attributes
|
||||
setattr(self, item, None)
|
||||
self.lock = threading.RLock()
|
||||
self.file_lock = threading.RLock()
|
||||
self.assembler_next_index = 0
|
||||
if isinstance(self.articles, list):
|
||||
# Converted from list to dict
|
||||
self.articles = {x: x for x in self.articles}
|
||||
for article in self.articles:
|
||||
article.lock = self.lock
|
||||
super().__setstate__(dict_.get("try_list", []))
|
||||
|
||||
def __lt__(self, other: "NzbFile"):
|
||||
return self.sort_key() < other.sort_key()
|
||||
|
||||
def __eq__(self, other: "NzbFile"):
|
||||
"""Assume it's the same file if the number bytes and first article
|
||||
are the same or if there are no articles left, use the filenames.
|
||||
Some NZB's are just a mess and report different sizes for the same article.
|
||||
We used to compare (__eq__) articles based on article-ID, however, this failed
|
||||
because some NZB's had the same article-ID twice within one NZF.
|
||||
"""
|
||||
if other and (self.bytes == other.bytes or len(self.decodetable) == len(other.decodetable)):
|
||||
if self.decodetable and other.decodetable:
|
||||
return self.decodetable[0].article == other.decodetable[0].article
|
||||
# Fallback to filename comparison
|
||||
return self.filename == other.filename
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
"""Required because we implement eq. The same file can be spread
|
||||
over multiple NZO's so we make every NZF unique. Even though
|
||||
it's considered bad practice.
|
||||
"""
|
||||
return id(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "<NzbFile: filename=%s, bytes=%s, nzf_id=%s>" % (self.filename, self.bytes, self.nzf_id)
|
||||
@@ -16,21 +16,22 @@
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
"""
|
||||
sabnzbd.nzbstuff - misc
|
||||
sabnzbd.nzb.object - NzbObject class for representing NZB download jobs
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import logging
|
||||
import datetime
|
||||
import threading
|
||||
import functools
|
||||
import difflib
|
||||
from typing import Any, Optional, Union, BinaryIO, Deque
|
||||
|
||||
# SABnzbd modules
|
||||
import sabnzbd
|
||||
import sabctools
|
||||
from sabnzbd.nzb.article import TryList, Article
|
||||
from sabnzbd.nzb.file import NzbFile
|
||||
from sabnzbd.constants import (
|
||||
GIGI,
|
||||
ATTRIB_FILE,
|
||||
@@ -60,6 +61,8 @@ from sabnzbd.misc import (
|
||||
opts_to_pp,
|
||||
pp_to_opts,
|
||||
duplicate_warning,
|
||||
scan_password,
|
||||
subject_name_extractor,
|
||||
)
|
||||
from sabnzbd.filesystem import (
|
||||
sanitize_foldername,
|
||||
@@ -89,463 +92,18 @@ from sabnzbd.filesystem import (
|
||||
remove_data,
|
||||
strip_extensions,
|
||||
get_ext,
|
||||
create_work_name,
|
||||
RAR_RE,
|
||||
)
|
||||
from sabnzbd.par2file import FilePar2Info, has_par2_in_filename, analyse_par2, parse_par2_file, is_par2_file
|
||||
from sabnzbd.decorators import synchronized
|
||||
import sabnzbd.config as config
|
||||
import sabnzbd.cfg as cfg
|
||||
import sabnzbd.nzbparser
|
||||
from sabnzbd.downloader import Server
|
||||
from sabnzbd.database import HistoryDB
|
||||
from sabnzbd.deobfuscate_filenames import is_probably_obfuscated
|
||||
|
||||
# Name patterns
|
||||
# In the subject, we expect the filename within double quotes
|
||||
RE_SUBJECT_FILENAME_QUOTES = re.compile(r'"([^"]*)"')
|
||||
# Otherwise something that looks like a filename
|
||||
RE_SUBJECT_BASIC_FILENAME = re.compile(r"\b([\w\-+()' .,]+(?:\[[\w\-/+()' .,]*][\w\-+()' .,]*)*\.[A-Za-z0-9]{2,4})\b")
|
||||
RE_RAR = re.compile(r"(\.rar|\.r\d\d|\.s\d\d|\.t\d\d|\.u\d\d|\.v\d\d)$", re.I)
|
||||
|
||||
|
||||
##############################################################################
|
||||
# Trylist
|
||||
##############################################################################
|
||||
|
||||
TRYLIST_LOCK = threading.RLock()
|
||||
|
||||
|
||||
class TryList:
|
||||
"""TryList keeps track of which servers have been tried for a specific article"""
|
||||
|
||||
# Pre-define attributes to save memory
|
||||
__slots__ = ("try_list",)
|
||||
|
||||
def __init__(self):
|
||||
# Sets are faster than lists
|
||||
self.try_list: set[Server] = set()
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def reset_try_list(self):
|
||||
"""Clean the list"""
|
||||
with TRYLIST_LOCK:
|
||||
self.try_list = set()
|
||||
|
||||
def __getstate__(self):
|
||||
"""Save the servers"""
|
||||
return set(server.id for server in self.try_list)
|
||||
|
||||
def __setstate__(self, servers_ids: list[str]):
|
||||
self.try_list = set()
|
||||
for server in sabnzbd.Downloader.servers:
|
||||
if server.id in servers_ids:
|
||||
self.add_to_try_list(server)
|
||||
|
||||
|
||||
##############################################################################
|
||||
# Article
|
||||
##############################################################################
|
||||
ArticleSaver = (
|
||||
"article",
|
||||
"art_id",
|
||||
"bytes",
|
||||
"lowest_partnum",
|
||||
"decoded",
|
||||
"file_size",
|
||||
"data_begin",
|
||||
"data_size",
|
||||
"on_disk",
|
||||
"nzf",
|
||||
"crc32",
|
||||
)
|
||||
|
||||
|
||||
class Article(TryList):
|
||||
"""Representation of one article"""
|
||||
|
||||
# Pre-define attributes to save memory
|
||||
__slots__ = ArticleSaver + ("fetcher", "fetcher_priority", "tries")
|
||||
|
||||
def __init__(self, article, article_bytes, nzf):
|
||||
super().__init__()
|
||||
self.article: str = article
|
||||
self.art_id: Optional[str] = None
|
||||
self.bytes: int = article_bytes
|
||||
self.lowest_partnum: bool = False
|
||||
self.fetcher: Optional[Server] = None
|
||||
self.fetcher_priority: int = 0
|
||||
self.tries: int = 0 # Try count
|
||||
self.decoded: bool = False
|
||||
self.file_size: Optional[int] = None
|
||||
self.data_begin: Optional[int] = None
|
||||
self.data_size: Optional[int] = None
|
||||
self.on_disk: bool = False
|
||||
self.crc32: Optional[int] = None
|
||||
self.nzf: NzbFile = nzf
|
||||
|
||||
@synchronized(TRYLIST_LOCK)
|
||||
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."""
|
||||
self.fetcher = None
|
||||
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()
|
||||
|
||||
def get_article(self, server: Server, servers: list[Server]):
|
||||
"""Return article when appropriate for specified server"""
|
||||
if self.fetcher or self.server_in_try_list(server):
|
||||
return None
|
||||
|
||||
if server.priority > self.fetcher_priority:
|
||||
# Check for higher priority server, taking advantage of servers list being sorted by priority
|
||||
for server_check in servers:
|
||||
if server_check.priority < server.priority:
|
||||
if server_check.active and not self.server_in_try_list(server_check):
|
||||
# There is a higher priority server, so set article priority and return
|
||||
self.fetcher_priority = server_check.priority
|
||||
return None
|
||||
else:
|
||||
# All servers with a higher priority have been checked
|
||||
break
|
||||
|
||||
# If no higher priority servers, use this server
|
||||
self.fetcher_priority = server.priority
|
||||
self.fetcher = server
|
||||
self.tries += 1
|
||||
return self
|
||||
|
||||
def get_art_id(self):
|
||||
"""Return unique article storage name, create if needed"""
|
||||
if not self.art_id:
|
||||
self.art_id = get_new_id("article", self.nzf.nzo.admin_path)
|
||||
return self.art_id
|
||||
|
||||
def search_new_server(self):
|
||||
"""Search for a new server for this article"""
|
||||
# Since we need a new server, this one can be listed as failed
|
||||
sabnzbd.BPSMeter.register_server_article_failed(self.fetcher.id)
|
||||
self.add_to_try_list(self.fetcher)
|
||||
# Servers-list could be modified during iteration, so we need a copy
|
||||
for server in sabnzbd.Downloader.servers[:]:
|
||||
if server.active and not self.server_in_try_list(server):
|
||||
if server.priority >= self.fetcher.priority:
|
||||
self.tries = 0
|
||||
# Allow all servers for this nzo and nzf again (but not this fetcher for this article)
|
||||
self.allow_new_fetcher(remove_fetcher_from_try_list=False)
|
||||
return True
|
||||
|
||||
logging.info("Article %s unavailable on all servers, discarding", self.article)
|
||||
return False
|
||||
|
||||
def __getstate__(self):
|
||||
"""Save to pickle file, selecting attributes"""
|
||||
dict_ = {}
|
||||
for item in ArticleSaver:
|
||||
dict_[item] = getattr(self, item)
|
||||
dict_["try_list"] = super().__getstate__()
|
||||
return dict_
|
||||
|
||||
def __setstate__(self, dict_):
|
||||
"""Load from pickle file, selecting attributes"""
|
||||
for item in ArticleSaver:
|
||||
try:
|
||||
setattr(self, item, dict_[item])
|
||||
except KeyError:
|
||||
# Handle new attributes
|
||||
setattr(self, item, None)
|
||||
super().__setstate__(dict_.get("try_list", []))
|
||||
self.fetcher = None
|
||||
self.fetcher_priority = 0
|
||||
self.tries = 0
|
||||
|
||||
def __repr__(self):
|
||||
return "<Article: article=%s, bytes=%s, art_id=%s>" % (self.article, self.bytes, self.art_id)
|
||||
|
||||
|
||||
##############################################################################
|
||||
# NzbFile
|
||||
##############################################################################
|
||||
class SkippedNzbFile(Exception):
|
||||
pass
|
||||
|
||||
|
||||
NzbFileSaver = (
|
||||
"date",
|
||||
"filename",
|
||||
"filename_checked",
|
||||
"filepath",
|
||||
"type",
|
||||
"is_par2",
|
||||
"vol",
|
||||
"blocks",
|
||||
"setname",
|
||||
"articles",
|
||||
"decodetable",
|
||||
"bytes",
|
||||
"bytes_left",
|
||||
"nzo",
|
||||
"nzf_id",
|
||||
"deleted",
|
||||
"import_finished",
|
||||
"crc32",
|
||||
"assembled",
|
||||
"md5of16k",
|
||||
)
|
||||
|
||||
|
||||
class NzbFile(TryList):
|
||||
"""Representation of one file consisting of multiple articles"""
|
||||
|
||||
# Pre-define attributes to save memory
|
||||
__slots__ = NzbFileSaver + ("lock",)
|
||||
|
||||
def __init__(self, date, subject, raw_article_db, file_bytes, nzo):
|
||||
"""Setup object"""
|
||||
super().__init__()
|
||||
self.lock = threading.RLock()
|
||||
|
||||
self.date: datetime.datetime = date
|
||||
self.type: Optional[str] = None
|
||||
self.filename: str = sanitize_filename(name_extractor(subject))
|
||||
self.filename_checked = False
|
||||
self.filepath: Optional[str] = None
|
||||
|
||||
# Identifiers for par2 files
|
||||
self.is_par2: bool = False
|
||||
self.vol: Optional[int] = None
|
||||
self.blocks: Optional[int] = None
|
||||
self.setname: Optional[str] = None
|
||||
|
||||
# Articles are removed from "articles" after being fetched
|
||||
self.articles: dict[Article, Article] = {}
|
||||
self.decodetable: list[Article] = []
|
||||
|
||||
self.bytes: int = file_bytes
|
||||
self.bytes_left: int = file_bytes
|
||||
|
||||
self.nzo: NzbObject = nzo
|
||||
self.deleted = False
|
||||
self.import_finished = False
|
||||
|
||||
self.crc32: Optional[int] = 0
|
||||
self.assembled: bool = False
|
||||
self.md5of16k: Optional[bytes] = None
|
||||
|
||||
# Add first article to decodetable, this way we can check
|
||||
# if this is maybe a duplicate nzf
|
||||
if raw_article_db:
|
||||
first_article = self.add_article(raw_article_db.pop(0))
|
||||
first_article.lowest_partnum = True
|
||||
|
||||
if self in nzo.files:
|
||||
logging.info("File %s occurred twice in NZB, skipping", self.filename)
|
||||
raise SkippedNzbFile
|
||||
|
||||
# Create file on disk, which can fail in case of disk errors
|
||||
self.nzf_id: str = get_new_id("nzf", nzo.admin_path)
|
||||
if not self.nzf_id:
|
||||
# Error already shown to user from get_new_id
|
||||
raise SkippedNzbFile
|
||||
|
||||
# Any articles left?
|
||||
if raw_article_db:
|
||||
# Save the rest
|
||||
save_data(raw_article_db, self.nzf_id, nzo.admin_path)
|
||||
else:
|
||||
# All imported
|
||||
self.import_finished = True
|
||||
|
||||
def finish_import(self):
|
||||
"""Load the article objects from disk"""
|
||||
logging.debug("Finishing import on %s", self.filename)
|
||||
if raw_article_db := load_data(self.nzf_id, self.nzo.admin_path, remove=False):
|
||||
for raw_article in raw_article_db:
|
||||
self.add_article(raw_article)
|
||||
|
||||
# Make sure we have labeled the lowest part number
|
||||
# Also when DirectUnpack is disabled we need to know
|
||||
self.decodetable[0].lowest_partnum = True
|
||||
|
||||
# Mark safe to continue
|
||||
self.import_finished = True
|
||||
|
||||
def add_article(self, article_info):
|
||||
"""Add article to object database and return article object"""
|
||||
article = Article(article_info[0], article_info[1], self)
|
||||
with self.lock:
|
||||
self.articles[article] = article
|
||||
self.decodetable.append(article)
|
||||
return article
|
||||
|
||||
def remove_article(self, article: Article, success: bool) -> int:
|
||||
"""Handle completed article, possibly end of file"""
|
||||
with self.lock:
|
||||
if self.articles.pop(article, None) is not None:
|
||||
if success:
|
||||
self.bytes_left -= article.bytes
|
||||
return len(self.articles)
|
||||
|
||||
def set_par2(self, setname, vol, blocks):
|
||||
"""Designate this file as a par2 file"""
|
||||
self.is_par2 = True
|
||||
self.setname = setname
|
||||
self.vol = vol
|
||||
self.blocks = int_conv(blocks)
|
||||
|
||||
def update_crc32(self, crc32: Optional[int], length: int) -> None:
|
||||
if self.crc32 is None or crc32 is None:
|
||||
self.crc32 = None
|
||||
else:
|
||||
self.crc32 = sabctools.crc32_combine(self.crc32, crc32, length)
|
||||
|
||||
def get_articles(self, server: Server, servers: list[Server], fetch_limit: int):
|
||||
"""Get next articles to be downloaded"""
|
||||
articles = server.article_queue
|
||||
with self.lock:
|
||||
for article in self.articles:
|
||||
if article := article.get_article(server, servers):
|
||||
articles.append(article)
|
||||
if len(articles) >= fetch_limit:
|
||||
return
|
||||
self.add_to_try_list(server)
|
||||
|
||||
@synchronized(TRYLIST_LOCK)
|
||||
def reset_all_try_lists(self):
|
||||
"""Reset all try lists. Locked so reset is performed
|
||||
for all items at the same time without chance of another
|
||||
thread changing any of the items while we are resetting"""
|
||||
with self.lock:
|
||||
for art in self.articles:
|
||||
art.reset_try_list()
|
||||
self.reset_try_list()
|
||||
|
||||
def first_article_processed(self) -> bool:
|
||||
"""Check if the first article has been processed.
|
||||
This ensures we have attempted to extract md5of16k and filename information
|
||||
before creating the filepath.
|
||||
"""
|
||||
# The first article of decodetable is always the lowest
|
||||
first_article = self.decodetable[0]
|
||||
# If it's still in nzo.first_articles, it hasn't been processed yet
|
||||
return first_article not in self.nzo.first_articles
|
||||
|
||||
def prepare_filepath(self):
|
||||
"""Do all checks before making the final path"""
|
||||
if not self.filepath:
|
||||
# Wait for the first article to be processed so we can get md5of16k
|
||||
# and proper filename before creating the filepath
|
||||
if not self.first_article_processed():
|
||||
return None
|
||||
|
||||
self.nzo.verify_nzf_filename(self)
|
||||
filename = sanitize_filename(self.filename)
|
||||
self.filepath = get_unique_filename(os.path.join(self.nzo.download_path, filename))
|
||||
self.filename = get_filename(self.filepath)
|
||||
return self.filepath
|
||||
|
||||
@property
|
||||
def completed(self):
|
||||
"""Is this file completed?"""
|
||||
if not self.import_finished:
|
||||
return False
|
||||
with self.lock:
|
||||
return not self.articles
|
||||
|
||||
def remove_admin(self):
|
||||
"""Remove article database from disk (sabnzbd_nzf_<id>)"""
|
||||
try:
|
||||
logging.debug("Removing article database for %s", self.nzf_id)
|
||||
remove_file(os.path.join(self.nzo.admin_path, self.nzf_id))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
self.lock.acquire()
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.lock.release()
|
||||
|
||||
def __getstate__(self):
|
||||
"""Save to pickle file, selecting attributes"""
|
||||
dict_ = {}
|
||||
for item in NzbFileSaver:
|
||||
dict_[item] = getattr(self, item)
|
||||
dict_["try_list"] = super().__getstate__()
|
||||
return dict_
|
||||
|
||||
def __setstate__(self, dict_):
|
||||
"""Load from pickle file, selecting attributes"""
|
||||
for item in NzbFileSaver:
|
||||
try:
|
||||
setattr(self, item, dict_[item])
|
||||
except KeyError:
|
||||
# Handle new attributes
|
||||
setattr(self, item, None)
|
||||
super().__setstate__(dict_.get("try_list", []))
|
||||
self.lock = threading.RLock()
|
||||
if isinstance(self.articles, list):
|
||||
# Converted from list to dict
|
||||
self.articles = {x: x for x in self.articles}
|
||||
|
||||
def __eq__(self, other: "NzbFile"):
|
||||
"""Assume it's the same file if the number bytes and first article
|
||||
are the same or if there are no articles left, use the filenames.
|
||||
Some NZB's are just a mess and report different sizes for the same article.
|
||||
We used to compare (__eq__) articles based on article-ID, however, this failed
|
||||
because some NZB's had the same article-ID twice within one NZF.
|
||||
"""
|
||||
if other and (self.bytes == other.bytes or len(self.decodetable) == len(other.decodetable)):
|
||||
if self.decodetable and other.decodetable:
|
||||
return self.decodetable[0].article == other.decodetable[0].article
|
||||
# Fallback to filename comparison
|
||||
return self.filename == other.filename
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
"""Required because we implement eq. The same file can be spread
|
||||
over multiple NZO's so we make every NZF unique. Even though
|
||||
it's considered bad practice.
|
||||
"""
|
||||
return id(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "<NzbFile: filename=%s, bytes=%s, nzf_id=%s>" % (self.filename, self.bytes, self.nzf_id)
|
||||
|
||||
|
||||
##############################################################################
|
||||
# NzbObject
|
||||
##############################################################################
|
||||
class NzbEmpty(Exception):
|
||||
pass
|
||||
|
||||
@@ -627,9 +185,6 @@ NzbObjectSaver = (
|
||||
|
||||
NzoAttributeSaver = ("cat", "pp", "script", "priority", "final_name", "password", "url")
|
||||
|
||||
# Lock to prevent errors when saving the NZO data
|
||||
NZO_LOCK = threading.RLock()
|
||||
|
||||
|
||||
class NzbObject(TryList):
|
||||
def __init__(
|
||||
@@ -651,6 +206,7 @@ class NzbObject(TryList):
|
||||
dup_check: bool = True,
|
||||
):
|
||||
super().__init__()
|
||||
self.lock: threading.RLock = threading.RLock()
|
||||
# Use original filename as basis
|
||||
self.work_name = self.filename = filename
|
||||
|
||||
@@ -1008,7 +564,7 @@ class NzbObject(TryList):
|
||||
|
||||
logging.info("File %s added to queue", nzf.filename)
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def remove_nzf(self, nzf: NzbFile) -> bool:
|
||||
if nzf in self.files:
|
||||
self.files.remove(nzf)
|
||||
@@ -1022,7 +578,7 @@ class NzbObject(TryList):
|
||||
"""Sort the files in the NZO based on name and type
|
||||
and then optimize for unwanted extensions search.
|
||||
"""
|
||||
self.files.sort(key=functools.cmp_to_key(nzf_cmp_name))
|
||||
self.files.sort()
|
||||
|
||||
# In the hunt for Unwanted Extensions:
|
||||
# The file with the unwanted extension often is in the first or the last rar file
|
||||
@@ -1032,7 +588,7 @@ class NzbObject(TryList):
|
||||
logging.debug("Unwanted Extension: putting last rar after first rar")
|
||||
firstrarpos = lastrarpos = 0
|
||||
for nzfposcounter, nzf in enumerate(self.files):
|
||||
if RE_RAR.search(nzf.filename.lower()):
|
||||
if RAR_RE.search(nzf.filename.lower()):
|
||||
# a NZF found with '.rar' in the name
|
||||
if firstrarpos == 0:
|
||||
# this is the first .rar found, so remember this position
|
||||
@@ -1051,7 +607,7 @@ class NzbObject(TryList):
|
||||
except Exception:
|
||||
logging.debug("The lastrar swap did not go well")
|
||||
|
||||
@synchronized(TRYLIST_LOCK)
|
||||
@synchronized()
|
||||
def reset_all_try_lists(self):
|
||||
"""Reset all try lists. Locked so reset is performed
|
||||
for all items at the same time without chance of another
|
||||
@@ -1060,7 +616,7 @@ class NzbObject(TryList):
|
||||
nzf.reset_all_try_lists()
|
||||
self.reset_try_list()
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def postpone_pars(self, parset: str):
|
||||
"""Move all vol-par files matching 'parset' to the extrapars table"""
|
||||
# Create new extrapars if it didn't already exist
|
||||
@@ -1091,7 +647,7 @@ class NzbObject(TryList):
|
||||
# Also re-parse all filenames in case par2 came after first articles
|
||||
self.verify_all_filenames_and_resort()
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def handle_par2(self, nzf: NzbFile, filepath):
|
||||
"""Check if file is a par2 and build up par2 collection"""
|
||||
# Need to remove it from the other set it might be in
|
||||
@@ -1143,7 +699,7 @@ class NzbObject(TryList):
|
||||
self.renamed_file(get_filename(new_fname), nzf.filename)
|
||||
nzf.filename = get_filename(new_fname)
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def promote_par2(self, nzf: NzbFile):
|
||||
"""In case of a broken par2 or missing par2, move another
|
||||
of the same set to the top (if we can find it)
|
||||
@@ -1204,7 +760,7 @@ class NzbObject(TryList):
|
||||
# Not enough
|
||||
return 0
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def remove_article(self, article: Article, success: bool):
|
||||
"""Remove article from the NzbFile and do check if it can succeed"""
|
||||
job_can_succeed = True
|
||||
@@ -1495,7 +1051,7 @@ class NzbObject(TryList):
|
||||
if self.duplicate:
|
||||
self.duplicate = DuplicateStatus.DUPLICATE_IGNORED
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def add_parfile(self, parfile: NzbFile) -> bool:
|
||||
"""Add parfile to the files to be downloaded
|
||||
Add it to the start so we try it first
|
||||
@@ -1510,7 +1066,7 @@ class NzbObject(TryList):
|
||||
return True
|
||||
return False
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def remove_extrapar(self, parfile: NzbFile):
|
||||
"""Remove par file from any/all sets"""
|
||||
for parset in list(self.extrapars):
|
||||
@@ -1521,7 +1077,7 @@ class NzbObject(TryList):
|
||||
if not self.extrapars[parset]:
|
||||
self.extrapars.pop(parset)
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def prospective_add(self, nzf: NzbFile):
|
||||
"""Add par2 files to compensate for missing articles"""
|
||||
# Get some blocks!
|
||||
@@ -1592,7 +1148,7 @@ class NzbObject(TryList):
|
||||
return False
|
||||
return True
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def set_download_report(self):
|
||||
"""Format the stats for the history information"""
|
||||
# Pretty-format the per-server stats
|
||||
@@ -1647,7 +1203,7 @@ class NzbObject(TryList):
|
||||
self.set_unpack_info("RSS", rss_feed, unique=True)
|
||||
self.set_unpack_info("Source", self.url or self.filename, unique=True)
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def increase_bad_articles_counter(self, bad_article_type: str):
|
||||
"""Record information about bad articles. Should be called before
|
||||
register_article, which triggers the availability check."""
|
||||
@@ -1710,7 +1266,7 @@ class NzbObject(TryList):
|
||||
# No articles for this server, block for next time
|
||||
self.add_to_try_list(server)
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def move_top_bulk(self, nzf_ids: list[str]):
|
||||
self.cleanup_nzf_ids(nzf_ids)
|
||||
if nzf_ids:
|
||||
@@ -1727,7 +1283,7 @@ class NzbObject(TryList):
|
||||
if target == keys:
|
||||
break
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def move_bottom_bulk(self, nzf_ids):
|
||||
self.cleanup_nzf_ids(nzf_ids)
|
||||
if nzf_ids:
|
||||
@@ -1744,7 +1300,7 @@ class NzbObject(TryList):
|
||||
if target == keys:
|
||||
break
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def move_up_bulk(self, nzf_ids, cleanup=True):
|
||||
if cleanup:
|
||||
self.cleanup_nzf_ids(nzf_ids)
|
||||
@@ -1761,7 +1317,7 @@ class NzbObject(TryList):
|
||||
self.files[pos - 1] = nzf
|
||||
self.files[pos] = tmp_nzf
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def move_down_bulk(self, nzf_ids, cleanup=True):
|
||||
if cleanup:
|
||||
self.cleanup_nzf_ids(nzf_ids)
|
||||
@@ -1814,7 +1370,7 @@ class NzbObject(TryList):
|
||||
self.renamed_file(yenc_filename, nzf.filename)
|
||||
nzf.filename = yenc_filename
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def verify_all_filenames_and_resort(self):
|
||||
"""Verify all filenames based on par2 info and then re-sort files.
|
||||
Locked so all files are verified at once without interruptions.
|
||||
@@ -1829,7 +1385,7 @@ class NzbObject(TryList):
|
||||
if self.direct_unpacker:
|
||||
self.direct_unpacker.set_volumes_for_nzo()
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def renamed_file(self, name_set, old_name=None):
|
||||
"""Save renames at various stages (Download/PP)
|
||||
to be used on Retry. Accepts strings and dicts.
|
||||
@@ -1857,7 +1413,7 @@ class NzbObject(TryList):
|
||||
"""Return remaining bytes"""
|
||||
return self.bytes - self.bytes_tried
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def purge_data(self, delete_all_data=True):
|
||||
"""Remove (all) job data"""
|
||||
logging.info(
|
||||
@@ -1869,6 +1425,7 @@ class NzbObject(TryList):
|
||||
|
||||
# Remove all cached files
|
||||
sabnzbd.ArticleCache.purge_articles(self.saved_articles)
|
||||
sabnzbd.Assembler.clear_ready_bytes(*self.files)
|
||||
|
||||
# Delete all, or just basic files
|
||||
if self.futuretype:
|
||||
@@ -1888,7 +1445,7 @@ class NzbObject(TryList):
|
||||
if nzf_id in self.files_table:
|
||||
return self.files_table[nzf_id]
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def set_unpack_info(self, key: str, msg: str, setname: Optional[str] = None, unique: bool = False):
|
||||
"""Builds a dictionary containing the stage name (key) and a message
|
||||
If unique is present, it will only have a single line message
|
||||
@@ -1916,7 +1473,7 @@ class NzbObject(TryList):
|
||||
# Make sure it's updated in the interface
|
||||
sabnzbd.misc.history_updated()
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def save_to_disk(self):
|
||||
"""Save job's admin to disk"""
|
||||
self.save_attribs()
|
||||
@@ -1953,7 +1510,7 @@ class NzbObject(TryList):
|
||||
# Rest is to be used directly in the NZO-init flow
|
||||
return attribs["cat"], attribs["pp"], attribs["script"]
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def build_pos_nzf_table(self, nzf_ids: list[str]) -> dict[int, NzbFile]:
|
||||
pos_nzf_table = {}
|
||||
for nzf_id in nzf_ids:
|
||||
@@ -1964,7 +1521,7 @@ class NzbObject(TryList):
|
||||
|
||||
return pos_nzf_table
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@synchronized()
|
||||
def cleanup_nzf_ids(self, nzf_ids: list[str]):
|
||||
for nzf_id in nzf_ids[:]:
|
||||
if nzf_id in self.files_table:
|
||||
@@ -2105,6 +1662,7 @@ class NzbObject(TryList):
|
||||
except KeyError:
|
||||
# Handle new attributes
|
||||
setattr(self, item, None)
|
||||
self.lock = threading.RLock()
|
||||
super().__setstate__(dict_.get("try_list", []))
|
||||
|
||||
# Set non-transferable values
|
||||
@@ -2134,109 +1692,3 @@ class NzbObject(TryList):
|
||||
|
||||
def __repr__(self):
|
||||
return "<NzbObject: filename=%s, bytes=%s, nzo_id=%s>" % (self.filename, self.bytes, self.nzo_id)
|
||||
|
||||
|
||||
def nzf_cmp_name(nzf1: NzbFile, nzf2: NzbFile):
|
||||
# The comparison will sort .par2 files to the top of the queue followed by .rar files,
|
||||
# they will then be sorted by name.
|
||||
nzf1_name = nzf1.filename.lower()
|
||||
nzf2_name = nzf2.filename.lower()
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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 = RE_RAR.search(nzf1_name)
|
||||
m2 = RE_RAR.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 cmp(nzf1_name, nzf2_name)
|
||||
|
||||
|
||||
def create_work_name(name: str) -> str:
|
||||
"""Remove ".nzb" and ".par(2)" and sanitize, skip URL's"""
|
||||
if name.find("://") < 0:
|
||||
# Invalid charters need to be removed before and after (see unit-tests)
|
||||
return sanitize_foldername(strip_extensions(sanitize_foldername(name)))
|
||||
else:
|
||||
return name.strip()
|
||||
|
||||
|
||||
def scan_password(name: str) -> tuple[str, Optional[str]]:
|
||||
"""Get password (if any) from the title"""
|
||||
if "http://" in name or "https://" in name:
|
||||
return name, None
|
||||
|
||||
# Strip any unwanted usenet-related extensions
|
||||
name = strip_extensions(name)
|
||||
|
||||
# Identify any braces
|
||||
braces = name[1:].find("{{")
|
||||
if braces < 0:
|
||||
braces = len(name)
|
||||
else:
|
||||
braces += 1
|
||||
slash = name.find("/")
|
||||
|
||||
# Look for name/password, but make sure that '/' comes before any {{
|
||||
if 0 < slash < braces and "password=" not in name:
|
||||
# Is it maybe in 'name / password' notation?
|
||||
if slash == name.find(" / ") + 1 and name[: slash - 1].strip(". "):
|
||||
# Remove the extra space after name and before password
|
||||
return name[: slash - 1].strip(". "), name[slash + 2 :]
|
||||
if name[:slash].strip(". "):
|
||||
return name[:slash].strip(". "), name[slash + 1 :]
|
||||
|
||||
# Look for "name password=password"
|
||||
pw = name.find("password=")
|
||||
if pw > 0 and name[:pw].strip(". "):
|
||||
return name[:pw].strip(". "), name[pw + 9 :]
|
||||
|
||||
# Look for name{{password}}
|
||||
if braces < len(name):
|
||||
closing_braces = name.rfind("}}")
|
||||
if closing_braces > braces and name[:braces].strip(". "):
|
||||
return name[:braces].strip(". "), name[braces + 2 : closing_braces]
|
||||
|
||||
# Look again for name/password
|
||||
if slash > 0 and name[:slash].strip(". "):
|
||||
return name[:slash].strip(". "), name[slash + 1 :]
|
||||
|
||||
# No password found
|
||||
return name, None
|
||||
|
||||
|
||||
def name_extractor(subject: str) -> str:
|
||||
"""Try to extract a file name from a subject line, return `subject` if in doubt"""
|
||||
# Filename nicely wrapped in quotes
|
||||
for name in re.findall(RE_SUBJECT_FILENAME_QUOTES, subject):
|
||||
if name := name.strip(' "'):
|
||||
return name
|
||||
|
||||
# Found nothing? Try a basic filename-like search
|
||||
for name in re.findall(RE_SUBJECT_BASIC_FILENAME, subject):
|
||||
if name := name.strip():
|
||||
return name
|
||||
|
||||
# Return the subject
|
||||
return subject
|
||||
@@ -18,6 +18,7 @@
|
||||
"""
|
||||
sabnzbd.nzbparser - Parse and import NZB files
|
||||
"""
|
||||
|
||||
import os
|
||||
import bz2
|
||||
import gzip
|
||||
@@ -33,7 +34,15 @@ import cherrypy._cpreqbody
|
||||
from typing import Optional, Any, Union
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd import nzbstuff
|
||||
from sabnzbd.nzb import (
|
||||
NzbObject,
|
||||
NzbEmpty,
|
||||
NzbRejected,
|
||||
NzbPreQueueRejected,
|
||||
NzbRejectToHistory,
|
||||
NzbFile,
|
||||
SkippedNzbFile,
|
||||
)
|
||||
from sabnzbd.encoding import utob, correct_cherrypy_encoding
|
||||
from sabnzbd.filesystem import (
|
||||
get_filename,
|
||||
@@ -204,7 +213,7 @@ def process_nzb_archive_file(
|
||||
if datap:
|
||||
nzo = None
|
||||
try:
|
||||
nzo = nzbstuff.NzbObject(
|
||||
nzo = NzbObject(
|
||||
name,
|
||||
pp=pp,
|
||||
script=script,
|
||||
@@ -220,13 +229,13 @@ def process_nzb_archive_file(
|
||||
dup_check=dup_check,
|
||||
)
|
||||
except (
|
||||
sabnzbd.nzbstuff.NzbEmpty,
|
||||
sabnzbd.nzbstuff.NzbRejected,
|
||||
sabnzbd.nzbstuff.NzbPreQueueRejected,
|
||||
NzbEmpty,
|
||||
NzbRejected,
|
||||
NzbPreQueueRejected,
|
||||
):
|
||||
# Empty or fully rejected (including pre-queue rejections)
|
||||
pass
|
||||
except sabnzbd.nzbstuff.NzbRejectToHistory as err:
|
||||
except NzbRejectToHistory as err:
|
||||
# Duplicate or unwanted extension directed to history
|
||||
sabnzbd.NzbQueue.fail_to_history(err.nzo)
|
||||
nzo_ids.append(err.nzo.nzo_id)
|
||||
@@ -315,7 +324,7 @@ def process_single_nzb(
|
||||
nzo = None
|
||||
nzo_ids = []
|
||||
try:
|
||||
nzo = nzbstuff.NzbObject(
|
||||
nzo = NzbObject(
|
||||
filename,
|
||||
pp=pp,
|
||||
script=script,
|
||||
@@ -330,16 +339,16 @@ def process_single_nzb(
|
||||
nzo_id=nzo_id,
|
||||
dup_check=dup_check,
|
||||
)
|
||||
except sabnzbd.nzbstuff.NzbEmpty:
|
||||
except NzbEmpty:
|
||||
# Malformed or might not be an NZB file
|
||||
result = AddNzbFileResult.NO_FILES_FOUND
|
||||
except sabnzbd.nzbstuff.NzbRejected:
|
||||
except NzbRejected:
|
||||
# Rejected as duplicate
|
||||
result = AddNzbFileResult.ERROR
|
||||
except sabnzbd.nzbstuff.NzbPreQueueRejected:
|
||||
except NzbPreQueueRejected:
|
||||
# Rejected by pre-queue script - should be silently ignored for URL fetches
|
||||
result = AddNzbFileResult.PREQUEUE_REJECTED
|
||||
except sabnzbd.nzbstuff.NzbRejectToHistory as err:
|
||||
except NzbRejectToHistory as err:
|
||||
# Duplicate or unwanted extension directed to history
|
||||
sabnzbd.NzbQueue.fail_to_history(err.nzo)
|
||||
nzo_ids.append(err.nzo.nzo_id)
|
||||
@@ -366,7 +375,7 @@ def process_single_nzb(
|
||||
|
||||
def nzbfile_parser(full_nzb_path: str, nzo):
|
||||
# For type-hinting
|
||||
nzo: sabnzbd.nzbstuff.NzbObject
|
||||
nzo: NzbObject
|
||||
|
||||
# Hash for dupe-checking
|
||||
md5sum = hashlib.md5()
|
||||
@@ -470,8 +479,8 @@ def nzbfile_parser(full_nzb_path: str, nzo):
|
||||
|
||||
# Create NZF
|
||||
try:
|
||||
nzf = sabnzbd.nzbstuff.NzbFile(file_date, file_name, raw_article_db_sorted, file_bytes, nzo)
|
||||
except sabnzbd.nzbstuff.SkippedNzbFile:
|
||||
nzf = NzbFile(file_date, file_name, raw_article_db_sorted, file_bytes, nzo)
|
||||
except SkippedNzbFile:
|
||||
# Did not meet requirements, so continue
|
||||
skipped_files += 1
|
||||
continue
|
||||
|
||||
@@ -26,7 +26,7 @@ import cherrypy._cpreqbody
|
||||
from typing import Union, Optional
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.nzbstuff import NzbObject, Article
|
||||
from sabnzbd.nzb import Article, NzbObject
|
||||
from sabnzbd.misc import exit_sab, cat_to_opts, int_conv, caller_name, safe_lower, duplicate_warning
|
||||
from sabnzbd.filesystem import get_admin_path, remove_all, globber_full, remove_file, is_valid_script
|
||||
from sabnzbd.nzbparser import process_single_nzb
|
||||
@@ -730,20 +730,16 @@ class NzbQueue:
|
||||
|
||||
articles_left, file_done, post_done = nzo.remove_article(article, success)
|
||||
|
||||
# Write data if file is done or at trigger time
|
||||
# Skip if the file is already queued, since all available articles will then be written
|
||||
if (
|
||||
file_done
|
||||
or (article.lowest_partnum and nzf.filename_checked and not nzf.import_finished)
|
||||
or (articles_left and (articles_left % sabnzbd.ArticleCache.assembler_write_trigger) == 0)
|
||||
):
|
||||
if not nzo.precheck:
|
||||
# The type is only set if sabctools could decode the article
|
||||
if nzf.type:
|
||||
sabnzbd.Assembler.process(nzo, nzf, file_done)
|
||||
elif sabnzbd.par2file.has_par2_in_filename(nzf.filename):
|
||||
# Broken par2 file, try to get another one
|
||||
nzo.promote_par2(nzf)
|
||||
if not nzo.precheck:
|
||||
# Mark as on_disk so assembler knows it can skip this article
|
||||
if not success:
|
||||
article.on_disk = True
|
||||
# The type is only set if sabctools could decode the article
|
||||
if nzf.type:
|
||||
sabnzbd.Assembler.process(nzo, nzf, file_done, article=article)
|
||||
elif sabnzbd.par2file.has_par2_in_filename(nzf.filename):
|
||||
# Broken par2 file, try to get another one
|
||||
nzo.promote_par2(nzf)
|
||||
|
||||
# Save bookkeeping in case of crash
|
||||
if file_done and (nzo.next_save is None or time.time() > nzo.next_save):
|
||||
@@ -783,6 +779,7 @@ class NzbQueue:
|
||||
if not nzo.nzo_id:
|
||||
self.add(nzo, quiet=True)
|
||||
self.remove(nzo.nzo_id, cleanup=False)
|
||||
sabnzbd.Assembler.clear_ready_bytes(*nzo.files)
|
||||
sabnzbd.PostProcessor.process(nzo)
|
||||
|
||||
def actives(self, grabs: bool = True) -> int:
|
||||
@@ -893,7 +890,7 @@ class NzbQueue:
|
||||
|
||||
if nzf.all_servers_in_try_list(active_servers):
|
||||
# Check for articles where all active servers have already been tried
|
||||
with nzf:
|
||||
with nzf.lock:
|
||||
for article in nzf.articles:
|
||||
if article.all_servers_in_try_list(active_servers):
|
||||
logging.debug(
|
||||
@@ -904,6 +901,29 @@ class NzbQueue:
|
||||
|
||||
logging.info("Resetting bad trylist for file %s in job %s", nzf.filename, nzo.final_name)
|
||||
nzf.reset_try_list()
|
||||
if not nzf.assembled and not nzf.articles:
|
||||
logging.debug("Not assembled but no remaining articles for file %s", nzf.filename)
|
||||
if not nzf.assembled and (next_article := nzf.assembler_next_article):
|
||||
logging.debug(
|
||||
"Next article to assemble for file %s is %s, decoded: %s, on_disk: %s, decoded_size: %s",
|
||||
nzf.filename,
|
||||
next_article,
|
||||
next_article.decoded,
|
||||
next_article.on_disk,
|
||||
next_article.decoded_size,
|
||||
)
|
||||
|
||||
for article in nzo.first_articles.copy():
|
||||
logging.debug(
|
||||
"First article for file %s is %s, decoded: %s, on_disk: %s, decoded_size: %s, has_fetcher: %s, tries: %s",
|
||||
article.nzf.filename,
|
||||
article,
|
||||
article.decoded,
|
||||
article.on_disk,
|
||||
article.decoded_size,
|
||||
article.fetcher is not None,
|
||||
article.tries,
|
||||
)
|
||||
|
||||
# Reset main try list, minimal performance impact
|
||||
logging.info("Resetting bad trylist for job %s", nzo.final_name)
|
||||
|
||||
@@ -66,14 +66,12 @@ def MSG_BAD_NEWS():
|
||||
|
||||
def MSG_BAD_PORT():
|
||||
return (
|
||||
T(
|
||||
r"""
|
||||
T(r"""
|
||||
SABnzbd needs a free tcp/ip port for its internal web server.<br>
|
||||
Port %s on %s was tried , but it is not available.<br>
|
||||
Some other software uses the port or SABnzbd is already running.<br>
|
||||
<br>
|
||||
Please restart SABnzbd with a different port number."""
|
||||
)
|
||||
Please restart SABnzbd with a different port number.""")
|
||||
+ """<br>
|
||||
<br>
|
||||
%s<br>
|
||||
@@ -85,14 +83,12 @@ def MSG_BAD_PORT():
|
||||
|
||||
def MSG_BAD_HOST():
|
||||
return (
|
||||
T(
|
||||
r"""
|
||||
T(r"""
|
||||
SABnzbd needs a valid host address for its internal web server.<br>
|
||||
You have specified an invalid address.<br>
|
||||
Safe values are <b>localhost</b> and <b>0.0.0.0</b><br>
|
||||
<br>
|
||||
Please restart SABnzbd with a proper host address."""
|
||||
)
|
||||
Please restart SABnzbd with a proper host address.""")
|
||||
+ """<br>
|
||||
<br>
|
||||
%s<br>
|
||||
@@ -104,15 +100,13 @@ def MSG_BAD_HOST():
|
||||
|
||||
def MSG_BAD_QUEUE():
|
||||
return (
|
||||
T(
|
||||
r"""
|
||||
T(r"""
|
||||
SABnzbd detected saved data from an other SABnzbd version<br>
|
||||
but cannot re-use the data of the other program.<br><br>
|
||||
You may want to finish your queue first with the other program.<br><br>
|
||||
After that, start this program with the "--clean" option.<br>
|
||||
This will erase the current queue and history!<br>
|
||||
SABnzbd read the file "%s"."""
|
||||
)
|
||||
SABnzbd read the file "%s".""")
|
||||
+ """<br>
|
||||
<br>
|
||||
%s<br>
|
||||
@@ -123,13 +117,11 @@ def MSG_BAD_QUEUE():
|
||||
|
||||
|
||||
def MSG_BAD_TEMPL():
|
||||
return T(
|
||||
r"""
|
||||
return T(r"""
|
||||
SABnzbd cannot find its web interface files in %s.<br>
|
||||
Please install the program again.<br>
|
||||
<br>
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
def MSG_OTHER():
|
||||
@@ -137,14 +129,12 @@ def MSG_OTHER():
|
||||
|
||||
|
||||
def MSG_SQLITE():
|
||||
return T(
|
||||
r"""
|
||||
return T(r"""
|
||||
SABnzbd detected that the file sqlite3.dll is missing.<br><br>
|
||||
Some poorly designed virus-scanners remove this file.<br>
|
||||
Please check your virus-scanner, try to re-install SABnzbd and complain to your virus-scanner vendor.<br>
|
||||
<br>
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
def panic_message(panic_code, a=None, b=None):
|
||||
@@ -280,8 +270,7 @@ def error_page_401(status, message, traceback, version):
|
||||
|
||||
def error_page_404(status, message, traceback, version):
|
||||
"""Custom handler for 404 error, redirect to main page"""
|
||||
return (
|
||||
r"""
|
||||
return r"""
|
||||
<html>
|
||||
<head>
|
||||
<script type="text/javascript">
|
||||
@@ -292,6 +281,4 @@ def error_page_404(status, message, traceback, version):
|
||||
</head>
|
||||
<body><br/></body>
|
||||
</html>
|
||||
"""
|
||||
% cfg.url_base()
|
||||
)
|
||||
""" % cfg.url_base()
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"""
|
||||
sabnzbd.par2file - All par2-related functionality
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"""
|
||||
sabnzbd.postproc - threaded post-processing of jobs
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import functools
|
||||
@@ -73,7 +74,7 @@ from sabnzbd.filesystem import (
|
||||
get_ext,
|
||||
get_filename,
|
||||
)
|
||||
from sabnzbd.nzbstuff import NzbObject
|
||||
from sabnzbd.nzb import NzbObject
|
||||
from sabnzbd.sorting import Sorter
|
||||
from sabnzbd.constants import (
|
||||
REPAIR_PRIORITY,
|
||||
@@ -95,7 +96,6 @@ import sabnzbd.utils.rarvolinfo as rarvolinfo
|
||||
import sabnzbd.utils.checkdir
|
||||
import sabnzbd.deobfuscate_filenames as deobfuscate
|
||||
|
||||
|
||||
MAX_FAST_JOB_COUNT = 3
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import subprocess
|
||||
import logging
|
||||
import time
|
||||
|
||||
|
||||
##############################################################################
|
||||
# Power management for Windows
|
||||
##############################################################################
|
||||
|
||||
1342
sabnzbd/rss.py
1342
sabnzbd/rss.py
File diff suppressed because it is too large
Load Diff
@@ -574,6 +574,11 @@ SKIN_TEXT = {
|
||||
"For unreliable servers, will be ignored longer in case of failures"
|
||||
), #: Explain server optional tickbox
|
||||
"srv-enable": TT("Enable"), #: Enable server tickbox
|
||||
"srv-pipelining_requests": TT("Articles per request"),
|
||||
"explain-pipelining_requests": TT(
|
||||
"Request multiple articles per connection without waiting for each response first.<br />"
|
||||
"This can improve download speeds, especially on connections with higher latency."
|
||||
),
|
||||
"button-addServer": TT("Add Server"), #: Button: Add server
|
||||
"button-delServer": TT("Remove Server"), #: Button: Remove server
|
||||
"button-testServer": TT("Test Server"), #: Button: Test server
|
||||
@@ -899,6 +904,7 @@ SKIN_TEXT = {
|
||||
"Glitter-notification-removing1": TT("Removing job"), # Notification window
|
||||
"Glitter-notification-removing": TT("Removing jobs"), # Notification window
|
||||
"Glitter-notification-shutdown": TT("Shutting down"), # Notification window
|
||||
"Glitter-notification-upload-failed": TT("Failed to upload file: %s"), # Notification window
|
||||
# Wizard
|
||||
"wizard-quickstart": TT("SABnzbd Quick-Start Wizard"),
|
||||
"wizard-version": TT("SABnzbd Version"),
|
||||
@@ -927,11 +933,9 @@ SKIN_TEXT = {
|
||||
"wizard-test-server-required": TT("Click on Test Server before continuing"), #: Tooltip for disabled Next button
|
||||
"restore-backup": TT("Restore backup"),
|
||||
# Special
|
||||
"yourRights": TT(
|
||||
"""
|
||||
"yourRights": TT("""
|
||||
SABnzbd comes with ABSOLUTELY NO WARRANTY.
|
||||
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.
|
||||
"""
|
||||
),
|
||||
"""),
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@ from sabnzbd.constants import (
|
||||
GUESSIT_SORT_TYPES,
|
||||
)
|
||||
from sabnzbd.misc import is_sample, from_units, sort_to_opts
|
||||
from sabnzbd.nzbstuff import NzbObject, scan_password
|
||||
from sabnzbd.misc import scan_password
|
||||
from sabnzbd.nzb import NzbObject
|
||||
|
||||
# Do not rename .vob files as they are usually DVD's
|
||||
EXCLUDED_FILE_EXTS = (".vob", ".bin")
|
||||
|
||||
@@ -51,7 +51,7 @@ import sabnzbd.notifier as notifier
|
||||
from sabnzbd.decorators import NZBQUEUE_LOCK
|
||||
from sabnzbd.encoding import ubtou, utob
|
||||
from sabnzbd.nzbparser import AddNzbFileResult
|
||||
from sabnzbd.nzbstuff import NzbObject, NzbRejected, NzbRejectToHistory
|
||||
from sabnzbd.nzb import NzbObject
|
||||
|
||||
|
||||
class URLGrabber(Thread):
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
Note: extension always contains a leading dot
|
||||
"""
|
||||
|
||||
|
||||
import puremagic
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -47,7 +47,6 @@ application callbacks) are always unicode instances.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
__author__ = "Christopher Stawarz <cstawarz@csail.mit.edu>"
|
||||
__version__ = "1.1.1"
|
||||
__revision__ = int("$Revision: 6125 $".split()[1])
|
||||
@@ -59,7 +58,6 @@ import re
|
||||
import socket
|
||||
import sys
|
||||
|
||||
|
||||
################################################################################
|
||||
#
|
||||
# Global setup
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"""
|
||||
sabnzbd.utils.rarvolinfo - Find out volume number and/or original extension of a rar file. Useful with obfuscated files
|
||||
"""
|
||||
|
||||
import os
|
||||
import rarfile
|
||||
|
||||
|
||||
@@ -20,11 +20,9 @@
|
||||
sabnzbd.utils.sleepless - Keep macOS awake by setting power assertions
|
||||
"""
|
||||
|
||||
|
||||
import objc
|
||||
from Foundation import NSBundle
|
||||
|
||||
|
||||
# https://developer.apple.com/documentation/iokit/iopowersources.h?language=objc
|
||||
IOKit = NSBundle.bundleWithIdentifier_("com.apple.framework.IOKit")
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import socket
|
||||
import uuid
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
# You MUST use double quotes (so " and not ')
|
||||
# Do not forget to update the appdata file for every major release!
|
||||
|
||||
__version__ = "4.6.0Alpha2"
|
||||
__version__ = "5.0.0Beta1"
|
||||
__baseline__ = "unknown"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user