Compare commits

..

23 Commits

Author SHA1 Message Date
mnightingale
a637d218c4 Refactor preparing the rss config (#3242)
* Refactor preparing the rss config

* Defaults, bool, and iteration
2025-12-24 21:47:15 +01:00
Safihre
63c03b42a9 Update text files for 4.6.0Beta2 2025-12-22 22:00:02 +01:00
SABnzbd Automation
4539837fad Update translatable texts
[skip ci]
2025-12-22 20:53:43 +00:00
Safihre
a0cd48e3f5 Notify user if they run AMD64 version on ARM64 Windows machine
Closes #3235
2025-12-22 21:52:58 +01:00
Safihre
ceeb7cb162 Add Windows ARM64 binary 2025-12-22 21:25:17 +01:00
SABnzbd Automation
f9f4e1b028 Update translatable texts
[skip ci]
2025-12-22 15:40:48 +00:00
Safihre
6487944c6c Move Pipelining setting to Server-level 2025-12-22 16:38:46 +01:00
renovate[bot]
239fddf39c Update all dependencies (develop) (#3238)
* Update all dependencies

* Compare fakefs result after sorting

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Safihre <safihre@sabnzbd.org>
2025-12-22 12:45:00 +00:00
SABnzbd Automation
8ada8b2fd9 Update translatable texts
[skip ci]
2025-12-19 11:41:18 +00:00
Safihre
b19bd65495 Show error in case of failed NZB upload
Closes #3233
2025-12-19 12:40:34 +01:00
Safihre
e3ea5fdd64 Update appdata file with Flathub suggestions
@jcfp
2025-12-19 11:54:53 +01:00
Safihre
4fdb89701a Add release URL's to appdata 2025-12-19 11:42:36 +01:00
SABnzbd Automation
9165c4f304 Update translatable texts
[skip ci]
2025-12-18 20:10:14 +00:00
mnightingale
4152f0ba6a Increase max pipelining (#3234) 2025-12-18 20:09:24 +00:00
SABnzbd Automation
3eaab17739 Update translatable texts
[skip ci]
2025-12-16 09:04:29 +00:00
Safihre
578bfd083d Update text files 4.6.0 Beta 1 2025-12-16 10:03:41 +01:00
renovate[bot]
dd464456e4 Update all dependencies (#3231)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 02:08:17 +00:00
mnightingale
e7a0255359 Make behaviour after reset more robust (#3229)
* Make behaviour after reset more robust

* Remove use of hasattr and rename to generation

* I had a feeling this would be a circular reference

* Reset and increment generation under lock
2025-12-14 22:39:48 +01:00
mnightingale
2e1281d9e8 Fix nzb types (#3230) 2025-12-14 15:46:14 +01:00
SABnzbd Automation
efecefdd3b Update translatable texts
[skip ci]
2025-12-09 20:22:55 +00:00
Safihre
a91e718ef5 Split nzbstuff into separate files for Article, NzbFile and NzbObject (#3221) 2025-12-09 21:21:51 +01:00
mnightingale
b420975267 Fix read/write actions after reset_nw (#3223) 2025-12-09 19:39:49 +01:00
SABnzbd Automation
c4211df8dc Update translatable texts
[skip ci]
2025-12-08 21:37:36 +00:00
69 changed files with 1941 additions and 2708 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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: |

View File

@@ -1,12 +1,12 @@
Release Notes - SABnzbd 4.6.0 Alpha 2
Release Notes - SABnzbd 4.6.0 Beta 2
=========================================================
This is the second test release of version 4.6.
This is the second beta release of version 4.6.
## New features in 4.6.0
* Added default support for NNTP Pipelining which eliminates idle waiting
between requests, significantly improving speeds on high-latency connections.
* 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.
* Improved disk speed measurement in Status window.
@@ -15,13 +15,15 @@ This is the second test release of version 4.6.
* If a download only has `.nzb` files inside, the new downloads
will include the name of the original download.
* 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.
* Windows: Tray icon disappears after Explorer restart.
* macOS: Slow to start on some network setups.

View File

@@ -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,

View File

@@ -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",
]
)

View File

@@ -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,
)

View File

@@ -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']" />

View File

@@ -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">

View File

@@ -726,6 +726,9 @@ function ViewModel() {
$('#nzbname').val('')
$('.btn-file em').html(glitterTranslate.chooseFile + '&hellip;')
}
}).fail(function(xhr, status, error) {
// Update the uploading notification text to show error
showNotification('.main-notification-box-uploading-failed', 0, error)
});
}

View File

@@ -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 {

View File

@@ -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="4.6.0" date="2025-12-24" type="stable">
<url type="details">https://github.com/sabnzbd/sabnzbd/releases/tag/4.6.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>

View File

@@ -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 ""
@@ -884,7 +889,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 +1305,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 ""
@@ -3482,6 +3402,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 +4117,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 +4531,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"

View File

@@ -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"
@@ -955,7 +960,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 +1393,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"
@@ -3672,6 +3592,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 +4344,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 +4768,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"

View File

@@ -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"
@@ -996,7 +1001,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 +1437,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"
@@ -3845,6 +3765,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 +4551,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 +4989,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"

View File

@@ -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"
@@ -1036,7 +1041,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 +1484,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"
@@ -3954,6 +3871,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 +4665,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 +5104,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"

View File

@@ -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"
@@ -1020,7 +1025,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 +1476,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"
@@ -3924,6 +3842,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 +4635,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 +5076,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"

View File

@@ -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"
@@ -961,7 +966,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 +1392,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"
@@ -3763,6 +3683,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 +4450,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 +4885,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"

View File

@@ -157,6 +157,11 @@ 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 ""
#. Warning message
#: sabnzbd/__init__.py
msgid ""
@@ -347,7 +352,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"
@@ -1025,7 +1030,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 +1471,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"
@@ -3505,6 +3425,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 +3857,17 @@ msgstr ""
msgid "Enable"
msgstr "Activer"
#: 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"
@@ -4720,6 +4653,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 +5096,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"

View File

@@ -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 "בוטל, סיומת בלתי רצויה התגלתה"
@@ -973,7 +978,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 +1409,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 "בעיה עם"
@@ -3781,6 +3701,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 +4477,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 +4915,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"

View File

@@ -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"
@@ -1016,7 +1021,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 +1459,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"
@@ -3894,6 +3814,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 +4606,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 +5047,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"

View File

@@ -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"
@@ -958,7 +963,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 +1390,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"
@@ -3742,6 +3662,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 +4424,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 +4857,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"

View File

@@ -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"
@@ -1019,7 +1024,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 +1462,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"
@@ -3894,6 +3814,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 +4604,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 +5043,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"

View File

@@ -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"
@@ -961,7 +966,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 +1395,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"
@@ -3754,6 +3674,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 +4436,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 +4867,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"

View File

@@ -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"
@@ -973,7 +978,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 +1404,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"
@@ -3765,6 +3685,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 +4447,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 +4878,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"

View File

@@ -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"
@@ -983,7 +988,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 +1422,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"
@@ -3786,6 +3706,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 +4467,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 +4900,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"

View File

@@ -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 ""
@@ -312,7 +317,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 ""
@@ -957,7 +962,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 +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 ""
#: 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 "Проблема с"
@@ -3743,6 +3663,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 +4431,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 +4863,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"

View File

@@ -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"
@@ -953,7 +958,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 +1385,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 "Проблем са"
@@ -3729,6 +3649,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 +4409,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 +4840,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"

View File

@@ -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"
@@ -955,7 +960,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 +1389,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"
@@ -3741,6 +3661,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 +4422,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 +4854,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"

View File

@@ -3,14 +3,14 @@
#
# Translators:
# Taylan Tatlı, 2025
# mauron, 2025
# Safihre <safihre@sabnzbd.org>, 2025
# mauron, 2025
#
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>, 2025\n"
"Last-Translator: mauron, 2025\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 ""
#. 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"
@@ -1008,7 +1013,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 +1450,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"
@@ -3461,6 +3381,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 +3803,17 @@ msgstr ""
msgid "Enable"
msgstr "Etkinleştir"
#: 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"
@@ -4661,6 +4594,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 +5038,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"

View File

@@ -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 "已中止,侦测到不需要的扩展名"
@@ -951,7 +956,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 +1381,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 "问题"
@@ -3690,6 +3610,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 +4373,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 +4801,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"

View File

@@ -10,7 +10,7 @@ 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
@@ -67,7 +67,7 @@ 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
urllib3==2.6.2
certifi==2025.11.12
oauthlib==3.3.1
PyJWT==2.10.1

View File

@@ -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
@@ -269,7 +270,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)
@@ -482,6 +482,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()))

