Compare commits

..

70 Commits

Author SHA1 Message Date
Safihre
11ba9ae12a Set version to 4.5.5 2025-10-24 12:58:51 +02:00
Safihre
a61a5539a7 Merge branch '4.5.x' 2025-10-24 12:58:35 +02:00
Safihre
77f7490aea Update text files for 4.5.5 2025-10-24 12:56:26 +02:00
Safihre
a7198b6a81 Merge branch 'develop' into 4.5.x 2025-10-24 12:47:36 +02:00
Safihre
4c77954526 Add 4.5.5 to appdata 2025-10-24 12:47:26 +02:00
SABnzbd Automation
a229a2a5ea Update translatable texts
[skip ci]
2025-10-24 10:35:59 +00:00
Safihre
0a2f3865ee Check if all macOS binary files support the minimal required version 2025-10-24 12:27:48 +02:00
Safihre
900e68bb9a Resolve PyGithub deprecation warnings 2025-10-22 23:25:42 +02:00
Safihre
977dbc805f Set version to 4.5.4 2025-10-22 23:09:30 +02:00
Safihre
abcca19820 Merge branch '4.5.x' 2025-10-22 23:09:14 +02:00
Safihre
52a7b5dcff Update text files for 4.5.4 2025-10-22 23:05:40 +02:00
Safihre
9518714885 Merge branch 'develop' into 4.5.x 2025-10-22 22:35:07 +02:00
Safihre
1de674a532 Correct appdata between branches 2025-10-22 22:33:55 +02:00
Safihre
e1dad3e4c4 Start SABnzbd service after installation, if installed 2025-10-20 13:18:44 +02:00
SABnzbd Automation
44f2eb8620 Update translatable texts
[skip ci]
2025-10-20 07:57:56 +00:00
renovate[bot]
70945a9c5b Update all dependencies (#3167)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 09:57:12 +02:00
SABnzbd Automation
fdfca97dfa Update translatable texts
[skip ci]
2025-10-16 08:58:27 +00:00
Safihre
b84900dcb5 Add extra warning to Remove All Orphans
See https://forums.sabnzbd.org/viewtopic.php?p=133922
2025-10-16 10:57:31 +02:00
Safihre
d989ec928a Small styling issue for tooltip in Night mode 2025-10-14 11:12:06 +02:00
Safihre
4a89fcf8ea Update text files for 4.5.4RC1 2025-10-13 16:25:48 +02:00
SABnzbd Automation
d7fa3e1f7b Update translatable texts
[skip ci]
2025-10-13 14:24:54 +00:00
Safihre
d11e757c6e Merge branch 'develop' into 4.5.x 2025-10-13 16:24:19 +02:00
Safihre
c1417c319d Add tooltip that users need to Test Server before saving/next 2025-10-13 16:24:04 +02:00
Safihre
6689939cc9 Large par2 files could require more parsing to get all crc32 slices
Closes #3164
2025-10-13 15:26:55 +02:00
Safihre
09347d0766 Switch everything to Python 3.14 2025-10-13 07:44:35 +02:00
SABnzbd Automation
41db09057c Update translatable texts
[skip ci]
2025-10-13 01:10:52 +00:00
renovate[bot]
6983058f49 Update all dependencies (#3165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 01:09:52 +00:00
SABnzbd Automation
fb2d412c97 Update translatable texts
[skip ci]
2025-10-08 21:01:13 +00:00
Safihre
1c0b1205b2 Add quota notifications
Closes #2926
2025-10-08 22:58:08 +02:00
Safihre
f556cea488 Use release version of Python 3.14 in CI 2025-10-08 20:07:39 +02:00
Sander
a2447253a0 Local ipv4 with socks5 proxy (#3161)
* Update all dependencies

* use socks5 server as test server

* make black happy

* improved active_socks5_proxy(): default port = 1080

* improved local_ipv4()

* use int_conv

* black

* use socks.socksocket.default_proxy directly

* active_socks5_proxy cleaner with int_conv

* correct to windows-2022

* socks.socksocket.default_proxy as check

* uniform naming socks5host/port

Closes #3154
---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: sanderjo <sander.jonkers+github@github.com>
2025-10-08 19:10:47 +02:00
Safihre
3393d7c976 Changing server name shows button "failure" instead of "saving..."
Closes #1551
2025-10-06 15:54:11 +02:00
renovate[bot]
06572bdf7d Update all dependencies (#3159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 02:33:57 +00:00
Safihre
4f9ed7803f Update text files for 4.5.4Beta2 2025-10-05 23:08:14 +02:00
Safihre
95bc069af9 Merge branch 'develop' into 4.5.x 2025-10-05 22:58:51 +02:00
Safihre
d4411f1b8f Replace vendored rarfile with official one
Closes #2560
2025-10-05 22:40:30 +02:00
SABnzbd Automation
1bfd1b8f41 Update translatable texts
[skip ci]
2025-10-05 20:38:14 +00:00
Safihre
c47dbfdc26 Let unrar handle rename of chars invalid on Windows filesystem
Closes #1574

Add tests for long paths

Make sure long path is >260

Add rar test file with invalid Windows filenames

Add rar_unpack tests for unicode and passworded sets

Simplify Unrar command building

Add test for rar_invalid_windows

Remove check for 260 chars in rar_unpack

Should never happen anymore

Let Unrar rename invalid filenames

Check full path output if rar_unpack

Add helper for check

Correct test_rar_unpack_invalid_windows_filenames

Apply changes also to Direct Unpacker

Extend testing to make sure full paths are tested

Add tests for long paths inside rar

Unrar auto-rename message is different on Linux
2025-10-05 22:37:22 +02:00
Safihre
b5e55cd9b2 Unselect Multi-Operations Play/Resume on second click
Closes #2725
2025-10-03 15:06:37 +02:00
Hugo Lloreda
85c98d7203 Add option to bind outgoing connections (#3155)
* refactor outgoing interface

* refactor

* rollback old change

* We actually don't need another port


Closes #3153

* refactor

* refactor

* refactor to be compatible with old python versions

* forgot to remove match

* fix no route to host on mac

* fix no route to host on mac + rename interface to ip

* fix black + try to fix windows error

* fix black + try to fix windows error

* fix windows error

* fix windows failure

* rollback optional changes

* Remove optional type

* rollback changes + fix issue

* black change

* refactor

* missing refactor
2025-10-03 11:28:44 +02:00
Safihre
9e95717619 Enable Make Windows compatible if we cannot write special characters 2025-09-30 12:18:18 +02:00
SABnzbd Automation
90b4ff2720 Update translatable texts
[skip ci]
2025-09-30 09:28:01 +00:00
Safihre
0f97a9fdfc Workaround for macOS statvfs no longer needed
Part of Python 3.13 and above.
https://github.com/python/cpython/pull/99570
2025-09-30 11:21:14 +02:00
Safihre
90caf0c164 Lock changes to job properties during URL-grabbed NZO creation
Closes #1908
2025-09-29 15:51:26 +02:00
Safihre
9b3fe470a0 Run tests on Python 3.14
Force newer pytest

Force beta release of tavern

Unfix werkzeug

Allow latest tavern only on Python 3.11 and above

Fix test failure in Python 3.14
2025-09-29 13:29:50 +02:00
SABnzbd Automation
ab318729ab Update translatable texts
[skip ci]
2025-09-29 10:43:50 +00:00
Safihre
9576554426 Move to top/bottom for Multi edit
Closes #1088
2025-09-29 12:40:44 +02:00
Safihre
3cd819b78d Refactor history API call handling 2025-09-29 11:56:03 +02:00
renovate[bot]
bb24f3f04e Update all dependencies (#3156)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 07:28:52 +00:00
Safihre
6f4416236d Prevent renovate from updating macOS version
Due to #3131
2025-09-29 09:10:23 +02:00
SABnzbd Automation
47dcccd17f Update translatable texts
[skip ci]
2025-09-26 14:29:44 +00:00
Safihre
6b026d8274 Add way to mark job as Completed and remove Incomplete
Closes #1174
2025-09-26 16:28:57 +02:00
Safihre
ec18606557 Require correct server test in Wizard
Closes #3148
General refactor.
2025-09-25 13:40:25 +02:00
Safihre
d1d9bab65a Update text files for 4.5.4 Beta 1 2025-09-22 14:11:13 +02:00
Safihre
e2560bf214 Merge branch 'develop' into 4.5.x 2025-09-22 14:10:50 +02:00
Safihre
895c8549ba Add 4.5.4 to appdata 2025-09-22 14:09:54 +02:00
Safihre
0d80efb898 Update Python to 3.13.7 2025-09-22 13:54:16 +02:00
Safihre
deace9f8ae Add SignPath to the release notes 2025-09-22 13:30:43 +02:00
Safihre
1c96dff133 Implement SignPath binary signing for Windows releases
Keep zip structure

Download all signed artifacts for release step

Correctly download all releases

Only sign when tagging release

Restore CI tests

Test production certificate

Closes #2870
2025-09-22 12:16:10 +02:00
renovate[bot]
1734b11338 Update all dependencies (#3144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 08:34:30 +00:00
Safihre
5f3c4d17da Prevent Renovate from updating GitHub Actions Windows runner 2025-09-22 10:18:58 +02:00
jcfp
4ffe0e27fb Handle weird anime episode notation (#3146)
* handle weird anime episode notation

* make black even happier /s
2025-09-15 22:55:07 +02:00
renovate[bot]
951bc0c957 Update all dependencies (#3142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 20:00:14 +00:00
SABnzbd Automation
60f985ba00 Update translatable texts
[skip ci]
2025-09-08 19:37:06 +00:00
Safihre
a42a2db196 Github Actions Windows 2025 runners do not included NSIS 2025-09-08 21:36:13 +02:00
SABnzbd Automation
64034c5636 Update translatable texts
[skip ci]
2025-08-26 13:30:14 +00:00
Safihre
e03a031342 Add Run SABnzbd to Windows Installer 2025-08-26 15:29:34 +02:00
Safihre
da3d72b484 No longer reduce threads counter on connection loss
Closes #3137
2025-08-25 13:48:00 +02:00
SABnzbd Automation
e3042a6106 Update translatable texts
[skip ci]
2025-08-25 00:59:25 +00:00
renovate[bot]
55f1253a56 Update all dependencies (#3138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 00:58:50 +00:00
100 changed files with 2605 additions and 3781 deletions

View File

@@ -26,6 +26,11 @@
"werkzeug"
],
"packageRules": [
{
"matchManagers": ["github-actions"],
"matchPackageNames": ["windows", "macos"],
"enabled": false
},
{
"matchPackagePatterns": [
"*"

View File

@@ -9,14 +9,14 @@ env:
jobs:
build_windows:
name: Build Windows binary
runs-on: windows-latest
runs-on: windows-2022
timeout-minutes: 30
steps:
- uses: actions/checkout@v5
- name: Set up Python 3.13
uses: actions/setup-python@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
python-version: "3.14"
architecture: "x64"
cache: pip
cache-dependency-path: "**/requirements.txt"
@@ -27,18 +27,59 @@ jobs:
python -m pip install --upgrade pip wheel
pip install --upgrade -r requirements.txt --no-dependencies
pip install --upgrade -r builder/requirements.txt --no-dependencies
- name: Build Windows standalone binary and installer
run: python builder/package.py installer
- name: Upload Windows standalone binary
- name: Build Windows standalone binary
id: windows_binary
run: python builder/package.py binary
- name: Upload Windows standalone binary (unsigned)
uses: actions/upload-artifact@v4
id: upload-unsigned-binary
with:
path: "*-win64-bin.zip"
name: Windows standalone binary
- name: Sign Windows standalone binary
uses: signpath/github-action-submit-signing-request@v1
if: contains(github.ref, 'refs/tags/')
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ secrets.SIGNPATH_ORG_ID }}
project-slug: "sabnzbd"
artifact-configuration-slug: "sabnzbd-binary"
signing-policy-slug: "release-signing"
github-artifact-id: ${{ steps.upload-unsigned-binary.outputs.artifact-id }}
wait-for-completion: true
output-artifact-directory: "signed"
- name: Upload Windows standalone binary (signed)
uses: actions/upload-artifact@v4
if: contains(github.ref, 'refs/tags/')
with:
name: Windows standalone binary (signed)
path: "signed"
- name: Build Windows installer
run: python builder/package.py installer
- name: Upload Windows installer
uses: actions/upload-artifact@v4
id: upload-unsigned-installer
with:
path: "*-win-setup.exe"
name: Windows installer
- name: Sign Windows installer
uses: signpath/github-action-submit-signing-request@v1
if: contains(github.ref, 'refs/tags/')
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ secrets.SIGNPATH_ORG_ID }}
project-slug: "sabnzbd"
artifact-configuration-slug: "sabnzbd-installer"
signing-policy-slug: "release-signing"
github-artifact-id: ${{ steps.upload-unsigned-installer.outputs.artifact-id }}
wait-for-completion: true
output-artifact-directory: "signed"
- name: Upload Windows installer (signed)
if: contains(github.ref, 'refs/tags/')
uses: actions/upload-artifact@v4
with:
name: Windows installer (signed)
path: "signed/*-win-setup.exe"
build_macos:
name: Build macOS binary
@@ -48,18 +89,18 @@ jobs:
# We need the official Python, because the GA ones only support newer macOS versions
# The deployment target is picked up by the Python build tools automatically
# If updated, make sure to also set LSMinimumSystemVersion in SABnzbd.spec
PYTHON_VERSION: "3.13.5"
MACOSX_DEPLOYMENT_TARGET: "10.13"
PYTHON_VERSION: "3.14.0"
MACOSX_DEPLOYMENT_TARGET: "10.15"
# We need to force compile for universal2 support
CFLAGS: -arch x86_64 -arch arm64
ARCHFLAGS: -arch x86_64 -arch arm64
steps:
- uses: actions/checkout@v5
- name: Set up Python 3.13
- name: Set up Python
# Only use this for the caching of pip packages!
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.13"
python-version: "3.14"
cache: pip
cache-dependency-path: "**/requirements.txt"
- name: Cache Python download
@@ -89,8 +130,8 @@ jobs:
if: env.CERTIFICATES_P12
run: |
echo $CERTIFICATES_P12 | base64 --decode > certificate.p12
security create-keychain -p "$MACOS_KEYCHAIN_TEMP_PASSWORD" build.keychain
security default-keychain -s build.keychain
security create-keychain -p "$MACOS_KEYCHAIN_TEMP_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$MACOS_KEYCHAIN_TEMP_PASSWORD" build.keychain
security set-keychain-settings -lut 21600 build.keychain
security import certificate.p12 -k build.keychain -P "$CERTIFICATES_P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productsign -T /usr/bin/xcrun
@@ -161,7 +202,7 @@ jobs:
path: ${{ steps.snapcraft.outputs.snap }}
- name: Publish snap
uses: snapcore/action-publish@v1
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
if: contains(github.ref, 'refs/tags/')
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_TOKEN }}
with:
@@ -176,17 +217,24 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.x"
python-version: "3.14"
cache: pip
cache-dependency-path: "builder/release-requirements.txt"
- name: Download all artifacts
- name: Download Source distribution artifact
uses: actions/download-artifact@v5
with:
path: dist
- name: Move all artifacts to main folder
run: find dist -type f -exec mv {} . \;
name: Source distribution
- name: Download macOS artifact
uses: actions/download-artifact@v5
with:
name: macOS binary
- name: Download Windows artifacts
uses: actions/download-artifact@v5
with:
pattern: ${{ (contains(github.ref, 'refs/tags/')) && '*signed*' || '*Windows*' }}
merge-multiple: true
- name: Prepare official release
env:
AUTOMATION_GITHUB_TOKEN: ${{ secrets.AUTOMATION_GITHUB_TOKEN }}

View File

@@ -31,21 +31,21 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
name: ["Linux"]
os: [ubuntu-latest]
include:
- name: macOS
os: macos-13
python-version: "3.13"
python-version: "3.14"
- name: Windows
os: windows-latest
python-version: "3.13"
os: windows-2022
python-version: "3.14"
steps:
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: pip

View File

@@ -10,7 +10,7 @@ jobs:
if: github.repository_owner == 'sabnzbd'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v10
with:
days-before-stale: 21
days-before-close: 7

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@v6.0.1
uses: stefanzweifel/git-auto-commit-action@v7.0.0
if: env.TX_TOKEN
with:
commit_message: |

View File

@@ -66,3 +66,12 @@ Conditions:
- Bugfixes created specifically for a release branch are done there (because they are specific, they're not cherry-picked to `develop`).
- Bugfixes done on `develop` may be cherry-picked to a release branch.
- We will not release a 1.0.2 if a 1.1.0 has already been released.
## Privacy Policy
This program will not transfer any information to other networked systems unless
specifically requested by the user or the person installing or operating it.
## Code Signing Policy
For our Windows release, free code signing is provided by [SignPath.io](https://signpath.io), certificate by [SignPath Foundation](https://signpath.org).

View File

@@ -1,6 +1,42 @@
Release Notes - SABnzbd 4.5.3
Release Notes - SABnzbd 4.5.5
=========================================================
## Bug fixes and changes in 4.5.5
* macOS: Failed to start on versions of macOS older than 11.
Python 3.14 dropped support for macOS 10.13 and 10.14.
Because of that macOS 10.15 is required to run 4.5.5.
## Bug fixes and changes in 4.5.4
### New Features
* History details now includes option to mark job as `Completed`.
* `Quota` notifications available for all notification services.
- Sends alerts at 75%, 90%, and 100% quota usage.
* Multi-Operations now supports Move to Top/Bottom.
* New `outgoing_nntp_ip` option to bind outgoing NNTP connections to specific IP address.
### Improvements
* Setup wizard now requires successful Server Test before proceeding.
* Anime episode notation `S04 - 10` now supported for Sorting and Duplicate Detection.
* Multi-Operations: Play/Resume button unselects on second click for better usability.
* Unrar now handles renaming of invalid characters on Windows filesystem.
* Switched from vendored `sabnzbd.rarfile` module to `rarfile>=4.2`.
* Warning displayed when removing all Orphaned jobs (clears Temporary Download folder).
### Bug Fixes
* Active connections counter in Status window now updates correctly.
* Job setting changes during URL-grabbing no longer ignored.
* Incomplete `.par2` file parsing no longer leaves files behind.
* `Local IPv4 address` now detectable when using Socks5 proxy.
* Server configuration changes no longer show `Failure` message during page reload.
### Platform-Specific
* Linux: `Make Windows compatible` automatically enabled when needed.
* Windows: Executables are now signed using SignPath Foundation certificate.
* Windows: Can now start SABnzbd directly from installer.
* Windows and macOS: Binaries now use Python 3.14.
## Bug fixes and changes in 4.5.3
* Remember if `Permanently delete` was previously checked.
@@ -52,16 +88,19 @@ Release Notes - SABnzbd 4.5.3
## Upgrade notices
* You can directly upgrade from version 3.0.0 and newer.
* Upgrading from older versions will require performing a `Queue repair`.
* Downgrading from version 4.2.0 or newer to 3.7.2 or older will require
performing a `Queue repair` due to changes in the internal data format.
* Direct upgrade supported from version 3.0.0 and newer.
* Older versions require performing a `Queue repair` after upgrading.
## Known problems and solutions
* Read `ISSUES.txt` or https://sabnzbd.org/wiki/introduction/known-issues
## Code Signing Policy
Windows code signing is provided by SignPath.io using a SignPath Foundation certificate.
## About
SABnzbd is an open-source cross-platform binary newsreader.
It simplifies the process of downloading from Usenet dramatically, thanks to its web-based
user interface and advanced built-in post-processing options that automatically verify, repair,

View File

@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import glob
import platform
import re
import sys
import os
@@ -28,6 +27,7 @@ import tarfile
import urllib.request
import urllib.error
import configobj
import packaging.version
from typing import List
from constants import (
@@ -70,9 +70,9 @@ def delete_files_glob(glob_pattern: str, allow_no_matches: bool = False):
raise FileNotFoundError(f"No files found that match '{glob_pattern}'")
def run_external_command(command: List[str], print_output: bool = True):
def run_external_command(command: List[str], print_output: bool = True, **kwargs):
"""Wrapper to ease the use of calling external programs"""
process = subprocess.Popen(command, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
process = subprocess.Popen(command, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs)
output, _ = process.communicate()
ret = process.wait()
if (output and print_output) or ret != 0:
@@ -109,6 +109,52 @@ def patch_version_file(release_name):
ver.write(version_file)
def test_macos_min_version(binary_path: str):
# Skip check if nothing was set
if macos_min_version := os.environ.get("MACOSX_DEPLOYMENT_TARGET"):
# Skip any arm64 specific files
if "arm64" in binary_path:
print(f"Skipping arm64 binary {binary_path}")
return
# Check minimum macOS version is at least mac OS10.13
# We only check the x86_64 since for arm64 it's always macOS 11+
print(f"Checking if binary supports macOS {macos_min_version} and above: {binary_path}")
otool_output = run_external_command(
[
"otool",
"-arch",
"x86_64",
"-l",
binary_path,
],
print_output=False,
)
# Parse the output for LC_BUILD_VERSION minos
# The output is very large, so that's why we enumerate over it
req_version = packaging.version.parse(macos_min_version)
bin_version = None
lines = otool_output.split("\n")
for line_nr, line in enumerate(lines):
if "LC_VERSION_MIN_MACOSX" in line:
# Display the version in the next lines
bin_version = packaging.version.parse(lines[line_nr + 2].split()[1])
elif "minos" in line:
bin_version = packaging.version.parse(line.split()[1])
if bin_version and bin_version > req_version:
raise ValueError(f"{binary_path} requires {bin_version}, we want {req_version}")
else:
# We got the information we need
break
else:
print(lines)
raise RuntimeError(f"Could not determine minimum macOS version for {binary_path}")
else:
print(f"Skipping macOS version check, MACOSX_DEPLOYMENT_TARGET not set")
def test_sab_binary(binary_path: str):
"""Wrapper to have a simple start-up test for the binary"""
with tempfile.TemporaryDirectory() as config_dir:
@@ -201,23 +247,21 @@ if __name__ == "__main__":
if not os.path.exists("locale"):
raise FileNotFoundError("Failed to compile language files")
# Make sure we remove any existing build-folders
safe_remove("build")
safe_remove("dist")
safe_remove(RELEASE_NAME)
# Copy the specification
shutil.copyfile("builder/SABnzbd.spec", "SABnzbd.spec")
if "binary" in sys.argv or "installer" in sys.argv:
if "binary" in sys.argv:
# Must be run on Windows
if sys.platform != "win32":
raise RuntimeError("Binary should be created on Windows")
# Make sure we remove any existing build-folders
safe_remove("build")
safe_remove("dist")
# Remove any leftovers
safe_remove(RELEASE_NAME)
safe_remove(RELEASE_BINARY)
# Run PyInstaller and check output
shutil.copyfile("builder/SABnzbd.spec", "SABnzbd.spec")
run_external_command([sys.executable, "-O", "-m", "PyInstaller", "SABnzbd.spec"])
shutil.copytree("dist/SABnzbd-console", "dist/SABnzbd", dirs_exist_ok=True)
@@ -228,33 +272,49 @@ if __name__ == "__main__":
delete_files_glob("dist/SABnzbd/api-ms-win*.dll", allow_no_matches=True)
delete_files_glob("dist/SABnzbd/ucrtbase.dll", allow_no_matches=True)
if "installer" in sys.argv:
# Compile NSIS translations
safe_remove("NSIS_Installer.nsi")
safe_remove("NSIS_Installer.nsi.tmp")
shutil.copyfile("builder/win/NSIS_Installer.nsi", "NSIS_Installer.nsi")
run_external_command([sys.executable, "tools/make_mo.py", "nsis"])
# Run NSIS to build installer
run_external_command(
[
"makensis.exe",
"/V3",
"/DSAB_VERSION=%s" % RELEASE_VERSION,
"/DSAB_VERSIONKEY=%s" % ".".join(map(str, RELEASE_VERSION_TUPLE)),
"/DSAB_FILE=%s" % RELEASE_INSTALLER,
"NSIS_Installer.nsi.tmp",
]
)
# Rename the folder
shutil.copytree("dist/SABnzbd", RELEASE_NAME)
# Test the release
test_sab_binary("dist/SABnzbd/SABnzbd.exe")
# Create the archive
run_external_command(["win/7zip/7za.exe", "a", RELEASE_BINARY, RELEASE_NAME])
run_external_command(["win/7zip/7za.exe", "a", RELEASE_BINARY, "SABnzbd"], cwd="dist")
shutil.move(f"dist/{RELEASE_BINARY}", RELEASE_BINARY)
# Test the release, as the very last step to not mess with any release code
test_sab_binary("dist/SABnzbd/SABnzbd.exe")
if "installer" in sys.argv:
# Check if we have the dist folder
if not os.path.exists("dist/SABnzbd/SABnzbd.exe"):
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}"):
print("Using signed version of SABnzbd binaries")
safe_remove("dist/SABnzbd")
run_external_command(["win/7zip/7za.exe", "x", "-odist", f"signed/{RELEASE_BINARY}"])
# Make sure it exists
if not os.path.exists("dist/SABnzbd/SABnzbd.exe"):
raise FileNotFoundError("SABnzbd executable not found, signed zip extraction failed")
elif RELEASE_THIS:
raise FileNotFoundError("Signed SABnzbd executable not found, required for release!")
else:
print("Using unsigned version of SABnzbd binaries")
# Compile NSIS translations
safe_remove("NSIS_Installer.nsi")
safe_remove("NSIS_Installer.nsi.tmp")
shutil.copyfile("builder/win/NSIS_Installer.nsi", "NSIS_Installer.nsi")
run_external_command([sys.executable, "tools/make_mo.py", "nsis"])
# Run NSIS to build installer
run_external_command(
[
"makensis.exe",
"/V3",
"/DSAB_VERSION=%s" % RELEASE_VERSION,
"/DSAB_VERSIONKEY=%s" % ".".join(map(str, RELEASE_VERSION_TUPLE)),
"/DSAB_FILE=%s" % RELEASE_INSTALLER,
"NSIS_Installer.nsi.tmp",
]
)
if "app" in sys.argv:
# Must be run on macOS
@@ -276,7 +336,11 @@ if __name__ == "__main__":
"macos/7zip/7zz",
]
for file_to_sign in files_to_sign:
print("Signing %s with hardended runtime" % file_to_sign)
# Make sure it supports the macOS versions we want first
test_macos_min_version(file_to_sign)
# Then sign in
print("Signing %s with hardened runtime" % file_to_sign)
run_external_command(
[
"codesign",
@@ -296,17 +360,21 @@ if __name__ == "__main__":
print("Signed %s!" % file_to_sign)
# Run PyInstaller and check output
shutil.copyfile("builder/SABnzbd.spec", "SABnzbd.spec")
run_external_command([sys.executable, "-O", "-m", "PyInstaller", "SABnzbd.spec"])
# Make sure we created a fully universal2 release when releasing or during CI
if RELEASE_THIS or ON_GITHUB_ACTIONS:
for bin_to_check in glob.glob("dist/SABnzbd.app/Contents/MacOS/**/*.so", recursive=True):
for bin_to_check in glob.glob("dist/SABnzbd.app/**/*.so", recursive=True):
print("Checking if binary is universal2: %s" % bin_to_check)
file_output = run_external_command(["file", bin_to_check], print_output=False)
# Make sure we have both arm64 and x86
if not ("x86_64" in file_output and "arm64" in file_output):
raise RuntimeError("Non-universal2 binary found!")
# Make sure it supports the macOS versions we want
test_macos_min_version(bin_to_check)
# Only continue if we can sign
if authority:
# We use PyInstaller to sign the main SABnzbd executable and the SABnzbd.app

View File

@@ -1,2 +1,2 @@
PyGithub==2.7.0
PyGithub==2.8.1
praw==7.8.1

View File

@@ -75,7 +75,7 @@ print("----")
# Check if tagged as release and check for token
gh_token = os.environ.get("AUTOMATION_GITHUB_TOKEN", "")
if RELEASE_THIS and gh_token:
gh_obj = github.Github(gh_token)
gh_obj = github.Github(auth=github.Auth.Token(gh_token))
gh_repo = gh_obj.get_repo("sabnzbd/sabnzbd")
# Read the release notes
@@ -86,7 +86,7 @@ if RELEASE_THIS and gh_token:
for release in gh_repo.get_releases():
if release.tag_name == RELEASE_VERSION:
gh_release = release
print("Found existing release %s" % gh_release.title)
print("Found existing release %s" % gh_release.name)
break
else:
# Did not find it, so create the release, use the GitHub tag we got as input

View File

@@ -1,10 +1,10 @@
# Basic build requirements
# Note that not all sub-dependencies are listed, but only ones we know could cause trouble
pyinstaller==6.15.0
pyinstaller==6.16.0
packaging==25.0
pyinstaller-hooks-contrib==2025.8
pyinstaller-hooks-contrib==2025.9
altgraph==0.17.4
wrapt==1.17.3
wrapt==2.0.0
setuptools==80.9.0
# For the Windows build
@@ -16,4 +16,4 @@ dmgbuild==1.6.5; sys_platform == 'darwin'
mac-alias==2.2.2; sys_platform == 'darwin'
macholib==1.16.3; sys_platform == 'darwin'
ds-store==1.3.1; sys_platform == 'darwin'
PyNaCl==1.5.0; sys_platform == 'darwin'
PyNaCl==1.6.0; sys_platform == 'darwin'

View File

@@ -29,6 +29,7 @@ Unicode true
!include "nsProcess.nsh"
!include "x64.nsh"
!include "servicelib.nsh"
!include "StdUtils.nsh"
;------------------------------------------------------------------
;
@@ -139,9 +140,9 @@ Unicode true
!insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER
!insertmacro MUI_PAGE_INSTFILES
; !define MUI_FINISHPAGE_RUN
; !define MUI_FINISHPAGE_RUN_FUNCTION PageFinishRun
; !define MUI_FINISHPAGE_RUN_TEXT $(MsgRunSAB)
!define MUI_FINISHPAGE_RUN
!define MUI_FINISHPAGE_RUN_FUNCTION PageFinishRun
!define MUI_FINISHPAGE_RUN_TEXT $(MsgRunSAB)
!define MUI_FINISHPAGE_SHOWREADME "$INSTDIR\README.txt"
!define MUI_FINISHPAGE_SHOWREADME_TEXT $(MsgShowRelNote)
!define MUI_FINISHPAGE_LINK $(MsgSupportUs)
@@ -154,12 +155,21 @@ Unicode true
!insertmacro MUI_UNPAGE_COMPONENTS
!insertmacro MUI_UNPAGE_INSTFILES
;------------------------------------------------------------------
; Run as user-level at end of install
; DOES NOT WORK
; Function PageFinishRun
; !insertmacro UAC_AsUser_ExecShell "" "$INSTDIR\SABnzbd.exe" "" "" ""
; FunctionEnd
Function PageFinishRun
; Check if SABnzbd service is installed
!insertmacro SERVICE "installed" "SABnzbd" ""
Pop $0 ;response
${If} $0 == true
; Service is installed, start the service
!insertmacro SERVICE "start" "SABnzbd" ""
${Else}
; Service not installed, run executable as user
${StdUtils.ExecShellAsUser} $0 "$INSTDIR\SABnzbd.exe" "" ""
${EndIf}
FunctionEnd
;------------------------------------------------------------------
@@ -188,7 +198,6 @@ Unicode true
!insertmacro MUI_LANGUAGE "SimpChinese"
;------------------------------------------------------------------
;Reserve Files
;If you are using solid compression, files that are required before
@@ -361,14 +370,6 @@ Function .onInit
FunctionEnd
;------------------------------------------------------------------
; Show the shortcuts at end of install so user can start SABnzbd
; This is instead of us trying to run SAB from the installer
;
Function .onInstSuccess
ExecShell "open" "$SMPROGRAMS\$STARTMENU_FOLDER"
FunctionEnd
;--------------------------------
; begin uninstall settings/section
UninstallText $(MsgUninstall)
@@ -410,6 +411,8 @@ SectionEnd
;Language strings
LangString MsgShowRelNote ${LANG_ENGLISH} "Show Release Notes"
LangString MsgRunSAB ${LANG_ENGLISH} "Run SABnzbd"
LangString MsgSupportUs ${LANG_ENGLISH} "Support the project, Donate!"
LangString MsgServChange ${LANG_ENGLISH} "The SABnzbd Windows Service changed in SABnzbd 3.0.0. $\nYou will need to reinstall the SABnzbd service. $\n$\nClick `OK` to remove the existing services or `Cancel` to cancel this upgrade."

View File

@@ -0,0 +1,501 @@
#################################################################################
# StdUtils plug-in for NSIS
# Copyright (C) 2004-2018 LoRd_MuldeR <MuldeR2@GMX.de>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# http://www.gnu.org/licenses/lgpl-2.1.txt
#################################################################################
# DEVELOPER NOTES:
# - Please see "https://github.com/lordmulder/stdutils/" for news and updates!
# - Please see "Docs\StdUtils\StdUtils.html" for detailed function descriptions!
# - Please see "Examples\StdUtils\StdUtilsTest.nsi" for usage examples!
#################################################################################
# FUNCTION DECLARTIONS
#################################################################################
!ifndef ___STDUTILS__NSH___
!define ___STDUTILS__NSH___
!define StdUtils.Time '!insertmacro _StdU_Time' #time(), as in C standard library
!define StdUtils.GetMinutes '!insertmacro _StdU_GetMinutes' #GetSystemTimeAsFileTime(), returns the number of minutes
!define StdUtils.GetHours '!insertmacro _StdU_GetHours' #GetSystemTimeAsFileTime(), returns the number of hours
!define StdUtils.GetDays '!insertmacro _StdU_GetDays' #GetSystemTimeAsFileTime(), returns the number of days
!define StdUtils.Rand '!insertmacro _StdU_Rand' #rand(), as in C standard library
!define StdUtils.RandMax '!insertmacro _StdU_RandMax' #rand(), as in C standard library, with maximum value
!define StdUtils.RandMinMax '!insertmacro _StdU_RandMinMax' #rand(), as in C standard library, with minimum/maximum value
!define StdUtils.RandList '!insertmacro _StdU_RandList' #rand(), as in C standard library, with list support
!define StdUtils.RandBytes '!insertmacro _StdU_RandBytes' #Generates random bytes, returned as Base64-encoded string
!define StdUtils.FormatStr '!insertmacro _StdU_FormatStr' #sprintf(), as in C standard library, one '%d' placeholder
!define StdUtils.FormatStr2 '!insertmacro _StdU_FormatStr2' #sprintf(), as in C standard library, two '%d' placeholders
!define StdUtils.FormatStr3 '!insertmacro _StdU_FormatStr3' #sprintf(), as in C standard library, three '%d' placeholders
!define StdUtils.ScanStr '!insertmacro _StdU_ScanStr' #sscanf(), as in C standard library, one '%d' placeholder
!define StdUtils.ScanStr2 '!insertmacro _StdU_ScanStr2' #sscanf(), as in C standard library, two '%d' placeholders
!define StdUtils.ScanStr3 '!insertmacro _StdU_ScanStr3' #sscanf(), as in C standard library, three '%d' placeholders
!define StdUtils.TrimStr '!insertmacro _StdU_TrimStr' #Remove whitspaces from string, left and right
!define StdUtils.TrimStrLeft '!insertmacro _StdU_TrimStrLeft' #Remove whitspaces from string, left side only
!define StdUtils.TrimStrRight '!insertmacro _StdU_TrimStrRight' #Remove whitspaces from string, right side only
!define StdUtils.RevStr '!insertmacro _StdU_RevStr' #Reverse a string, e.g. "reverse me" <-> "em esrever"
!define StdUtils.ValidFileName '!insertmacro _StdU_ValidFileName' #Test whether string is a valid file name - no paths allowed
!define StdUtils.ValidPathSpec '!insertmacro _StdU_ValidPathSpec' #Test whether string is a valid full(!) path specification
!define StdUtils.ValidDomainName '!insertmacro _StdU_ValidDomain' #Test whether string is a valid host name or domain name
!define StdUtils.StrToUtf8 '!insertmacro _StdU_StrToUtf8' #Convert string from Unicode (UTF-16) or ANSI to UTF-8 bytes
!define StdUtils.StrFromUtf8 '!insertmacro _StdU_StrFromUtf8' #Convert string from UTF-8 bytes to Unicode (UTF-16) or ANSI
!define StdUtils.SHFileMove '!insertmacro _StdU_SHFileMove' #SHFileOperation(), using the FO_MOVE operation
!define StdUtils.SHFileCopy '!insertmacro _StdU_SHFileCopy' #SHFileOperation(), using the FO_COPY operation
!define StdUtils.AppendToFile '!insertmacro _StdU_AppendToFile' #Append contents of an existing file to another file
!define StdUtils.ExecShellAsUser '!insertmacro _StdU_ExecShlUser' #ShellExecute() as NON-elevated user from elevated installer
!define StdUtils.InvokeShellVerb '!insertmacro _StdU_InvkeShlVrb' #Invokes a "shell verb", e.g. for pinning items to the taskbar
!define StdUtils.ExecShellWaitEx '!insertmacro _StdU_ExecShlWaitEx' #ShellExecuteEx(), returns the handle of the new process
!define StdUtils.WaitForProcEx '!insertmacro _StdU_WaitForProcEx' #WaitForSingleObject(), e.g. to wait for a running process
!define StdUtils.GetParameter '!insertmacro _StdU_GetParameter' #Get the value of a specific command-line option
!define StdUtils.TestParameter '!insertmacro _StdU_TestParameter' #Test whether a specific command-line option has been set
!define StdUtils.ParameterCnt '!insertmacro _StdU_ParameterCnt' #Get number of command-line tokens, similar to argc in main()
!define StdUtils.ParameterStr '!insertmacro _StdU_ParameterStr' #Get the n-th command-line token, similar to argv[i] in main()
!define StdUtils.GetAllParameters '!insertmacro _StdU_GetAllParams' #Get complete command-line, but without executable name
!define StdUtils.GetRealOSVersion '!insertmacro _StdU_GetRealOSVer' #Get the *real* Windows version number, even on Windows 8.1+
!define StdUtils.GetRealOSBuildNo '!insertmacro _StdU_GetRealOSBld' #Get the *real* Windows build number, even on Windows 8.1+
!define StdUtils.GetRealOSName '!insertmacro _StdU_GetRealOSStr' #Get the *real* Windows version, as a "friendly" name
!define StdUtils.GetOSEdition '!insertmacro _StdU_GetOSEdition' #Get the Windows edition, i.e. "workstation" or "server"
!define StdUtils.GetOSReleaseId '!insertmacro _StdU_GetOSRelIdNo' #Get the Windows release identifier (on Windows 10)
!define StdUtils.GetOSReleaseName '!insertmacro _StdU_GetOSRelIdStr' #Get the Windows release (on Windows 10), as a "friendly" name
!define StdUtils.VerifyOSVersion '!insertmacro _StdU_VrfyRealOSVer' #Compare *real* operating system to an expected version number
!define StdUtils.VerifyOSBuildNo '!insertmacro _StdU_VrfyRealOSBld' #Compare *real* operating system to an expected build number
!define StdUtils.HashText '!insertmacro _StdU_HashText' #Compute hash from text string (CRC32, MD5, SHA1/2/3, BLAKE2)
!define StdUtils.HashFile '!insertmacro _StdU_HashFile' #Compute hash from file (CRC32, MD5, SHA1/2/3, BLAKE2)
!define StdUtils.NormalizePath '!insertmacro _StdU_NormalizePath' #Simplifies the path to produce a direct, well-formed path
!define StdUtils.GetParentPath '!insertmacro _StdU_GetParentPath' #Get parent path by removing the last component from the path
!define StdUtils.SplitPath '!insertmacro _StdU_SplitPath' #Split the components of the given path
!define StdUtils.GetDrivePart '!insertmacro _StdU_GetDrivePart' #Get drive component of path
!define StdUtils.GetDirectoryPart '!insertmacro _StdU_GetDirPart' #Get directory component of path
!define StdUtils.GetFileNamePart '!insertmacro _StdU_GetFNamePart' #Get file name component of path
!define StdUtils.GetExtensionPart '!insertmacro _StdU_GetExtnPart' #Get file extension component of path
!define StdUtils.TimerCreate '!insertmacro _StdU_TimerCreate' #Create a new event-timer that will be triggered periodically
!define StdUtils.TimerDestroy '!insertmacro _StdU_TimerDestroy' #Destroy a running timer created with TimerCreate()
!define StdUtils.ProtectStr '!insertmacro _StdU_PrtctStr' #Protect a given String using Windows' DPAPI
!define StdUtils.UnprotectStr '!insertmacro _StdU_UnprtctStr' #Unprotect a string that was protected via ProtectStr()
!define StdUtils.GetLibVersion '!insertmacro _StdU_GetLibVersion' #Get the current StdUtils library version (for debugging)
!define StdUtils.SetVerbose '!insertmacro _StdU_SetVerbose' #Enable or disable "verbose" mode (for debugging)
#################################################################################
# MACRO DEFINITIONS
#################################################################################
!macro _StdU_Time out
StdUtils::Time /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_GetMinutes out
StdUtils::GetMinutes /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_GetHours out
StdUtils::GetHours /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_GetDays out
StdUtils::GetDays /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_Rand out
StdUtils::Rand /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_RandMax out max
push ${max}
StdUtils::RandMax /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_RandMinMax out min max
push ${min}
push ${max}
StdUtils::RandMinMax /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_RandList count max
push ${max}
push ${count}
StdUtils::RandList /NOUNLOAD
!macroend
!macro _StdU_RandBytes out count
push ${count}
StdUtils::RandBytes /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_FormatStr out format val
push `${format}`
push ${val}
StdUtils::FormatStr /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_FormatStr2 out format val1 val2
push `${format}`
push ${val1}
push ${val2}
StdUtils::FormatStr2 /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_FormatStr3 out format val1 val2 val3
push `${format}`
push ${val1}
push ${val2}
push ${val3}
StdUtils::FormatStr3 /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_ScanStr out format input default
push `${format}`
push `${input}`
push ${default}
StdUtils::ScanStr /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_ScanStr2 out1 out2 format input default1 default2
push `${format}`
push `${input}`
push ${default1}
push ${default2}
StdUtils::ScanStr2 /NOUNLOAD
pop ${out1}
pop ${out2}
!macroend
!macro _StdU_ScanStr3 out1 out2 out3 format input default1 default2 default3
push `${format}`
push `${input}`
push ${default1}
push ${default2}
push ${default3}
StdUtils::ScanStr3 /NOUNLOAD
pop ${out1}
pop ${out2}
pop ${out3}
!macroend
!macro _StdU_TrimStr var
push ${var}
StdUtils::TrimStr /NOUNLOAD
pop ${var}
!macroend
!macro _StdU_TrimStrLeft var
push ${var}
StdUtils::TrimStrLeft /NOUNLOAD
pop ${var}
!macroend
!macro _StdU_TrimStrRight var
push ${var}
StdUtils::TrimStrRight /NOUNLOAD
pop ${var}
!macroend
!macro _StdU_RevStr var
push ${var}
StdUtils::RevStr /NOUNLOAD
pop ${var}
!macroend
!macro _StdU_ValidFileName out test
push `${test}`
StdUtils::ValidFileName /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_ValidPathSpec out test
push `${test}`
StdUtils::ValidPathSpec /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_ValidDomain out test
push `${test}`
StdUtils::ValidDomainName /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_StrToUtf8 out str
push `${str}`
StdUtils::StrToUtf8 /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_StrFromUtf8 out trnc str
push ${trnc}
push `${str}`
StdUtils::StrFromUtf8 /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_SHFileMove out from to hwnd
push `${from}`
push `${to}`
push ${hwnd}
StdUtils::SHFileMove /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_SHFileCopy out from to hwnd
push `${from}`
push `${to}`
push ${hwnd}
StdUtils::SHFileCopy /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_AppendToFile out from dest offset maxlen
push `${from}`
push `${dest}`
push ${offset}
push ${maxlen}
StdUtils::AppendToFile /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_ExecShlUser out file verb args
push `${file}`
push `${verb}`
push `${args}`
StdUtils::ExecShellAsUser /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_InvkeShlVrb out path file verb_id
push "${path}"
push "${file}"
push ${verb_id}
StdUtils::InvokeShellVerb /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_ExecShlWaitEx out_res out_val file verb args
push `${file}`
push `${verb}`
push `${args}`
StdUtils::ExecShellWaitEx /NOUNLOAD
pop ${out_res}
pop ${out_val}
!macroend
!macro _StdU_WaitForProcEx out handle
push `${handle}`
StdUtils::WaitForProcEx /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_GetParameter out name default
push `${name}`
push `${default}`
StdUtils::GetParameter /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_TestParameter out name
push `${name}`
StdUtils::TestParameter /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_ParameterCnt out
StdUtils::ParameterCnt /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_ParameterStr out index
push ${index}
StdUtils::ParameterStr /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_GetAllParams out truncate
push `${truncate}`
StdUtils::GetAllParameters /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_GetRealOSVer out_major out_minor out_spack
StdUtils::GetRealOsVersion /NOUNLOAD
pop ${out_major}
pop ${out_minor}
pop ${out_spack}
!macroend
!macro _StdU_GetRealOSBld out
StdUtils::GetRealOsBuildNo /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_GetRealOSStr out
StdUtils::GetRealOsName /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_VrfyRealOSVer out major minor spack
push `${major}`
push `${minor}`
push `${spack}`
StdUtils::VerifyRealOsVersion /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_VrfyRealOSBld out build
push `${build}`
StdUtils::VerifyRealOsBuildNo /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_GetOSEdition out
StdUtils::GetOsEdition /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_GetOSRelIdNo out
StdUtils::GetOsReleaseId /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_GetOSRelIdStr out
StdUtils::GetOsReleaseName /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_HashText out type text
push `${type}`
push `${text}`
StdUtils::HashText /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_HashFile out type file
push `${type}`
push `${file}`
StdUtils::HashFile /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_NormalizePath out path
push `${path}`
StdUtils::NormalizePath /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_GetParentPath out path
push `${path}`
StdUtils::GetParentPath /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_SplitPath out_drive out_dir out_fname out_ext path
push `${path}`
StdUtils::SplitPath /NOUNLOAD
pop ${out_drive}
pop ${out_dir}
pop ${out_fname}
pop ${out_ext}
!macroend
!macro _StdU_GetDrivePart out path
push `${path}`
StdUtils::GetDrivePart /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_GetDirPart out path
push `${path}`
StdUtils::GetDirectoryPart /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_GetFNamePart out path
push `${path}`
StdUtils::GetFileNamePart /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_GetExtnPart out path
push `${path}`
StdUtils::GetExtensionPart /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_TimerCreate out callback interval
GetFunctionAddress ${out} ${callback}
push ${out}
push ${interval}
StdUtils::TimerCreate /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_TimerDestroy out timer_id
push ${timer_id}
StdUtils::TimerDestroy /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_PrtctStr out dpsc salt text
push `${dpsc}`
push `${salt}`
push `${text}`
StdUtils::ProtectStr /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_UnprtctStr out trnc salt data
push `${trnc}`
push `${salt}`
push `${data}`
StdUtils::UnprotectStr /NOUNLOAD
pop ${out}
!macroend
!macro _StdU_GetLibVersion out_ver out_tst
StdUtils::GetLibVersion /NOUNLOAD
pop ${out_ver}
pop ${out_tst}
!macroend
!macro _StdU_SetVerbose enable
Push ${enable}
StdUtils::SetVerboseMode /NOUNLOAD
!macroend
#################################################################################
# MAGIC NUMBERS
#################################################################################
!define StdUtils.Const.ShellVerb.PinToTaskbar 0
!define StdUtils.Const.ShellVerb.UnpinFromTaskbar 1
!define StdUtils.Const.ShellVerb.PinToStart 2
!define StdUtils.Const.ShellVerb.UnpinFromStart 3
!endif # !___STDUTILS__NSH___

View File

Binary file not shown.

View File

@@ -132,7 +132,7 @@
<textarea name="notes" id="notes" rows="3" cols="50"></textarea>
</div>
<div class="field-pair no-field-pair-bg">
<button class="btn btn-default addNewServer" disabled><span class="glyphicon glyphicon-plus"></span> $T('button-addServer')</button>
<button class="btn btn-default addNewServer" disabled data-toggle="tooltip" data-placement="top" title="$T('wizard-test-server-required')"><span class="glyphicon glyphicon-plus"></span> $T('button-addServer')</button>
<button class="btn btn-default testServer" type="button"><span class="glyphicon glyphicon-sort"></span> $T('button-testServer')</button>
</div>
<div class="field-pair result-box">
@@ -464,14 +464,14 @@
When finished loading
**/
jQuery(document).ready(function(){
// Initialize tooltips
jQuery('[data-toggle="tooltip"]').tooltip()
// Reload form in case we change items that make the servers appear different
jQuery('input[name="priority"], input[name="displayname"], textarea[name="notes"]').on('change', function() {
jQuery('.fullform').submit(function() {
// No ajax this time
jQuery('input[name="ajax"]').val('')
// Skip the fancy stuff, just submit
this.submit()
})
jQuery('input[name="priority"], input[name="displayname"], textarea[name="notes"]').on('change', function(event) {
var parentForm = jQuery(event.target).parents("form")
parentForm.unbind("submit")
parentForm.find('input[name="ajax"]').val('')
})
/**
@@ -563,6 +563,9 @@
// Allow adding the new server if we are in the new-server section
if(theButton.parents("form[action='addServer']").length) {
jQuery(".addNewServer").removeAttr("disabled")
jQuery(".addNewServer").removeAttr("data-toggle")
jQuery(".addNewServer").removeAttr("title")
jQuery(".addNewServer").tooltip("destroy")
}
} else {
resultBox.addClass('alert-danger')
@@ -571,6 +574,10 @@
// Disable the adding of new server, just to be sure
if(theButton.parents("form[action='addServer']").length) {
jQuery(".addNewServer").attr("disabled", "disabled")
jQuery(".addNewServer").attr("data-toggle", "tooltip")
jQuery(".addNewServer").attr("data-placement", "top")
jQuery(".addNewServer").attr("title", "$T('wizard-test-server-required')")
jQuery(".addNewServer").tooltip()
}
}
});

View File

@@ -50,7 +50,6 @@ textarea,
.navbar-default .navbar-nav>li>a:focus,
.navbar-logo:hover,
.quoteBlock,
.selected,
.server-disabled,
#serverResponse,
.table>tbody>tr:nth-child(odd),
@@ -62,30 +61,10 @@ select:hover {
color: #EBEBEB !important;
}
.correct {
border: 2px solid #00cc22 !important;
}
.failed,
.required-star,
.error-text {
.failed {
color: #ff3333 !important;
}
.unselected,
.selected {
border: 1px solid #EBEBEB !important;
color: #EBEBEB !important;
}
.incorrect {
border: 2px solid #ff3333 !important;
}
.disabled-text {
color: #777 !important;
}
#rightGreyText,
small {
color: #c7c7c7 !important;
@@ -306,6 +285,14 @@ col2 h3 a,
border-top-color: #E4E4E4 !important;
}
.tooltip.left .tooltip-arrow {
border-left-color: #E4E4E4 !important;
}
.tooltip.right .tooltip-arrow {
border-right-color: #E4E4E4 !important;
}
.Special .glyphicon-asterisk {
color: #E4E4E4 !important;
}

View File

@@ -93,7 +93,10 @@
</div>
<div class="row">
<div class="col-sm-2">$T('status')</div>
<div class="col-sm-10" data-bind="text: glitterTranslate.status[historyStatus.status()] ? glitterTranslate.status[historyStatus.status()] : statusText()"></div>
<div class="col-sm-10">
<span data-bind="text: glitterTranslate.status[historyStatus.status()] ? glitterTranslate.status[historyStatus.status()] : statusText()"></span>
<a href="#" class="mark-completed-link" data-bind="visible: failed(), click: markAsCompleted" title="$T('button-mark-completed')"><span class="glyphicon glyphicon-ok"></span> $T('post-Completed')</a>
</div>
</div>
<div class="row">
<div class="col-sm-2">$T('size')</div>
@@ -146,6 +149,7 @@
<a href="#" class="hover-button history-archive" title="$T('showArchive') / $T('showAllHis')" data-tooltip="true" data-placement="top" data-bind="click: history.toggleShowArchive, css: { 'history-options-show-failed': history.showArchive }"><svg viewBox="6 6 36 36" height="14" width="14" class="archive-icon"><path d="M41.09 10.45l-2.77-3.36c-.56-.66-1.39-1.09-2.32-1.09h-24c-.93 0-1.76.43-2.31 1.09l-2.77 3.36c-.58.7-.92 1.58-.92 2.55v25c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4v-25c0-.97-.34-1.85-.91-2.55zm-17.09 24.55l-11-11h7v-4h8v4h7l-11 11zm-13.75-25l1.63-2h24l1.87 2h-27.5z"/></svg></a>
<a href="#" class="hover-button" title="$T('showFailedHis') / $T('showAllHis')" data-tooltip="true" data-placement="top" data-bind="click: history.toggleShowFailed, css: { 'history-options-show-failed': history.showFailed }"><span class="glyphicon glyphicon-exclamation-sign"></span></a>
<a href="#" class="hover-button" title="$T('link-retryAll')" data-tooltip="true" data-placement="top" data-bind="click: history.retryAllFailed"><span class="glyphicon glyphicon-repeat"></span></a>
<a href="#" class="hover-button" title="$T('button-mark-completed')" data-bind="visible: (history.isMultiEditing() && hasHistory()), click: history.doMultiMarkCompleted" data-tooltip="true" data-placement="top"><span class="glyphicon glyphicon-ok"></span></a>
<div data-bind="visible: (history.isMultiEditing() && hasHistory())">
<span class="label label-default" data-bind="text: history.multiEditItems().length">0</span>

View File

@@ -179,14 +179,20 @@
</a>
</div>
<div class="add-nzb-inputbox add-nzb-inputbox-small">
<label for="multiedit-play">
<label for="multiedit-play" data-bind="event: { mousedown: queue.handleMultiEditStatusMouseDown }">
<input type="radio" name="multiedit-status" value="resume" id="multiedit-play" data-bind="event: { change: queue.doMultiEditUpdate }" />
<span class="glyphicon glyphicon-play" title="$T('link-resume')" data-tooltip="true" data-placement="top"></span>
</label>
<label for="multiedit-pause">
<label for="multiedit-pause" data-bind="event: { mousedown: queue.handleMultiEditStatusMouseDown }">
<input type="radio" name="multiedit-status" value="pause" id="multiedit-pause" data-bind="event: { change: queue.doMultiEditUpdate }" />
<span class="glyphicon glyphicon-pause" title="$T('link-pause')" data-tooltip="true" data-placement="top"></span>
</label>
<a href="#" class="hover-button" title="$T('Glitter-top')" data-bind="click: queue.doMultiMoveToTop" data-tooltip="true" data-placement="top">
<span class="glyphicon glyphicon-chevron-up"></span>
</a>
<a href="#" class="hover-button" title="$T('Glitter-bottom')" data-bind="click: queue.doMultiMoveToBottom" data-tooltip="true" data-placement="top">
<span class="glyphicon glyphicon-chevron-down"></span>
</a>
<span class="label label-default" data-bind="text: queue.multiEditItems().length">0</span>
</div>
<div class="add-nzb-inputbox-clear"></div>

View File

@@ -52,16 +52,15 @@
var glitterTranslate = new Object();
glitterTranslate.paused = "$T('post-Paused')";
glitterTranslate.left = "$T('Glitter-left')";
glitterTranslate.clearWarn = "$T('confirm')";
glitterTranslate.clearOrphanWarning = "$T('Glitter-clearOrphanWarning')";
glitterTranslate.pausePromptFail = "$T('Glitter-pausePromptFail')"
glitterTranslate.pauseFor = "$T('pauseFor')"
glitterTranslate.minutes = "$T('mins')"
glitterTranslate.shutdown = "$T('shutdownOK?')";
glitterTranslate.restart = "$T('explain-Restart') $T('explain-needNewLogin')".replace(/\<br(\s*\/|)\>/g, '\n');
glitterTranslate.repair = "$T('explain-Repair')".replace(/<br \/>/g, "\n").replace(/&quot;/g,'"');
glitterTranslate.deleteMsg = "$T('nzo-delete')";
glitterTranslate.removeDown = "$T('confirm')";
glitterTranslate.removeDow1 = "$T('confirm')";
glitterTranslate.confirm = "$T('confirm')";
glitterTranslate.markComplete = "$T('button-mark-completed')";
glitterTranslate.renameAbort = "$T('Glitter-confirmAbortDirectUnpack')\n$T('confirm')";
glitterTranslate.retryAll = "$T('link-retryAll')?";
glitterTranslate.fetch = "$T('Glitter-fetch')";

View File

@@ -420,6 +420,42 @@ function HistoryListModel(parent) {
self.triggerRemoveDownload(self.multiEditItems())
}
// Mark jobs as completed
self.markAsCompleted = function(items) {
// Confirm
if(!confirm(glitterTranslate.markComplete)) {
return
}
// Single or multiple items?
var strIDs = '';
if(items.length) {
$.each(items, function(index) {
strIDs = strIDs + this.id + ',';
})
} else {
strIDs = items.id
}
// Send the API call
callAPI({
mode: 'history',
name: 'mark_as_completed',
value: strIDs
}).then(function(response) {
// Force refresh to update the UI
self.parent.refresh(true);
});
}
// Mark all selected as completed
self.doMultiMarkCompleted = function() {
// Anything selected?
if(self.multiEditItems().length < 1) return;
// Mark them
self.markAsCompleted(self.multiEditItems());
}
// Focus on the confirm button
$('#modal-delete-history-job').on("shown.bs.modal", function() {
$('#modal-delete-history-job .btn[type="submit"]').focus()
@@ -571,6 +607,11 @@ function HistoryModel(parent, data) {
$('#modal-retry-job').modal("show")
};
// Mark as completed button
self.markAsCompleted = function() {
parent.markAsCompleted(self);
};
// Update information only on click
self.updateAllHistoryInfo = function(data, event) {
// Show

View File

@@ -895,7 +895,7 @@ function ViewModel() {
// Orphaned folder deletion of all
self.removeAllOrphaned = function() {
if (!self.confirmDeleteHistory() || confirm(glitterTranslate.clearWarn)) {
if (confirm(glitterTranslate.clearOrphanWarning)) {
// Show notification
showNotification('.main-notification-box-removing-multiple', 0, self.statusInfo.folders().length)
// Delete them all
@@ -912,7 +912,7 @@ function ViewModel() {
// Orphaned folder adding of all
self.addAllOrphaned = function() {
if (!self.confirmDeleteHistory() || confirm(glitterTranslate.clearWarn)) {
if (confirm(glitterTranslate.confirm)) {
// Show notification
showNotification('.main-notification-box-sendback')
// Delete them all

View File

@@ -423,6 +423,21 @@ function QueueListModel(parent) {
}
// Handle mousedown to capture state before change
self.handleMultiEditStatusMouseDown = function(item, event) {
var clickedValue = $(event.currentTarget).find("input").val();
// If this radio was already selected (same value as previous), clear it
if ($('.multioperations-selector input[name="multiedit-status"]:checked').val() === clickedValue) {
// Clear all radio buttons in this group after the click finished
// Hacky, but it works
setTimeout(function () {
$('.multioperations-selector input[name="multiedit-status"]').prop('checked', false);
}, 200)
}
return true;
}
// Remove downloads from queue
self.removeDownloads = function(form) {
// Hide modal and show notification
@@ -456,6 +471,50 @@ function QueueListModel(parent) {
self.triggerRemoveDownload(self.multiEditItems())
}
// Move all selected to top
self.doMultiMoveToTop = function() {
// Anything selected?
if(self.multiEditItems().length < 1) return;
// Move each item to the top, starting from the last one in the sorted list
var arrayList = self.multiEditItems()
var movePromises = [];
for(var i = arrayList.length - 1; i >= 0; i--) {
movePromises.push(callAPI({
mode: "switch",
value: arrayList[i].id,
value2: 0
}));
}
// Wait for all moves to complete then refresh
Promise.all(movePromises).then(function() {
self.parent.refresh();
});
}
// Move all selected to bottom
self.doMultiMoveToBottom = function() {
// Anything selected?
if(self.multiEditItems().length < 1) return;
// Move each item to the bottom, starting from the first one in the sorted list
var arrayList = self.multiEditItems()
var movePromises = [];
for(var i = 0; i < arrayList.length; i++) {
movePromises.push(callAPI({
mode: "switch",
value: arrayList[i].id,
value2: self.totalItems() - 1
}));
}
// Wait for all moves to complete then refresh
Promise.all(movePromises).then(function() {
self.parent.refresh();
});
}
// Focus on the confirm button
$('#modal-delete-queue-job').on("shown.bs.modal", function() {
$('#modal-delete-queue-job .btn[type="submit"]').focus()

View File

@@ -860,7 +860,7 @@ tr.queue-item>td:first-child>a {
}
.multioperations-selector .add-nzb-inputbox {
width: 20%;
width: 19%;
float: left;
}
@@ -871,7 +871,7 @@ tr.queue-item>td:first-child>a {
}
.multioperations-selector .add-nzb-inputbox-small {
width: 80px;
width: 115px;
float: right;
padding-left: 0;
padding-top: 12px;
@@ -1097,6 +1097,13 @@ tr.queue-item>td:first-child>a {
font-weight: bold;
}
.mark-completed-link {
font-weight: bold !important;
color: #28a745 !important;
text-decoration: underline;
margin-left: 10px;
}
.history-status-hidden {
display: none;
}

View File

@@ -163,7 +163,7 @@ tr.queue-item>td:first-child>a {
}
.multioperations-selector .add-nzb-inputbox-small {
width: 72px;
width: 115px;
}
.multioperations-selector .add-nzb-inputbox-clear {

View File

@@ -7,7 +7,9 @@
<h1>$T('wizard-quickstart')</h1>
<hr />
<script type="text/javascript">
var txtTestServer = "$T('wizard-server-text')";
var txtChecking = "$T('srv-testing')";
var txtTestRequired = "$T('wizard-test-server-required')";
<!--#include raw $webdir + "/static/javascript/checkserver.js"#-->
</script>
<h3>$T('wizard-server')</h3>
@@ -22,7 +24,7 @@
<div class="form-group">
<label for="host" class="col-sm-4 control-label">$T('srv-host')</label>
<div class="col-sm-8">
<input type="text" class="form-control" name="host" id="host" value="$host" placeholder="$T('wizard-example') news.newshosting.com" />
<input type="text" class="form-control" name="host" id="host" value="$host" placeholder="$T('wizard-example') news.newshosting.com" required />
</div>
</div>
<div class="form-group">
@@ -57,13 +59,13 @@
<div class="form-group">
<label for="port" class="col-sm-4 control-label">$T('srv-port')</label>
<div class="col-sm-8">
<input type="number" class="form-control" name="port" id="port" value="<!--#if $port then $port else '563' #-->" min="0" max="65535" />
<input type="number" class="form-control" name="port" id="port" value="<!--#if $port then $port else '563' #-->" min="0" max="65535" required />
</div>
</div>
<div class="form-group">
<label for="connections" class="col-sm-4 control-label">$T('srv-connections')</label>
<div class="col-sm-8">
<input type="number" class="form-control" name="connections" id="connections" value="<!--#if $connections then $connections else '8'#-->" min="1" max="500" data-toggle="tooltip" data-placement="right" title="$T('wizard-server-con-explain') $T('wizard-server-con-eg')" />
<input type="number" class="form-control" name="connections" id="connections" value="<!--#if $connections then $connections else '8'#-->" min="1" max="500" data-toggle="tooltip" data-placement="right" title="$T('wizard-server-con-explain') $T('wizard-server-con-eg')" required />
</div>
</div>
<div class="form-group">
@@ -81,7 +83,7 @@
</div>
<div class="row">
<div class="col-sm-4">
<button id="serverTest" class="btn btn-default"><span class="glyphicon glyphicon-sort"></span> $T('wizard-button-testServer')</button>
<button id="serverTest" class="btn btn-default" data-toggle="tooltip" data-placement="left"><span class="glyphicon glyphicon-sort"></span> $T('wizard-button-testServer')</button>
</div>
<div class="col-sm-8">
<div id="serverResponse" class="well well-sm">$T('wizard-server-text')</div>

View File

@@ -1,9 +1,31 @@
// Variable to track server test results
var serverTestSuccessful = false;
function resetTestResult() {
serverTestSuccessful = false;
$('#serverResponse').html(txtTestServer);
checkRequired();
}
function setTestResult(success) {
serverTestSuccessful = success;
checkRequired();
}
function checkRequired() {
if ($("#host").val() && $("#connections").val()) {
// Check if form is valid using HTML5 validation and if server test passed
if ($("form").get(0).checkValidity() && serverTestSuccessful) {
$("#next-button").removeClass('disabled')
$("#next-button").removeAttr('data-toggle')
$("#next-button").removeAttr('title')
$("#next-button").tooltip('destroy')
return true;
} else {
$("#next-button").addClass('disabled')
$("#next-button").attr('data-toggle', 'tooltip')
$("#next-button").attr('data-placement', 'left')
$("#next-button").attr('title', txtTestRequired)
$("#next-button").tooltip()
return false;
}
}
@@ -12,8 +34,13 @@ $(document).ready(function() {
// Add tooltips
$('[data-toggle="tooltip"]').tooltip()
// On form-submit
// On server test button click
$("#serverTest").click(function() {
// Check HTML5 form validation before testing server
if (!$("form").get(0).reportValidity()) {
return false;
}
$('#serverResponse').html(txtChecking);
$.getJSON(
"../api?mode=config&name=test_server&output=json",
@@ -21,8 +48,10 @@ $(document).ready(function() {
function(result) {
if (result.value.result) {
r = '<span class="success"><span class="glyphicon glyphicon-ok"></span> ' + result.value.message + '</span>';
setTestResult(true);
} else {
r = '<span class="failed"><span class="glyphicon glyphicon-minus-sign"></span> ' + result.value.message + '</span>';
setTestResult(false);
}
r = r.replace('https://sabnzbd.org/certificate-errors', '<a href="https://sabnzbd.org/certificate-errors" class="failed" target="_blank">https://sabnzbd.org/certificate-errors</a>')
$('#serverResponse').html(r);
@@ -31,26 +60,9 @@ $(document).ready(function() {
return false;
});
$("#port, #connections").bind('keyup blur', function() {
if (this.value > 0) {
$(this).removeClass("incorrect");
$(this).addClass("correct");
} else {
$(this).removeClass("correct");
$(this).addClass("incorrect");
}
checkRequired()
});
$("#host, #username, #password").bind('keyup blur', function() {
if (this.value) {
$(this).removeClass("incorrect");
$(this).addClass("correct");
} else {
$(this).removeClass("correct");
$(this).addClass("incorrect");
}
checkRequired();
// Reset test result when any form field changes
$("#host, #username, #password, #port, #connections, #ssl_verify").bind('input change', function() {
resetTestResult();
});
$('#ssl').click(function() {
@@ -65,13 +77,14 @@ $(document).ready(function() {
$('#port').val('119')
}
}
resetTestResult();
})
checkRequired()
$('form').submit(function(event) {
// Double check
if(!checkRequired()) {
// Check if server test passed (HTML5 validation is automatic)
if(!serverTestSuccessful) {
event.preventDefault();
}
})

View File

@@ -98,43 +98,15 @@ label {
.align-center {
text-align: center;
}
.unselected,
.selected {
display: inline-block;
}
.unselected {
padding: 6px 10px 6px 10px;
border: 1px solid #636363;
margin-left: 8px;
margin-right: 8px;
color: #636363;
}
.selected {
padding: 6px 10px 6px 10px;
color: white;
background-color: #636363;
border: 1px solid #636363;
margin-left: 8px;
margin-right: 8px;
}
.bigger {
font-size: 14px;
}
.bigger input {
font-size: 16px;
}
.required-star {
color: red;
}
.full-width {
width: 100%;
}
.correct {
border: 2px solid #00cc22;
}
.incorrect {
border: 2px solid red;
}
.hidden {
display: none !important;
}
@@ -150,20 +122,12 @@ label {
.input-group-bw {
width: 150px;
}
.disabled-text {
text-decoration: line-through;
color: #ccc;
}
#serverResponse {
padding: 6px 10px;
}
#host-tip {
margin-bottom: 5px;
}
.error-text {
display: inline;
color: red;
}
#bandwidth {
display: inline-block;
}

View File

@@ -30,6 +30,8 @@
<url type="faq">https://sabnzbd.org/wiki/faq</url>
<url type="contact">https://sabnzbd.org/live-chat.html</url>
<releases>
<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"/>

View File

@@ -298,6 +298,19 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr ""
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py, sabnzbd/skintext.py
msgid "Quota"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr ""
@@ -1001,10 +1014,6 @@ msgstr ""
msgid "Unpacking failed, disk full"
msgstr ""
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr ""
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr ""
@@ -2244,6 +2253,11 @@ msgstr ""
msgid "Retry"
msgstr ""
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3301,10 +3315,6 @@ msgstr ""
msgid "Naming"
msgstr ""
#: sabnzbd/skintext.py
msgid "Quota"
msgstr ""
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr ""
@@ -3402,7 +3412,7 @@ msgid "Warn 5 days in advance of account expiration date."
msgstr ""
#: sabnzbd/skintext.py
msgid "Quota for this account, counted from the time it is set. In bytes, optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few minutes."
msgid "Quota for this server, counted from the time it is set. In bytes, optionally follow with K,M,G.<br />Checked every few minutes. Notification is sent when quota is spent."
msgstr ""
#. Server's retention time in days
@@ -4308,6 +4318,10 @@ msgstr ""
msgid "Retry all"
msgstr ""
#: sabnzbd/skintext.py
msgid "Are you sure you want to delete all folders in your Temporary Download Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr ""
@@ -4546,6 +4560,11 @@ msgstr ""
msgid "Start Wizard"
msgstr ""
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr ""

View File

@@ -335,6 +335,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Kvóta přesažena, pozastavuji stahování"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Nesprávný parametr"
@@ -1076,10 +1090,6 @@ msgstr "Rozbalování selhalo, chyba zápisu nebo plný disk?"
msgid "Unpacking failed, disk full"
msgstr ""
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "Rozbalování selhalo, cesta k souboru je příliš dlouhá."
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "Rozbalení selhalo, archiv vyžaduje heslo"
@@ -2334,6 +2344,11 @@ msgstr "Jméno"
msgid "Retry"
msgstr "Opakovat"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3473,10 +3488,6 @@ msgstr ""
msgid "Naming"
msgstr ""
#: sabnzbd/skintext.py
msgid "Quota"
msgstr ""
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr ""
@@ -3579,9 +3590,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days
@@ -4524,6 +4535,12 @@ msgstr ""
msgid "Retry all"
msgstr "Opakovat vše"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "Získat NZB z URL"
@@ -4770,6 +4787,11 @@ msgstr ""
msgid "Start Wizard"
msgstr ""
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr ""

View File

@@ -352,6 +352,20 @@ msgstr "Job \"%s\" er sandsynligvis krypteret: \"password\" i filnavnet \"%s\""
msgid "Quota spent, pausing downloading"
msgstr "Kvote brugt, pause downloading"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kvota"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Fejl parameter"
@@ -1116,10 +1130,6 @@ msgstr "Udpakning mislykkedes, skrivefejl eller disken fuld?"
msgid "Unpacking failed, disk full"
msgstr "Udpakning mislykkedes, disken er fuld"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "Udpakningen mislykkedes, stien er for lang"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "Udpakning mislykkedes, arkivet kræver adgangskode"
@@ -2408,6 +2418,11 @@ msgstr "Navn"
msgid "Retry"
msgstr "Forsøg igen"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3600,10 +3615,6 @@ msgstr "Efterbehandling"
msgid "Naming"
msgstr "Navngivning"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kvota"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Hvor meget der kan downloades i denne måned (K/M/G)"
@@ -3711,9 +3722,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days
@@ -4674,6 +4685,12 @@ msgstr "Fjern alle"
msgid "Retry all"
msgstr "Prøv igen alle"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "Hent NZB fra URL"
@@ -4924,6 +4941,11 @@ msgstr "Afslut SABnzbd"
msgid "Start Wizard"
msgstr "Start guide"
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr ""

View File

@@ -376,6 +376,20 @@ msgstr "Aufgabe \"%s\" ist wahrscheinlich verschlüsselt: \"Passwort\" im Datein
msgid "Quota spent, pausing downloading"
msgstr "Kontingent aufgebraucht, Downloads werden angehalten"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kontingent"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Fehlerhafter Parameter"
@@ -1157,10 +1171,6 @@ msgstr ""
msgid "Unpacking failed, disk full"
msgstr "Fehler beim Entpacken: Festplatte voll"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "Entpacken fehlgeschlagen, Pfad ist zu lang"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "Entpacken fehlgeschlagen. Archiv benötigt ein Passwort."
@@ -2465,6 +2475,11 @@ msgstr "Name"
msgid "Retry"
msgstr "Erneut versuchen"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3729,10 +3744,6 @@ msgstr "Nachbearbeitung"
msgid "Naming"
msgstr "Benennung"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kontingent"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Wie viel kann in diesem Monat heruntergeladen werden (K/M/G)?"
@@ -3846,13 +3857,10 @@ msgstr "5 Tage vor dem Ablauf des Accounts warnen."
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
"Kontingent für dieses Konto, gezählt ab dem Zeitpunkt, an dem es festgelegt "
"wird. In Bytes, optional gefolgt von K, M, G.<br />Warne, wenn es 0 "
"erreicht, wird alle paar Minuten überprüft."
#. Server's retention time in days
#: sabnzbd/skintext.py
@@ -4843,6 +4851,12 @@ msgstr "Alle löschen"
msgid "Retry all"
msgstr "Alle wiederholen"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "NZB aus URL laden"
@@ -5098,6 +5112,11 @@ msgstr "SABnzbd beenden"
msgid "Start Wizard"
msgstr "Assistenten starten"
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr "Backup wiederherstellen"

View File

@@ -365,6 +365,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Quota gastado, pausando cola"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Cuota"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Parámetro incorrecto"
@@ -1148,10 +1162,6 @@ msgstr ""
msgid "Unpacking failed, disk full"
msgstr "Error al descomprimir, disco lleno"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "Aperture de archivo fallo, la via es muy larga"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "Error al descomprimir; El archivo está protegido por contraseña"
@@ -2456,6 +2466,11 @@ msgstr "Nombre"
msgid "Retry"
msgstr "Reintentar"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3703,10 +3718,6 @@ msgstr "Post procesado"
msgid "Naming"
msgstr "Nombrado"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Cuota"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Cantidad de descarga permitida este mes (K/M/G)"
@@ -3816,9 +3827,9 @@ msgstr "Advertir 5 días antes de la fecha de expiración de la cuenta."
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days
@@ -4812,6 +4823,12 @@ msgstr "Eliminar todo"
msgid "Retry all"
msgstr "Re-intentar todo"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "Descargar NZB desde URL"
@@ -5067,6 +5084,11 @@ msgstr "Salir SABnzbd"
msgid "Start Wizard"
msgstr "Iniciar Asistente"
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr "Restaurar copia de seguridad"

View File

@@ -334,6 +334,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Latausrajoitus saavutettu, keskeytetään lataukset"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Latausrajoitus"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Virheellinen parametri"
@@ -1079,10 +1093,6 @@ msgstr "Purkaminen epäonnistui, kirjoitusvirhe tai levy täynnä?"
msgid "Unpacking failed, disk full"
msgstr ""
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "Purkaminen epäonnistui, polku on liian pitkä"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "Purkaminen epäonnistui, arkisto vaatii salasanan"
@@ -2361,6 +2371,11 @@ msgstr "Nimi"
msgid "Retry"
msgstr "Yritä uudelleen"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3561,10 +3576,6 @@ msgstr "Jälkikäsittely"
msgid "Naming"
msgstr "Nimeäminen"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Latausrajoitus"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Kuinka paljon voidaan ladata tässä kuussa (K/M/G)"
@@ -3670,9 +3681,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days
@@ -4631,6 +4642,12 @@ msgstr "Poista kaikki"
msgid "Retry all"
msgstr "Yritä uudelleen kaikki"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "Nouda NZB osoitteesta"
@@ -4883,6 +4900,11 @@ msgstr "Poistu SABnzbd:stä"
msgid "Start Wizard"
msgstr "Käynnistä velho"
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr ""

View File

@@ -2,7 +2,7 @@
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
#
# Translators:
# Safihre <safihre@sabnzbd.org>, 2023
# Safihre <safihre@sabnzbd.org>, 2025
# Fred L <88com88@gmail.com>, 2025
#
msgid ""
@@ -370,6 +370,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Quota atteint, téléchargement mis en pause"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Quota"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr "Avertissement de limite de quota (%d%%)"
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr "Le téléchargement a repris après la réinitialisation du quota."
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Paramètre incorrect"
@@ -1151,10 +1165,6 @@ msgstr ""
msgid "Unpacking failed, disk full"
msgstr "Échec de l'extraction, disque plein"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "Extraction échoué, le chemin est trop long"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "Échec de l'extraction, l'archive nécessite un mot de passe"
@@ -2451,6 +2461,11 @@ msgstr "Nom"
msgid "Retry"
msgstr "Réessayer"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr "Marquer comme terminé & supprimer les fichiers temporaires"
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3713,10 +3728,6 @@ msgstr "Post-traitement"
msgid "Naming"
msgstr "Appellation"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Quota"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Combien peut-être télécharger ce mois (K/M/G)"
@@ -3827,13 +3838,13 @@ msgstr "Avertir 5 jours avant la date d'expiration du compte."
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
"Quota pour ce compte calculé à partir du moment où il est défini. En octets,"
" éventuellement suivi de K,M,G.<br />Avertir quand il atteint 0, vérifié "
"toutes les quelques minutes."
"Quota pour ce serveur, calculé à partir du moment où il est défini. En "
"octets, suivi éventuellement de K,M,G.<br />Vérifié toutes les quelques "
"minutes. Une notification est envoyée lorsque le quota est atteint."
#. Server's retention time in days
#: sabnzbd/skintext.py
@@ -4827,6 +4838,14 @@ msgstr "Tout supprimer"
msgid "Retry all"
msgstr "Réessayer tous"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
"Êtes-vous sûr de vouloir supprimer tous les dossiers de votre Dossier de "
"Téléchargement Temporaire ? Cette opération ne peut pas être annulée !"
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "Importer le NZB depuis l'URL"
@@ -5085,6 +5104,11 @@ msgstr "Quitter SABnzbd"
msgid "Start Wizard"
msgstr "Lancer l'assistant"
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr "Cliquez sur Tester le Serveur avant de continuer"
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr "Restaurer la sauvegarde"

View File

@@ -335,6 +335,20 @@ msgstr "העבודה \"%s\" כנראה מוצפנת: \"סיסמה\" בשם הק
msgid "Quota spent, pausing downloading"
msgstr "מכסה נוצלה, משהה הורדה"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "מכסה"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "פרמטר שגוי"
@@ -1089,10 +1103,6 @@ msgstr "פריקה נכשלה, שגיאת כתיבה או דיסק מלא?"
msgid "Unpacking failed, disk full"
msgstr "פריקה נכשלה, דיסק מלא"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "פריקה נכשלה, נתיב ארוך מדי"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "פריקה נכשלה, ארכיון דורש סיסמה"
@@ -2374,6 +2384,11 @@ msgstr "שם"
msgid "Retry"
msgstr "נסה שוב"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3565,10 +3580,6 @@ msgstr "בתר־עיבוד"
msgid "Naming"
msgstr "מתן שמות"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "מכסה"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "כמה ניתן להוריד החודש (ק״ב/מ״ב/ג״ב)"
@@ -3673,12 +3684,10 @@ msgstr "הזהר 5 ימים טרם תאריך תפוגת החשבון."
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
"מכסה עבור חשבון זה, נספרת מהזמן שהיא הוגדרה. בבתים, יכולה לבוא עם K,M,G.<br "
"/>הזהר כאשר המכסה מגיעה אל 0, היא נבדקת כל כמה דקות."
#. Server's retention time in days
#: sabnzbd/skintext.py
@@ -4641,6 +4650,12 @@ msgstr "מחק הכל"
msgid "Retry all"
msgstr "נסה שוב הכל"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "משוך NZB מכתובת"
@@ -4895,6 +4910,11 @@ msgstr "צא מן SABnzbd"
msgid "Start Wizard"
msgstr "התחל אשף"
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr "שחזר גיבוי"

View File

@@ -361,6 +361,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Quota esaurita, download in pausa"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Quota"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Parametro non corretto"
@@ -1134,10 +1148,6 @@ msgstr "Estrazione fallita, errore di scrittura o disco pieno?"
msgid "Unpacking failed, disk full"
msgstr "Estrazione fallita, disco pieno"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "Estrazione fallita, percorso troppo lungo"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "Estrazione fallita, l'archivio richiede una password"
@@ -2429,6 +2439,11 @@ msgstr "Nome"
msgid "Retry"
msgstr "Riprova"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3672,10 +3687,6 @@ msgstr "Post-elaborazione"
msgid "Naming"
msgstr "Denominazione"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Quota"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Quanto può essere scaricato questo mese (K/M/G)"
@@ -3785,13 +3796,10 @@ msgstr "Avvisa 5 giorni prima della data di scadenza dell'account."
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
"Quota per questo account, contata dal momento in cui è impostata. In byte, "
"opzionalmente seguito da K,M,G.<br />Avvisa quando raggiunge 0, controllato "
"ogni pochi minuti."
#. Server's retention time in days
#: sabnzbd/skintext.py
@@ -4779,6 +4787,12 @@ msgstr "Elimina tutto"
msgid "Retry all"
msgstr "Riprova tutto"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "Recupera NZB da URL"
@@ -5035,6 +5049,11 @@ msgstr "Esci da SABnzbd"
msgid "Start Wizard"
msgstr "Avvia procedura guidata"
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr "Ripristina backup"

View File

@@ -332,6 +332,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Kvote oppbrukt, setter nedlasting på pause"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kvote"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Feil parameter"
@@ -1076,10 +1090,6 @@ msgstr "Utpakking mislyktes, skrivefeil eller er disken full?"
msgid "Unpacking failed, disk full"
msgstr ""
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "Utpakking feilet, stien er for lang"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "Utpakking mislyktes, arkivet krever passord"
@@ -2359,6 +2369,11 @@ msgstr "Navn"
msgid "Retry"
msgstr "Prøv igjen"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3538,10 +3553,6 @@ msgstr "Postprosessering"
msgid "Naming"
msgstr "Filnavn"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kvote"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Hvor mye can lastes ned denne måneden (K/M/G)"
@@ -3649,9 +3660,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days
@@ -4605,6 +4616,12 @@ msgstr "Ta bort alle"
msgid "Retry all"
msgstr "Prøv alle på nytt"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "Hent NZB fra URL"
@@ -4855,6 +4872,11 @@ msgstr "Avslutt SABnzbd"
msgid "Start Wizard"
msgstr "Start Veiviser"
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr ""

View File

@@ -358,6 +358,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Quotum verbruikt, download is gestopt"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Quotum"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Incorrecte parameter"
@@ -1138,10 +1152,6 @@ msgstr "Uitpakken mislukt, schrijffout of schijf vol?"
msgid "Unpacking failed, disk full"
msgstr "Uitpakken mislukt, de schijf is vol"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "Uitpakken mislukt, bestandspad is te lang"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "Uitpakken mislukt, archief vereist wachtwoord"
@@ -2432,6 +2442,11 @@ msgstr "Naam"
msgid "Retry"
msgstr "Opnieuw"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3672,10 +3687,6 @@ msgstr "Nabewerking"
msgid "Naming"
msgstr "Naamgeving"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Quotum"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Hoeval mag deze maand worden gedownload (K/M/G)"
@@ -3787,14 +3798,10 @@ msgstr "Ontvang 5 dagen voor de verloopdatum een waarschuwing."
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
"Quotum voor dit account, wordt geteld vanaf het moment dat het voor het "
"eerst ingesteld wordt. In bytes, in K,M,G notatie.<br />Er wordt een "
"waarschuwing gegeven als het quotum bereikt is, dit wordt elke paar minuten "
"gecontroleerd."
#. Server's retention time in days
#: sabnzbd/skintext.py
@@ -4777,6 +4784,12 @@ msgstr "Alles wissen"
msgid "Retry all"
msgstr "Alles opnieuw proberen"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "Haal NZB op via URL"
@@ -5032,6 +5045,11 @@ msgstr "Stop SABnzbd"
msgid "Start Wizard"
msgstr "Wizard starten"
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr "Backup herstellen"

View File

@@ -331,6 +331,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Przekroczono limit, wstrzymywanie pobierania"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Limit pobierania"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Błędny parametr"
@@ -1079,10 +1093,6 @@ msgstr "Rozpakowywanie nie powiodło się, błąd zapisu lub zapełniony dysk?"
msgid "Unpacking failed, disk full"
msgstr ""
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "Rozpakowywanie nie powiodło się, zbyt długa ścieżka"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "Rozpakowywanie nie powiodło się, archiwum wymaga podania hasła"
@@ -2368,6 +2378,11 @@ msgstr "Nazwa"
msgid "Retry"
msgstr "Ponów"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3549,10 +3564,6 @@ msgstr "Przetwarzanie końcowe"
msgid "Naming"
msgstr "Nazwy"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Limit pobierania"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Ile danych można pobrać w miesiącu (K/M/G)"
@@ -3661,9 +3672,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days
@@ -4617,6 +4628,12 @@ msgstr "Usuń wszystko"
msgid "Retry all"
msgstr "Ponów wszystkie"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "Pobierz NZB z URL"
@@ -4865,6 +4882,11 @@ msgstr "Wyjście z SABnzbd"
msgid "Start Wizard"
msgstr "Uruchom kreatora konfiguracji"
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr ""

View File

@@ -343,6 +343,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Quota esgotada, pausando o download"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Quota"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Parâmetro incorreto"
@@ -1091,10 +1105,6 @@ msgstr "A descompactação falhou. Erro de escrita ou disco cheio?"
msgid "Unpacking failed, disk full"
msgstr ""
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "Descompactação falhou, o caminho é muito extenso"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "A descompactação falhou. O arquivo exige uma senha"
@@ -2379,6 +2389,11 @@ msgstr "Nome"
msgid "Retry"
msgstr "Repetir"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3561,10 +3576,6 @@ msgstr "Pós-processamento"
msgid "Naming"
msgstr "Nomeando"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Quota"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Quanto pode ser baixado neste mês (K/M/G)"
@@ -3672,9 +3683,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days
@@ -4628,6 +4639,12 @@ msgstr "Excluir Todos"
msgid "Retry all"
msgstr "Repetir todos"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "Buscar NZB de uma URL"
@@ -4876,6 +4893,11 @@ msgstr "Sair do SABnzbd"
msgid "Start Wizard"
msgstr "Iniciar o Assistente"
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr ""

View File

@@ -347,6 +347,20 @@ msgstr "Sarcina „%s” este probabil criptată: „parolă” în fișierul
msgid "Quota spent, pausing downloading"
msgstr "Cotă epuizată, întrerupem descărcarea"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Cotă"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Parametru Incorect"
@@ -1104,10 +1118,6 @@ msgstr "Dezarhivare nereuşită, eroare scriere sau disc plin?"
msgid "Unpacking failed, disk full"
msgstr ""
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "Dezarhivare eșuată, calea este prea lungă"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "Dezarhivare nereuşită, arhiva necesită o parolă"
@@ -2397,6 +2407,11 @@ msgstr "Nume"
msgid "Retry"
msgstr "Reîncearcă"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3580,10 +3595,6 @@ msgstr "Post procesare"
msgid "Naming"
msgstr "Redenumire"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Cotă"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Cât de mult poate fi descărcat în acestă lună (K/M/G)"
@@ -3692,9 +3703,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days
@@ -4647,6 +4658,12 @@ msgstr "Șterge tot"
msgid "Retry all"
msgstr "Reîncearcă toate"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "Descarcă NZB din URL"
@@ -4898,6 +4915,11 @@ msgstr "Închide SABnzbd"
msgid "Start Wizard"
msgstr "Porneşte Vrăjitor"
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr ""

View File

@@ -331,6 +331,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Квота исчерпана. Загрузка приостановлена"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Квота"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Неправильный параметр"
@@ -1075,10 +1089,6 @@ msgstr "Не удалось распаковать: ошибка записи и
msgid "Unpacking failed, disk full"
msgstr ""
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr ""
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "Ошибка распаковки: архив защищён паролем"
@@ -2361,6 +2371,11 @@ msgstr "Название"
msgid "Retry"
msgstr "Повторить"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3540,10 +3555,6 @@ msgstr "Пост-обработка"
msgid "Naming"
msgstr "Именование"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Квота"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Объем, который можно загрузить в месяц (K/M/G)"
@@ -3650,9 +3661,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days
@@ -4611,6 +4622,12 @@ msgstr "Удалить всё"
msgid "Retry all"
msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr ""
@@ -4861,6 +4878,11 @@ msgstr ""
msgid "Start Wizard"
msgstr ""
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr ""

View File

@@ -328,6 +328,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Kvota utrošena, pauziram preuzimanja"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Квота"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Погрешан параметар"
@@ -1071,10 +1085,6 @@ msgstr "Neuspašno raspakivanje, greška u pisanju ili je disk pun?"
msgid "Unpacking failed, disk full"
msgstr ""
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "Neuspešno raspakivanje, putanja je predugačka"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "Neuspešno raspakivanje, arhiva zahteva lozinku"
@@ -2354,6 +2364,11 @@ msgstr "Име"
msgid "Retry"
msgstr "Покушај опет"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3525,10 +3540,6 @@ msgstr "Накнадна обрада"
msgid "Naming"
msgstr "Именовање"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Квота"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Колико може да се преузме овог месеца (К/М/Г)"
@@ -3636,9 +3647,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days
@@ -4590,6 +4601,12 @@ msgstr "Избриши све"
msgid "Retry all"
msgstr "Ponovo pokušaj sve"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "Povuci NZB sa URL"
@@ -4838,6 +4855,11 @@ msgstr "Затвори SABnzbd"
msgid "Start Wizard"
msgstr "Покрени чаробњака"
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr ""

View File

@@ -328,6 +328,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Din kvot är uppnådd, pausar nerladdning"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kvot"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Fel parameter"
@@ -1073,10 +1087,6 @@ msgstr "Uppackning misslyckades, skrivfel eller disken full?"
msgid "Unpacking failed, disk full"
msgstr ""
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "Uppackning misslyckades, sökvägen är för lång"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "Uppackning misslyckades, arkivet kräver lösenord"
@@ -2360,6 +2370,11 @@ msgstr "Namn"
msgid "Retry"
msgstr "Försök igen"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3537,10 +3552,6 @@ msgstr "Efterbehandling"
msgid "Naming"
msgstr "Döpning"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kvot"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Hur mycket kan laddas ner denna månad (K/M/G)"
@@ -3648,9 +3659,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days
@@ -4602,6 +4613,12 @@ msgstr "Ta bort alla"
msgid "Retry all"
msgstr "Starta om alla"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "Hämta NZB från URL"
@@ -4852,6 +4869,11 @@ msgstr "Avsluta SABnzbd"
msgid "Start Wizard"
msgstr "Starta guide"
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr ""

View File

@@ -43,6 +43,8 @@ msgid ""
"Unable to link to OpenSSL, optimized SSL connection functions will not be "
"used."
msgstr ""
"OpenSSL unsuruna bağlanılamıyor, en uygun hale getirilmiş SSL bağlantı "
"işlevleri kullanılmayacaktır."
#. Error message
#: SABnzbd.py
@@ -360,6 +362,20 @@ msgstr "\"%s\" işi muhtemelen şifrelenmiştir: \"parola\", \"%s\" dosya ismind
msgid "Quota spent, pausing downloading"
msgstr "Kota kullanıldı, indirme duraklatılıyor"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kota"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr "Kota sınır ikazı (%d%%)"
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr "İndirme kota sıfırlamasının ardından devam etti"
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Yanlış parametre"
@@ -1125,10 +1141,6 @@ msgstr "Açma başarısız oldu, yazma hatası mı yoksa disk doldu mu?"
msgid "Unpacking failed, disk full"
msgstr "Açma başarısız oldu, disk dolu"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "Açma başarısız oldu, yok çok uzun"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "Açma başarısız oldu, arşiv bir parola gerektiriyor"
@@ -2420,6 +2432,11 @@ msgstr "İsim"
msgid "Retry"
msgstr "Tekrar dene"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr "Tamamlanmış Olarak İşaretle ve Geçici Dosyaları Kaldır"
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3658,10 +3675,6 @@ msgstr "Post processing"
msgid "Naming"
msgstr "İsimlendirme"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kota"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Bu ay ne kadar indirme yapılabileceği (K/M/G)"
@@ -3769,13 +3782,14 @@ msgstr "Sonlanma tarihinden 5 gün evvel ikaz et."
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
"Bu hesap için kota, bu seçeneğin ayarlanmasından itibaren hesaplanır. Bayt "
"olarak, seçime dayalı bir şekilde K,M,G takip edebilir.<br />0 değerine "
"ulaştığında ikazda bulun, her birkaç dakikada bir kontrol edilir."
"Bu sunucu için, ayarlandığı zamandan itibaren hesaplanan kota. Bayt olarak, "
"seçime dayalı bir şekilde K, M, G takip edebilir. <br /> Her birkaç "
"dakikada bir kontrol edilir. Kota sonuna ulaşıldığında bir bildirim "
"gönderilir."
#. Server's retention time in days
#: sabnzbd/skintext.py
@@ -4766,6 +4780,14 @@ msgstr "Tümünü Sil"
msgid "Retry all"
msgstr "Tümünü tekrar dene"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
"Geçici İndirme Dizini'ndeki tüm dizinleri silmek istediğinizden emin "
"misiniz? Bu geri alınamaz!"
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "URL konumundan NZB al"
@@ -5022,6 +5044,11 @@ msgstr "SABnzb'den çık"
msgid "Start Wizard"
msgstr "Sihirbazı Başlat"
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr "Devam etmeden evvel Sunucuyu Dene unsuruna tıklayın"
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr "Yedeklemeyi geri getir"

View File

@@ -327,6 +327,20 @@ msgstr "任务 \"%s\" 可能受加密保护:文件名 \"%s\" 中有 \"password
msgid "Quota spent, pausing downloading"
msgstr "配额已耗尽,暂停下载"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "配额"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "参数不正确"
@@ -1067,10 +1081,6 @@ msgstr "解压失败,写入出错或磁盘已满?"
msgid "Unpacking failed, disk full"
msgstr ""
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, path is too long"
msgstr "解压失败,路径过长"
#: sabnzbd/newsunpack.py
msgid "Unpacking failed, archive requires a password"
msgstr "解压失败,压缩文件需要密码"
@@ -2348,6 +2358,11 @@ msgstr "名称"
msgid "Retry"
msgstr "重试"
#. History page button
#: sabnzbd/skintext.py
msgid "Mark as Completed & Remove Temporary Files"
msgstr ""
#. Queue page table, script selection menu
#: sabnzbd/skintext.py
msgid "Scripts"
@@ -3485,10 +3500,6 @@ msgstr "后期处理"
msgid "Naming"
msgstr "命名"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "配额"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "本月能下载多少数据量 (K/M/G)"
@@ -3591,9 +3602,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days
@@ -4542,6 +4553,12 @@ msgstr "全部删除"
msgid "Retry all"
msgstr "全部重试"
#: sabnzbd/skintext.py
msgid ""
"Are you sure you want to delete all folders in your Temporary Download "
"Folder? This cannot be undone!"
msgstr ""
#: sabnzbd/skintext.py
msgid "Fetch NZB from URL"
msgstr "从 URL 装取 NZB"
@@ -4788,6 +4805,11 @@ msgstr "退出 SABnzbd"
msgid "Start Wizard"
msgstr "启动向导"
#. Tooltip for disabled Next button
#: sabnzbd/skintext.py
msgid "Click on Test Server before continuing"
msgstr ""
#: sabnzbd/skintext.py
msgid "Restore backup"
msgstr ""

View File

@@ -16,6 +16,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr ""
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr ""
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr ""

View File

@@ -21,6 +21,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "Zobrazit poznámky k vydání"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "Spustit SABnzbd"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "Podpořte projekt!"

View File

@@ -20,6 +20,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "Vis udgivelsesbemærkninger"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "Kør SABnzbd"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "Støt projektet, donér!"

View File

@@ -2,15 +2,15 @@
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
#
# Translators:
# Safihre <safihre@sabnzbd.org>, 2020
# HandyDandy04, 2024
# Gjelbrim Haskaj, 2024
# Safihre <safihre@sabnzbd.org>, 2025
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.6.0\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Gjelbrim Haskaj, 2024\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
"Language-Team: German (https://app.transifex.com/sabnzbd/teams/111101/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -22,6 +22,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "Versionshinweise anzeigen"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "SABnzbd starten"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "Bitte unterstützen Sie das Projekt durch eine Spende!"

View File

@@ -21,6 +21,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "Mostrar notas de la versión"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "Ejecutar SABnzbd"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "¡Apoye el proyecto, haga una donación!"

View File

@@ -20,6 +20,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "Näytä julkaisutiedot"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "Käynnistä SABnzbd"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "Tue projektia, lahjoita!"

View File

@@ -2,14 +2,14 @@
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
#
# Translators:
# Safihre <safihre@sabnzbd.org>, 2020
# Fred L <88com88@gmail.com>, 2024
# Safihre <safihre@sabnzbd.org>, 2025
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.6.0\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Fred L <88com88@gmail.com>, 2024\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
"Language-Team: French (https://app.transifex.com/sabnzbd/teams/111101/fr/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -21,6 +21,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "Afficher les notes de version"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "Lancer SABnzbd"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "Soutenez le projet, faites un don !"

View File

@@ -2,14 +2,14 @@
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
#
# Translators:
# Safihre <safihre@sabnzbd.org>, 2020
# ION, 2024
# Safihre <safihre@sabnzbd.org>, 2025
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.6.0\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: ION, 2024\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
"Language-Team: Hebrew (https://app.transifex.com/sabnzbd/teams/111101/he/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -21,6 +21,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "הראה הערות שחרור"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "הפעל את SABnzbd"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "תמוך במיזם, תרום!"

View File

@@ -20,6 +20,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "Mostra note di rilascio"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "Avvia SABnzbd"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "Sostieni il progetto, dona!"

View File

@@ -20,6 +20,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "Vis versjonsmerknader"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "Kjør SABnzbd"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "Støtt prosjektet, donèr!"

View File

@@ -2,13 +2,13 @@
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
#
# Translators:
# Safihre <safihre@sabnzbd.org>, 2024
# Safihre <safihre@sabnzbd.org>, 2025
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.6.0\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2024\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
"Language-Team: Dutch (https://app.transifex.com/sabnzbd/teams/111101/nl/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -20,6 +20,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "Toon opmerkingen bij deze uitgave"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "SABnzbd starten"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "Steun het project, doneer!"

View File

@@ -20,6 +20,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "Pokaż informacje o wydaniu"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "Uruchom SABnzbd"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "Wspomóż projekt!"

View File

@@ -20,6 +20,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "Mostrar Notas de Lançamento"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "Executar o SABnzbd"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "Apoie o projeto. Faça uma doação!"

View File

@@ -20,6 +20,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "Arată Notele de Publicare"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "Rulați SABnzbd"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "Susţine proiectul, Donează!"

View File

@@ -20,6 +20,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "Показать заметки о выпуске"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "Запустить SABnzbd"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "Поддержите проект. Сделайте пожертвование!"

View File

@@ -20,6 +20,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "Прикажи белешке о издању"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "Покрени SABnzbd"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "Подржите пројекат, дајте добровољан прилог!"

View File

@@ -21,6 +21,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "Visa releasenoteringar"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "Kör SABnzbd"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "Donera och stöd detta projekt!"

View File

@@ -3,12 +3,13 @@
#
# Translators:
# mauron, 2025
# Safihre <safihre@sabnzbd.org>, 2025
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.6.0\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: mauron, 2025\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 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"
@@ -20,6 +21,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "Yayın Notlarını Göster"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "SABnzbd'yi çalıştır"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "Projeye destek olun, Bağış Yapın!"

View File

@@ -20,6 +20,10 @@ msgstr ""
msgid "Show Release Notes"
msgstr "显示版本说明"
#: builder/win/NSIS_Installer.nsi
msgid "Run SABnzbd"
msgstr "运行 SABnzbd"
#: builder/win/NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "支持该项目,捐助!"

View File

@@ -1,22 +1,22 @@
# Main requirements
# Note that not all sub-dependencies are listed, but only ones we know could cause trouble
apprise==1.9.4
apprise==1.9.5
sabctools==8.2.6
CT3==3.4.0
cffi==1.17.1
pycparser==2.22
feedparser==6.0.11
cffi==2.0.0
pycparser==2.23
feedparser==6.0.12
configobj==5.0.9
cheroot==10.0.1
cheroot==11.0.0
six==1.17.0
cherrypy==18.10.0
jaraco.functools==4.2.1
jaraco.functools==4.3.0
jaraco.collections==5.0.0
jaraco.text==3.8.1 # Newer version introduces irrelevant extra dependencies
jaraco.classes==3.4.0
jaraco.context==4.3.0
more-itertools==10.7.0
zc.lockfile==3.0.post1
more-itertools==10.8.0
zc.lockfile==4.0
python-dateutil==2.9.0.post0
tempora==5.8.1
pytz==2025.2
@@ -25,18 +25,19 @@ portend==3.2.1
chardet==5.2.0
PySocks==1.7.1
puremagic==1.30
rarfile==4.2
guessit==3.8.0
babelfish==0.6.1
rebulk==3.2.0
# Recent cryptography versions require Rust. If you run into issues compiling this
# SABnzbd will also work with older pre-Rust versions such as cryptography==3.3.2
cryptography==45.0.6
cryptography==46.0.3
# We recommend using "orjson" as it is 2x as fast as "ujson". However, it requires
# Rust so SABnzbd works just as well with "ujson" or the Python built in "json" module
ujson==5.10.0
orjson==3.11.2
ujson==5.11.0
orjson==3.11.3
# Windows system integration
pywin32==311; sys_platform == 'win32'
@@ -46,27 +47,27 @@ winrt-Windows.Data.Xml.Dom==3.2.1; sys_platform == 'win32'
winrt-Windows.Foundation==3.2.1; sys_platform == 'win32'
winrt-Windows.Foundation.Collections==3.2.1; sys_platform == 'win32'
winrt-Windows.UI.Notifications==3.2.1; sys_platform == 'win32'
typing_extensions==4.14.1; sys_platform == 'win32'
typing_extensions==4.15.0; sys_platform == 'win32'
# macOS system calls
pyobjc-core==11.1; sys_platform == 'darwin'
pyobjc-framework-Cocoa==11.1; sys_platform == 'darwin'
pyobjc-core==12.0; sys_platform == 'darwin'
pyobjc-framework-Cocoa==12.0; sys_platform == 'darwin'
# Linux notifications
notify2==0.3.1; sys_platform != 'win32' and sys_platform != 'darwin'
# Apprise Requirements
requests==2.32.4
requests==2.32.5
requests-oauthlib==2.0.0
PyYAML==6.0.2
markdown==3.8.2
PyYAML==6.0.3
markdown==3.9
paho-mqtt==1.6.1 # Pinned, newer versions don't work with AppRise yet
# Requests Requirements
charset_normalizer==3.4.3
idna==3.10
charset_normalizer==3.4.4
idna==3.11
urllib3==2.5.0
certifi==2025.8.3
certifi==2025.10.5
oauthlib==3.3.1
PyJWT==2.10.1
blinker==1.9.0

View File

@@ -480,8 +480,78 @@ def _api_add_all_orphan(value: str, kwargs: Dict[str, Union[str, List[str]]]) ->
def _api_history(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
"""API: accepts value(=nzo_id), start, limit, search, nzo_ids"""
"""API: Dispatcher for mode=history"""
value = kwargs.get("value", "")
return _api_history_table.get(name, (_api_history_default, 2))[0](value, kwargs)
def _api_history_delete(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
"""API: accepts value(=nzo_id or special), search, archive, del_files"""
search = kwargs.get("search")
archive = True
# Only skip archive if specifically requested
if kwargs.get("archive") == "0" or cfg.disable_archive():
archive = False
special = value.lower()
del_files = bool_conv(kwargs.get("del_files"))
if special in ("all", "failed", "completed"):
history_db = sabnzbd.get_db_connection()
if special in ("all", "failed"):
if del_files:
del_job_files(history_db.get_failed_paths(search))
if archive:
history_db.archive_with_status(Status.FAILED, search)
else:
history_db.remove_with_status(Status.FAILED, search)
if special in ("all", "completed"):
if archive:
history_db.archive_with_status(Status.COMPLETED, search)
else:
history_db.remove_with_status(Status.COMPLETED, search)
history_updated()
return report()
elif value:
for job in clean_comma_separated_list(value):
if sabnzbd.PostProcessor.get_path(job):
# This is always a permanent delete, no archiving
sabnzbd.PostProcessor.delete(job, del_files=del_files)
else:
history_db = sabnzbd.get_db_connection()
if del_files:
remove_all(history_db.get_incomplete_path(job), recursive=True)
if archive:
history_db.archive(job)
else:
history_db.remove(job)
history_updated()
return report()
else:
return report(_MSG_NO_VALUE)
def _api_history_mark_as_completed(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
"""API: accepts value(=nzo_id)"""
if value:
history_db = sabnzbd.get_db_connection()
for job in clean_comma_separated_list(value):
# Get incomplete path before marking as completed
incomplete_path = history_db.get_incomplete_path(job)
history_db.mark_as_completed(job)
# Remove incomplete folder if it exists
if incomplete_path:
remove_all(incomplete_path, recursive=True)
history_updated()
return report()
else:
return report(_MSG_NO_VALUE)
def _api_history_default(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
"""API: accepts start, limit, search, failed_only, archive, cat, status, nzo_ids"""
start = int_conv(kwargs.get("start"))
limit = int_conv(kwargs.get("limit"))
last_history_update = int_conv(kwargs.get("last_history_update", 0))
@@ -491,84 +561,38 @@ def _api_history(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
failed_only = bool_conv(kwargs.get("failed_only"))
nzo_ids = clean_comma_separated_list(kwargs.get("nzo_ids"))
archive = True
# Do we need to send anything?
if last_history_update == sabnzbd.LAST_HISTORY_UPDATE:
return report(keyword="history", data=False)
if name == "delete":
# Only skip archive if specifically requested
if kwargs.get("archive") == "0" or cfg.disable_archive():
archive = False
if failed_only:
# We ignore any other statuses, having both doesn't make sense
statuses = [Status.FAILED]
special = value.lower()
del_files = bool_conv(kwargs.get("del_files"))
if special in ("all", "failed", "completed"):
history_db = sabnzbd.get_db_connection()
if special in ("all", "failed"):
if del_files:
del_job_files(history_db.get_failed_paths(search))
if archive:
history_db.archive_with_status(Status.FAILED, search)
else:
history_db.remove_with_status(Status.FAILED, search)
if special in ("all", "completed"):
if archive:
history_db.archive_with_status(Status.COMPLETED, search)
else:
history_db.remove_with_status(Status.COMPLETED, search)
history_updated()
return report()
elif value:
for job in clean_comma_separated_list(value):
if sabnzbd.PostProcessor.get_path(job):
# This is always a permanent delete, no archiving
sabnzbd.PostProcessor.delete(job, del_files=del_files)
else:
history_db = sabnzbd.get_db_connection()
if del_files:
remove_all(history_db.get_incomplete_path(job), recursive=True)
if archive:
history_db.archive(job)
else:
history_db.remove(job)
history_updated()
return report()
else:
return report(_MSG_NO_VALUE)
elif not name:
# Do we need to send anything?
if last_history_update == sabnzbd.LAST_HISTORY_UPDATE:
return report(keyword="history", data=False)
if not limit:
limit = cfg.history_limit()
if failed_only:
# We ignore any other statuses, having both doesn't make sense
statuses = [Status.FAILED]
# Only show archive if specifically requested
archive = bool(int_conv(kwargs.get("archive")))
if not limit:
limit = cfg.history_limit()
# Only show archive if specifically requested
if not int_conv(kwargs.get("archive")):
archive = False
history = {}
grand, month, week, day = sabnzbd.BPSMeter.get_sums()
history["total_size"] = to_units(grand)
history["month_size"] = to_units(month)
history["week_size"] = to_units(week)
history["day_size"] = to_units(day)
history["slots"], history["ppslots"], history["noofslots"] = build_history(
start=start,
limit=limit,
archive=archive,
search=search,
categories=categories,
statuses=statuses,
nzo_ids=nzo_ids,
)
history["last_history_update"] = sabnzbd.LAST_HISTORY_UPDATE
history["version"] = sabnzbd.__version__
return report(keyword="history", data=history)
else:
return report(_MSG_NOT_IMPLEMENTED)
history = {}
grand, month, week, day = sabnzbd.BPSMeter.get_sums()
history["total_size"] = to_units(grand)
history["month_size"] = to_units(month)
history["week_size"] = to_units(week)
history["day_size"] = to_units(day)
history["slots"], history["ppslots"], history["noofslots"] = build_history(
start=start,
limit=limit,
archive=archive,
search=search,
categories=categories,
statuses=statuses,
nzo_ids=nzo_ids,
)
history["last_history_update"] = sabnzbd.LAST_HISTORY_UPDATE
history["version"] = sabnzbd.__version__
return report(keyword="history", data=history)
def _api_get_files(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
@@ -1051,6 +1075,12 @@ _api_queue_table = {
"sort": (_api_queue_sort, 2),
}
_api_history_table = {
"delete": (_api_history_delete, 2),
"mark_as_completed": (_api_history_mark_as_completed, 2),
}
_api_status_table = {
"unblock_server": (_api_unblock_server, 2),
"delete_orphan": (_api_delete_orphan, 2),
@@ -1075,6 +1105,8 @@ def api_level(mode: str, name: str) -> int:
"""Return access level required for this API call"""
if mode == "queue" and name in _api_queue_table:
return _api_queue_table[name][1]
if mode == "history" and name in _api_history_table:
return _api_history_table[name][1]
if mode == "status" and name in _api_status_table:
return _api_status_table[name][1]
if mode == "config" and name in _api_config_table:

View File

@@ -26,9 +26,10 @@ import re
from threading import Thread
import ctypes
from typing import Tuple, Optional, List
import rarfile
import sabnzbd
from sabnzbd.misc import get_all_passwords, match_str
from sabnzbd.misc import get_all_passwords, match_str, SABRarFile
from sabnzbd.filesystem import (
set_permissions,
clip_path,
@@ -42,7 +43,6 @@ from sabnzbd.constants import Status, GIGI, MAX_ASSEMBLER_QUEUE
import sabnzbd.cfg as cfg
from sabnzbd.nzbstuff import NzbObject, NzbFile
import sabnzbd.par2file as par2file
import sabnzbd.utils.rarfile as rarfile
class Assembler(Thread):
@@ -295,7 +295,7 @@ def check_encrypted_and_unwanted_files(nzo: NzbObject, filepath: str) -> Tuple[b
# Is it even a rarfile?
if rarfile.is_rarfile(filepath):
# Open the rar
zf = rarfile.RarFile(filepath, single_file_check=True)
zf = SABRarFile(filepath, part_only=True)
# Check for encryption
if (
@@ -322,12 +322,7 @@ def check_encrypted_and_unwanted_files(nzo: NzbObject, filepath: str) -> Tuple[b
logging.info('Trying password "%s" on job "%s"', password, nzo.final_name)
try:
zf.setpassword(password)
except rarfile.Error:
# On weird passwords the setpassword() will fail
# but the actual testrar() will work
pass
try:
zf.testrar()
zf.trigger_parse()
password_hit = password
break
except rarfile.RarWrongPassword:

View File

@@ -122,6 +122,7 @@ class BPSMeter:
"q_hour",
"q_minute",
"quota_enabled",
"quota_notifications_sent",
)
def __init__(self):
@@ -161,6 +162,7 @@ class BPSMeter:
self.q_hour = 0 # Quota reset hour
self.q_minute = 0 # Quota reset minute
self.quota_enabled: bool = True # Scheduled quota enable/disable
self.quota_notifications_sent: int = 0 # Track highest quota threshold that has been notified
def save(self):
"""Save admin to disk"""
@@ -323,10 +325,7 @@ class BPSMeter:
# Quota check
if self.have_quota and self.quota_enabled:
self.left -= self.sum_cached_amount
if self.left <= 0.0:
if not sabnzbd.Downloader.paused:
sabnzbd.Downloader.pause()
logging.warning(T("Quota spent, pausing downloading"))
self.check_quota()
# Speedometer
try:
@@ -431,15 +430,47 @@ class BPSMeter:
# We record every second, but display at the user's refresh-rate
return self.bps_list[::refresh_rate]
def check_quota(self):
"""Pause the queue when all quota is spent
Notify at specific quota usages (75%, 90%, 100%)
"""
if self.left <= 0.0:
if not sabnzbd.Downloader.paused:
sabnzbd.Downloader.pause()
logging.warning(T("Quota spent, pausing downloading"))
# Guard against zero division
if self.quota:
# Check for quota notifications (75%, 90%, 100%)
# Only send notification for the highest applicable threshold that hasn't been notified yet
used_percentage = ((self.quota - self.left) / self.quota) * 100
if used_percentage >= 100 and self.quota_notifications_sent < 100:
sabnzbd.notifier.send_notification(T("Quota"), T("Quota spent, pausing downloading"), "quota")
elif used_percentage >= 90 and self.quota_notifications_sent < 90:
sabnzbd.notifier.send_notification(
T("Quota"),
T("Quota limit warning (%d%%)") % used_percentage,
"quota",
)
elif used_percentage >= 75 and self.quota_notifications_sent < 75:
sabnzbd.notifier.send_notification(
T("Quota"),
T("Quota limit warning (%d%%)") % used_percentage,
"quota",
)
self.quota_notifications_sent = used_percentage
def reset_quota(self, force: bool = False):
"""Check if it's time to reset the quota, optionally resuming
Return True, when still paused or should be paused
"""
if force or (self.have_quota and time.time() > (self.q_time - 50)):
self.quota = self.left = cfg.quota_size.get_float()
self.quota_notifications_sent = 0
logging.info("Quota was reset to %s", self.quota)
if cfg.quota_resume():
logging.info("Auto-resume due to quota reset")
sabnzbd.notifier.send_notification(T("Quota"), T("Downloading resumed after quota reset"), "quota")
sabnzbd.Downloader.resume()
self.next_reset()
return False

View File

@@ -531,6 +531,7 @@ switchinterval = OptionNumber("misc", "switchinterval", 0.005, minval=0.001)
ssdp_broadcast_interval = OptionNumber("misc", "ssdp_broadcast_interval", 15, minval=1, maxval=600)
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")
##############################################################################
@@ -558,6 +559,7 @@ ncenter_prio_pp = OptionBool("ncenter", "ncenter_prio_pp", False)
ncenter_prio_complete = OptionBool("ncenter", "ncenter_prio_complete", True)
ncenter_prio_failed = OptionBool("ncenter", "ncenter_prio_failed", True)
ncenter_prio_disk_full = OptionBool("ncenter", "ncenter_prio_disk_full", True)
ncenter_prio_quota = OptionBool("ncenter", "ncenter_prio_quota", True)
ncenter_prio_new_login = OptionBool("ncenter", "ncenter_prio_new_login", False)
ncenter_prio_warning = OptionBool("ncenter", "ncenter_prio_warning", False)
ncenter_prio_error = OptionBool("ncenter", "ncenter_prio_error", False)
@@ -574,6 +576,7 @@ acenter_prio_pp = OptionBool("acenter", "acenter_prio_pp", False)
acenter_prio_complete = OptionBool("acenter", "acenter_prio_complete", True)
acenter_prio_failed = OptionBool("acenter", "acenter_prio_failed", True)
acenter_prio_disk_full = OptionBool("acenter", "acenter_prio_disk_full", True)
acenter_prio_quota = OptionBool("acenter", "acenter_prio_quota", True)
acenter_prio_new_login = OptionBool("acenter", "acenter_prio_new_login", False)
acenter_prio_warning = OptionBool("acenter", "acenter_prio_warning", False)
acenter_prio_error = OptionBool("acenter", "acenter_prio_error", False)
@@ -590,6 +593,7 @@ ntfosd_prio_pp = OptionBool("ntfosd", "ntfosd_prio_pp", False)
ntfosd_prio_complete = OptionBool("ntfosd", "ntfosd_prio_complete", True)
ntfosd_prio_failed = OptionBool("ntfosd", "ntfosd_prio_failed", True)
ntfosd_prio_disk_full = OptionBool("ntfosd", "ntfosd_prio_disk_full", True)
ntfosd_prio_quota = OptionBool("ntfosd", "ntfosd_prio_quota", True)
ntfosd_prio_new_login = OptionBool("ntfosd", "ntfosd_prio_new_login", False)
ntfosd_prio_warning = OptionBool("ntfosd", "ntfosd_prio_warning", False)
ntfosd_prio_error = OptionBool("ntfosd", "ntfosd_prio_error", False)
@@ -607,6 +611,7 @@ prowl_prio_pp = OptionNumber("prowl", "prowl_prio_pp", -3)
prowl_prio_complete = OptionNumber("prowl", "prowl_prio_complete", 0)
prowl_prio_failed = OptionNumber("prowl", "prowl_prio_failed", 1)
prowl_prio_disk_full = OptionNumber("prowl", "prowl_prio_disk_full", 1)
prowl_prio_quota = OptionNumber("prowl", "prowl_prio_quota", 0)
prowl_prio_new_login = OptionNumber("prowl", "prowl_prio_new_login", -3)
prowl_prio_warning = OptionNumber("prowl", "prowl_prio_warning", -3)
prowl_prio_error = OptionNumber("prowl", "prowl_prio_error", -3)
@@ -628,6 +633,7 @@ pushover_prio_pp = OptionNumber("pushover", "pushover_prio_pp", -3)
pushover_prio_complete = OptionNumber("pushover", "pushover_prio_complete", -1)
pushover_prio_failed = OptionNumber("pushover", "pushover_prio_failed", -1)
pushover_prio_disk_full = OptionNumber("pushover", "pushover_prio_disk_full", 1)
pushover_prio_quota = OptionNumber("pushover", "pushover_prio_quota", -1)
pushover_prio_new_login = OptionNumber("pushover", "pushover_prio_new_login", -3)
pushover_prio_warning = OptionNumber("pushover", "pushover_prio_warning", 1)
pushover_prio_error = OptionNumber("pushover", "pushover_prio_error", 1)
@@ -646,6 +652,7 @@ pushbullet_prio_pp = OptionBool("pushbullet", "pushbullet_prio_pp", False)
pushbullet_prio_complete = OptionBool("pushbullet", "pushbullet_prio_complete", True)
pushbullet_prio_failed = OptionBool("pushbullet", "pushbullet_prio_failed", True)
pushbullet_prio_disk_full = OptionBool("pushbullet", "pushbullet_prio_disk_full", True)
pushbullet_prio_quota = OptionBool("pushbullet", "pushbullet_prio_quota", True)
pushbullet_prio_new_login = OptionBool("pushbullet", "pushbullet_prio_new_login", False)
pushbullet_prio_warning = OptionBool("pushbullet", "pushbullet_prio_warning", False)
pushbullet_prio_error = OptionBool("pushbullet", "pushbullet_prio_error", False)
@@ -670,6 +677,8 @@ apprise_target_failed = OptionStr("apprise", "apprise_target_failed")
apprise_target_failed_enable = OptionBool("apprise", "apprise_target_failed_enable", True)
apprise_target_disk_full = OptionStr("apprise", "apprise_target_disk_full")
apprise_target_disk_full_enable = OptionBool("apprise", "apprise_target_disk_full_enable", False)
apprise_target_quota = OptionStr("apprise", "apprise_target_quota")
apprise_target_quota_enable = OptionBool("apprise", "apprise_target_quota_enable", True)
apprise_target_new_login = OptionStr("apprise", "apprise_target_new_login")
apprise_target_new_login_enable = OptionBool("apprise", "apprise_target_new_login_enable", True)
apprise_target_warning = OptionStr("apprise", "apprise_target_warning")
@@ -693,6 +702,7 @@ nscript_prio_pp = OptionBool("nscript", "nscript_prio_pp", False)
nscript_prio_complete = OptionBool("nscript", "nscript_prio_complete", True)
nscript_prio_failed = OptionBool("nscript", "nscript_prio_failed", True)
nscript_prio_disk_full = OptionBool("nscript", "nscript_prio_disk_full", True)
nscript_prio_quota = OptionBool("nscript", "nscript_prio_quota", True)
nscript_prio_new_login = OptionBool("nscript", "nscript_prio_new_login", False)
nscript_prio_warning = OptionBool("nscript", "nscript_prio_warning", False)
nscript_prio_error = OptionBool("nscript", "nscript_prio_error", False)

View File

@@ -232,6 +232,11 @@ class HistoryDB:
logging.info("Removing all jobs with status=%s", status)
self.execute("""DELETE FROM history WHERE name LIKE ? AND status = ?""", (search, status))
def mark_as_completed(self, job: str):
"""Mark a job as completed in the history"""
self.execute("""UPDATE history SET status = ? WHERE nzo_id = ?""", (Status.COMPLETED, job))
logging.info("[%s] Marked job %s as completed", caller_name(), job)
def get_failed_paths(self, search: Optional[str] = None) -> List[str]:
"""Return list of all storage paths of failed jobs (may contain non-existing or empty paths)"""
search = convert_search(search)

View File

@@ -70,6 +70,8 @@ def conditional_cache(cache_time: int):
Empty results (None, empty collections, empty strings, False, 0) are not cached.
If a keyword argument of `force=True` is used, the cache is skipped.
Unhashable types (such as List) can not be used as an input to the wrapped function in the current implementation!
:param cache_time: Time in seconds to cache non-empty results
"""
@@ -82,6 +84,8 @@ def conditional_cache(cache_time: int):
# Create cache key using functools._make_key
try:
key = functools._make_key(args, kwargs, typed=False)
# Make sure it's a hashable to be used as key, this changed in Python 3.14
hash(key)
except TypeError:
# If args/kwargs aren't hashable, skip caching entirely
return func(*args, **kwargs)

View File

@@ -30,13 +30,14 @@ from typing import Optional, Dict, List, Tuple
import sabnzbd
import sabnzbd.cfg as cfg
from sabnzbd.misc import int_conv, format_time_string, build_and_run_command
from sabnzbd.filesystem import long_path, remove_all, real_path, remove_file, get_basename
from sabnzbd.filesystem import remove_all, real_path, remove_file, get_basename, clip_path
from sabnzbd.nzbstuff import NzbObject, NzbFile
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
from sabnzbd.postproc import prepare_extraction_path
from sabnzbd.utils.rarfile import RarFile
from sabnzbd.misc import SABRarFile
import rarfile
from sabnzbd.utils.diskspeed import diskspeedmeasure
# Need a lock to make sure start and stop is handled correctly
@@ -415,40 +416,24 @@ class DirectUnpacker(threading.Thread):
# Generate command
rarfile_path = os.path.join(self.nzo.download_path, self.rarfile_nzf.filename)
if sabnzbd.WINDOWS:
# On Windows, UnRar uses a custom argument parser
# See: https://github.com/sabnzbd/sabnzbd/issues/1043
# The -scf forces the output to be UTF8
command = [
sabnzbd.newsunpack.RAR_COMMAND,
action,
"-vp",
"-idp",
"-scf",
"-o+",
"-ai",
password_command,
rarfile_path,
"%s\\" % long_path(extraction_path),
]
else:
# The -scf forces the output to be UTF8
command = [
sabnzbd.newsunpack.RAR_COMMAND,
action,
"-vp",
"-idp",
"-scf",
"-o+",
"-ai",
password_command,
rarfile_path,
"%s/" % extraction_path,
]
# The -scf forces the output to be UTF8
command = [
sabnzbd.newsunpack.RAR_COMMAND,
action,
"-vp",
"-idp",
"-scf",
"-o+",
"-ai",
password_command,
rarfile_path,
clip_path(extraction_path),
]
if cfg.ignore_unrar_dates():
command.insert(3, "-tsm-")
if unrar_parameters := cfg.unrar_parameters().strip().split():
if unrar_parameters := cfg.unrar_parameters().split():
for param in unrar_parameters:
command.insert(-2, param)
@@ -456,6 +441,8 @@ class DirectUnpacker(threading.Thread):
self.cur_volume = 1
# Need to disable buffer to have direct feedback
# On Windows, UnRar uses a custom argument parser
# See: https://github.com/sabnzbd/sabnzbd/issues/1043
self.active_instance = build_and_run_command(
command, windows_unrar_command=True, text_mode=False, stdin=subprocess.PIPE
)
@@ -508,8 +495,8 @@ class DirectUnpacker(threading.Thread):
if one_folder:
# RarFile can fail for mysterious reasons
try:
rar_contents = RarFile(
os.path.join(self.nzo.download_path, rarfile_nzf.filename), single_file_check=True
rar_contents = SABRarFile(
os.path.join(self.nzo.download_path, rarfile_nzf.filename), part_only=True
).filelist()
for rm_file in rar_contents:
# Flat-unpack, so remove foldername from RarFile output

View File

@@ -123,7 +123,7 @@ class Server:
self.host: str = host
self.port: int = port
self.timeout: int = timeout
self.threads: int = threads
self.threads: int = threads # Total number of configured connections, not dynamic
self.priority: int = priority
self.ssl: bool = use_ssl
self.ssl_verify: int = ssl_verify
@@ -490,7 +490,7 @@ class Downloader(Thread):
# Optional and active server had too many problems.
# Disable it now and send a re-enable plan to the scheduler
if server.optional and server.active and (server.threads < 1 or (server.bad_cons / server.threads) > 3):
if server.optional and server.active and (server.bad_cons / server.threads) > 0.3:
# Deactivate server
server.bad_cons = 0
server.deactivate()
@@ -870,7 +870,6 @@ class Downloader(Thread):
# Don't count this for the tries (max_art_tries) on this server
self.__reset_nw(nw)
self.plan_server(server, _PENALTY_TOOMANY)
server.threads -= 1
elif error.code in (502, 481, 482) and clues_too_many_ip(error.msg):
# Login from (too many) different IP addresses
errormsg = T(
@@ -1144,6 +1143,11 @@ def check_server_quota():
if server.quota():
if server.quota.get_int() + server.usage_at_start() < sabnzbd.BPSMeter.grand_total.get(srv, 0):
logging.warning(T("Server %s has used the specified quota"), server.displayname())
sabnzbd.notifier.send_notification(
T("Quota"),
T("Server %s has used the specified quota") % server.displayname(),
"quota",
)
server.quota.set("")
config.save_config()

View File

@@ -33,7 +33,6 @@ import fnmatch
import stat
import ctypes
import random
import functools
from typing import Union, List, Tuple, Any, Dict, Optional, BinaryIO
try:
@@ -55,7 +54,7 @@ from sabnzbd.constants import (
DEX_FILE_EXTENSION_MAX,
)
from sabnzbd.encoding import correct_unknown_encoding, utob, limit_encoded_length
from sabnzbd.utils import rarfile
import rarfile
# For Windows: determine executable extensions
@@ -991,50 +990,6 @@ def remove_all(path: str, pattern: str = "*", keep_folder: bool = False, recursi
##############################################################################
# Diskfree
##############################################################################
def disk_free_macos_clib_statfs64(directory: str) -> Tuple[int, int]:
# MacOS only!
# direct system call to c-lib's statfs(), not python's os.statvfs()
# because statvfs() on MacOS has a rollover at 4TB (possibly a 32bit rollover with 10bit block size)
# See https://bugs.python.org/issue43638
# Based on code of pudquick and blackntan
# Input: directory.
# Output: disksize and available space, in bytes
# format & parameters: on MacOS, see "man statfs", lines starting at
# "struct statfs { /* when _DARWIN_FEATURE_64_BIT_INODE is defined */"
class statfs64(ctypes.Structure):
_fields_ = [
("f_bsize", ctypes.c_uint32),
("f_iosize", ctypes.c_int32),
("f_blocks", ctypes.c_uint64),
("f_bfree", ctypes.c_uint64),
("f_bavail", ctypes.c_uint64),
("f_files", ctypes.c_uint64),
("f_ffree", ctypes.c_uint64),
("f_fsid", ctypes.c_uint64),
("f_owner", ctypes.c_uint32),
("f_type", ctypes.c_uint32),
("f_flags", ctypes.c_uint32),
("f_fssubtype", ctypes.c_uint32),
("f_fstypename", ctypes.c_char * 16),
("f_mntonname", ctypes.c_char * 1024),
("f_mntfromname", ctypes.c_char * 1024),
("f_reserved", ctypes.c_uint32 * 8),
]
fs_info64 = statfs64() # set up the parameters to be filled out
result = sabnzbd.MACOSLIBC.statfs64(
ctypes.create_string_buffer(utob(directory)), ctypes.byref(fs_info64)
) # fs_info64 gets filled out via the byref()
if result == 0:
# result = 0: "Upon successful completion, a value of 0 is returned."
return fs_info64.f_blocks * fs_info64.f_bsize, fs_info64.f_bavail * fs_info64.f_bsize
else:
# result = -1: "Otherwise, -1 is returned and the global variable errno is set to indicate the error."
logging.debug("Call to MACOSLIBC.statfs64 not successful. Value of errno is %s", ctypes.get_errno())
return 0, 0
def diskspace_base(dir_to_check: str) -> Tuple[float, float]:
"""Return amount of free and used diskspace in GBytes"""
# Find first folder level that exists in the path
@@ -1049,10 +1004,6 @@ def diskspace_base(dir_to_check: str) -> Tuple[float, float]:
return disk_size / GIGI, available / GIGI
except Exception:
return 0.0, 0.0
elif sabnzbd.MACOS:
# MacOS diskfree ... via c-lib call statfs()
disk_size, available = disk_free_macos_clib_statfs64(dir_to_check)
return disk_size / GIGI, available / GIGI
elif hasattr(os, "statvfs"):
# posix diskfree
try:
@@ -1290,6 +1241,10 @@ def check_filesystem_capabilities(test_dir: str) -> bool:
# if not on Windows, check special chars like \ and :
if not sabnzbd.WINDOWS and not directory_is_writable_with_file(test_dir, "sab_test \\ bla :: , bla.txt"):
# Always enable "Make Windows Compatible"
sabnzbd.cfg.sanitize_safe.set(True)
# However, external programs like unrar can still try to write them so we still warn the user
sabnzbd.misc.helpful_warning(
T("%s is not writable with special character filenames. This can cause problems."), test_dir
)

View File

@@ -73,9 +73,11 @@ def addresslookup6(myhost):
def active_socks5_proxy() -> Optional[str]:
"""Return the active proxy"""
if socket.socket == socks.socksocket:
return "%s:%s" % socks.socksocket.default_proxy[1:3]
"""Return the active proxy. And None if no proxy is set"""
if socks.socksocket.default_proxy:
socks5host = socks.socksocket.default_proxy[1]
socks5port = sabnzbd.misc.int_conv(socks.socksocket.default_proxy[2], default=1080)
return f"{socks5host}:{socks5port}"
return None
@@ -92,11 +94,21 @@ def dnslookup() -> bool:
def local_ipv4() -> Optional[str]:
"""return IPv4 address of default local LAN interface"""
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s_ipv4:
# Option: use 100.64.1.1 (IANA-Reserved IPv4 Prefix for Shared Address Space)
s_ipv4.connect(("10.255.255.255", 80))
ipv4 = s_ipv4.getsockname()[0]
if not socks.socksocket.default_proxy:
# No socks5 proxy, so we can use UDP (SOCK_DGRAM) and a non-reachable host
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s_ipv4:
s_ipv4.connect(("10.255.255.255", 80))
ipv4 = s_ipv4.getsockname()[0]
else:
# socks5 proxy set, so we must use TCP (SOCK_STREAM) and a reachable host: the proxy server
socks5host = socks.socksocket.default_proxy[1]
socks5port = sabnzbd.misc.int_conv(socks.socksocket.default_proxy[2], default=1080)
logging.debug(f"Using proxy {socks5host} on port {socks5port} to determine local IPv4 address")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s_ipv4:
s_ipv4.connect((socks5host, socks5port))
ipv4 = s_ipv4.getsockname()[0]
except socket.error:
ipv4 = None

View File

@@ -911,6 +911,7 @@ SPECIAL_VALUE_LIST = (
"selftest_host",
"ssdp_broadcast_interval",
"unrar_parameters",
"outgoing_nntp_ip",
)
SPECIAL_LIST_LIST = (
"rss_odd_titles",
@@ -2028,6 +2029,7 @@ NOTIFY_OPTIONS = {
"ncenter_prio_complete",
"ncenter_prio_failed",
"ncenter_prio_disk_full",
"ncenter_prio_quota",
"ncenter_prio_warning",
"ncenter_prio_error",
"ncenter_prio_queue_done",
@@ -2044,6 +2046,7 @@ NOTIFY_OPTIONS = {
"acenter_prio_complete",
"acenter_prio_failed",
"acenter_prio_disk_full",
"acenter_prio_quota",
"acenter_prio_warning",
"acenter_prio_error",
"acenter_prio_queue_done",
@@ -2060,6 +2063,7 @@ NOTIFY_OPTIONS = {
"ntfosd_prio_complete",
"ntfosd_prio_failed",
"ntfosd_prio_disk_full",
"ntfosd_prio_quota",
"ntfosd_prio_warning",
"ntfosd_prio_error",
"ntfosd_prio_queue_done",
@@ -2077,6 +2081,7 @@ NOTIFY_OPTIONS = {
"prowl_prio_complete",
"prowl_prio_failed",
"prowl_prio_disk_full",
"prowl_prio_quota",
"prowl_prio_warning",
"prowl_prio_error",
"prowl_prio_queue_done",
@@ -2096,6 +2101,7 @@ NOTIFY_OPTIONS = {
"pushover_prio_complete",
"pushover_prio_failed",
"pushover_prio_disk_full",
"pushover_prio_quota",
"pushover_prio_warning",
"pushover_prio_error",
"pushover_prio_queue_done",
@@ -2116,6 +2122,7 @@ NOTIFY_OPTIONS = {
"pushbullet_prio_complete",
"pushbullet_prio_failed",
"pushbullet_prio_disk_full",
"pushbullet_prio_quota",
"pushbullet_prio_warning",
"pushbullet_prio_error",
"pushbullet_prio_queue_done",
@@ -2140,6 +2147,8 @@ NOTIFY_OPTIONS = {
"apprise_target_failed_enable",
"apprise_target_disk_full",
"apprise_target_disk_full_enable",
"apprise_target_quota",
"apprise_target_quota_enable",
"apprise_target_warning",
"apprise_target_warning_enable",
"apprise_target_error",
@@ -2163,6 +2172,7 @@ NOTIFY_OPTIONS = {
"nscript_prio_complete",
"nscript_prio_failed",
"nscript_prio_disk_full",
"nscript_prio_quota",
"nscript_prio_warning",
"nscript_prio_error",
"nscript_prio_queue_done",

View File

@@ -24,7 +24,6 @@ import platform
import ssl
import sys
import logging
import functools
import urllib.request
import urllib.parse
import re
@@ -39,6 +38,7 @@ import html
import ipaddress
import socks
import math
import rarfile
from threading import Thread
from collections.abc import Iterable
from typing import Union, Tuple, Any, AnyStr, Optional, List, Dict, Collection
@@ -1585,3 +1585,40 @@ def convert_history_retention():
cfg.history_retention_number.set(to_keep)
elif to_keep < 0:
cfg.history_retention_option.set("all-delete")
##
## SABnzbd patched rarfile classes
## Patch for https://github.com/markokr/rarfile/issues/56#issuecomment-711146569
##
class SABRarFile(rarfile.RarFile):
"""SABnzbd patched RarFile class with info_callback fix for multi-volume archives"""
def __init__(self, *args, **kwargs):
"""Patch RarFile-call when using `part_only`
to store filenames inside the RAR-files"""
if kwargs.get("part_only"):
kwargs["info_callback"] = self.info_callback
# Let RarFile handle the rest!
super().__init__(*args, **kwargs)
def info_callback(self, rar_obj: rarfile.RarInfo):
"""Called for every RarInfo-object found"""
# We only care about files inside the Rar
# For Rar5 there is a separate object, for Rar3 we need to check if a filename was parsed
if isinstance(rar_obj, (rarfile.Rar5FileInfo, rarfile.Rar3Info)) and rar_obj.filename:
# Avoid duplicates
if rar_obj not in self._file_parser._info_list:
self._file_parser._info_list.append(rar_obj)
self._file_parser._info_map[rar_obj.filename.rstrip("/")] = rar_obj
def filelist(self):
"""Return list of filenames in archive."""
return [f.filename for f in self.infolist() if not f.isdir()]
def trigger_parse(self):
"""Force re-parse, wich is needed to trigger password checking logic"""
self._parse()

View File

@@ -28,11 +28,11 @@ import time
import io
import shutil
import functools
import rarfile
from typing import Tuple, List, BinaryIO, Optional, Dict, Any, Union, Set
import sabnzbd
from sabnzbd.encoding import correct_unknown_encoding, ubtou
import sabnzbd.utils.rarfile as rarfile
from sabnzbd.misc import (
format_time_string,
find_on_path,
@@ -44,6 +44,7 @@ from sabnzbd.misc import (
build_and_run_command,
format_time_left,
is_none,
SABRarFile,
)
from sabnzbd.filesystem import (
make_script_path,
@@ -66,7 +67,7 @@ from sabnzbd.filesystem import (
)
from sabnzbd.nzbstuff import NzbObject
import sabnzbd.cfg as cfg
from sabnzbd.constants import Status, JOB_ADMIN
from sabnzbd.constants import Status
# Regex globals
@@ -647,15 +648,14 @@ def rar_extract_core(
"""
start = time.time()
logging.debug("rar_extract(): Extractionpath: %s", extraction_path)
logging.debug("Extraction path: %s", extraction_path)
logging.debug("Found rar version: %s", rarfile.is_rarfile(rarfile_path))
if password:
password_command = "-p%s" % password
else:
password_command = "-p-"
############################################################################
if one_folder or cfg.flat_unpack():
action = "e"
else:
@@ -667,24 +667,7 @@ def rar_extract_core(
overwrite = "-o-" # Disable overwrite
rename = "-or" # Auto renaming
if sabnzbd.WINDOWS:
# On Windows, UnRar uses a custom argument parser
# See: https://github.com/sabnzbd/sabnzbd/issues/1043
# The -scf forces the output to be UTF8
command = [
RAR_COMMAND,
action,
"-idp",
"-scf",
overwrite,
rename,
"-ai",
password_command,
rarfile_path,
"%s\\" % long_path(extraction_path),
]
elif RAR_PROBLEM:
if RAR_PROBLEM:
# Use only oldest options, specifically no "-or" or "-scf"
command = [
RAR_COMMAND,
@@ -693,10 +676,11 @@ def rar_extract_core(
overwrite,
password_command,
rarfile_path,
"%s/" % extraction_path,
extraction_path,
]
else:
# The -scf forces the output to be UTF8
# On Windows, specifically remove long path from destination so Unrar handles it
command = [
RAR_COMMAND,
action,
@@ -707,17 +691,17 @@ def rar_extract_core(
"-ai",
password_command,
rarfile_path,
"%s/" % extraction_path,
clip_path(extraction_path),
]
if cfg.ignore_unrar_dates():
command.insert(3, "-tsm-")
if not RAR_PROBLEM and (unrar_parameters := cfg.unrar_parameters().strip().split()):
for param in unrar_parameters:
command.insert(-2, param)
if cfg.ignore_unrar_dates():
command.insert(3, "-tsm-")
if unrar_parameters := cfg.unrar_parameters().split():
for param in unrar_parameters:
command.insert(-2, param)
# Get list of all the volumes part of this set
logging.debug("Analyzing rar file ... %s found", rarfile.is_rarfile(rarfile_path))
# On Windows, UnRar uses a custom argument parser
# See: https://github.com/sabnzbd/sabnzbd/issues/1043
p = build_and_run_command(command, windows_unrar_command=True)
sabnzbd.PostProcessor.external_process = p
@@ -793,11 +777,21 @@ def rar_extract_core(
requires_kill = True
elif line.startswith("Cannot create"):
line2 = p.stdout.readline()
if "must not exceed 260" in line2:
msg = "%s: %s" % (T("Unpacking failed, path is too long"), line[13:])
else:
msg = "%s %s" % (T("Unpacking failed, write error or disk is full?"), line[13:])
# Check if maybe it can be salvaged
line = p.stdout.readline()
lines.append(line.strip())
# Error is different on Linux and Windows
if line.startswith(("Invalid argument", "The filename, directory name, or volume label syntax")):
# Read another line
line = p.stdout.readline()
lines.append(line.strip())
# Will it try to correct?
if line.startswith("WARNING: Attempting to correct"):
# Great! Let it try
logging.info("Unrar detected invalid filename and is attempting to correct")
continue
msg = "%s %s" % (T("Unpacking failed, write error or disk is full?"), line)
nzo.fail_msg = msg
nzo.set_unpack_info("Unpack", msg, setname)
fail = 1
@@ -1474,7 +1468,7 @@ def rar_volumelist(rarfile_path: str, password: str, known_volumes: List[str]) -
# UnRar is required to read some RAR files
# RarFile can fail in special cases
try:
zf = rarfile.RarFile(rarfile_path)
zf = SABRarFile(rarfile_path)
# setpassword can fail due to bugs in RarFile
if password:

View File

@@ -342,6 +342,20 @@ class NNTP:
self.sock.settimeout(self.nw.server.timeout)
# Connect
if outgoing_nntp_ip := sabnzbd.cfg.outgoing_nntp_ip():
try:
self.sock.bind((outgoing_nntp_ip, 0))
socket_info = self.sock.getsockname()
logging.debug(
"%s@%s: Successfully bound to following ip address: %s at following port: %d",
self.nw.thrdnum,
self.nw.server.host,
socket_info[0],
socket_info[1],
)
except socket.error:
raise ConnectionError(f"Could not bind to outgoing interface {outgoing_nntp_ip}")
self.sock.connect(self.addrinfo.sockaddr)
# Secured or unsecured?

View File

@@ -89,6 +89,7 @@ NOTIFICATION_TYPES = {
"warning": TT("Warning"), #: Notification
"error": TT("Error"), #: Notification
"disk_full": TT("Disk full"), #: Notification
"quota": TT("Quota"), #: Notification
"queue_done": TT("Queue finished"), #: Notification
"new_login": TT("User logged in"), #: Notification
"other": TT("Other Messages"), #: Notification
@@ -323,6 +324,8 @@ def send_apprise(title, msg, notification_type, force=False, test=None):
"error": apprise.common.NotifyType.FAILURE,
# Disk full
"disk_full": apprise.common.NotifyType.WARNING,
# Quota
"quota": apprise.common.NotifyType.WARNING,
# Queue finished
"queue_done": apprise.common.NotifyType.INFO,
# User logged in

View File

@@ -43,7 +43,8 @@ from sabnzbd.filesystem import (
)
from sabnzbd.misc import name_to_cat, cat_pp_script_sanitizer
from sabnzbd.constants import DEFAULT_PRIORITY, VALID_ARCHIVES, AddNzbFileResult
from sabnzbd.utils import rarfile
from sabnzbd.misc import SABRarFile
import rarfile
def add_nzbfile(
@@ -169,7 +170,7 @@ def process_nzb_archive_file(
if zipfile.is_zipfile(path):
zf = zipfile.ZipFile(path)
elif rarfile.is_rarfile(path):
zf = rarfile.RarFile(path)
zf = SABRarFile(path)
elif sabnzbd.newsunpack.is_sevenfile(path):
zf = sabnzbd.newsunpack.SevenZip(path)
else:

View File

@@ -246,7 +246,9 @@ class NzbQueue:
def set_top_only(self, value):
self.__top_only = value
@NzbQueueLocker
def change_opts(self, nzo_ids: List[str], pp: int) -> int:
"""Locked so changes during URLGrabbing are correctly passed to new job"""
result = 0
for nzo_id in nzo_ids:
if nzo_id in self.__nzo_table:
@@ -254,7 +256,9 @@ class NzbQueue:
result += 1
return result
@NzbQueueLocker
def change_script(self, nzo_ids: List[str], script: str) -> int:
"""Locked so changes during URLGrabbing are correctly passed to new job"""
result = 0
if (script is None) or is_valid_script(script):
for nzo_id in nzo_ids:
@@ -264,7 +268,9 @@ class NzbQueue:
result += 1
return result
@NzbQueueLocker
def change_cat(self, nzo_ids: List[str], cat: str) -> int:
"""Locked so changes during URLGrabbing are correctly passed to new job"""
result = 0
for nzo_id in nzo_ids:
if nzo_id in self.__nzo_table:
@@ -278,7 +284,9 @@ class NzbQueue:
result += 1
return result
@NzbQueueLocker
def change_name(self, nzo_id: str, name: str, password: str = None) -> bool:
"""Locked so changes during URLGrabbing are correctly passed to new job"""
if nzo_id in self.__nzo_table:
nzo = self.__nzo_table[nzo_id]
logging.info("Renaming %s to %s", nzo.final_name, name)
@@ -440,7 +448,9 @@ class NzbQueue:
handled.append(nzo_id)
return handled
@NzbQueueLocker
def pause_nzo(self, nzo_id: str) -> List[str]:
"""Locked so changes during URLGrabbing are correctly passed to new job"""
handled = []
if nzo_id in self.__nzo_table:
nzo = self.__nzo_table[nzo_id]

View File

@@ -194,9 +194,10 @@ def parse_par2_file(fname: str, md5of16k: Dict[bytes, str]) -> Tuple[str, Dict[s
for i in range(48, pack_len - 32, 20):
filecrc32[fileid].append(struct.unpack("<I", data[i + 16 : i + 20])[0])
# On large files, we stop after seeing all the listings
# On large files, we stop after seeing all the listings and have crc32 data for all listings
# Our unit-tests do not include large par2 files, so we cannot verify cases like #3164!
# On smaller files, we scan them fully to get the par2-creator
if total_size > SCAN_LIMIT and len(filepar2info) == nr_files:
if total_size > SCAN_LIMIT and len(filepar2info) == nr_files == len(filecrc32):
break
# Process all the data

View File

@@ -26,6 +26,7 @@ import time
import re
import gc
import queue
import rarfile
from typing import List, Optional, Tuple
import sabnzbd
@@ -46,6 +47,7 @@ from sabnzbd.misc import (
change_queue_complete_action,
run_script,
is_none,
SABRarFile,
)
from sabnzbd.filesystem import (
real_path,
@@ -89,7 +91,6 @@ import sabnzbd.config as config
import sabnzbd.cfg as cfg
import sabnzbd.database as database
import sabnzbd.notifier as notifier
import sabnzbd.utils.rarfile as rarfile
import sabnzbd.utils.rarvolinfo as rarvolinfo
import sabnzbd.utils.checkdir
import sabnzbd.deobfuscate_filenames as deobfuscate
@@ -892,7 +893,7 @@ def try_rar_check(nzo: NzbObject, rars: List[str]) -> bool:
nzo.set_action_line(T("Trying RAR-based verification"), "...")
try:
# Requires de-unicode for RarFile to work!
zf = rarfile.RarFile(rars[0])
zf = SABRarFile(rars[0])
# Skip if it's encrypted
if zf.needs_password():
@@ -952,8 +953,8 @@ def rar_renamer(nzo: NzbObject) -> int:
continue
if rarfile.is_rarfile(file_to_check):
# if a rar file is fully encrypted, rarfile.RarFile() will return an empty list:
if not rarfile.RarFile(file_to_check, single_file_check=True).filelist():
# if a rar file is fully encrypted, RarFile() will return an empty list:
if not SABRarFile(file_to_check, part_only=True).filelist():
logging.info(
"Download %s contains a fully encrypted & obfuscated rar-file: %s.",
nzo.final_name,
@@ -969,9 +970,7 @@ def rar_renamer(nzo: NzbObject) -> int:
logging.debug("Detected volume-number %s from RAR-header: %s ", rar_vol, file_to_check)
volnrext[file_to_check] = (rar_vol, new_extension)
# The files inside rar file
rar_contents = rarfile.RarFile(
os.path.join(nzo.download_path, file_to_check), single_file_check=True
).filelist()
rar_contents = SABRarFile(os.path.join(nzo.download_path, file_to_check), part_only=True).filelist()
try:
rarvolnr[rar_vol]
except Exception:

View File

@@ -171,6 +171,7 @@ SKIN_TEXT = {
"mode": TT("Processing"), #: Queue page table column header
"name": TT("Name"), #: Queue page table column header
"button-retry": TT("Retry"), #: Queue page button
"button-mark-completed": TT("Mark as Completed & Remove Temporary Files"), #: History page button
"eoq-scripts": TT("Scripts"), #: Queue page table, script selection menu
"purgeQueue": TT("Purge Queue"), #: Queue page button
"purgeQueueConf": TT("Delete all items from the queue?"), #: Confirmation popup
@@ -544,7 +545,8 @@ SKIN_TEXT = {
"srv-expire_date": TT("Account expiration date"),
"srv-explain-expire_date": TT("Warn 5 days in advance of account expiration date."),
"srv-explain-quota": TT(
"Quota for this account, counted from the time it is set. In bytes, optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally follow with K,M,G.<br />"
"Checked every few minutes. Notification is sent when quota is spent."
),
"srv-retention": TT("Retention time"), #: Server's retention time in days
"srv-ssl": TT("SSL"), #: Server SSL tickbox
@@ -838,6 +840,9 @@ SKIN_TEXT = {
"Glitter-backToQueue": TT("Send back to queue"),
"Glitter-purgeOrphaned": TT("Delete All"),
"Glitter-retryAllOrphaned": TT("Retry all"),
"Glitter-clearOrphanWarning": TT(
"Are you sure you want to delete all folders in your Temporary Download Folder? This cannot be undone!"
),
"Glitter-deleteJobAndFolders": TT("Remove NZB & Delete Files"),
"Glitter-addFromURL": TT("Fetch NZB from URL"),
"Glitter-addFromFile": TT("Upload NZB"),
@@ -914,6 +919,7 @@ SKIN_TEXT = {
"wizard-goto": TT("Go to SABnzbd"), #: Wizard step
"wizard-exit": TT("Exit SABnzbd"), #: Wizard EXIT button on first page
"wizard-start": TT("Start Wizard"), #: Wizard START button on first page
"wizard-test-server-required": TT("Click on Test Server before continuing"), #: Tooltip for disabled Next button
"restore-backup": TT("Restore backup"),
# Special
"yourRights": TT(

View File

@@ -660,6 +660,16 @@ def guess_what(name: str) -> MatchesDict:
# Unfix the title
guess["title"] = guess.get("title", "")[len(digit_fix) :]
# Handle weird anime episode notation, that results in the episode number ending up as the episode title
if (
guess.get("type") == "episode"
and not "episode" in guess
and "season" in guess
and guess.get("episode_title", "").isdigit()
):
guess.setdefault("episode", default=int(guess.get("episode_title")))
guess.pop("episode_title")
# Force season to 1 for seasonless episodes with no date
if guess.get("type") == "episode" and "date" not in guess:
guess.setdefault("season", 1)

View File

@@ -48,6 +48,7 @@ import sabnzbd.filesystem
import sabnzbd.cfg as cfg
import sabnzbd.emailer as emailer
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
@@ -263,21 +264,24 @@ class URLGrabber(Thread):
# If the user resumed a duplicate detected URL, skip the check
dup_check = future_nzo.duplicate != DuplicateStatus.DUPLICATE_IGNORED
# Add the new job to the queue
res, _ = sabnzbd.nzbparser.add_nzbfile(
path,
pp=future_nzo.pp,
script=future_nzo.script,
cat=future_nzo.cat,
priority=future_nzo.priority,
nzbname=future_nzo.custom_name,
nzo_info=nzo_info,
url=future_nzo.url,
keep=False,
password=future_nzo.password,
nzo_id=future_nzo.nzo_id,
dup_check=dup_check,
)
# Locked, so that changes to the future_nzo are picked up by the new nzo
with NZBQUEUE_LOCK:
# Add the new job to the queue
res, _ = sabnzbd.nzbparser.add_nzbfile(
path,
pp=future_nzo.pp,
script=future_nzo.script,
cat=future_nzo.cat,
priority=future_nzo.priority,
nzbname=future_nzo.custom_name,
nzo_info=nzo_info,
url=future_nzo.url,
keep=False,
password=future_nzo.password,
nzo_id=future_nzo.nzo_id,
dup_check=dup_check,
)
if res is AddNzbFileResult.RETRY:
logging.info("Incomplete NZB, retry after 5 min %s", url)
self.add(url, future_nzo, when=300)

View File

File diff suppressed because it is too large Load Diff

View File

@@ -19,14 +19,10 @@
"""
sabnzbd.utils.rarvolinfo - Find out volume number and/or original extension of a rar file. Useful with obfuscated files
"""
import logging
import os
try:
import sabnzbd.utils.rarfile as rarfile
except ImportError:
import rarfile
import rarfile
def get_rar_extension(myrarfile):
@@ -40,9 +36,9 @@ def get_rar_extension(myrarfile):
org_extension = False
try:
rar_ver = rarfile.is_rarfile(myrarfile)
rar_ver = rarfile.get_rar_version(myrarfile)
with open(myrarfile, "rb") as fh:
if rar_ver.endswith("3"):
if rar_ver == rarfile.RAR_V3:
# As it's rar3, let's first find the numbering scheme: old (rNN) or new (partNN.rar)
mybuf = fh.read(100) # first 100 bytes is enough
HEAD_FLAGS_LSB = mybuf[10] # LSB = Least Significant Byte
@@ -62,7 +58,7 @@ def get_rar_extension(myrarfile):
else:
org_extension = "r%02d" % (volumenumber - 2)
elif rar_ver.endswith("5"):
elif rar_ver == rarfile.RAR_V5:
mybuf = fh.read(100) # first 100 bytes is enough
# Get (and skip) the first 8 + 4 bytes

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.5.3"
__baseline__ = "7a5ca5b226d8f41b599bb88add1ec4cb6d815fc8"
__version__ = "4.5.5"
__baseline__ = "a61a5539a7e0e0dc1f9ae140222436ba8f9fe679"

View File

Binary file not shown.

View File

Binary file not shown.

View File

@@ -3,13 +3,14 @@ pytest
setuptools
selenium
requests
pyfakefs>=5.6.0
werkzeug<2.1.0 # Breaks httpbin in newer versions
pyfakefs
werkzeug
pytest-httpbin
pytest-httpserver
flaky
xmltodict
tavern
tavern==3.0.0; python_version >= '3.11' # Latest version only supported on Python 3.11 and above
flask
tavalidate
importlib_metadata

View File

@@ -31,6 +31,7 @@ import sabnzbd
import sabnzbd.newsunpack as newsunpack
from sabnzbd.constants import JOB_ADMIN
from sabnzbd.misc import format_time_string
from sabnzbd.filesystem import long_path, create_all_dirs, listdir_full
class TestNewsUnpackFunctions:
@@ -276,3 +277,355 @@ class TestPar2Repair:
call("Verifying repair", "01/01"),
]
)
@pytest.mark.usefixtures("clean_cache_dir")
class TestRarUnpack:
@staticmethod
def _create_test_nzo(temp_dir, filename="test.nzb"):
"""Create a mock NZO object for testing"""
nzo = mock.Mock()
nzo.download_path = temp_dir
nzo.admin_path = os.path.join(temp_dir, JOB_ADMIN)
nzo.fail_msg = ""
nzo.final_name = filename
nzo.delete = True # Enable deletion of extracted files
nzo.direct_unpacker = None # No direct unpacker
nzo.set_unpack_info = mock.Mock()
nzo.set_action_line = mock.Mock()
# Mock password-related attributes
nzo.password = "" # No password by default
nzo.nzo_info = {} # Empty nzo_info
nzo.meta = {} # Empty meta data
nzo.correct_password = "" # No correct password found yet
return nzo
@staticmethod
def _run_rar_unpack(
test_dir,
rar_files,
one_folder=False,
custom_temp_test_dir=None,
custom_temp_complete_dir=None,
custom_nzo_settings=None,
):
"""Run rar_unpack with test data"""
# Base
temp_test_dir_base = temp_test_dir = long_path(os.path.join(SAB_CACHE_DIR, "rar_unpack_temp"))
temp_complete_dir_base = temp_complete_dir = long_path(os.path.join(SAB_CACHE_DIR, "rar_complete_temp"))
# Extend if needed
if custom_temp_test_dir:
temp_test_dir = os.path.join(temp_test_dir, custom_temp_test_dir)
if custom_temp_complete_dir:
temp_complete_dir = os.path.join(temp_complete_dir, custom_temp_complete_dir)
assert create_all_dirs(temp_test_dir), f"Failed to create {temp_test_dir}"
assert create_all_dirs(temp_complete_dir), f"Failed to create {temp_complete_dir}"
# Copy test files to temp directory
copied_rars = []
for rar_file in rar_files:
src_path = os.path.join(test_dir, rar_file)
if os.path.exists(src_path):
dst_path = os.path.join(temp_test_dir, rar_file)
shutil.copy(src_path, dst_path)
copied_rars.append(dst_path)
# Make sure all programs are found
newsunpack.find_programs(".")
# Mock PostProcessor that's needed for RAR extraction
sabnzbd.PostProcessor = mock.Mock()
# Create mock NZO
nzo = TestRarUnpack._create_test_nzo(temp_test_dir)
# Apply custom NZO settings if provided
if custom_nzo_settings:
for key, value in custom_nzo_settings.items():
setattr(nzo, key, value)
try:
# Run the rar_unpack function
error_code, extracted_files = newsunpack.rar_unpack(nzo, temp_complete_dir, one_folder, copied_rars)
# Get directory contents with full paths
complete_contents = listdir_full(temp_complete_dir) if os.path.exists(temp_complete_dir) else []
download_contents = os.listdir(temp_test_dir) if os.path.exists(temp_test_dir) else []
return error_code, extracted_files, complete_contents, download_contents, nzo, temp_complete_dir
finally:
# Cleanup
shutil.rmtree(temp_test_dir_base)
shutil.rmtree(temp_complete_dir_base)
def _assert_successful_extraction(
self,
error_code,
extracted_files,
complete_contents,
download_contents,
temp_complete_dir,
expected_files,
should_delete_original=True,
original_files=None,
):
"""Helper method to assert common successful extraction conditions"""
# Check that extraction was successful
assert error_code == 0, "RAR extraction should succeed"
assert len(extracted_files) > 0, "Should have extracted files"
assert len(complete_contents) > 0, "Should have files in complete directory"
# Check file deletion behavior
if should_delete_original and original_files:
for original_file in original_files:
rar_still_exists = any(original_file in f for f in download_contents)
assert not rar_still_exists, f"Original RAR file {original_file} should be deleted after extraction"
elif not should_delete_original and original_files:
for original_file in original_files:
rar_still_exists = any(original_file in f for f in download_contents)
assert rar_still_exists, f"Original RAR file {original_file} should still exist when delete=False"
# Verify full paths, but since extracted_files also includes the in-between folders we use issubset
complete_contents_set = set(complete_contents)
extracted_files_set = set(extracted_files)
assert complete_contents_set.issubset(
extracted_files_set
), f"{complete_contents_set} should be in {extracted_files_set}"
# Verify the expected files are present using full paths
expected_full_paths = {os.path.join(temp_complete_dir, filename) for filename in expected_files}
assert expected_full_paths.issubset(
extracted_files_set
), f"{expected_full_paths} should be in {extracted_files_set}"
def test_basic_rar_unpack(self):
"""Test basic RAR unpacking functionality"""
test_dir = "tests/data/basic_rar5"
rar_files = ["testfile.rar"]
expected_files = {"Testfile_1234.bin", "testfile.bin", "My_Test_Download.bin"}
error_code, extracted_files, complete_contents, download_contents, nzo, temp_complete_dir = (
self._run_rar_unpack(test_dir, rar_files)
)
self._assert_successful_extraction(
error_code,
extracted_files,
complete_contents,
download_contents,
temp_complete_dir,
expected_files,
should_delete_original=True,
original_files=rar_files,
)
def test_rar_unpack_no_delete(self):
"""Test RAR unpacking without deleting the original files"""
test_dir = "tests/data/basic_rar5"
rar_files = ["testfile.rar"]
expected_files = {"Testfile_1234.bin", "testfile.bin", "My_Test_Download.bin"}
custom_nzo_settings = {"delete": False}
error_code, extracted_files, complete_contents, download_contents, nzo, temp_complete_dir = (
self._run_rar_unpack(test_dir, rar_files, custom_nzo_settings=custom_nzo_settings)
)
self._assert_successful_extraction(
error_code,
extracted_files,
complete_contents,
download_contents,
temp_complete_dir,
expected_files,
should_delete_original=False,
original_files=rar_files,
)
def test_rar_unpack_long_path(self):
"""Test RAR unpacking with very long paths (>260 characters) for both download and complete directories"""
# Create very long paths that exceed 260 characters on all platforms
# This tests handling of long paths universally, not just on Windows
# Build long nested directory structure to guarantee >260 character paths
long_dir_name = "very_long_directory_name_" + "x" * 100 # 82 characters
nested_path_parts = [long_dir_name] * 4 # 4 levels of 82-char names = 328
temp_test_dir = os.path.join(*nested_path_parts)
temp_complete_dir = os.path.join(*nested_path_parts)
assert len(temp_test_dir) > 260, "Should have test directory > 260 characters"
assert len(temp_complete_dir) > 0, "Should have complete directory > 260 characters"
test_dir = "tests/data/basic_rar5"
rar_files = ["testfile.rar"]
expected_files = {"Testfile_1234.bin", "testfile.bin", "My_Test_Download.bin"}
error_code, extracted_files, complete_contents, download_contents, nzo, actual_temp_complete_dir = (
self._run_rar_unpack(
test_dir, rar_files, custom_temp_test_dir=temp_test_dir, custom_temp_complete_dir=temp_complete_dir
)
)
self._assert_successful_extraction(
error_code,
extracted_files,
complete_contents,
download_contents,
actual_temp_complete_dir,
expected_files,
should_delete_original=True,
original_files=rar_files,
)
def test_rar_unpack_rar_long_path_inside(self):
"""Test RAR unpacking functionality for file with long paths inside"""
# Test with the basic rar5 test file
test_dir = "tests/data/rar_long_path_inside"
rar_files = ["long_path_in_rar.rar"]
expected_files = {"Testfile_1234.bin", "testfile.bin", "My_Test_Download.bin"}
# The long nested directory structure inside the rar is build the same as test_rar_unpack_long_path
long_dir_name = "very_long_directory_name_" + "x" * 100 # 82 characters
nested_path_parts = [long_dir_name] * 4 # 4 levels of 82-char names = 328
expected_files = {os.path.join(*nested_path_parts, expected_file) for expected_file in expected_files}
error_code, extracted_files, complete_contents, download_contents, nzo, temp_complete_dir = (
self._run_rar_unpack(test_dir, rar_files)
)
self._assert_successful_extraction(
error_code,
extracted_files,
complete_contents,
download_contents,
temp_complete_dir,
expected_files,
should_delete_original=True,
original_files=rar_files,
)
def test_rar_unpack_multipart_unicode(self):
"""Test multi-part RAR unpacking with unicode filenames"""
# Test with unicode multi-part RAR files
test_dir = "tests/data/unicode_rar"
rar_files = [
"我喜欢编程.part1.rar",
"我喜欢编程.part2.rar",
"我喜欢编程.part3.rar",
"我喜欢编程.part4.rar",
"我喜欢编程.part5.rar",
"我喜欢编程.part6.rar",
]
expected_files = {"我喜欢编程_My_Test_Download.bin"}
error_code, extracted_files, complete_contents, download_contents, nzo, temp_complete_dir = (
self._run_rar_unpack(test_dir, rar_files)
)
self._assert_successful_extraction(
error_code,
extracted_files,
complete_contents,
download_contents,
temp_complete_dir,
expected_files,
should_delete_original=True,
original_files=rar_files,
)
def test_rar_unpack_passworded(self):
"""Test RAR unpacking with password-protected file"""
# Test with password-protected RAR file
test_dir = "tests/data/test_passworded{{secret}}"
rar_files = ["passworded-file.rar"]
expected_files = {"testfile.bin", "My_Test_Download.bin"}
# Set NZO with the correct password
custom_nzo_settings = {
"password": "secret", # The password is "secret"
"nzo_info": {"password": "secret"}, # Also set in nzo_info
"meta": {"password": ["secret"]}, # And in meta for get_all_passwords
}
error_code, extracted_files, complete_contents, download_contents, nzo, temp_complete_dir = (
self._run_rar_unpack(test_dir, rar_files, custom_nzo_settings=custom_nzo_settings)
)
self._assert_successful_extraction(
error_code,
extracted_files,
complete_contents,
download_contents,
temp_complete_dir,
expected_files,
should_delete_original=True,
original_files=rar_files,
)
def test_rar_unpack_wrong_password(self):
"""Test RAR unpacking with wrong password fails appropriately"""
# Test with password-protected RAR file but wrong password
test_dir = "tests/data/test_passworded{{secret}}"
rar_files = ["passworded-file.rar"]
# Set NZO with the wrong password
custom_nzo_settings = {
"password": "wrongpassword", # Wrong password
"nzo_info": {"password": "wrongpassword"},
"meta": {"password": ["wrongpassword"]},
}
error_code, extracted_files, complete_contents, download_contents, nzo, temp_complete_dir = (
self._run_rar_unpack(test_dir, rar_files, custom_nzo_settings=custom_nzo_settings)
)
# Check that extraction failed with wrong password (error_code 2 = wrong password)
assert error_code == 2, "Password-protected RAR extraction should fail with wrong password (error_code 2)"
assert len(extracted_files) == 0, "Should have no extracted files with wrong password"
assert len(complete_contents) == 0, "Should have no files in complete directory with wrong password"
# Verify that the original RAR file still exists (extraction failed)
rar_still_exists = any("passworded-file.rar" in f for f in download_contents)
assert rar_still_exists, "Original RAR file should still exist when extraction fails"
def test_rar_unpack_invalid_windows_filenames(self):
"""Test RAR unpacking with Windows-invalid filenames (allowed to fail on Windows)
This test contains a RAR file with filenames that are invalid on Windows
(e.g., files named CON, AUX, PRN, etc. or containing invalid characters).
On Windows, this extraction may fail, which is acceptable behavior.
"""
# Test with RAR containing Windows-invalid filenames
test_dir = "tests/data/rar_invalid_windows"
rar_files = ["rar_invalid_on_windows.rar"]
# Check for expected corrected filenames, Unrar corrects it on Windows
if sabnzbd.WINDOWS:
expected_files = {"blabla __ bla _ bla __ __ bla ___ CON.bin"}
else:
expected_files = {'blabla :: bla " bla << || bla ??? CON.bin'}
error_code, extracted_files, complete_contents, download_contents, nzo, temp_complete_dir = (
self._run_rar_unpack(test_dir, rar_files)
)
self._assert_successful_extraction(
error_code,
extracted_files,
complete_contents,
download_contents,
temp_complete_dir,
expected_files,
should_delete_original=True,
original_files=rar_files,
)

View File

@@ -18,15 +18,19 @@
"""
tests.test_newswrapper - Tests of various functions in newswrapper
"""
import errno
import ipaddress
import logging
import os.path
import socket
import sys
import tempfile
import threading
import ssl
import time
import warnings
from typing import Optional
from enum import Enum
from typing import Optional, Tuple
import portend
from flaky import flaky
@@ -40,6 +44,11 @@ TEST_PORT = portend.find_available_local_port()
TEST_DATA = b"connection_test"
class IPProtocolVersion(Enum):
IPV4 = 4
IPV6 = 6
def socket_test_server(ssl_context: ssl.SSLContext):
"""Support function that starts a mini-server"""
# Allow reuse of the address, because our CI is too fast for the socket closing
@@ -61,6 +70,42 @@ def socket_test_server(ssl_context: ssl.SSLContext):
server_socket.close()
def get_local_ip(protocol_version: IPProtocolVersion) -> Optional[str]:
"""
Find the ip address that would be used to send traffic towards internet. Uses the UDP Socket trick: connect is not
sending any traffic but already prefills what would be the sender ip address.
"""
s: Optional[socket.socket] = None
address_to_connect_to: Optional[Tuple[str, int]] = None
if protocol_version == IPProtocolVersion.IPV4:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Google DNS IPv4
address_to_connect_to = ("8.8.8.8", 80)
elif protocol_version == IPProtocolVersion.IPV6:
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
# Google DNS IPv6
address_to_connect_to = ("2001:4860:4860::8888", 80)
else:
raise ValueError(f"Unknown protocol version: {protocol_version}")
assert s is not None, "Socket has not been assigned!"
assert address_to_connect_to is not None, "Address to connect to has not been assigned!"
try:
s.connect(address_to_connect_to)
local_ip = s.getsockname()[0]
except OSError as e:
# If the network is unreachable, it's probably that we don't have an IP for this Protocol
# On Linux, we would get ENETUNREACH where on Mac OS we would get EHOSTUNREACH
if e.errno == errno.ENETUNREACH or e.errno == errno.EHOSTUNREACH:
return None
else:
raise
finally:
s.close()
return local_ip
@flaky
class TestNewsWrapper:
cert_file = os.path.join(tempfile.mkdtemp(), "test.cert")
@@ -139,3 +184,63 @@ class TestNewsWrapper:
if server_thread.is_alive():
raise RuntimeError("Test server was not stopped")
time.sleep(1.0)
@pytest.mark.parametrize(
"local_ip, ip_protocol",
[
(get_local_ip(protocol_version=IPProtocolVersion.IPV4), IPProtocolVersion.IPV4),
(get_local_ip(protocol_version=IPProtocolVersion.IPV6), IPProtocolVersion.IPV6),
("", None),
],
)
def test_socket_binding_outgoing_ip(
self, local_ip: Optional[str], ip_protocol: Optional[IPProtocolVersion], monkeypatch
):
"""Test to make sure that the binding of outgoing interface works as expected."""
if local_ip is None and ip_protocol is not None:
pytest.skip(f"No available ip for this protocol: {ip_protocol}")
elif ip_protocol is not None:
# We want to make sure the local ip is matching the version of the expected IP Protocol
assert ipaddress.ip_address(local_ip).version == ip_protocol.value
nw = mock.Mock()
nw.blocking = True
nw.thrdnum = 1
nw.server = mock.Mock()
nw.server.host = TEST_HOST
nw.server.port = TEST_PORT
nw.server.info = AddrInfo(*socket.getaddrinfo(TEST_HOST, TEST_PORT, 0, socket.SOCK_STREAM)[0])
nw.server.timeout = 10
nw.server.ssl = True
nw.server.ssl_context = None
nw.server.ssl_verify = 0
nw.server.ssl_ciphers = None
sabnzbd.cfg.outgoing_nntp_ip.set(local_ip)
# We mock the connect as it's being called in the Init, we want to have a "functional" newswrapper.NNTP instance
def mock_connect(self):
pass
monkeypatch.setattr("sabnzbd.newswrapper.NNTP.connect", mock_connect)
nntp = newswrapper.NNTP(nw, nw.server.info)
monkeypatch.undo()
# The connection has crashed but the socket should have been bound to the provided ip in the configuration
with pytest.raises(OSError) as excinfo:
nntp.connect()
if sys.platform == "win32":
# On Windows, the error code for this is WSAECONNREFUSED (10061)
assert excinfo.value.errno == errno.WSAECONNREFUSED
else:
# On Linux and macOS, the error code is ECONNREFUSED
assert excinfo.value.errno == errno.ECONNREFUSED
current_ip, _ = nntp.sock.getsockname()
if local_ip != "":
assert current_ip == local_ip
else:
assert current_ip is not None
nntp.close(send_quit=False)

View File

@@ -34,84 +34,92 @@ from tests.testhelper import *
@pytest.mark.usefixtures("clean_cache_dir")
class TestPostProc:
# Tests of rar_renamer() (=deobfuscate) against various input directories
def test_rar_renamer(self):
# Function to deobfuscate one directory with rar_renamer()
def deobfuscate_dir(sourcedir, expected_filename_matches):
# We create a workingdir inside the sourcedir, because the filenames are really changed
workingdir = os.path.join(sourcedir, "workingdir")
# Helper function for rar_renamer tests
def _deobfuscate_dir(self, sourcedir, expected_filename_matches):
"""Function to deobfuscate one directory with rar_renamer()"""
# We create a workingdir inside the sourcedir, because the filenames are really changed
workingdir = os.path.join(SAB_CACHE_DIR, "workingdir_test_rar_renamer")
# if workingdir is still there from previous run, remove it:
if os.path.isdir(workingdir):
try:
shutil.rmtree(workingdir)
except PermissionError:
pytest.fail("Could not remove existing workingdir %s for rar_renamer" % workingdir)
# create a fresh copy
try:
shutil.copytree(sourcedir, workingdir)
except Exception:
pytest.fail("Could not create copy of files for rar_renamer")
# And now let the magic happen:
nzo = mock.Mock()
nzo.final_name = "somedownloadname"
nzo.download_path = workingdir
number_renamed_files = postproc.rar_renamer(nzo)
# run check on the resulting files
if expected_filename_matches:
for filename_match in expected_filename_matches:
if len(globber_full(workingdir, filename_match)) != expected_filename_matches[filename_match]:
pytest.fail("Failed filename_match %s in %s" % (filename_match, workingdir))
# Remove workingdir again
# if workingdir is still there from previous run, remove it:
if os.path.isdir(workingdir):
try:
shutil.rmtree(workingdir)
except Exception:
except PermissionError:
pytest.fail("Could not remove existing workingdir %s for rar_renamer" % workingdir)
return number_renamed_files
# create a fresh copy
try:
shutil.copytree(sourcedir, workingdir)
except Exception:
pytest.fail("Could not create copy of files for rar_renamer")
# obfuscated, single rar set
# And now let the magic happen:
nzo = mock.Mock()
nzo.final_name = "somedownloadname"
nzo.download_path = workingdir
number_renamed_files = postproc.rar_renamer(nzo)
# run check on the resulting files
if expected_filename_matches:
for filename_match in expected_filename_matches:
if len(globber_full(workingdir, filename_match)) != expected_filename_matches[filename_match]:
pytest.fail("Failed filename_match %s in %s" % (filename_match, workingdir))
# Remove workingdir again
try:
shutil.rmtree(workingdir)
except Exception:
pytest.fail("Could not remove existing workingdir %s for rar_renamer" % workingdir)
return number_renamed_files
def test_rar_renamer_obfuscated_single_rar_set(self):
"""Test rar_renamer with obfuscated single rar set"""
sourcedir = os.path.join(SAB_DATA_DIR, "obfuscated_single_rar_set")
# Now define the filematches we want to see, in which amount ("*-*-*-*-*" are the input files):
expected_filename_matches = {"*part007.rar": 1, "*-*-*-*-*": 0}
assert deobfuscate_dir(sourcedir, expected_filename_matches) == 7
assert self._deobfuscate_dir(sourcedir, expected_filename_matches) == 7
# obfuscated, two rar sets
def test_rar_renamer_obfuscated_two_rar_sets(self):
"""Test rar_renamer with obfuscated two rar sets"""
sourcedir = os.path.join(SAB_DATA_DIR, "obfuscated_two_rar_sets")
expected_filename_matches = {"*part007.rar": 2, "*part009.rar": 1, "*-*-*-*-*": 0}
assert deobfuscate_dir(sourcedir, expected_filename_matches) == 16
assert self._deobfuscate_dir(sourcedir, expected_filename_matches) == 16
# obfuscated, but not a rar set
def test_rar_renamer_obfuscated_but_no_rar(self):
"""Test rar_renamer with obfuscated files that are not rar sets"""
sourcedir = os.path.join(SAB_DATA_DIR, "obfuscated_but_no_rar")
expected_filename_matches = {"*.rar": 0, "*-*-*-*-*": 6}
assert deobfuscate_dir(sourcedir, expected_filename_matches) == 0
assert self._deobfuscate_dir(sourcedir, expected_filename_matches) == 0
def test_rar_renamer_single_rar_set_missing_first_rar(self):
"""Test rar_renamer with single rar set missing first rar"""
# One obfuscated rar set, but first rar (.part1.rar) is missing
sourcedir = os.path.join(SAB_DATA_DIR, "obfuscated_single_rar_set_missing_first_rar")
# single rar set (of 6 obfuscated rar files), so we expect renaming
# thus result must 6 rar files, and 0 obfuscated files
expected_filename_matches = {"*.rar": 6, "*-*-*-*-*": 0}
# 6 files should have been renamed
assert deobfuscate_dir(sourcedir, expected_filename_matches) == 6
assert self._deobfuscate_dir(sourcedir, expected_filename_matches) == 6
def test_rar_renamer_double_rar_set_missing_rar(self):
"""Test rar_renamer with two rar sets where some rars are missing"""
# Two obfuscated rar sets, but some rars are missing
sourcedir = os.path.join(SAB_DATA_DIR, "obfuscated_double_rar_set_missing_rar")
# Two sets, missing rar, so we expect no renaming
# thus result should be 0 rar files, and still 8 obfuscated files
expected_filename_matches = {"*.rar": 0, "*-*-*-*-*": 8}
# 0 files should have been renamed
assert deobfuscate_dir(sourcedir, expected_filename_matches) == 0
assert self._deobfuscate_dir(sourcedir, expected_filename_matches) == 0
def test_rar_renamer_fully_encrypted_and_obfuscated(self):
"""Test rar_renamer with fully encrypted and obfuscated rar set"""
# fully encrypted rar-set, and obfuscated rar names
sourcedir = os.path.join(SAB_DATA_DIR, "fully_encrypted_and_obfuscated_rars")
# SABnzbd cannot do anything with this, so we expect no renaming
expected_filename_matches = {"*.rar": 0, "*-*-*-*-*": 6}
# 0 files should have been renamed
assert deobfuscate_dir(sourcedir, expected_filename_matches) == 0
assert self._deobfuscate_dir(sourcedir, expected_filename_matches) == 0
@pytest.mark.parametrize("category", ["testcat", "Default", None])
@pytest.mark.parametrize("has_jobdir", [True, False]) # With or without a job dir

View File

@@ -76,6 +76,10 @@ class TestSortingFunctions:
("Test Date Detection 22.07.14", {"date": datetime.date(2022, 7, 14)}),
(None, None), # Jobname missing
("", None),
(
"[PrettyPlease] Who Cares S6 - 42 (720p) [1A2B3C4D]",
{"season": 6, "episode": 42, "episode_title": None, "title": "Who Cares"},
), # Anime
],
)
def test_guess_what(self, name, result):

View File

@@ -326,7 +326,7 @@ class DownloadFlowBasics(SABnzbdBaseTest):
self.selenium_wrapper(self.driver.find_element, By.ID, "next-button").click()
self.no_page_crash()
check_result = self.selenium_wrapper(self.driver.find_element, By.CLASS_NAME, "quoteBlock").text
assert "http://%s:%s/" % (SAB_HOST, SAB_PORT) in check_result
assert "http://%s:%s" % (SAB_HOST, SAB_PORT) in check_result
# Go to SAB!
self.selenium_wrapper(self.driver.find_element, By.CSS_SELECTOR, ".btn.btn-success").click()