mirror of
https://github.com/sabnzbd/sabnzbd.git
synced 2026-01-01 20:20:02 -05:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11ba9ae12a | ||
|
|
a61a5539a7 | ||
|
|
77f7490aea | ||
|
|
a7198b6a81 | ||
|
|
4c77954526 | ||
|
|
a229a2a5ea | ||
|
|
0a2f3865ee | ||
|
|
900e68bb9a | ||
|
|
977dbc805f | ||
|
|
abcca19820 | ||
|
|
52a7b5dcff | ||
|
|
9518714885 | ||
|
|
1de674a532 | ||
|
|
e1dad3e4c4 | ||
|
|
44f2eb8620 | ||
|
|
70945a9c5b | ||
|
|
fdfca97dfa | ||
|
|
b84900dcb5 | ||
|
|
d989ec928a | ||
|
|
4a89fcf8ea | ||
|
|
d7fa3e1f7b | ||
|
|
d11e757c6e | ||
|
|
c1417c319d | ||
|
|
6689939cc9 | ||
|
|
09347d0766 | ||
|
|
41db09057c | ||
|
|
6983058f49 | ||
|
|
fb2d412c97 | ||
|
|
1c0b1205b2 | ||
|
|
f556cea488 | ||
|
|
a2447253a0 | ||
|
|
3393d7c976 | ||
|
|
06572bdf7d | ||
|
|
4f9ed7803f | ||
|
|
95bc069af9 | ||
|
|
d4411f1b8f | ||
|
|
1bfd1b8f41 | ||
|
|
c47dbfdc26 | ||
|
|
b5e55cd9b2 | ||
|
|
85c98d7203 | ||
|
|
9e95717619 | ||
|
|
90b4ff2720 | ||
|
|
0f97a9fdfc | ||
|
|
90caf0c164 | ||
|
|
9b3fe470a0 | ||
|
|
ab318729ab | ||
|
|
9576554426 | ||
|
|
3cd819b78d | ||
|
|
bb24f3f04e | ||
|
|
6f4416236d | ||
|
|
47dcccd17f | ||
|
|
6b026d8274 | ||
|
|
ec18606557 | ||
|
|
d1d9bab65a | ||
|
|
e2560bf214 | ||
|
|
895c8549ba | ||
|
|
0d80efb898 | ||
|
|
deace9f8ae | ||
|
|
1c96dff133 | ||
|
|
1734b11338 | ||
|
|
5f3c4d17da | ||
|
|
4ffe0e27fb | ||
|
|
951bc0c957 | ||
|
|
60f985ba00 | ||
|
|
a42a2db196 | ||
|
|
64034c5636 | ||
|
|
e03a031342 | ||
|
|
da3d72b484 | ||
|
|
e3042a6106 | ||
|
|
55f1253a56 |
5
.github/renovate.json
vendored
5
.github/renovate.json
vendored
@@ -26,6 +26,11 @@
|
||||
"werkzeug"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"matchPackageNames": ["windows", "macos"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"*"
|
||||
|
||||
90
.github/workflows/build_release.yml
vendored
90
.github/workflows/build_release.yml
vendored
@@ -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 }}
|
||||
|
||||
10
.github/workflows/integration_testing.yml
vendored
10
.github/workflows/integration_testing.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
run: |
|
||||
python3 tools/make_mo.py
|
||||
- name: Push translatable and translated texts back to repo
|
||||
uses: stefanzweifel/git-auto-commit-action@v6.0.1
|
||||
uses: stefanzweifel/git-auto-commit-action@v7.0.0
|
||||
if: env.TX_TOKEN
|
||||
with:
|
||||
commit_message: |
|
||||
|
||||
@@ -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).
|
||||
|
||||
49
README.mkd
49
README.mkd
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
PyGithub==2.7.0
|
||||
PyGithub==2.8.1
|
||||
praw==7.8.1
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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."
|
||||
|
||||
501
builder/win/nsis/Include/StdUtils.nsh
Normal file
501
builder/win/nsis/Include/StdUtils.nsh
Normal 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___
|
||||
BIN
builder/win/nsis/Plugins/StdUtils.dll
Normal file
BIN
builder/win/nsis/Plugins/StdUtils.dll
Normal file
Binary file not shown.
@@ -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()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(/"/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')";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "שחזר גיבוי"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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 !"
|
||||
|
||||
@@ -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 "תמוך במיזם, תרום!"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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ă!"
|
||||
|
||||
@@ -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 "Поддержите проект. Сделайте пожертвование!"
|
||||
|
||||
@@ -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 "Подржите пројекат, дајте добровољан прилог!"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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 "支持该项目,捐助!"
|
||||
|
||||
@@ -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
|
||||
|
||||
182
sabnzbd/api.py
182
sabnzbd/api.py
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
BIN
tests/data/rar_invalid_windows/rar_invalid_on_windows.rar
Normal file
BIN
tests/data/rar_invalid_windows/rar_invalid_on_windows.rar
Normal file
Binary file not shown.
BIN
tests/data/rar_long_path_inside/long_path_in_rar.rar
Normal file
BIN
tests/data/rar_long_path_inside/long_path_in_rar.rar
Normal file
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user