View File

@@ -57,6 +57,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
@@ -85,7 +86,7 @@ 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
@@ -1006,7 +1007,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 +1310,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 +1347,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,
)

View File

@@ -27,7 +27,7 @@ from typing import Collection
import sabnzbd
from sabnzbd.decorators import synchronized
from sabnzbd.constants import GIGI, ANFO, ASSEMBLER_WRITE_THRESHOLD
from sabnzbd.nzbstuff import Article
from sabnzbd.nzb import Article
# Operations on the article table are handled via try/except.
# The counters need to be made atomic to ensure consistency.

View File

@@ -41,7 +41,7 @@ from sabnzbd.filesystem import (
)
from sabnzbd.constants import Status, GIGI
import sabnzbd.cfg as cfg
from sabnzbd.nzbstuff import NzbObject, NzbFile
from sabnzbd.nzb import NzbFile, NzbObject
import sabnzbd.par2file as par2file

View File

@@ -535,7 +535,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)
##############################################################################

View File

@@ -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
@@ -444,6 +445,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 +478,7 @@ class ConfigServer:
"enable",
"required",
"optional",
"pipelining_requests",
"retention",
"expire_date",
"quota",
@@ -511,6 +514,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()

View File

@@ -103,7 +103,7 @@ SOFT_ASSEMBLER_QUEUE_LIMIT = 0.5
ASSEMBLER_WRITE_THRESHOLD = 5
NNTP_BUFFER_SIZE = int(256 * KIBI)
NTTP_MAX_BUFFER_SIZE = int(10 * MEBI)
DEF_PIPELINING_REQUESTS = 2
DEF_PIPELINING_REQUESTS = 1
REPAIR_PRIORITY = 3
FORCE_PRIORITY = 2

View File

@@ -498,9 +498,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")

View File

@@ -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

View File

@@ -189,7 +189,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 +227,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 +317,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 +342,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")

View File

@@ -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

View File

@@ -85,6 +85,7 @@ class Server:
"retention",
"username",
"password",
"pipelining_requests",
"busy_threads",
"next_busy_threads_check",
"idle_threads",
@@ -113,6 +114,7 @@ class Server:
use_ssl,
ssl_verify,
ssl_ciphers,
pipelining_requests,
username=None,
password=None,
required=False,
@@ -137,6 +139,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 +154,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:
@@ -325,6 +328,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 +359,7 @@ class Downloader(Thread):
ssl,
ssl_verify,
ssl_ciphers,
pipelining_requests,
username,
password,
required,
@@ -540,7 +545,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:
@@ -694,7 +699,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,20 +744,26 @@ 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"""
# Drop stale items
if nw.generation != generation:
return
if event & selectors.EVENT_READ:
self.process_nw_read(nw)
self.process_nw_read(nw, generation)
# If read caused a reset, don't proceed to write
if nw.generation != generation:
return
if event & selectors.EVENT_WRITE:
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.decoder and nw.generation == generation:
try:
n, bytes_pending = nw.read(nbytes=bytes_pending)
n, bytes_pending = nw.read(nbytes=bytes_pending, generation=generation)
bytes_received += n
except ssl.SSLWantReadError:
return
@@ -768,6 +780,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:
@@ -900,7 +916,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:

View File

@@ -1367,3 +1367,54 @@ 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 nzf_cmp_name(nzf1, nzf2):
"""Comparison function for sorting NZB files.
The comparison will sort .par2 files to the top of the queue followed by .rar files,
they will then be sorted by name.
Note: nzf1 and nzf2 should be NzbFile objects, but we can't import that here
to avoid circular dependencies.
"""
nzf1_name = nzf1.filename.lower()
nzf2_name = nzf2.filename.lower()
# 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 = RAR_RE.search(nzf1_name)
m2 = RAR_RE.search(nzf2_name)
if m1 and not (is_par2 or m2):
return -1
elif m2 and not (is_par1 or m1):
return 1
# Force .rar to come before 'r00'
if m1 and m1.group(1) == ".rar":
nzf1_name = nzf1_name.replace(".rar", ".r//")
if m2 and m2.group(1) == ".rar":
nzf2_name = nzf2_name.replace(".rar", ".r//")
return sabnzbd.misc.cmp(nzf1_name, nzf2_name)

View File

@@ -913,7 +913,6 @@ SPECIAL_VALUE_LIST = (
"ssdp_broadcast_interval",
"unrar_parameters",
"outgoing_nntp_ip",
"pipelining_requests",
)
SPECIAL_LIST_LIST = (
"rss_odd_titles",

View File

@@ -57,7 +57,7 @@ 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:
@@ -85,6 +85,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"))
@@ -1591,6 +1595,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

View File

@@ -66,7 +66,7 @@ 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
@@ -118,7 +118,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:

View File

@@ -74,12 +74,14 @@ class NewsWrapper:
"_response_queue",
"selector_events",
"lock",
"generation",
)
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
self.timeout: Optional[float] = None
@@ -97,16 +99,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()
@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:
@@ -177,12 +179,12 @@ class NewsWrapper:
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,7 +198,7 @@ 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)
@@ -282,12 +284,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 not self.decoder:
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
@@ -304,7 +315,12 @@ class NewsWrapper:
self.decoder.process(bytes_recv)
for response in self.decoder:
if self.generation != generation:
break
with self.lock:
# Re-check under lock to avoid racing with hard_reset
if self.generation != generation or not self._response_queue:
break
article = self._response_queue.popleft()
if on_response:
on_response(response.status_code, response.message)
@@ -379,7 +395,7 @@ class NewsWrapper:
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)
and (not server.active or server.restart or not self.timeout or time.time() > self.timeout)
):
# Make socket available again
server.busy_threads.discard(self)
@@ -415,8 +431,9 @@ class NewsWrapper:
self.nntp.close(send_quit=self.connected)
self.nntp = None
# Reset all variables (including the NNTP connection)
self.__init__(self.server, self.thrdnum)
with self.lock:
# Reset all variables (including the NNTP connection) and increment the generation counter
self.__init__(self.server, self.thrdnum, generation=self.generation + 1)
# Wait before re-using this newswrapper
if wait:
@@ -428,7 +445,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:

59
sabnzbd/nzb/__init__.py Normal file
View File

@@ -0,0 +1,59 @@
#!/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, TRYLIST_LOCK
# File-related classes
from sabnzbd.nzb.file import NzbFile, NzbFileSaver, SkippedNzbFile
# Object-related classes
from sabnzbd.nzb.object import (
NzbObject,
NzbObjectSaver,
NzoAttributeSaver,
NZO_LOCK,
NzbEmpty,
NzbRejected,
NzbPreQueueRejected,
NzbRejectToHistory,
)
__all__ = [
# Article
"Article",
"ArticleSaver",
"TryList",
"TRYLIST_LOCK",
# File
"NzbFile",
"NzbFileSaver",
"SkippedNzbFile",
# Object
"NzbObject",
"NzbObjectSaver",
"NzoAttributeSaver",
"NZO_LOCK",
"NzbEmpty",
"NzbRejected",
"NzbPreQueueRejected",
"NzbRejectToHistory",
]

214
sabnzbd/nzb/article.py Normal file
View File

@@ -0,0 +1,214 @@
#!/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
##############################################################################
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 = nzf # NzbFile reference
@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)

290
sabnzbd/nzb/file.py Normal file
View File

@@ -0,0 +1,290 @@
#!/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
import sabctools
from sabnzbd.nzb.article import TryList, Article, TRYLIST_LOCK
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,
)
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",)
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(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
# 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)

View File

@@ -16,7 +16,7 @@
# 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
@@ -30,7 +30,8 @@ from typing import Any, Optional, Union, BinaryIO, Deque
# SABnzbd modules
import sabnzbd
import sabctools
from sabnzbd.nzb.article import TryList, Article, TRYLIST_LOCK
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,19 @@ from sabnzbd.filesystem import (
remove_data,
strip_extensions,
get_ext,
create_work_name,
nzf_cmp_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
@@ -1032,7 +591,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
@@ -2134,109 +1693,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

View File

@@ -33,7 +33,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 +212,7 @@ def process_nzb_archive_file(
if datap:
nzo = None
try:
nzo = nzbstuff.NzbObject(
nzo = NzbObject(
name,
pp=pp,
script=script,
@@ -220,13 +228,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 +323,7 @@ def process_single_nzb(
nzo = None
nzo_ids = []
try:
nzo = nzbstuff.NzbObject(
nzo = NzbObject(
filename,
pp=pp,
script=script,
@@ -330,16 +338,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 +374,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 +478,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

View File

@@ -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

View File

@@ -73,7 +73,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,

View File

@@ -25,6 +25,8 @@ import time
import datetime
import threading
import urllib.parse
from dataclasses import dataclass, field
from typing import Union, Optional, Iterator
import sabnzbd
from sabnzbd.constants import RSS_FILE_NAME, DEFAULT_PRIORITY
@@ -142,43 +144,7 @@ class RSSReader:
return T('Incorrect RSS feed description "%s"') % feed
uris = feeds.uri()
defCat = feeds.cat()
if not notdefault(defCat) or defCat not in sabnzbd.api.list_cats(default=False):
defCat = None
defPP = feeds.pp()
if not notdefault(defPP):
defPP = None
defScript = feeds.script()
if not notdefault(defScript):
defScript = None
defPrio = feeds.priority()
if not notdefault(defPrio):
defPrio = None
# Preparations, convert filters to regex's
regexes = []
reTypes = []
reCats = []
rePPs = []
rePrios = []
reScripts = []
reEnabled = []
for feed_filter in feeds.filters():
reCat = feed_filter[0]
if defCat in ("", "*"):
reCat = None
reCats.append(reCat)
rePPs.append(feed_filter[1])
reScripts.append(feed_filter[2])
reTypes.append(feed_filter[3])
if feed_filter[3] in ("<", ">", "F", "S"):
regexes.append(feed_filter[4])
else:
regexes.append(convert_filter(feed_filter[4]))
rePrios.append(feed_filter[5])
reEnabled.append(feed_filter[6] != "0")
regcount = len(regexes)
filters = prepare_feed(feeds)
# Set first if this is the very first scan of this URI
first = (feed not in self.jobs) and ignoreFirst
@@ -302,71 +268,71 @@ class RSSReader:
# Match this title against all filters
logging.debug("Trying title %s", title)
result = False
myCat = defCat
myPP = defPP
myScript = defScript
myPrio = defPrio
myCat = filters.default_category
myPP = filters.default_pp
myScript = filters.default_script
myPrio = filters.default_priority
n = 0
if ("F" in reTypes or "S" in reTypes) and (not season or not episode):
if filters.has_type("F", "S") and (not season or not episode):
show_analysis = sabnzbd.sorting.BasicAnalyzer(title)
season = show_analysis.info.get("season_num")
episode = show_analysis.info.get("episode_num")
# Match against all filters until an positive or negative match
logging.debug("Size %s", size)
for n in range(regcount):
if reEnabled[n]:
if category and reTypes[n] == "C":
found = re.search(regexes[n], category)
for rule in filters:
if rule.enabled:
if category and rule.type == "C":
found = re.search(rule.regex, category)
if not found:
logging.debug("Filter rejected on rule %d", n)
result = False
break
elif reTypes[n] == "<" and size and from_units(regexes[n]) < size:
elif rule.type == "<" and size and from_units(rule.regex) < size:
# "Size at most" : too large
logging.debug("Filter rejected on rule %d", n)
result = False
break
elif reTypes[n] == ">" and size and from_units(regexes[n]) > size:
elif rule.type == ">" and size and from_units(rule.regex) > size:
# "Size at least" : too small
logging.debug("Filter rejected on rule %d", n)
result = False
break
elif reTypes[n] == "F" and not ep_match(season, episode, regexes[n]):
elif rule.type == "F" and not ep_match(season, episode, rule.regex):
# "Starting from SxxEyy", too early episode
logging.debug("Filter requirement match on rule %d", n)
result = False
break
elif reTypes[n] == "S" and ep_match(season, episode, regexes[n], title):
elif rule.type == "S" and ep_match(season, episode, rule.regex, title):
logging.debug("Filter matched on rule %d", n)
result = True
break
else:
if regexes[n]:
found = re.search(regexes[n], title)
if rule.regex:
found = re.search(rule.regex, title)
else:
found = False
if reTypes[n] == "M" and not found:
if rule.type == "M" and not found:
logging.debug("Filter rejected on rule %d", n)
result = False
break
if found and reTypes[n] == "A":
if found and rule.type == "A":
logging.debug("Filter matched on rule %d", n)
result = True
break
if found and reTypes[n] == "R":
if found and rule.type == "R":
logging.debug("Filter rejected on rule %d", n)
result = False
break
if len(reCats):
if not result and defCat:
if filters and (rule := filters.rules[-1]):
if not result and filters.default_category:
# Apply Feed-category on non-matched items
myCat = defCat
elif result and notdefault(reCats[n]):
myCat = filters.default_category
elif result and notdefault(rule.category):
# Use the matched info
myCat = reCats[n]
elif category and not defCat:
myCat = rule.category
elif category and not filters.default_category:
# No result and no Feed-category
myCat = cat_convert(category)
@@ -374,17 +340,17 @@ class RSSReader:
myCat, catPP, catScript, catPrio = cat_to_opts(myCat)
else:
myCat = catPP = catScript = catPrio = None
if notdefault(rePPs[n]):
myPP = rePPs[n]
elif not (reCats[n] or category):
if notdefault(rule.pp):
myPP = rule.pp
elif not (rule.category or category):
myPP = catPP
if notdefault(reScripts[n]):
myScript = reScripts[n]
elif not (notdefault(reCats[n]) or category):
if notdefault(rule.script):
myScript = rule.script
elif not (notdefault(rule.category) or category):
myScript = catScript
if rePrios[n] not in (str(DEFAULT_PRIORITY), ""):
myPrio = rePrios[n]
elif not ((rePrios[n] != str(DEFAULT_PRIORITY)) or category):
if rule.priority not in (str(DEFAULT_PRIORITY), ""):
myPrio = rule.priority
elif not ((rule.priority != str(DEFAULT_PRIORITY)) or category):
myPrio = catPrio
act = download and not first
@@ -532,6 +498,83 @@ class RSSReader:
self.jobs[feed][item]["status"] = "D-"
@dataclass
class FeedRule:
regex: Union[str, re.Pattern]
type: str
category: Optional[str] = None
pp: Optional[str] = None
priority: Optional[int] = None
script: Optional[str] = None
enabled: bool = True
def __post_init__(self):
# Convert regex if needed
if self.type not in {"<", ">", "F", "S"}:
self.regex = convert_filter(self.regex)
@dataclass
class FeedConfig:
default_category: Optional[str] = None
default_pp: Optional[str] = None
default_script: Optional[str] = None
default_priority: Optional[int] = None
rules: list[FeedRule] = field(default_factory=list)
def __post_init__(self):
# Normalise categories for all rules automatically
if self.default_category in ("", "*"):
for rule in self.rules:
rule.category = None
def __bool__(self):
return bool(self.rules) # True if there are any rules
def __iter__(self) -> Iterator[FeedRule]:
"""Allow iteration directly over FeedConfig to access rules."""
return iter(self.rules)
def has_type(self, *types: str) -> bool:
"""Check if any rule matches the given types"""
return any(rule.type in types for rule in self.rules)
def prepare_feed(c: config.ConfigRSS) -> FeedConfig:
def normalise_default(value):
return value if notdefault(value) else None
default_category = normalise_default(c.cat())
if default_category not in sabnzbd.api.list_cats(default=False):
default_category = None
default_pp = normalise_default(c.pp())
default_script = normalise_default(c.script())
default_priority = normalise_default(c.priority())
# Preparations, convert filters to regex's
rules: list[FeedRule] = []
for cat, pp, script, ftype, regex, priority, enabled in c.filters():
rules.append(
FeedRule(
regex=regex,
type=ftype,
category=cat,
pp=pp,
priority=priority,
script=script,
enabled=(enabled != "0"),
)
)
return FeedConfig(
default_category=default_category,
default_pp=default_pp,
default_script=default_script,
default_priority=default_priority,
rules=rules,
)
def patch_feedparser():
"""Apply options that work for SABnzbd
Add additional parsing of attributes

View File

@@ -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"),

View File

@@ -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")

View File

@@ -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):

View File

@@ -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__ = "4.6.0Beta2"
__baseline__ = "unknown"

View File

@@ -28,7 +28,7 @@ from unittest import mock
import sabctools
import sabnzbd.decoder as decoder
from sabnzbd.nzbstuff import Article
from sabnzbd.nzb import Article
def uu(data: bytes):

View File

@@ -663,7 +663,7 @@ class TestListdirFull(ffs.TestCase):
):
self.fs.create_file(file)
assert os.path.exists(file) is True
assert filesystem.listdir_full("/rsc") == ["/rsc/base_file", "/rsc/not._base_file"]
assert sorted(filesystem.listdir_full("/rsc")) == ["/rsc/base_file", "/rsc/not._base_file"]
def test_invalid_file_argument(self):
# This is obviously not intended use; the function expects a directory
@@ -750,7 +750,7 @@ class TestListdirFullWin(ffs.TestCase):
):
self.fs.create_file(file)
assert os.path.exists(file) is True
assert filesystem.listdir_full(r"f:\rsc") == [r"f:\rsc\base_file", r"f:\rsc\not._base_file"]
assert sorted(filesystem.listdir_full(r"f:\rsc")) == [r"f:\rsc\base_file", r"f:\rsc\not._base_file"]
def test_invalid_file_argument(self):
# This is obviously not intended use; the function expects a directory
@@ -1256,3 +1256,21 @@ class TestOtherFileSystemFunctions:
)
def test_strip_extensions(self, name, ext_to_remove, output):
assert filesystem.strip_extensions(name, ext_to_remove) == output
@pytest.mark.parametrize(
"file_name, clean_file_name",
[
("my_awesome_nzb_file.pAr2.nZb", "my_awesome_nzb_file"),
("my_awesome_nzb_file.....pAr2.nZb", "my_awesome_nzb_file"),
("my_awesome_nzb_file....par2..", "my_awesome_nzb_file"),
(" my_awesome_nzb_file .pAr.nZb", "my_awesome_nzb_file"),
("with.extension.and.period.par2.", "with.extension.and.period"),
("nothing.in.here", "nothing.in.here"),
(" just.space ", "just.space"),
("http://test.par2 ", "http://test.par2"),
],
)
def test_create_work_name(self, file_name, clean_file_name):
# Only test stuff specific for create_work_name
# The sanitizing is already tested in tests for sanitize_foldername
assert filesystem.create_work_name(file_name) == clean_file_name

View File

@@ -837,6 +837,94 @@ class TestMisc:
_func()
@pytest.mark.parametrize(
"argument, name, password",
[
("my_awesome_nzb_file{{password}}", "my_awesome_nzb_file", "password"),
("file_with_text_after_pw{{passw0rd}}_[180519]", "file_with_text_after_pw", "passw0rd"),
("file_without_pw", "file_without_pw", None),
("multiple_pw{{first-pw}}_{{second-pw}}", "multiple_pw", "first-pw}}_{{second-pw"), # Greed is Good
("デビアン", "デビアン", None), # Unicode
("Gentoo_Hobby_Edition {{secret}}", "Gentoo_Hobby_Edition", "secret"), # Space between name and password
("Test {{secret}}.nzb", "Test", "secret"),
("Mandrake{{top{{secret}}", "Mandrake", "top{{secret"), # Double opening {{
("Красная}}{{Шляпа}}", "Красная}}", "Шляпа"), # Double closing }}
("{{Jobname{{PassWord}}", "{{Jobname", "PassWord"), # {{ at start
("Hello/kITTY", "Hello", "kITTY"), # Notation with slash
("Hello/kITTY.nzb", "Hello", "kITTY"), # Notation with slash and extension
("/Jobname", "/Jobname", None), # Slash at start
("Jobname/Top{{Secret}}", "Jobname", "Top{{Secret}}"), # Slash with braces
("Jobname / Top{{Secret}}", "Jobname", "Top{{Secret}}"), # Slash with braces and extra spaces
("Jobname / Top{{Secret}}.nzb", "Jobname", "Top{{Secret}}"),
("לינוקס/معلومات سرية", "לינוקס", "معلومات سرية"), # LTR with slash
("לינוקס{{معلومات سرية}}", "לינוקס", "معلومات سرية"), # LTR with brackets
("thư điện tử password=mật_khẩu", "thư điện tử", "mật_khẩu"), # Password= notation
("password=PartOfTheJobname", "password=PartOfTheJobname", None), # Password= at the start
("Job password=Test.par2", "Job", "Test"), # Password= including extension
("Job}}Name{{FTW", "Job}}Name{{FTW", None), # Both {{ and }} present but incorrect order (no password)
("./Text", "./Text", None), # Name would end up empty after the function strips the dot
],
)
def test_scan_password(self, argument, name, password):
assert misc.scan_password(argument) == (name, password)
@pytest.mark.parametrize(
"subject, filename",
[
('Great stuff (001/143) - "Filename.txt" yEnc (1/1)', "Filename.txt"),
(
'"910a284f98ebf57f6a531cd96da48838.vol01-03.par2" yEnc (1/3)',
"910a284f98ebf57f6a531cd96da48838.vol01-03.par2",
),
('Subject-KrzpfTest [02/30] - ""KrzpfTest.part.nzb"" yEnc', "KrzpfTest.part.nzb"),
(
'[PRiVATE]-[WtFnZb]-[Supertje-_S03E11-12_-blabla_+_blabla_WEBDL-480p.mkv]-[4/12] - "" yEnc 9786 (1/1366)',
"Supertje-_S03E11-12_-blabla_+_blabla_WEBDL-480p.mkv",
),
(
'[N3wZ] MAlXD245333\\::[PRiVATE]-[WtFnZb]-[Show.S04E04.720p.AMZN.WEBRip.x264-GalaxyTV.mkv]-[1/2] - "" yEnc 293197257 (1/573)',
"Show.S04E04.720p.AMZN.WEBRip.x264-GalaxyTV.mkv",
),
(
'reftestnzb bf1664007a71 [1/6] - "20b9152c-57eb-4d02-9586-66e30b8e3ac2" yEnc (1/22) 15728640',
"20b9152c-57eb-4d02-9586-66e30b8e3ac2",
),
(
"Re: REQ Author Child's The Book-Thanks much - Child, Lee - Author - The Book.epub (1/1)",
"REQ Author Child's The Book-Thanks much - Child, Lee - Author - The Book.epub",
),
('63258-0[001/101] - "63258-2.0" yEnc (1/250) (1/250)', "63258-2.0"),
# If specified between ", the extension is allowed to be too long
('63258-0[001/101] - "63258-2.0toolong" yEnc (1/250) (1/250)', "63258-2.0toolong"),
(
"Singer - A Album (2005) - [04/25] - 02 Sweetest Somebody (I Know).flac",
"Singer - A Album (2005) - [04/25] - 02 Sweetest Somebody (I Know).flac",
),
("<>random!>", "<>random!>"),
("nZb]-[Supertje-_S03E11-12_", "nZb]-[Supertje-_S03E11-12_"),
("Bla [Now it's done.exe]", "Now it's done.exe"),
# If specified between [], the extension should be a valid one
("Bla [Now it's done.123nonsense]", "Bla [Now it's done.123nonsense]"),
('[PRiVATE]-[WtFnZb]-[00000.clpi]-[1/46] - "" yEnc 788 (1/1)', "00000.clpi"),
(
'[PRiVATE]-[WtFnZb]-[Video_(2001)_AC5.1_-RELEASE_[TAoE].mkv]-[1/23] - "" yEnc 1234567890 (1/23456)',
"Video_(2001)_AC5.1_-RELEASE_[TAoE].mkv",
),
(
"[PRiVATE]-[WtFnZb]-[219]-[1/series.name.s01e01.1080p.web.h264-group.mkv] - "
" yEnc (1/[PRiVATE] \\c2b510b594\\::686ea969999193.155368eba4965e56a8cd263382e012.f2712fdc::/97bd201cf931/) 1 (1/0)",
"series.name.s01e01.1080p.web.h264-group.mkv",
),
(
"[PRiVATE]-[WtFnZb]-[/More.Bla.S02E01.1080p.WEB.h264-EDITH[eztv.re].mkv-WtF[nZb]/"
'More.Bla.S02E01.1080p.WEB.h264-EDITH.mkv]-[1/2] - "" yEnc 2990558544 (1/4173)',
"More.Bla.S02E01.1080p.WEB.h264-EDITH[eztv.re].mkv",
),
],
)
def test_name_extractor(self, subject, filename):
assert misc.subject_name_extractor(subject) == filename
class TestBuildAndRunCommand:
# Path should exist

69
tests/test_nzbarticle.py Normal file
View File

@@ -0,0 +1,69 @@
#!/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.
"""
tests.test_nzbarticle - Testing functions in nzbarticle.py
"""
from sabnzbd.nzb import Article
from tests.testhelper import *
class Server:
def __init__(self, host, priority, active):
self.host = host
self.priority = priority
self.active = active
class TestArticle:
def test_get_article(self):
article_id = "test@host" + os.urandom(8).hex() + ".sab"
article = Article(article_id, randint(4321, 54321), None)
servers = []
servers.append(Server("testserver1", 10, True))
servers.append(Server("testserver2", 20, True))
servers.append(Server("testserver3", 30, True))
# Test fetching top priority server
server = servers[0]
assert article.get_article(server, servers) == article
assert article.fetcher_priority == 10
assert article.fetcher == server
assert article.get_article(server, servers) == None
article.fetcher = None
article.add_to_try_list(server)
assert article.get_article(server, servers) == None
# Test fetching when there is a higher priority server available
server = servers[2]
assert article.fetcher_priority == 10
assert article.get_article(server, servers) == None
assert article.fetcher_priority == 20
# Server should be used even if article.fetcher_priority is a higher number than server.priority
article.fetcher_priority = 30
server = servers[1]
assert article.get_article(server, servers) == article
# Inactive servers in servers list should be ignored
article.fetcher = None
article.fetcher_priority = 0
servers[1].active = False
server = servers[2]
assert article.get_article(server, servers) == article
assert article.tries == 3

67
tests/test_nzbobject.py Normal file
View File

@@ -0,0 +1,67 @@
#!/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.
"""
tests.test_nzbobject - Testing functions in nzbobject.py
"""
from sabnzbd.nzb import NzbObject
from sabnzbd.config import ConfigCat
from sabnzbd.constants import NORMAL_PRIORITY
from sabnzbd.filesystem import globber
from tests.testhelper import *
@pytest.mark.usefixtures("clean_cache_dir")
class TestNZO:
@set_config({"download_dir": SAB_CACHE_DIR})
def test_nzo_basic(self):
# Need to create the Default category, as we would in normal instance
# Otherwise it will try to save the config
def_cat = ConfigCat("*", {"pp": 3, "script": "None", "priority": NORMAL_PRIORITY})
# Create empty object, normally used to grab URL's
nzo = NzbObject("test_basic")
assert nzo.work_name == "test_basic"
assert not nzo.files
# Create NZB-file to import
nzb_fp = create_and_read_nzb_fp("basic_rar5")
# Very basic test of NZO creation with data
nzo = NzbObject("test_basic_data", nzb_fp=nzb_fp)
assert nzo.final_name == "test_basic_data"
assert nzo.files
assert nzo.files[0].filename == "testfile.rar"
assert nzo.bytes == 283
assert nzo.files[0].bytes == 283
# work_name can be trimmed in Windows due to max-path-length
assert "test_basic_data".startswith(nzo.work_name)
assert os.path.exists(nzo.admin_path)
# Check if there's an nzf file and the backed-up nzb
assert globber(nzo.admin_path, "*.nzb.gz")
assert globber(nzo.admin_path, "SABnzbd_nzf*")
# Should have picked up the default category settings
assert nzo.cat == "*"
assert nzo.script == def_cat.script() == "None"
assert nzo.priority == def_cat.priority() == NORMAL_PRIORITY
assert nzo.repair and nzo.unpack and nzo.delete
# TODO: More checks!

View File

@@ -21,7 +21,7 @@ tests.test_nzbparser - Tests of basic NZB parsing
from tests.testhelper import *
import sabnzbd.nzbparser as nzbparser
from sabnzbd import nzbstuff
from sabnzbd.nzb import NzbObject
from sabnzbd.filesystem import save_compressed
@@ -29,7 +29,7 @@ from sabnzbd.filesystem import save_compressed
class TestNzbParser:
@set_config({"download_dir": SAB_CACHE_DIR})
def test_nzbparser(self):
nzo = nzbstuff.NzbObject("test_basic")
nzo = NzbObject("test_basic")
# Create test file
metadata = {"category": "test", "password": "testpass"}
nzb_fp = create_and_read_nzb_fp("..", metadata=metadata)

View File

@@ -1,221 +0,0 @@
#!/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.
"""
tests.test_nzbstuff - Testing functions in nzbstuff.py
"""
import sabnzbd.nzbstuff as nzbstuff
from sabnzbd.config import ConfigCat
from sabnzbd.constants import NORMAL_PRIORITY
from sabnzbd.filesystem import globber
from tests.testhelper import *
@pytest.mark.usefixtures("clean_cache_dir")
class TestNZO:
@set_config({"download_dir": SAB_CACHE_DIR})
def test_nzo_basic(self):
# Need to create the Default category, as we would in normal instance
# Otherwise it will try to save the config
def_cat = ConfigCat("*", {"pp": 3, "script": "None", "priority": NORMAL_PRIORITY})
# Create empty object, normally used to grab URL's
nzo = nzbstuff.NzbObject("test_basic")
assert nzo.work_name == "test_basic"
assert not nzo.files
# Create NZB-file to import
nzb_fp = create_and_read_nzb_fp("basic_rar5")
# Very basic test of NZO creation with data
nzo = nzbstuff.NzbObject("test_basic_data", nzb_fp=nzb_fp)
assert nzo.final_name == "test_basic_data"
assert nzo.files
assert nzo.files[0].filename == "testfile.rar"
assert nzo.bytes == 283
assert nzo.files[0].bytes == 283
# work_name can be trimmed in Windows due to max-path-length
assert "test_basic_data".startswith(nzo.work_name)
assert os.path.exists(nzo.admin_path)
# Check if there's an nzf file and the backed-up nzb
assert globber(nzo.admin_path, "*.nzb.gz")
assert globber(nzo.admin_path, "SABnzbd_nzf*")
# Should have picked up the default category settings
assert nzo.cat == "*"
assert nzo.script == def_cat.script() == "None"
assert nzo.priority == def_cat.priority() == NORMAL_PRIORITY
assert nzo.repair and nzo.unpack and nzo.delete
# TODO: More checks!
class Server:
def __init__(self, host, priority, active):
self.host = host
self.priority = priority
self.active = active
class TestArticle:
def test_get_article(self):
article_id = "test@host" + os.urandom(8).hex() + ".sab"
article = nzbstuff.Article(article_id, randint(4321, 54321), None)
servers = []
servers.append(Server("testserver1", 10, True))
servers.append(Server("testserver2", 20, True))
servers.append(Server("testserver3", 30, True))
# Test fetching top priority server
server = servers[0]
assert article.get_article(server, servers) == article
assert article.fetcher_priority == 10
assert article.fetcher == server
assert article.get_article(server, servers) == None
article.fetcher = None
article.add_to_try_list(server)
assert article.get_article(server, servers) == None
# Test fetching when there is a higher priority server available
server = servers[2]
assert article.fetcher_priority == 10
assert article.get_article(server, servers) == None
assert article.fetcher_priority == 20
# Server should be used even if article.fetcher_priority is a higher number than server.priority
article.fetcher_priority = 30
server = servers[1]
assert article.get_article(server, servers) == article
# Inactive servers in servers list should be ignored
article.fetcher = None
article.fetcher_priority = 0
servers[1].active = False
server = servers[2]
assert article.get_article(server, servers) == article
assert article.tries == 3
class TestNZBStuffHelpers:
@pytest.mark.parametrize(
"argument, name, password",
[
("my_awesome_nzb_file{{password}}", "my_awesome_nzb_file", "password"),
("file_with_text_after_pw{{passw0rd}}_[180519]", "file_with_text_after_pw", "passw0rd"),
("file_without_pw", "file_without_pw", None),
("multiple_pw{{first-pw}}_{{second-pw}}", "multiple_pw", "first-pw}}_{{second-pw"), # Greed is Good
("デビアン", "デビアン", None), # Unicode
("Gentoo_Hobby_Edition {{secret}}", "Gentoo_Hobby_Edition", "secret"), # Space between name and password
("Test {{secret}}.nzb", "Test", "secret"),
("Mandrake{{top{{secret}}", "Mandrake", "top{{secret"), # Double opening {{
("Красная}}{{Шляпа}}", "Красная}}", "Шляпа"), # Double closing }}
("{{Jobname{{PassWord}}", "{{Jobname", "PassWord"), # {{ at start
("Hello/kITTY", "Hello", "kITTY"), # Notation with slash
("Hello/kITTY.nzb", "Hello", "kITTY"), # Notation with slash and extension
("/Jobname", "/Jobname", None), # Slash at start
("Jobname/Top{{Secret}}", "Jobname", "Top{{Secret}}"), # Slash with braces
("Jobname / Top{{Secret}}", "Jobname", "Top{{Secret}}"), # Slash with braces and extra spaces
("Jobname / Top{{Secret}}.nzb", "Jobname", "Top{{Secret}}"),
("לינוקס/معلومات سرية", "לינוקס", "معلومات سرية"), # LTR with slash
("לינוקס{{معلومات سرية}}", "לינוקס", "معلومات سرية"), # LTR with brackets
("thư điện tử password=mật_khẩu", "thư điện tử", "mật_khẩu"), # Password= notation
("password=PartOfTheJobname", "password=PartOfTheJobname", None), # Password= at the start
("Job password=Test.par2", "Job", "Test"), # Password= including extension
("Job}}Name{{FTW", "Job}}Name{{FTW", None), # Both {{ and }} present but incorrect order (no password)
("./Text", "./Text", None), # Name would end up empty after the function strips the dot
],
)
def test_scan_password(self, argument, name, password):
assert nzbstuff.scan_password(argument) == (name, password)
@pytest.mark.parametrize(
"file_name, clean_file_name",
[
("my_awesome_nzb_file.pAr2.nZb", "my_awesome_nzb_file"),
("my_awesome_nzb_file.....pAr2.nZb", "my_awesome_nzb_file"),
("my_awesome_nzb_file....par2..", "my_awesome_nzb_file"),
(" my_awesome_nzb_file .pAr.nZb", "my_awesome_nzb_file"),
("with.extension.and.period.par2.", "with.extension.and.period"),
("nothing.in.here", "nothing.in.here"),
(" just.space ", "just.space"),
("http://test.par2 ", "http://test.par2"),
],
)
def test_create_work_name(self, file_name, clean_file_name):
# Only test stuff specific for create_work_name
# The sanitizing is already tested in tests for sanitize_foldername
assert nzbstuff.create_work_name(file_name) == clean_file_name
@pytest.mark.parametrize(
"subject, filename",
[
('Great stuff (001/143) - "Filename.txt" yEnc (1/1)', "Filename.txt"),
(
'"910a284f98ebf57f6a531cd96da48838.vol01-03.par2" yEnc (1/3)',
"910a284f98ebf57f6a531cd96da48838.vol01-03.par2",
),
('Subject-KrzpfTest [02/30] - ""KrzpfTest.part.nzb"" yEnc', "KrzpfTest.part.nzb"),
(
'[PRiVATE]-[WtFnZb]-[Supertje-_S03E11-12_-blabla_+_blabla_WEBDL-480p.mkv]-[4/12] - "" yEnc 9786 (1/1366)',
"Supertje-_S03E11-12_-blabla_+_blabla_WEBDL-480p.mkv",
),
(
'[N3wZ] MAlXD245333\\::[PRiVATE]-[WtFnZb]-[Show.S04E04.720p.AMZN.WEBRip.x264-GalaxyTV.mkv]-[1/2] - "" yEnc 293197257 (1/573)',
"Show.S04E04.720p.AMZN.WEBRip.x264-GalaxyTV.mkv",
),
(
'reftestnzb bf1664007a71 [1/6] - "20b9152c-57eb-4d02-9586-66e30b8e3ac2" yEnc (1/22) 15728640',
"20b9152c-57eb-4d02-9586-66e30b8e3ac2",
),
(
"Re: REQ Author Child's The Book-Thanks much - Child, Lee - Author - The Book.epub (1/1)",
"REQ Author Child's The Book-Thanks much - Child, Lee - Author - The Book.epub",
),
('63258-0[001/101] - "63258-2.0" yEnc (1/250) (1/250)', "63258-2.0"),
# If specified between ", the extension is allowed to be too long
('63258-0[001/101] - "63258-2.0toolong" yEnc (1/250) (1/250)', "63258-2.0toolong"),
(
"Singer - A Album (2005) - [04/25] - 02 Sweetest Somebody (I Know).flac",
"Singer - A Album (2005) - [04/25] - 02 Sweetest Somebody (I Know).flac",
),
("<>random!>", "<>random!>"),
("nZb]-[Supertje-_S03E11-12_", "nZb]-[Supertje-_S03E11-12_"),
("Bla [Now it's done.exe]", "Now it's done.exe"),
# If specified between [], the extension should be a valid one
("Bla [Now it's done.123nonsense]", "Bla [Now it's done.123nonsense]"),
('[PRiVATE]-[WtFnZb]-[00000.clpi]-[1/46] - "" yEnc 788 (1/1)', "00000.clpi"),
(
'[PRiVATE]-[WtFnZb]-[Video_(2001)_AC5.1_-RELEASE_[TAoE].mkv]-[1/23] - "" yEnc 1234567890 (1/23456)',
"Video_(2001)_AC5.1_-RELEASE_[TAoE].mkv",
),
(
"[PRiVATE]-[WtFnZb]-[219]-[1/series.name.s01e01.1080p.web.h264-group.mkv] - "
" yEnc (1/[PRiVATE] \\c2b510b594\\::686ea969999193.155368eba4965e56a8cd263382e012.f2712fdc::/97bd201cf931/) 1 (1/0)",
"series.name.s01e01.1080p.web.h264-group.mkv",
),
(
"[PRiVATE]-[WtFnZb]-[/More.Bla.S02E01.1080p.WEB.h264-EDITH[eztv.re].mkv-WtF[nZb]/"
'More.Bla.S02E01.1080p.WEB.h264-EDITH.mkv]-[1/2] - "" yEnc 2990558544 (1/4173)',
"More.Bla.S02E01.1080p.WEB.h264-EDITH[eztv.re].mkv",
),
],
)
def test_name_extractor(self, subject, filename):
assert nzbstuff.name_extractor(subject) == filename

BIN
win/par2/arm64/par2.exe Normal file
View File

Binary file not shown.