mirror of
https://github.com/sabnzbd/sabnzbd.git
synced 2026-01-06 14:39:41 -05:00
Compare commits
124 Commits
4.5.2RC2
...
4.6.0Alpha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb544d85c7 | ||
|
|
ad85a241df | ||
|
|
e4d8642b4f | ||
|
|
77b35e7904 | ||
|
|
f8a0b3db52 | ||
|
|
9c8b26ab4e | ||
|
|
67a5a552fd | ||
|
|
80f57a2b9a | ||
|
|
baaf7edc89 | ||
|
|
2d9f480af1 | ||
|
|
2266ac33aa | ||
|
|
1ba479398c | ||
|
|
f71a81f7a8 | ||
|
|
1916c01bd9 | ||
|
|
699d97965c | ||
|
|
399935ad21 | ||
|
|
0824fdc7c7 | ||
|
|
a3f8e89af8 | ||
|
|
f9f17731c8 | ||
|
|
b052325ea7 | ||
|
|
daca14f97e | ||
|
|
daa26bc1a6 | ||
|
|
70d5134d28 | ||
|
|
a32458d9a9 | ||
|
|
4c77954526 | ||
|
|
a229a2a5ea | ||
|
|
0a2f3865ee | ||
|
|
900e68bb9a | ||
|
|
1de674a532 | ||
|
|
e1dad3e4c4 | ||
|
|
44f2eb8620 | ||
|
|
70945a9c5b | ||
|
|
fdfca97dfa | ||
|
|
b84900dcb5 | ||
|
|
d989ec928a | ||
|
|
d7fa3e1f7b | ||
|
|
c1417c319d | ||
|
|
6689939cc9 | ||
|
|
09347d0766 | ||
|
|
41db09057c | ||
|
|
6983058f49 | ||
|
|
fb2d412c97 | ||
|
|
1c0b1205b2 | ||
|
|
f556cea488 | ||
|
|
a2447253a0 | ||
|
|
3393d7c976 | ||
|
|
06572bdf7d | ||
|
|
d4411f1b8f | ||
|
|
1bfd1b8f41 | ||
|
|
c47dbfdc26 | ||
|
|
b5e55cd9b2 | ||
|
|
85c98d7203 | ||
|
|
9e95717619 | ||
|
|
90b4ff2720 | ||
|
|
0f97a9fdfc | ||
|
|
90caf0c164 | ||
|
|
9b3fe470a0 | ||
|
|
ab318729ab | ||
|
|
9576554426 | ||
|
|
3cd819b78d | ||
|
|
bb24f3f04e | ||
|
|
6f4416236d | ||
|
|
47dcccd17f | ||
|
|
6b026d8274 | ||
|
|
ec18606557 | ||
|
|
895c8549ba | ||
|
|
0d80efb898 | ||
|
|
deace9f8ae | ||
|
|
1c96dff133 | ||
|
|
1734b11338 | ||
|
|
5f3c4d17da | ||
|
|
4ffe0e27fb | ||
|
|
951bc0c957 | ||
|
|
60f985ba00 | ||
|
|
a42a2db196 | ||
|
|
64034c5636 | ||
|
|
e03a031342 | ||
|
|
da3d72b484 | ||
|
|
e3042a6106 | ||
|
|
55f1253a56 | ||
|
|
5e432bea37 | ||
|
|
2d0cc08987 | ||
|
|
627797f8c7 | ||
|
|
e37a777f29 | ||
|
|
13a76e5824 | ||
|
|
e4c64cac12 | ||
|
|
c6694483e4 | ||
|
|
bc793e11c4 | ||
|
|
4980fc70a0 | ||
|
|
8afac4f6fb | ||
|
|
c78b633da8 | ||
|
|
a1ee1677dc | ||
|
|
511bb153d7 | ||
|
|
c1af36f6b0 | ||
|
|
6028824966 | ||
|
|
49a7300ad6 | ||
|
|
8e8e560eac | ||
|
|
e51da569ca | ||
|
|
6ce43eed5f | ||
|
|
73ec6d8323 | ||
|
|
27a7531f79 | ||
|
|
423bdb4f81 | ||
|
|
7f8081e2cc | ||
|
|
50c2d5e2ab | ||
|
|
552bfd4b72 | ||
|
|
3d522c8205 | ||
|
|
b6b0d10367 | ||
|
|
8d4d69d56b | ||
|
|
0e475e593a | ||
|
|
78424318ce | ||
|
|
57c90b2554 | ||
|
|
9b94d22621 | ||
|
|
5c30b0ee29 | ||
|
|
3a1c60a3ed | ||
|
|
bd07a79c97 | ||
|
|
c562c9a468 | ||
|
|
fdd3f590cd | ||
|
|
77e9627e64 | ||
|
|
c40d1274d2 | ||
|
|
d1948071fc | ||
|
|
4f79d924e6 | ||
|
|
872cf835df | ||
|
|
69bb1a87a4 | ||
|
|
e3339a1ab4 |
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -3,7 +3,7 @@ description: >
|
||||
Did you discover a bug in SABnzbd? Report it here!
|
||||
If you are not 100% certain this is a bug please go to our forums, Reddit or Discord server first.
|
||||
labels:
|
||||
- Bug
|
||||
- Support
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Feature request
|
||||
description: What new feature would you like to have added to SABnzbd?
|
||||
labels:
|
||||
- Feature request
|
||||
- Support
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
8
.github/renovate.json
vendored
8
.github/renovate.json
vendored
@@ -23,9 +23,15 @@
|
||||
"jaraco.collections",
|
||||
"sabctools",
|
||||
"paho-mqtt",
|
||||
"werkzeug"
|
||||
"werkzeug",
|
||||
"tavern"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"matchPackageNames": ["windows", "macos"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"*"
|
||||
|
||||
165
.github/workflows/build_release.yml
vendored
165
.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@v4
|
||||
- name: Set up Python 3.13
|
||||
uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@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
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Build Windows standalone binary
|
||||
id: windows_binary
|
||||
run: python builder/package.py binary
|
||||
- name: Upload Windows standalone binary (unsigned)
|
||||
uses: actions/upload-artifact@v5
|
||||
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@v2
|
||||
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@v5
|
||||
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
|
||||
uses: actions/upload-artifact@v5
|
||||
id: upload-unsigned-installer
|
||||
with:
|
||||
path: "*-win-setup.exe"
|
||||
name: Windows installer
|
||||
- name: Sign Windows installer
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ secrets.SIGNPATH_ORG_ID }}
|
||||
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@v5
|
||||
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@v4
|
||||
- name: Set up Python 3.13
|
||||
- uses: actions/checkout@v5
|
||||
- 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
|
||||
@@ -99,7 +140,7 @@ jobs:
|
||||
# Run this on macOS so the line endings are correct by default
|
||||
run: python builder/package.py source
|
||||
- name: Upload source distribution
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
path: "*-src.tar.gz"
|
||||
name: Source distribution
|
||||
@@ -112,27 +153,88 @@ jobs:
|
||||
python3 builder/package.py app
|
||||
python3 builder/make_dmg.py
|
||||
- name: Upload macOS binary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
path: "*-macos.dmg"
|
||||
name: macOS binary
|
||||
|
||||
build-snap:
|
||||
name: Build Snap Packages (${{ matrix.linux_arch }})
|
||||
timeout-minutes: 30
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
linux_arch: amd64
|
||||
- os: ubuntu-24.04-arm
|
||||
linux_arch: arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Cache par2cmdline-turbo tarball
|
||||
uses: actions/cache@v4
|
||||
id: cache-par2cmdline
|
||||
# Clearing the cache in case of new version requires manual clearing in GitHub!
|
||||
with:
|
||||
path: snap/par2cmdline.tar.gz
|
||||
key: cache-par2cmdline
|
||||
- name: Download par2cmdline-turbo tarball
|
||||
if: steps.cache-par2cmdline.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
PAR2_TARBALL=$(curl -sL https://api.github.com/repos/animetosho/par2cmdline-turbo/releases/latest | jq -r '.tarball_url')
|
||||
curl -o snap/par2cmdline.tar.gz -L "$PAR2_TARBALL"
|
||||
- uses: snapcore/action-build@v1
|
||||
name: Build snap
|
||||
id: snapcraft
|
||||
- name: Test snap installation
|
||||
run: |
|
||||
sudo snap install --dangerous *.snap
|
||||
sudo snap connect sabnzbd:removable-media
|
||||
# Basic smoke test - check that the binary exists and can show help
|
||||
timeout 10s snap run sabnzbd --help || true
|
||||
sudo snap remove sabnzbd
|
||||
- name: Upload snap
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Snap package (${{ matrix.linux_arch }})
|
||||
path: ${{ steps.snapcraft.outputs.snap }}
|
||||
- name: Publish snap
|
||||
uses: snapcore/action-publish@v1
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_TOKEN }}
|
||||
with:
|
||||
store_login: ${{ secrets.SNAP_TOKEN }}
|
||||
snap: ${{ steps.snapcraft.outputs.snap }}
|
||||
release: stable
|
||||
|
||||
release:
|
||||
name: Prepare Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build_windows, build_macos]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
python-version: "3.14"
|
||||
cache: pip
|
||||
cache-dependency-path: "builder/release-requirements.txt"
|
||||
- name: Download Source distribution artifact
|
||||
uses: actions/download-artifact@v6
|
||||
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@v6
|
||||
with:
|
||||
name: macOS binary
|
||||
- name: Download Windows artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
pattern: ${{ (contains(github.ref, 'refs/tags/')) && '*signed*' || '*Windows*' }}
|
||||
merge-multiple: true
|
||||
- name: Prepare official release
|
||||
env:
|
||||
AUTOMATION_GITHUB_TOKEN: ${{ secrets.AUTOMATION_GITHUB_TOKEN }}
|
||||
@@ -140,10 +242,3 @@ jobs:
|
||||
run: |
|
||||
pip3 install -r builder/release-requirements.txt
|
||||
python3 builder/release.py
|
||||
- name: Release latest available Snap
|
||||
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_TOKEN }}
|
||||
run: |
|
||||
sudo snap install snapcraft --classic
|
||||
python3 snap/local/release_snap.py
|
||||
|
||||
17
.github/workflows/integration_testing.yml
vendored
17
.github/workflows/integration_testing.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
name: Black Code Formatter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Black Code Formatter
|
||||
uses: lgeiger/black-action@master
|
||||
with:
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
builder/SABnzbd.spec
|
||||
tests
|
||||
--line-length=120
|
||||
--target-version=py38
|
||||
--target-version=py39
|
||||
--check
|
||||
--diff
|
||||
|
||||
@@ -31,24 +31,23 @@ 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-latest
|
||||
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@v4
|
||||
- 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 }}
|
||||
architecture: "x64"
|
||||
cache: pip
|
||||
cache-dependency-path: "**/requirements.txt"
|
||||
- name: Install system dependencies
|
||||
|
||||
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
|
||||
|
||||
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
env:
|
||||
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
token: ${{ secrets.AUTOMATION_GITHUB_TOKEN }}
|
||||
- name: Generate translatable texts
|
||||
@@ -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: |
|
||||
|
||||
@@ -52,7 +52,7 @@ Specific guides to install from source are available for Windows and macOS:
|
||||
https://sabnzbd.org/wiki/installation/install-macos
|
||||
https://sabnzbd.org/wiki/installation/install-from-source-windows
|
||||
|
||||
Only Python 3.8 and above is supported.
|
||||
Only Python 3.9 and above is supported.
|
||||
|
||||
On Linux systems you need to install:
|
||||
par2 unrar python3-setuptools python3-pip
|
||||
|
||||
11
README.md
11
README.md
@@ -16,7 +16,7 @@ If you want to know more you can head over to our website: https://sabnzbd.org.
|
||||
|
||||
SABnzbd has a few dependencies you'll need before you can get running. If you've previously run SABnzbd from one of the various Linux packages, then you likely already have all the needed dependencies. If not, here's what you're looking for:
|
||||
|
||||
- `python` (Python 3.8 and above, often called `python3`)
|
||||
- `python` (Python 3.9 and above, often called `python3`)
|
||||
- Python modules listed in `requirements.txt`. Install with `python3 -m pip install -r requirements.txt -U`
|
||||
- `par2` (Multi-threaded par2 installation guide can be found [here](https://sabnzbd.org/wiki/installation/multicore-par2))
|
||||
- `unrar` (make sure you get the "official" non-free version of unrar)
|
||||
@@ -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).
|
||||
|
||||
32
README.mkd
32
README.mkd
@@ -1,23 +1,22 @@
|
||||
Release Notes - SABnzbd 4.5.0 Release Candidate 1
|
||||
Release Notes - SABnzbd 4.6.0 Alpha 1
|
||||
=========================================================
|
||||
|
||||
This is the first Release Candidate for the 4.5.0 release.
|
||||
This is the first test release of version 4.6.
|
||||
|
||||
## New features in 4.5.0
|
||||
## New features in 4.6.0
|
||||
|
||||
* Improved failure detection by downloading additional par2 files right away.
|
||||
* Added more diagnostic information about the system.
|
||||
* Use XFF headers for login validation if `verify_xff_header` is enabled.
|
||||
* Added Turkish translation (by @cardpuncher).
|
||||
* Added `unrar_parameters` option to supply custom Unrar parameters.
|
||||
* Windows: Removed MultiPar support.
|
||||
* Windows and macOS: Updated Python to 3.13.2, 7zip to 24.09,
|
||||
Unrar to 7.10 and par2cmdline-turbo to 1.2.0.
|
||||
* Dynamically increase Assembler limits on faster connections.
|
||||
* Improved disk speed measurement in Status window.
|
||||
* Enable `verify_xff_header` by default.
|
||||
* Dropped support for Python 3.8.
|
||||
|
||||
## Bug fixes since 4.4.0
|
||||
## Bug fixes since 4.5.0
|
||||
|
||||
* `Check before download` could get stuck or fail to reject.
|
||||
* Windows: Tray icon disappears after Explorer restart.
|
||||
* Correct mobile layout if `Full Width` is enabled.
|
||||
* macOS: Slow to start on some network setups.
|
||||
|
||||
* Handle filenames that exceed maximum filesystem lengths.
|
||||
* Directly decompress gzip responses when retrieving NZB's.
|
||||
|
||||
## Upgrade notices
|
||||
|
||||
@@ -30,7 +29,12 @@ This is the first Release Candidate for the 4.5.0 release.
|
||||
|
||||
* 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,
|
||||
|
||||
36
SABnzbd.py
36
SABnzbd.py
@@ -19,8 +19,8 @@ import sys
|
||||
|
||||
# Trick to show a better message on older Python
|
||||
# releases that don't support walrus operator
|
||||
if Python_38_is_required_to_run_SABnzbd := sys.hexversion < 0x03080000:
|
||||
print("Sorry, requires Python 3.8 or above")
|
||||
if Python_39_is_required_to_run_SABnzbd := sys.hexversion < 0x03090000:
|
||||
print("Sorry, requires Python 3.9 or above")
|
||||
print("You can read more at: https://sabnzbd.org/wiki/installation/install-off-modules")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -40,7 +40,7 @@ import re
|
||||
import gc
|
||||
import threading
|
||||
import http.cookies
|
||||
from typing import List, Dict, Any
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
import sabctools
|
||||
@@ -142,7 +142,7 @@ class GUIHandler(logging.Handler):
|
||||
"""Initializes the handler"""
|
||||
logging.Handler.__init__(self)
|
||||
self._size: int = size
|
||||
self.store: List[Dict[str, Any]] = []
|
||||
self.store: list[dict[str, Any]] = []
|
||||
|
||||
def emit(self, record: logging.LogRecord):
|
||||
"""Emit a record by adding it to our private queue"""
|
||||
@@ -426,10 +426,7 @@ def print_modules():
|
||||
# Check if we managed to link, warning for now
|
||||
# It won't work on OpenSSL < 1.1.1 anyway, so we skip the check there
|
||||
if not sabnzbd.decoder.SABCTOOLS_OPENSSL_LINKED and ssl.OPENSSL_VERSION_INFO >= (1, 1, 1):
|
||||
logging.warning(
|
||||
"Could not link to OpenSSL library, please report here: "
|
||||
"https://github.com/sabnzbd/sabnzbd/issues/2421"
|
||||
)
|
||||
helpful_warning(T("Unable to link to OpenSSL, optimized SSL connection functions will not be used."))
|
||||
else:
|
||||
# Wrong SABCTools version, if it was fully missing it would fail to start due to check at the very top
|
||||
logging.error(
|
||||
@@ -543,21 +540,19 @@ def get_webhost(web_host, web_port, https_port):
|
||||
# If only APIPA's or IPV6 are found, fall back to localhost
|
||||
ipv4 = ipv6 = False
|
||||
localhost = hostip = "localhost"
|
||||
|
||||
try:
|
||||
info = socket.getaddrinfo(socket.gethostname(), None)
|
||||
# Valid user defined name?
|
||||
info = socket.getaddrinfo(web_host, None)
|
||||
except socket.error:
|
||||
# Hostname does not resolve
|
||||
if not is_localhost(web_host):
|
||||
web_host = "0.0.0.0"
|
||||
try:
|
||||
# Valid user defined name?
|
||||
info = socket.getaddrinfo(web_host, None)
|
||||
info = socket.getaddrinfo(localhost, None)
|
||||
except socket.error:
|
||||
if not is_localhost(web_host):
|
||||
web_host = "0.0.0.0"
|
||||
try:
|
||||
info = socket.getaddrinfo(localhost, None)
|
||||
except socket.error:
|
||||
info = socket.getaddrinfo("127.0.0.1", None)
|
||||
localhost = "127.0.0.1"
|
||||
info = socket.getaddrinfo("127.0.0.1", None)
|
||||
localhost = "127.0.0.1"
|
||||
|
||||
for item in info:
|
||||
ip = str(item[4][0])
|
||||
if ip.startswith("169.254."):
|
||||
@@ -1103,12 +1098,13 @@ def main():
|
||||
logging_level = sabnzbd.cfg.log_level()
|
||||
else:
|
||||
sabnzbd.cfg.log_level.set(logging_level)
|
||||
sabnzbd.LOGFILE = os.path.join(logdir, DEF_LOG_FILE)
|
||||
|
||||
logformat = "%(asctime)s::%(levelname)s::[%(module)s:%(lineno)d] %(message)s"
|
||||
logger.setLevel(LOGLEVELS[logging_level + 1])
|
||||
|
||||
try:
|
||||
if not no_file_log:
|
||||
sabnzbd.LOGFILE = os.path.join(logdir, DEF_LOG_FILE)
|
||||
rollover_log = logging.handlers.RotatingFileHandler(
|
||||
sabnzbd.LOGFILE, "a+", sabnzbd.cfg.log_size(), sabnzbd.cfg.log_backups()
|
||||
)
|
||||
|
||||
@@ -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,7 +27,7 @@ import tarfile
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import configobj
|
||||
from typing import List
|
||||
import packaging.version
|
||||
|
||||
from constants import (
|
||||
RELEASE_VERSION,
|
||||
@@ -70,9 +69,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 +108,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 +246,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 +271,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
|
||||
@@ -271,13 +330,16 @@ if __name__ == "__main__":
|
||||
if authority:
|
||||
files_to_sign = [
|
||||
"macos/par2/par2",
|
||||
"macos/par2/arm64/par2",
|
||||
"macos/unrar/unrar",
|
||||
"macos/unrar/arm64/unrar",
|
||||
"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",
|
||||
@@ -297,17 +359,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.6.1
|
||||
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
|
||||
@@ -112,7 +112,7 @@ if RELEASE_THIS and gh_token:
|
||||
print("Removing existing asset %s " % gh_asset.name)
|
||||
gh_asset.delete_asset()
|
||||
# Upload the new one
|
||||
print("Uploading %s to release %s" % (file_to_check, gh_release.title))
|
||||
print("Uploading %s to release %s" % (file_to_check, gh_release.name))
|
||||
gh_release.upload_asset(file_to_check)
|
||||
|
||||
# Check if we now have all files
|
||||
|
||||
@@ -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.14.1
|
||||
pyinstaller==6.16.0
|
||||
packaging==25.0
|
||||
pyinstaller-hooks-contrib==2025.5
|
||||
pyinstaller-hooks-contrib==2025.9
|
||||
altgraph==0.17.4
|
||||
wrapt==1.17.2
|
||||
wrapt==2.0.1
|
||||
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.1; 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.
@@ -187,7 +187,7 @@
|
||||
<td><label for="apprise_enable"> $T('opt-apprise_enable')</label></td>
|
||||
</tr>
|
||||
</table>
|
||||
<em>$T('explain-apprise_enable')</em><br>
|
||||
<p>$T('explain-apprise_enable')</p>
|
||||
<p>$T('version'): ${apprise.__version__}</p>
|
||||
|
||||
$show_cat_box('apprise')
|
||||
@@ -197,7 +197,7 @@
|
||||
<div class="field-pair">
|
||||
<label class="config" for="apprise_urls">$T('opt-apprise_urls')</label>
|
||||
<input type="text" name="apprise_urls" id="apprise_urls" value="$apprise_urls" />
|
||||
<span class="desc">$T('explain-apprise_urls'). <br>$T('readwiki')</span>
|
||||
<span class="desc">$T('explain-apprise_urls')</span>
|
||||
</div>
|
||||
<div class="field-pair">
|
||||
<span class="desc">$T('explain-apprise_extra_urls')</span>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -83,13 +83,20 @@
|
||||
<div class="col-sm-2">$T('name')</div>
|
||||
<div class="col-sm-10" data-bind="text: historyStatus.name"></div>
|
||||
</div>
|
||||
<div class="row" data-bind="visible: historyStatus.time_added">
|
||||
<div class="col-sm-2">$T('rss-added')</div>
|
||||
<div class="col-sm-10" data-bind="text: timeAdded(), attr: { 'data-timestamp': historyStatus.time_added }"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-2">$T('post-Completed')</div>
|
||||
<div class="col-sm-10" data-bind="text: completedOn, attr: { 'data-timestamp': completed }"></div>
|
||||
</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>
|
||||
@@ -142,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>
|
||||
|
||||
@@ -681,9 +681,9 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="checkbox">
|
||||
<div class="checkbox" data-bind="visible: !history.showArchive()">
|
||||
<label>
|
||||
<input type="checkbox" data-bind="checked: history.showArchive()"> <span>$T('permanently-delete')</span>
|
||||
<input type="checkbox" data-bind="checked: history.permanentlyDelete"> <span>$T('permanently-delete')</span>
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">$T('cancel')</button>
|
||||
@@ -802,7 +802,7 @@
|
||||
<div class="modal-footer">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" data-bind="checked: history.showArchive()"> <span>$T('permanently-delete')</span>
|
||||
<input type="checkbox" data-bind="checked: history.permanentlyDelete"> <span>$T('permanently-delete')</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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')";
|
||||
|
||||
@@ -10,6 +10,7 @@ function HistoryListModel(parent) {
|
||||
self.historyItems = ko.observableArray([])
|
||||
self.showFailed = ko.observable(false).extend({ persist: 'historyShowFailed' });
|
||||
self.showArchive = ko.observable(false).extend({ persist: 'historyShowArchive' });
|
||||
self.permanentlyDelete = ko.observable(false).extend({ persist: 'permanentlyDelete' });
|
||||
self.isLoading = ko.observable(false).extend({ rateLimit: 100 });
|
||||
self.searchTerm = ko.observable('').extend({ rateLimit: { timeout: 400, method: "notifyWhenChangesStop" } });
|
||||
self.paginationLimit = ko.observable(10).extend({ persist: 'historyPaginationLimit' });
|
||||
@@ -391,6 +392,10 @@ function HistoryListModel(parent) {
|
||||
}
|
||||
if(strIDsHistory !== "") {
|
||||
var skipArchive = $('#modal-delete-history-job input[type="checkbox"]').prop("checked")
|
||||
|
||||
// Permanently delete if we are on the Archive page
|
||||
if(self.showArchive()) skipArchive = true
|
||||
|
||||
callAPI({
|
||||
mode: 'history',
|
||||
name: 'delete',
|
||||
@@ -415,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()
|
||||
@@ -546,6 +587,11 @@ function HistoryModel(parent, data) {
|
||||
return displayDateTime(self.completed(), parent.parent.dateFormat(), 'X')
|
||||
});
|
||||
|
||||
// Format time added
|
||||
self.timeAdded = ko.pureComputed(function() {
|
||||
return displayDateTime(self.historyStatus.time_added(), parent.parent.dateFormat(), 'X')
|
||||
});
|
||||
|
||||
// Subscribe to retryEvent so we can load the password
|
||||
self.canRetry.subscribe(function() {
|
||||
self.updateAllHistory = true;
|
||||
@@ -561,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
|
||||
|
||||
@@ -704,7 +704,6 @@ function ViewModel() {
|
||||
data.append("apikey", apiKey);
|
||||
|
||||
// Add this one
|
||||
debugger
|
||||
$.ajax({
|
||||
url: "./api",
|
||||
type: "POST",
|
||||
@@ -896,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
|
||||
@@ -913,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;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.container-full-width .container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main-navbar {
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
@@ -163,7 +167,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,7 +30,11 @@
|
||||
<url type="faq">https://sabnzbd.org/wiki/faq</url>
|
||||
<url type="contact">https://sabnzbd.org/live-chat.html</url>
|
||||
<releases>
|
||||
<release version="4.5.2" date="2025-07-01" type="stable"/>
|
||||
<release version="4.6.0" date="2025-12-24" type="stable"/>
|
||||
<release version="4.5.5" date="2025-10-24" type="stable"/>
|
||||
<release version="4.5.4" date="2025-10-22" type="stable"/>
|
||||
<release version="4.5.3" date="2025-08-25" type="stable"/>
|
||||
<release version="4.5.2" date="2025-07-09" type="stable"/>
|
||||
<release version="4.5.1" date="2025-04-11" type="stable"/>
|
||||
<release version="4.5.0" date="2025-04-01" type="stable"/>
|
||||
<release version="4.4.1" date="2024-12-23" type="stable"/>
|
||||
|
||||
BIN
macos/7zip/7zz
BIN
macos/7zip/7zz
Binary file not shown.
Binary file not shown.
BIN
macos/par2/par2
BIN
macos/par2/par2
Binary file not shown.
@@ -32,6 +32,11 @@ msgstr ""
|
||||
msgid "Cannot find web template: %s, trying standard template"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
msgid "Unable to link to OpenSSL, optimized SSL connection functions will not be used."
|
||||
msgstr ""
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid "SABCTools disabled: no correct version found! (Found v%s, expecting v%s)"
|
||||
@@ -293,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 ""
|
||||
@@ -670,6 +688,14 @@ msgstr ""
|
||||
msgid "Refused connection with hostname \"%s\" from:"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "API Key missing, please enter the api key from Config->General into your 3rd party program:"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "API Key incorrect, Use the api key from Config->General in your 3rd party program:"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr ""
|
||||
@@ -679,14 +705,6 @@ msgstr ""
|
||||
msgid "User logged in"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "API Key missing, please enter the api key from Config->General into your 3rd party program:"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "API Key incorrect, Use the api key from Config->General in your 3rd party program:"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
@@ -996,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 ""
|
||||
@@ -2239,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"
|
||||
@@ -3296,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 ""
|
||||
@@ -3397,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
|
||||
@@ -3870,17 +3885,16 @@ msgid "Enable Apprise notifications"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Send notifications using Apprise to almost any notification service"
|
||||
msgid "Send notifications directly to any notification service you use.<br>For example: Slack, Discord, Telegram, or any service from over 100 supported services!"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgid "Apprise defines service connection information using URLs.<br>Read the Apprise wiki how to define the URL for each service.<br>Use a comma and/or space to identify more than one URL."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -4303,6 +4317,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 ""
|
||||
@@ -4541,6 +4559,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 ""
|
||||
|
||||
@@ -37,6 +37,13 @@ msgstr "Nezdařilo se spustit webové rozhraní"
|
||||
msgid "Cannot find web template: %s, trying standard template"
|
||||
msgstr "Šablona pro web nebyla nalezena: %s, zkouším standardní šablonu"
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
"Unable to link to OpenSSL, optimized SSL connection functions will not be "
|
||||
"used."
|
||||
msgstr ""
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
@@ -328,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"
|
||||
@@ -723,15 +744,6 @@ msgstr "Odmítnuto spojení z:"
|
||||
msgid "Refused connection with hostname \"%s\" from:"
|
||||
msgstr "Odmítnuté spojení s hostem \"%s\" z:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Uživatel přihlášen do webového rozhraní"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Uživatel přihlášen"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"API Key missing, please enter the api key from Config->General into your 3rd"
|
||||
@@ -748,6 +760,15 @@ msgstr ""
|
||||
"Nesprávný API klíč, použijte api klíč z Nastavení->Obecné ve vašem programu "
|
||||
"třetí strany:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Uživatel přihlášen do webového rozhraní"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Uživatel přihlášen"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
@@ -1069,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"
|
||||
@@ -2327,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"
|
||||
@@ -3466,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 ""
|
||||
@@ -3572,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
|
||||
@@ -4062,17 +4080,22 @@ msgid "Enable Apprise notifications"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Send notifications using Apprise to almost any notification service"
|
||||
msgid ""
|
||||
"Send notifications directly to any notification service you use.<br>For "
|
||||
"example: Slack, Discord, Telegram, or any service from over 100 supported "
|
||||
"services!"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgid ""
|
||||
"Apprise defines service connection information using URLs.<br>Read the "
|
||||
"Apprise wiki how to define the URL for each service.<br>Use a comma and/or "
|
||||
"space to identify more than one URL."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -4517,6 +4540,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"
|
||||
@@ -4763,6 +4792,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 ""
|
||||
|
||||
468
po/main/da.po
468
po/main/da.po
File diff suppressed because it is too large
Load Diff
109
po/main/de.po
109
po/main/de.po
@@ -15,14 +15,14 @@
|
||||
# Stefan Rodriguez Galeano, 2024
|
||||
# M Z, 2024
|
||||
# Gjelbrim Haskaj, 2024
|
||||
# Safihre <safihre@sabnzbd.org>, 2024
|
||||
# Media Cat, 2025
|
||||
# Safihre <safihre@sabnzbd.org>, 2025
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Media Cat, 2025\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"
|
||||
@@ -52,6 +52,15 @@ msgstr ""
|
||||
"Konnte Web-Vorlage nicht finden: %s Versuche die Standard-Vorlage zu "
|
||||
"verwenden."
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
"Unable to link to OpenSSL, optimized SSL connection functions will not be "
|
||||
"used."
|
||||
msgstr ""
|
||||
"OpenSSL kann nicht verknüpft werden, optimierte SSL-Verbindungsfunktionen "
|
||||
"werden nicht verwendet."
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
@@ -369,6 +378,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 "Warnung zur Kontingentgrenze (%d%%)"
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Downloading resumed after quota reset"
|
||||
msgstr "Download nach Kontingentzurücksetzung fortgesetzt"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/interface.py
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Fehlerhafter Parameter"
|
||||
@@ -798,15 +821,6 @@ msgstr "Abgelehnte Verbindung von:"
|
||||
msgid "Refused connection with hostname \"%s\" from:"
|
||||
msgstr "Verbindung vom Host \"%s\" abgelehnt von:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Benutzer im Web-Interface angemeldet"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Benutzer angemeldet"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"API Key missing, please enter the api key from Config->General into your 3rd"
|
||||
@@ -823,6 +837,15 @@ msgstr ""
|
||||
"API-Schlüssel ungültig. Bitte API-Schlüssel aus Einstellungen->Allgemein in "
|
||||
"die externe Anwendung eingeben:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Benutzer im Web-Interface angemeldet"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Benutzer angemeldet"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
@@ -1150,10 +1173,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."
|
||||
@@ -2458,6 +2477,11 @@ msgstr "Name"
|
||||
msgid "Retry"
|
||||
msgstr "Erneut versuchen"
|
||||
|
||||
#. History page button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Mark as Completed & Remove Temporary Files"
|
||||
msgstr "Als abgeschlossen markieren und temporäre Dateien entfernen"
|
||||
|
||||
#. Queue page table, script selection menu
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Scripts"
|
||||
@@ -3722,10 +3746,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)?"
|
||||
@@ -3839,13 +3859,13 @@ 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."
|
||||
"Kontingent für diesen Server, gezählt ab dem Zeitpunkt der Festlegung. In "
|
||||
"Bytes, optional gefolgt von K,M,G.<br />Wird alle paar Minuten überprüft. "
|
||||
"Benachrichtigung wird gesendet, wenn das Kontingent aufgebraucht ist."
|
||||
|
||||
#. Server's retention time in days
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -4352,22 +4372,30 @@ msgid "Enable Apprise notifications"
|
||||
msgstr "Aktivieren Sie Info-Benachrichtigungen"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Send notifications using Apprise to almost any notification service"
|
||||
msgid ""
|
||||
"Send notifications directly to any notification service you use.<br>For "
|
||||
"example: Slack, Discord, Telegram, or any service from over 100 supported "
|
||||
"services!"
|
||||
msgstr ""
|
||||
"Senden Sie Benachrichtigungen mit Anfragen an fast jeden "
|
||||
"Benachrichtigungsdienst"
|
||||
"Senden Sie Benachrichtigungen direkt an jeden von Ihnen genutzten "
|
||||
"Benachrichtigungsdienst.<br>Zum Beispiel: Slack, Discord, Telegram oder "
|
||||
"jeden anderen Dienst aus über 100 unterstützten Diensten!"
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgstr "Standard Apprise URLs"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr "Standard-Apprise-URLs verwenden"
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgid ""
|
||||
"Apprise defines service connection information using URLs.<br>Read the "
|
||||
"Apprise wiki how to define the URL for each service.<br>Use a comma and/or "
|
||||
"space to identify more than one URL."
|
||||
msgstr ""
|
||||
"Verwenden Sie ein Komma und/oder ein Leerzeichen, um mehr als eine URL zu "
|
||||
"kennzeichnen."
|
||||
"Apprise definiert Dienstverbindungsinformationen über URLs.<br>Lesen Sie das"
|
||||
" Apprise-Wiki, um zu erfahren, wie Sie die URL für jeden Dienst "
|
||||
"definieren.<br>Verwenden Sie ein Komma und/oder Leerzeichen, um mehr als "
|
||||
"eine URL anzugeben."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
@@ -4836,6 +4864,14 @@ 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 ""
|
||||
"Sind Sie sicher, dass Sie alle Ordner in Ihrem temporären Download-Ordner "
|
||||
"löschen möchten? Dies kann nicht rückgängig gemacht werden!"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Fetch NZB from URL"
|
||||
msgstr "NZB aus URL laden"
|
||||
@@ -5091,6 +5127,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 "Klicken Sie auf \"Server testen\", bevor Sie fortfahren"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Restore backup"
|
||||
msgstr "Backup wiederherstellen"
|
||||
|
||||
381
po/main/es.po
381
po/main/es.po
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,13 @@ msgstr "Web-käyttöliittymän käynnistys epäonnistui"
|
||||
msgid "Cannot find web template: %s, trying standard template"
|
||||
msgstr "Web-mallia %s ei löydy, yritetään käyttää oletusmallia"
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
"Unable to link to OpenSSL, optimized SSL connection functions will not be "
|
||||
"used."
|
||||
msgstr ""
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
@@ -327,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"
|
||||
@@ -729,15 +750,6 @@ msgstr ""
|
||||
msgid "Refused connection with hostname \"%s\" from:"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Käyttäjä kirjautui sisään web-käyttöliittymään"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Käyttäjä kirjautui sisään"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"API Key missing, please enter the api key from Config->General into your 3rd"
|
||||
@@ -754,6 +766,15 @@ msgstr ""
|
||||
"API avain virheellinen, käytä Asetukset->Yleiset löytyvää api avainta "
|
||||
"käyttämääsi kolmannen osapuolen ohjelmaan:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Käyttäjä kirjautui sisään web-käyttöliittymään"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Käyttäjä kirjautui sisään"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
@@ -1072,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"
|
||||
@@ -2354,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"
|
||||
@@ -3554,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)"
|
||||
@@ -3663,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
|
||||
@@ -4157,17 +4175,22 @@ msgid "Enable Apprise notifications"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Send notifications using Apprise to almost any notification service"
|
||||
msgid ""
|
||||
"Send notifications directly to any notification service you use.<br>For "
|
||||
"example: Slack, Discord, Telegram, or any service from over 100 supported "
|
||||
"services!"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgid ""
|
||||
"Apprise defines service connection information using URLs.<br>Read the "
|
||||
"Apprise wiki how to define the URL for each service.<br>Use a comma and/or "
|
||||
"space to identify more than one URL."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -4624,6 +4647,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"
|
||||
@@ -4876,6 +4905,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 ""
|
||||
|
||||
107
po/main/fr.po
107
po/main/fr.po
@@ -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 ""
|
||||
@@ -39,6 +39,15 @@ msgstr ""
|
||||
"Impossible de trouver le template de l'interface web : %s, nouvelle "
|
||||
"tentative avec le template standard"
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
"Unable to link to OpenSSL, optimized SSL connection functions will not be "
|
||||
"used."
|
||||
msgstr ""
|
||||
"Impossible d'établir une connexion avec OpenSSL, les fonctions de connexion "
|
||||
"SSL optimisées ne seront pas utilisées."
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
@@ -361,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"
|
||||
@@ -787,15 +810,6 @@ msgstr "Connexion refusée de:"
|
||||
msgid "Refused connection with hostname \"%s\" from:"
|
||||
msgstr "Connexion refusée avec le nom d'hôte \"%s\" à partir de :"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Utilisateur connecté à l'interface web"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Utilisateur connecté"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"API Key missing, please enter the api key from Config->General into your 3rd"
|
||||
@@ -812,6 +826,15 @@ msgstr ""
|
||||
"Clé API incorrecte, utilisez la clé API de la configuration générale dans "
|
||||
"votre application tierce :"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Utilisateur connecté à l'interface web"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Utilisateur connecté"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
@@ -1142,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"
|
||||
@@ -2442,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"
|
||||
@@ -3704,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)"
|
||||
@@ -3818,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
|
||||
@@ -4333,20 +4353,30 @@ msgid "Enable Apprise notifications"
|
||||
msgstr "Activer les notifications Apprise"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Send notifications using Apprise to almost any notification service"
|
||||
msgid ""
|
||||
"Send notifications directly to any notification service you use.<br>For "
|
||||
"example: Slack, Discord, Telegram, or any service from over 100 supported "
|
||||
"services!"
|
||||
msgstr ""
|
||||
"Envoyer des notifications en utilisant Apprise vers presque n'importe quel "
|
||||
"service de notification"
|
||||
"Envoyez des notifications directement vers n'importe quel service de "
|
||||
"notification que vous utilisez.<br>Par exemple : Slack, Discord, Telegram ou"
|
||||
" tout autre service parmi plus de 100 services pris en charge !"
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgstr "URLs par défaut d'Apprise"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr "Utiliser les URLs Apprise par défaut"
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgstr "Utilisez une virgule et/ou un espace pour identifier plusieurs URL."
|
||||
msgid ""
|
||||
"Apprise defines service connection information using URLs.<br>Read the "
|
||||
"Apprise wiki how to define the URL for each service.<br>Use a comma and/or "
|
||||
"space to identify more than one URL."
|
||||
msgstr ""
|
||||
"Apprise définit les informations de connexion au service à l'aide "
|
||||
"d'URL.<br>Consultez le wiki Apprise pour savoir comment définir l'URL de "
|
||||
"chaque service.<br>Utilisez une virgule et/ou un espace pour identifier "
|
||||
"plusieurs URL."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
@@ -4818,6 +4848,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"
|
||||
@@ -5076,6 +5114,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"
|
||||
|
||||
132
po/main/he.po
132
po/main/he.po
@@ -2,14 +2,14 @@
|
||||
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2023
|
||||
# ION, 2024
|
||||
# ION, 2025
|
||||
# Safihre <safihre@sabnzbd.org>, 2025
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+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"
|
||||
@@ -37,6 +37,13 @@ msgstr "כישלון בהתחלת ממשק רשת"
|
||||
msgid "Cannot find web template: %s, trying standard template"
|
||||
msgstr "לא ניתן למצוא תבניות רשת: %s, מנסה תבנית תקנית"
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
"Unable to link to OpenSSL, optimized SSL connection functions will not be "
|
||||
"used."
|
||||
msgstr "לא ניתן לקשר ל-OpenSSL, פונקציות חיבור SSL מותאמות לא יהיו בשימוש."
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
@@ -203,12 +210,16 @@ msgid ""
|
||||
"Could not connect to %s on port %s. Use the default usenet settings: port "
|
||||
"563 and SSL turned on"
|
||||
msgstr ""
|
||||
"לא ניתן להתחבר ל-%s בפורט %s. השתמש בהגדרות ברירת המחדל של usenet: פורט 563 "
|
||||
"ו-SSL מופעל"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid ""
|
||||
"Could not connect to %s on port %s. Use the default usenet settings: port "
|
||||
"119 and SSL turned off"
|
||||
msgstr ""
|
||||
"לא ניתן להתחבר ל-%s בפורט %s. השתמש בהגדרות ברירת המחדל של usenet: פורט 119 "
|
||||
"ו-SSL כבוי"
|
||||
|
||||
#: sabnzbd/api.py, sabnzbd/interface.py
|
||||
msgid "Server address \"%s:%s\" is not valid."
|
||||
@@ -328,6 +339,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 "אזהרת מגבלת מכסה (%d%%)"
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Downloading resumed after quota reset"
|
||||
msgstr "ההורדה התחדשה לאחר איפוס מכסה"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/interface.py
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "פרמטר שגוי"
|
||||
@@ -390,7 +415,7 @@ msgstr ""
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The par2 application was switched, any custom par2 parameters were removed"
|
||||
msgstr ""
|
||||
msgstr "יישום par2 הוחלף, כל פרמטרי par2 מותאמים אישית הוסרו"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/config.py
|
||||
@@ -466,7 +491,7 @@ msgstr "אי־האפלה שינתה שם של %d קבצים"
|
||||
|
||||
#: sabnzbd/deobfuscate_filenames.py
|
||||
msgid "Deobfuscate renamed %d subtitle file(s)"
|
||||
msgstr ""
|
||||
msgstr "בוצע ביטול ערפול של %d קבצי כתוביות ששמם שונה"
|
||||
|
||||
#: sabnzbd/directunpacker.py, sabnzbd/skintext.py
|
||||
msgid "Direct Unpack"
|
||||
@@ -738,15 +763,6 @@ msgstr "חיבור מסורב מאת:"
|
||||
msgid "Refused connection with hostname \"%s\" from:"
|
||||
msgstr "חיבור מסורב עם שם המארח \"%s\" מאת:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "משתמש התחבר לממשק הרשת"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "משתמש התחבר"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"API Key missing, please enter the api key from Config->General into your 3rd"
|
||||
@@ -761,6 +777,15 @@ msgid ""
|
||||
"program:"
|
||||
msgstr "מפתח API שגוי, השתמש במפתח ה־API מתצורה->כללי בתוכנית הצד השלישי שלך:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "משתמש התחבר לממשק הרשת"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "משתמש התחבר"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
@@ -1080,11 +1105,7 @@ msgstr "פריקה נכשלה, שגיאת כתיבה או דיסק מלא?"
|
||||
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpacking failed, disk full"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpacking failed, path is too long"
|
||||
msgstr "פריקה נכשלה, נתיב ארוך מדי"
|
||||
msgstr "פריקה נכשלה, דיסק מלא"
|
||||
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpacking failed, archive requires a password"
|
||||
@@ -1218,6 +1239,8 @@ msgid ""
|
||||
" locally injected certificate (for example by firewall or virus scanner). "
|
||||
"Try setting Certificate verification to Medium."
|
||||
msgstr ""
|
||||
"לא ניתן לאמת את האישור. זה יכול להיות בעיית שרת או בגלל אישור מוזרק מקומית "
|
||||
"(לדוגמה על ידי חומת אש או סורק וירוסים). נסה להגדיר את אימות האישור לבינוני."
|
||||
|
||||
#: sabnzbd/newswrapper.py
|
||||
msgid "Server %s uses an untrusted certificate [%s]"
|
||||
@@ -1298,7 +1321,7 @@ msgstr "כישלון בשליחת הודעת Prowl"
|
||||
#. Warning message
|
||||
#: sabnzbd/notifier.py
|
||||
msgid "Failed to send Apprise message - no URLs defined"
|
||||
msgstr ""
|
||||
msgstr "שליחת הודעת Apprise נכשלה - לא הוגדרו כתובות URL"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/notifier.py
|
||||
@@ -2367,6 +2390,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"
|
||||
@@ -2638,7 +2666,7 @@ msgstr "מטמון בשימוש"
|
||||
#. What platform we are on (e.g. Windows/macOS/Ubuntu/UnRaid/etc)
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Platform"
|
||||
msgstr ""
|
||||
msgstr "פלטפורמה"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
@@ -2911,7 +2939,7 @@ msgstr "העבר עבודות אל הארכיון אם ההיסטוריה חור
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Delete jobs if the history and archive exceeds specified number of jobs"
|
||||
msgstr ""
|
||||
msgstr "מחק עבודות אם ההיסטוריה והארכיון עוברים את מספר העבודות שצוין"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Move jobs to the archive after specified number of days"
|
||||
@@ -2920,7 +2948,7 @@ msgstr "העבר עבודות אל הארכיון לאחר מספר מצוין
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Delete jobs from the history and archive after specified number of days"
|
||||
msgstr ""
|
||||
msgstr "מחק עבודות מההיסטוריה והארכיון לאחר מספר הימים שצוין"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Move all completed jobs to archive"
|
||||
@@ -3558,10 +3586,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 "כמה ניתן להוריד החודש (ק״ב/מ״ב/ג״ב)"
|
||||
@@ -3666,12 +3690,12 @@ 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, היא נבדקת כל כמה דקות."
|
||||
"מכסה לשרת זה, נספרת מהרגע שהיא נקבעה. בבייטים, באופן אופציונלי ניתן להוסיף "
|
||||
"K,M,G.<br />נבדקת כל כמה דקות. הודעה נשלחת כאשר המכסה מוצתה."
|
||||
|
||||
#. Server's retention time in days
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -3702,6 +3726,11 @@ msgid ""
|
||||
"used. - Disabled: no certification verification. This is not secure at all, "
|
||||
"anyone could intercept your connection. "
|
||||
msgstr ""
|
||||
"כאשר SSL מופעל: - מחמיר: אכוף אימות אישור מלא. זוהי ההגדרה המאובטחת ביותר. -"
|
||||
" בינוני: אמת שהאישור תקף ותואם לכתובת השרת, אך אפשר אישורים המוזרקים מקומית "
|
||||
"(למשל על ידי חומת אש או סורק וירוסים). - מינימלי: אמת שהאישור תקף. זה לא "
|
||||
"מאובטח, כל אישור תקף יכול לשמש. - מושבת: ללא אימות אישור. זה לא מאובטח כלל, "
|
||||
"כל אחד יכול ליירט את החיבור שלך."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Disabled"
|
||||
@@ -3713,7 +3742,7 @@ msgstr "מזערי"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Medium"
|
||||
msgstr ""
|
||||
msgstr "בינוני"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Strict"
|
||||
@@ -4165,18 +4194,28 @@ msgid "Enable Apprise notifications"
|
||||
msgstr "אפשר התראות Apprise"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Send notifications using Apprise to almost any notification service"
|
||||
msgstr "שלח התראות ע״י שימוש בשירות Apprise אל כמעט כל שירות התראות"
|
||||
msgid ""
|
||||
"Send notifications directly to any notification service you use.<br>For "
|
||||
"example: Slack, Discord, Telegram, or any service from over 100 supported "
|
||||
"services!"
|
||||
msgstr ""
|
||||
"שלח הודעות ישירות לכל שירות הודעות שאתה משתמש בו.<br>לדוגמה: Slack, Discord,"
|
||||
" Telegram או כל שירות מתוך למעלה מ-100 שירותים נתמכים!"
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgstr "כתובות Apprise ברירות מחדל"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr "השתמש בכתובות URL של Apprise המוגדרות כברירת מחדל"
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgstr "השתמש בפסיק, ברווח או בשניהם כדי לזהות יותר מכתובת אחת."
|
||||
msgid ""
|
||||
"Apprise defines service connection information using URLs.<br>Read the "
|
||||
"Apprise wiki how to define the URL for each service.<br>Use a comma and/or "
|
||||
"space to identify more than one URL."
|
||||
msgstr ""
|
||||
"Apprise מגדיר מידע על חיבור שירות באמצעות כתובות URL.<br>קרא את הוויקי של "
|
||||
"Apprise כדי ללמוד כיצד להגדיר את כתובת ה-URL עבור כל שירות.<br>השתמש בפסיק "
|
||||
"ו/או רווח כדי לזהות יותר מכתובת URL אחת."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
@@ -4634,6 +4673,14 @@ 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 מכתובת"
|
||||
@@ -4888,6 +4935,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 "שחזר גיבוי"
|
||||
|
||||
104
po/main/it.po
104
po/main/it.po
@@ -36,6 +36,15 @@ msgstr "Impossibile avviare l'interfaccia web"
|
||||
msgid "Cannot find web template: %s, trying standard template"
|
||||
msgstr "Impossibile trovare il modello web: %s, si prova il modello standard"
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
"Unable to link to OpenSSL, optimized SSL connection functions will not be "
|
||||
"used."
|
||||
msgstr ""
|
||||
"Impossibile collegarsi a OpenSSL, le funzioni di connessione SSL ottimizzate"
|
||||
" non verranno utilizzate."
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
@@ -354,6 +363,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 "Avviso limite quota (%d%%)"
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Downloading resumed after quota reset"
|
||||
msgstr "Download ripreso dopo il ripristino della quota"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/interface.py
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Parametro non corretto"
|
||||
@@ -778,15 +801,6 @@ msgstr "Connessione rifiutata da:"
|
||||
msgid "Refused connection with hostname \"%s\" from:"
|
||||
msgstr "Connessione rifiutata con hostname \"%s\" da:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Utente ha effettuato l'accesso all'interfaccia web"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Utente ha effettuato l'accesso"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"API Key missing, please enter the api key from Config->General into your 3rd"
|
||||
@@ -803,6 +817,15 @@ msgstr ""
|
||||
"Chiave API non corretta, Usa la chiave API da Config->Generale nel tuo "
|
||||
"programma di terze parti:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Utente ha effettuato l'accesso all'interfaccia web"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Utente ha effettuato l'accesso"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
@@ -1127,10 +1150,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"
|
||||
@@ -2422,6 +2441,11 @@ msgstr "Nome"
|
||||
msgid "Retry"
|
||||
msgstr "Riprova"
|
||||
|
||||
#. History page button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Mark as Completed & Remove Temporary Files"
|
||||
msgstr "Segna come completato e rimuovi i file temporanei"
|
||||
|
||||
#. Queue page table, script selection menu
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Scripts"
|
||||
@@ -3665,10 +3689,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)"
|
||||
@@ -3778,13 +3798,13 @@ 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."
|
||||
"Quota per questo server, contata dal momento in cui viene impostata. In "
|
||||
"byte, opzionalmente seguito da K,M,G.<br />Controllato ogni pochi minuti. La"
|
||||
" notifica viene inviata quando la quota è esaurita."
|
||||
|
||||
#. Server's retention time in days
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -4289,18 +4309,29 @@ msgid "Enable Apprise notifications"
|
||||
msgstr "Abilita notifiche Apprise"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Send notifications using Apprise to almost any notification service"
|
||||
msgstr "Invia notifiche usando Apprise a quasi tutti i servizi di notifica"
|
||||
msgid ""
|
||||
"Send notifications directly to any notification service you use.<br>For "
|
||||
"example: Slack, Discord, Telegram, or any service from over 100 supported "
|
||||
"services!"
|
||||
msgstr ""
|
||||
"Invia notifiche direttamente a qualsiasi servizio di notifica che "
|
||||
"utilizzi.<br>Ad esempio: Slack, Discord, Telegram o qualsiasi servizio tra "
|
||||
"oltre 100 servizi supportati!"
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgstr "URL predefiniti di Apprise"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr "Usa URL Apprise predefiniti"
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgstr "Usa una virgola e/o uno spazio per identificare più di un URL."
|
||||
msgid ""
|
||||
"Apprise defines service connection information using URLs.<br>Read the "
|
||||
"Apprise wiki how to define the URL for each service.<br>Use a comma and/or "
|
||||
"space to identify more than one URL."
|
||||
msgstr ""
|
||||
"Apprise definisce le informazioni di connessione del servizio utilizzando "
|
||||
"URL.<br>Leggi il wiki di Apprise per sapere come definire l'URL per ogni "
|
||||
"servizio.<br>Usa una virgola e/o uno spazio per identificare più di un URL."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
@@ -4772,6 +4803,14 @@ 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 ""
|
||||
"Sei sicuro di voler eliminare tutte le cartelle nella tua cartella di "
|
||||
"download temporanei? Questo non può essere annullato!"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Fetch NZB from URL"
|
||||
msgstr "Recupera NZB da URL"
|
||||
@@ -5028,6 +5067,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 "Fai clic su Prova server prima di continuare"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Restore backup"
|
||||
msgstr "Ripristina backup"
|
||||
|
||||
@@ -36,6 +36,13 @@ msgstr "Kunne ikke starte webgrensesnittet"
|
||||
msgid "Cannot find web template: %s, trying standard template"
|
||||
msgstr "Kan ikke finne webmal: %s, prøver standardmal"
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
"Unable to link to OpenSSL, optimized SSL connection functions will not be "
|
||||
"used."
|
||||
msgstr ""
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
@@ -325,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"
|
||||
@@ -726,15 +747,6 @@ msgstr ""
|
||||
msgid "Refused connection with hostname \"%s\" from:"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Bruker logget inn i webgrensesnitt"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Bruker pålogget"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"API Key missing, please enter the api key from Config->General into your 3rd"
|
||||
@@ -751,6 +763,15 @@ msgstr ""
|
||||
"API-nøkkel er feil, bruk API-nøkkel fra Konfigurasjon->Generelt i ditt "
|
||||
"tredjepartsprogram:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Bruker logget inn i webgrensesnitt"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Bruker pålogget"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
@@ -1069,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"
|
||||
@@ -2352,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"
|
||||
@@ -3531,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)"
|
||||
@@ -3642,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
|
||||
@@ -4136,17 +4154,22 @@ msgid "Enable Apprise notifications"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Send notifications using Apprise to almost any notification service"
|
||||
msgid ""
|
||||
"Send notifications directly to any notification service you use.<br>For "
|
||||
"example: Slack, Discord, Telegram, or any service from over 100 supported "
|
||||
"services!"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgid ""
|
||||
"Apprise defines service connection information using URLs.<br>Read the "
|
||||
"Apprise wiki how to define the URL for each service.<br>Use a comma and/or "
|
||||
"space to identify more than one URL."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -4598,6 +4621,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"
|
||||
@@ -4848,6 +4877,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 ""
|
||||
|
||||
105
po/main/nl.po
105
po/main/nl.po
@@ -38,6 +38,15 @@ msgstr "Webinterface kan niet gestart worden"
|
||||
msgid "Cannot find web template: %s, trying standard template"
|
||||
msgstr "Websjabloon %s niet te vinden; het standaardsjabloon wordt gebruikt."
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
"Unable to link to OpenSSL, optimized SSL connection functions will not be "
|
||||
"used."
|
||||
msgstr ""
|
||||
"Kan niet koppelen aan OpenSSL, geoptimaliseerde SSL-verbindingsfuncties "
|
||||
"worden niet gebruikt."
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
@@ -351,6 +360,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 "Waarschuwing quotumlimiet (%d%%)"
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Downloading resumed after quota reset"
|
||||
msgstr "Downloaden hervat na quotumreset"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/interface.py
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Incorrecte parameter"
|
||||
@@ -781,15 +804,6 @@ msgstr "Verbinding geweigerd van: "
|
||||
msgid "Refused connection with hostname \"%s\" from:"
|
||||
msgstr "Verbinding met hostnaam \"%s\" geweigerd van:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Gebruiker heeft ingelogd"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Gebruiker ingelogd"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"API Key missing, please enter the api key from Config->General into your 3rd"
|
||||
@@ -806,6 +820,15 @@ msgstr ""
|
||||
"API-sleutel incorrect; vul de API-sleutel van 'Configuratie' => 'Algemeen' "
|
||||
"in bij het externe programma:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Gebruiker heeft ingelogd"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Gebruiker ingelogd"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
@@ -1131,10 +1154,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"
|
||||
@@ -2425,6 +2444,11 @@ msgstr "Naam"
|
||||
msgid "Retry"
|
||||
msgstr "Opnieuw"
|
||||
|
||||
#. History page button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Mark as Completed & Remove Temporary Files"
|
||||
msgstr "Markeer als voltooid en verwijder tijdelijke bestanden"
|
||||
|
||||
#. Queue page table, script selection menu
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Scripts"
|
||||
@@ -3665,10 +3689,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)"
|
||||
@@ -3780,14 +3800,13 @@ 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."
|
||||
"Quotum voor deze server, geteld vanaf het moment dat het is ingesteld. In "
|
||||
"bytes, optioneel gevolgd door K,M,G.<br />Wordt om de paar minuten "
|
||||
"gecontroleerd. Melding wordt verzonden wanneer het quotum is opgebruikt."
|
||||
|
||||
#. Server's retention time in days
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -4292,19 +4311,30 @@ msgid "Enable Apprise notifications"
|
||||
msgstr "Apprise-meldingen activeren"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Send notifications using Apprise to almost any notification service"
|
||||
msgid ""
|
||||
"Send notifications directly to any notification service you use.<br>For "
|
||||
"example: Slack, Discord, Telegram, or any service from over 100 supported "
|
||||
"services!"
|
||||
msgstr ""
|
||||
"Stuur meldingen met behulp van Apprise naar bijna elke bestaande service."
|
||||
"Stuur meldingen rechtstreeks naar elke meldingsservice die u "
|
||||
"gebruikt.<br>Bijvoorbeeld: Slack, Discord, Telegram of elke andere service "
|
||||
"uit meer dan 100 ondersteunde services!"
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgstr "Standaard Apprise-URL's"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr "Gebruik standaard Apprise-URL's"
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgstr "Gebruik een komma en/of spatie om meer dan één URL op te geven."
|
||||
msgid ""
|
||||
"Apprise defines service connection information using URLs.<br>Read the "
|
||||
"Apprise wiki how to define the URL for each service.<br>Use a comma and/or "
|
||||
"space to identify more than one URL."
|
||||
msgstr ""
|
||||
"Apprise definieert serviceverbindingsinformatie met behulp van "
|
||||
"URL's.<br>Lees de Apprise-wiki om te leren hoe u de URL voor elke service "
|
||||
"definieert.<br>Gebruik een komma en/of spatie om meer dan één URL te "
|
||||
"identificeren."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
@@ -4770,6 +4800,14 @@ 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 ""
|
||||
"Weet u zeker dat u alle mappen in uw tijdelijke downloadmap wilt "
|
||||
"verwijderen? Dit kan niet ongedaan worden gemaakt!"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Fetch NZB from URL"
|
||||
msgstr "Haal NZB op via URL"
|
||||
@@ -5025,6 +5063,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 "Klik op Test server voordat u doorgaat"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Restore backup"
|
||||
msgstr "Backup herstellen"
|
||||
|
||||
@@ -36,6 +36,13 @@ msgstr "Nie udało się uruchomić interfejsu WWW"
|
||||
msgid "Cannot find web template: %s, trying standard template"
|
||||
msgstr "Nie znaleziono szablonu: %s, próbuję użyć standardowego szablonu"
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
"Unable to link to OpenSSL, optimized SSL connection functions will not be "
|
||||
"used."
|
||||
msgstr ""
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
@@ -324,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"
|
||||
@@ -729,15 +750,6 @@ msgstr ""
|
||||
msgid "Refused connection with hostname \"%s\" from:"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr ""
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"API Key missing, please enter the api key from Config->General into your 3rd"
|
||||
@@ -754,6 +766,15 @@ msgstr ""
|
||||
"Klucz API jest nieprawidłowy, użyj klucza API z sekcji Konfiguracja->Ogólne "
|
||||
"w zewnętrznym programie:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr ""
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
@@ -1072,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"
|
||||
@@ -2361,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"
|
||||
@@ -3542,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)"
|
||||
@@ -3654,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
|
||||
@@ -4148,17 +4166,22 @@ msgid "Enable Apprise notifications"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Send notifications using Apprise to almost any notification service"
|
||||
msgid ""
|
||||
"Send notifications directly to any notification service you use.<br>For "
|
||||
"example: Slack, Discord, Telegram, or any service from over 100 supported "
|
||||
"services!"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgid ""
|
||||
"Apprise defines service connection information using URLs.<br>Read the "
|
||||
"Apprise wiki how to define the URL for each service.<br>Use a comma and/or "
|
||||
"space to identify more than one URL."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -4610,6 +4633,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"
|
||||
@@ -4858,6 +4887,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 ""
|
||||
|
||||
@@ -38,6 +38,13 @@ msgid "Cannot find web template: %s, trying standard template"
|
||||
msgstr ""
|
||||
"Não foi possível encontrar o template web: %s. Tentando o template padrão"
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
"Unable to link to OpenSSL, optimized SSL connection functions will not be "
|
||||
"used."
|
||||
msgstr ""
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
@@ -336,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"
|
||||
@@ -741,15 +762,6 @@ msgstr ""
|
||||
msgid "Refused connection with hostname \"%s\" from:"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr ""
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"API Key missing, please enter the api key from Config->General into your 3rd"
|
||||
@@ -766,6 +778,15 @@ msgstr ""
|
||||
"Chave de API incorreta. Use a chave de API de Configuração->Geral em seu "
|
||||
"programa de terceiros:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr ""
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
@@ -1084,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"
|
||||
@@ -2372,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"
|
||||
@@ -3554,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)"
|
||||
@@ -3665,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
|
||||
@@ -4159,17 +4177,22 @@ msgid "Enable Apprise notifications"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Send notifications using Apprise to almost any notification service"
|
||||
msgid ""
|
||||
"Send notifications directly to any notification service you use.<br>For "
|
||||
"example: Slack, Discord, Telegram, or any service from over 100 supported "
|
||||
"services!"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgid ""
|
||||
"Apprise defines service connection information using URLs.<br>Read the "
|
||||
"Apprise wiki how to define the URL for each service.<br>Use a comma and/or "
|
||||
"space to identify more than one URL."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -4621,6 +4644,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"
|
||||
@@ -4869,6 +4898,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 ""
|
||||
|
||||
@@ -37,6 +37,13 @@ msgstr "Pornirea interfeţei-web nereuşită"
|
||||
msgid "Cannot find web template: %s, trying standard template"
|
||||
msgstr "Nu se poate găsi şablon web:%s, se încearcă şablon standard"
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
"Unable to link to OpenSSL, optimized SSL connection functions will not be "
|
||||
"used."
|
||||
msgstr ""
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
@@ -340,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"
|
||||
@@ -749,15 +770,6 @@ msgstr ""
|
||||
msgid "Refused connection with hostname \"%s\" from:"
|
||||
msgstr "Conectare refuzată cu gazda „%s” de la:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Utilizatorul s-a autentificat în interfața web"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Utilizator logat"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"API Key missing, please enter the api key from Config->General into your 3rd"
|
||||
@@ -774,6 +786,15 @@ msgstr ""
|
||||
"Cheie API incorectă, Folosiţi cheia api din Configurare->General în "
|
||||
"programul dumneavoastră terţ:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Utilizatorul s-a autentificat în interfața web"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Utilizator logat"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
@@ -1097,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ă"
|
||||
@@ -2390,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"
|
||||
@@ -3573,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)"
|
||||
@@ -3685,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
|
||||
@@ -4180,17 +4198,22 @@ msgid "Enable Apprise notifications"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Send notifications using Apprise to almost any notification service"
|
||||
msgid ""
|
||||
"Send notifications directly to any notification service you use.<br>For "
|
||||
"example: Slack, Discord, Telegram, or any service from over 100 supported "
|
||||
"services!"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgid ""
|
||||
"Apprise defines service connection information using URLs.<br>Read the "
|
||||
"Apprise wiki how to define the URL for each service.<br>Use a comma and/or "
|
||||
"space to identify more than one URL."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -4640,6 +4663,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"
|
||||
@@ -4891,6 +4920,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 ""
|
||||
|
||||
@@ -38,6 +38,13 @@ msgstr ""
|
||||
"Не удаётся найти шаблон веб-интерфейса: %s. Выполняется попытка использовать"
|
||||
" стандартный шаблон"
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
"Unable to link to OpenSSL, optimized SSL connection functions will not be "
|
||||
"used."
|
||||
msgstr ""
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
@@ -324,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 "Неправильный параметр"
|
||||
@@ -725,15 +746,6 @@ msgstr ""
|
||||
msgid "Refused connection with hostname \"%s\" from:"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr ""
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"API Key missing, please enter the api key from Config->General into your 3rd"
|
||||
@@ -750,6 +762,15 @@ msgstr ""
|
||||
"Неправильный ключ API. Используйте в сторонней программе ключ API из раздела"
|
||||
" «Настройка -> Общие»:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr ""
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
@@ -1068,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 "Ошибка распаковки: архив защищён паролем"
|
||||
@@ -2354,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"
|
||||
@@ -3533,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)"
|
||||
@@ -3643,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
|
||||
@@ -4144,17 +4162,22 @@ msgid "Enable Apprise notifications"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Send notifications using Apprise to almost any notification service"
|
||||
msgid ""
|
||||
"Send notifications directly to any notification service you use.<br>For "
|
||||
"example: Slack, Discord, Telegram, or any service from over 100 supported "
|
||||
"services!"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgid ""
|
||||
"Apprise defines service connection information using URLs.<br>Read the "
|
||||
"Apprise wiki how to define the URL for each service.<br>Use a comma and/or "
|
||||
"space to identify more than one URL."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -4604,6 +4627,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 ""
|
||||
@@ -4854,6 +4883,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 ""
|
||||
|
||||
@@ -36,6 +36,13 @@ msgstr "Neuspešno pokretanje web interfejsa"
|
||||
msgid "Cannot find web template: %s, trying standard template"
|
||||
msgstr "Немогуће наћи веб модел: %s, програм покушава са стандардним моделом"
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
"Unable to link to OpenSSL, optimized SSL connection functions will not be "
|
||||
"used."
|
||||
msgstr ""
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
@@ -321,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 "Погрешан параметар"
|
||||
@@ -723,15 +744,6 @@ msgstr ""
|
||||
msgid "Refused connection with hostname \"%s\" from:"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr ""
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"API Key missing, please enter the api key from Config->General into your 3rd"
|
||||
@@ -746,6 +758,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"API кључ је погрешан, унети у спољни програм API кључ из Подешавања->Опште:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr ""
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
@@ -1064,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"
|
||||
@@ -2347,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"
|
||||
@@ -3518,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 "Колико може да се преузме овог месеца (К/М/Г)"
|
||||
@@ -3629,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
|
||||
@@ -4122,17 +4140,22 @@ msgid "Enable Apprise notifications"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Send notifications using Apprise to almost any notification service"
|
||||
msgid ""
|
||||
"Send notifications directly to any notification service you use.<br>For "
|
||||
"example: Slack, Discord, Telegram, or any service from over 100 supported "
|
||||
"services!"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgid ""
|
||||
"Apprise defines service connection information using URLs.<br>Read the "
|
||||
"Apprise wiki how to define the URL for each service.<br>Use a comma and/or "
|
||||
"space to identify more than one URL."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -4583,6 +4606,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"
|
||||
@@ -4831,6 +4860,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 ""
|
||||
|
||||
@@ -36,6 +36,13 @@ msgstr "Det gick inte att starta webbgränssnittet"
|
||||
msgid "Cannot find web template: %s, trying standard template"
|
||||
msgstr "Hittar inte webbmall: %s, försöker med standardmall"
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
"Unable to link to OpenSSL, optimized SSL connection functions will not be "
|
||||
"used."
|
||||
msgstr ""
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
msgid ""
|
||||
@@ -321,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"
|
||||
@@ -723,15 +744,6 @@ msgstr ""
|
||||
msgid "Refused connection with hostname \"%s\" from:"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr ""
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"API Key missing, please enter the api key from Config->General into your 3rd"
|
||||
@@ -748,6 +760,15 @@ msgstr ""
|
||||
"API-nyckel felaktig, använd api-nyckeln från Konfiguration-> Allmänt i ditt "
|
||||
"tredjepartsprogram:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr ""
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
@@ -1066,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"
|
||||
@@ -2353,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"
|
||||
@@ -3530,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)"
|
||||
@@ -3641,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
|
||||
@@ -4135,17 +4153,22 @@ msgid "Enable Apprise notifications"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Send notifications using Apprise to almost any notification service"
|
||||
msgid ""
|
||||
"Send notifications directly to any notification service you use.<br>For "
|
||||
"example: Slack, Discord, Telegram, or any service from over 100 supported "
|
||||
"services!"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr ""
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgid ""
|
||||
"Apprise defines service connection information using URLs.<br>Read the "
|
||||
"Apprise wiki how to define the URL for each service.<br>Use a comma and/or "
|
||||
"space to identify more than one URL."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -4595,6 +4618,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"
|
||||
@@ -4845,6 +4874,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 ""
|
||||
|
||||
106
po/main/tr.po
106
po/main/tr.po
@@ -4,12 +4,13 @@
|
||||
# Translators:
|
||||
# Taylan Tatlı, 2025
|
||||
# mauron, 2025
|
||||
# Safihre <safihre@sabnzbd.org>, 2025
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.6.0\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+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"
|
||||
@@ -37,6 +38,15 @@ msgstr "Web arayüzünün başlatılması başarısız oldu"
|
||||
msgid "Cannot find web template: %s, trying standard template"
|
||||
msgstr "Web şablonu bulunamadı: %s, standart şablon denenecek"
|
||||
|
||||
#. Warning message
|
||||
#: SABnzbd.py
|
||||
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
|
||||
msgid ""
|
||||
@@ -353,6 +363,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"
|
||||
@@ -771,15 +795,6 @@ msgstr "Şuradan bağlantı reddedildi:"
|
||||
msgid "Refused connection with hostname \"%s\" from:"
|
||||
msgstr "Şuradan \"%s\" makine ismi ile bağlantı reddedildi:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Kullanıcı web arayüzünde oturum açtı"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Kullanıcı oturum açtı"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"API Key missing, please enter the api key from Config->General into your 3rd"
|
||||
@@ -796,6 +811,15 @@ msgstr ""
|
||||
"API anahtarı yanlış, 3. taraf programınızda Yapılandırma->Genel'den api "
|
||||
"anahtarını kullanın:"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "User logged in to the web interface"
|
||||
msgstr "Kullanıcı web arayüzünde oturum açtı"
|
||||
|
||||
#. Notification
|
||||
#: sabnzbd/interface.py, sabnzbd/notifier.py
|
||||
msgid "User logged in"
|
||||
msgstr "Kullanıcı oturum açtı"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
@@ -1118,10 +1142,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"
|
||||
@@ -2413,6 +2433,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"
|
||||
@@ -3651,10 +3676,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)"
|
||||
@@ -3762,13 +3783,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
|
||||
@@ -4274,20 +4296,29 @@ msgid "Enable Apprise notifications"
|
||||
msgstr "Apprise bildirimlerini etkinleştir"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Send notifications using Apprise to almost any notification service"
|
||||
msgid ""
|
||||
"Send notifications directly to any notification service you use.<br>For "
|
||||
"example: Slack, Discord, Telegram, or any service from over 100 supported "
|
||||
"services!"
|
||||
msgstr ""
|
||||
"Apprise kullanarak neredeyse tüm bildirim hizmetlerine bildirim gönderin"
|
||||
"Bildirimleri kullandığınız herhangi bir bildirim hizmetine doğrudan "
|
||||
"gönderin.<br>Örneğin: Slack, Discord, Telegram veya 100'den fazla "
|
||||
"desteklenen hizmetten herhangi biri!"
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgstr "Varsayılan Apprise URL'leri"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr "Varsayılan Apprise URL'lerini kullan"
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgid ""
|
||||
"Apprise defines service connection information using URLs.<br>Read the "
|
||||
"Apprise wiki how to define the URL for each service.<br>Use a comma and/or "
|
||||
"space to identify more than one URL."
|
||||
msgstr ""
|
||||
"Birden fazla URL (adres) tanımlamak için virgül ve/veya boşluk kullanın."
|
||||
"Apprise, hizmet bağlantı bilgilerini URL'ler kullanarak tanımlar.<br>Her "
|
||||
"hizmet için URL'nin nasıl tanımlanacağını öğrenmek için Apprise wiki'sini "
|
||||
"okuyun.<br>Birden fazla URL tanımlamak için virgül ve/veya boşluk kullanın."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
@@ -4759,6 +4790,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"
|
||||
@@ -5015,6 +5054,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"
|
||||
|
||||
447
po/main/zh_CN.po
447
po/main/zh_CN.po
File diff suppressed because it is too large
Load Diff
@@ -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 ""
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
#
|
||||
# Translators:
|
||||
# Pavel C <quoing_transifex@mess.cz>, 2022
|
||||
# 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: Pavel C <quoing_transifex@mess.cz>, 2022\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Czech (https://app.transifex.com/sabnzbd/teams/111101/cs/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -18,7 +19,11 @@ msgstr ""
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr ""
|
||||
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!"
|
||||
@@ -30,39 +35,42 @@ msgid ""
|
||||
"reinstall the SABnzbd service. \\n\\nClick `OK` to remove the existing "
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"Služba SABnzbd pro Windows byla ve verzi SABnzbd 3.0.0 změněna.\\nBudete "
|
||||
"muset znovu nainstalovat službu SABnzbd.\\n\\nKlikněte na `OK` pro "
|
||||
"odstranění stávajících služeb nebo na `Zrušit` pro zrušení této aktualizace."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports 64-bit Windows."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd podporuje pouze 64bitové Windows."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports Windows 8.1 and above."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd podporuje pouze Windows 8.1 a novější."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Shutting down SABnzbd"
|
||||
msgstr ""
|
||||
msgstr "Vypínání SABnzbd"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr ""
|
||||
msgstr "Tímto odinstalujete SABnzbd z vašeho systému"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr ""
|
||||
msgstr "Spouštět při startu systému"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr ""
|
||||
msgstr "Ikona na ploše"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr ""
|
||||
msgstr "Přiřazení souborů NZB"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr ""
|
||||
msgstr "Odstranit program"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr ""
|
||||
msgstr "Smazat nastavení"
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
# 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>, 2020\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Danish (https://app.transifex.com/sabnzbd/teams/111101/da/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -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!"
|
||||
@@ -30,18 +34,21 @@ msgid ""
|
||||
"reinstall the SABnzbd service. \\n\\nClick `OK` to remove the existing "
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"SABnzbd Windows-tjenesten blev ændret i SABnzbd 3.0.0.\\nDu skal "
|
||||
"geninstallere SABnzbd-tjenesten.\\n\\nKlik på `OK` for at fjerne de "
|
||||
"eksisterende tjenester eller `Annuller` for at afbryde denne opgradering."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports 64-bit Windows."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd understøtter kun 64-bit Windows."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports Windows 8.1 and above."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd understøtter kun Windows 8.1 og nyere."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Shutting down SABnzbd"
|
||||
msgstr ""
|
||||
msgstr "Lukker SABnzbd"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
# Ester Molla Aragones <moarages@gmail.com>, 2020
|
||||
# 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: Ester Molla Aragones <moarages@gmail.com>, 2020\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Spanish (https://app.transifex.com/sabnzbd/teams/111101/es/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -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!"
|
||||
@@ -38,15 +42,15 @@ msgstr ""
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports 64-bit Windows."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd solo es compatible con Windows de 64 bits."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports Windows 8.1 and above."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd solo es compatible con Windows 8.1 y superiores."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Shutting down SABnzbd"
|
||||
msgstr ""
|
||||
msgstr "Apagando SABnzbd"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
# 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>, 2020\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Finnish (https://app.transifex.com/sabnzbd/teams/111101/fi/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\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!"
|
||||
@@ -30,18 +34,21 @@ msgid ""
|
||||
"reinstall the SABnzbd service. \\n\\nClick `OK` to remove the existing "
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"SABnzbdin Windows-palvelu muuttui versiossa SABnzbd 3.0.0.\\nSinun täytyy "
|
||||
"asentaa SABnzbd-palvelu uudelleen.\\n\\nNapsauta `OK` poistaaksesi olemassa "
|
||||
"olevat palvelut tai `Peruuta` peruuttaaksesi tämän päivityksen."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports 64-bit Windows."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd tukee vain 64-bittistä Windowsia."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports Windows 8.1 and above."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd tukee vain Windows 8.1:tä ja uudempia."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Shutting down SABnzbd"
|
||||
msgstr ""
|
||||
msgstr "Sammutetaan SABnzbd"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
# 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>, 2020\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Norwegian Bokmål (https://app.transifex.com/sabnzbd/teams/111101/nb/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -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!"
|
||||
@@ -30,18 +34,21 @@ msgid ""
|
||||
"reinstall the SABnzbd service. \\n\\nClick `OK` to remove the existing "
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"SABnzbd Windows-tjenesten ble endret i SABnzbd 3.0.0.\\nDu må installere "
|
||||
"SABnzbd-tjenesten på nytt.\\n\\nKlikk `OK` for å fjerne eksisterende "
|
||||
"tjenester eller `Avbryt` for å avbryte denne oppgraderingen."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports 64-bit Windows."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd støtter kun 64-bit Windows."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports Windows 8.1 and above."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd støtter kun Windows 8.1 og nyere."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Shutting down SABnzbd"
|
||||
msgstr ""
|
||||
msgstr "Slår av SABnzbd"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
# 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>, 2020\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Polish (https://app.transifex.com/sabnzbd/teams/111101/pl/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -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!"
|
||||
@@ -30,18 +34,21 @@ msgid ""
|
||||
"reinstall the SABnzbd service. \\n\\nClick `OK` to remove the existing "
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"Usługa SABnzbd dla Windows została zmieniona w wersji SABnzbd "
|
||||
"3.0.0.\\nMusisz ponownie zainstalować usługę SABnzbd.\\n\\nKliknij `OK`, aby"
|
||||
" usunąć istniejące usługi, lub `Anuluj`, aby przerwać tę aktualizację."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports 64-bit Windows."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd obsługuje tylko system Windows 64-bit."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports Windows 8.1 and above."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd obsługuje tylko Windows 8.1 i nowsze."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Shutting down SABnzbd"
|
||||
msgstr ""
|
||||
msgstr "Zamykanie SABnzbd"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
# 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>, 2020\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Portuguese (Brazil) (https://app.transifex.com/sabnzbd/teams/111101/pt_BR/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -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!"
|
||||
@@ -30,18 +34,21 @@ msgid ""
|
||||
"reinstall the SABnzbd service. \\n\\nClick `OK` to remove the existing "
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"O Serviço do Windows do SABnzbd mudou no SABnzbd 3.0.0.\\nVocê precisará "
|
||||
"reinstalar o serviço do SABnzbd.\\n\\nClique em `OK` para remover os "
|
||||
"serviços existentes ou em `Cancelar` para cancelar esta atualização."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports 64-bit Windows."
|
||||
msgstr ""
|
||||
msgstr "O SABnzbd oferece suporte apenas ao Windows de 64 bits."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports Windows 8.1 and above."
|
||||
msgstr ""
|
||||
msgstr "O SABnzbd oferece suporte apenas ao Windows 8.1 e superior."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Shutting down SABnzbd"
|
||||
msgstr ""
|
||||
msgstr "Encerrando o SABnzbd"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
# 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>, 2020\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Romanian (https://app.transifex.com/sabnzbd/teams/111101/ro/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -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ă!"
|
||||
@@ -30,18 +34,21 @@ msgid ""
|
||||
"reinstall the SABnzbd service. \\n\\nClick `OK` to remove the existing "
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"Serviciul SABnzbd pentru Windows s-a schimbat în SABnzbd 3.0.0.\\nVa trebui "
|
||||
"să reinstalați serviciul SABnzbd.\\n\\nFaceți clic pe `OK` pentru a elimina "
|
||||
"serviciile existente sau pe `Anulare` pentru a anula această actualizare."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports 64-bit Windows."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd acceptă doar Windows pe 64 de biți."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports Windows 8.1 and above."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd acceptă doar Windows 8.1 și versiunile ulterioare."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Shutting down SABnzbd"
|
||||
msgstr ""
|
||||
msgstr "Se oprește SABnzbd"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
# 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>, 2020\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Russian (https://app.transifex.com/sabnzbd/teams/111101/ru/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -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 "Поддержите проект. Сделайте пожертвование!"
|
||||
@@ -30,18 +34,21 @@ msgid ""
|
||||
"reinstall the SABnzbd service. \\n\\nClick `OK` to remove the existing "
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"Служба SABnzbd для Windows была изменена в SABnzbd 3.0.0.\\nВам необходимо "
|
||||
"переустановить службу SABnzbd.\\n\\nНажмите «ОК», чтобы удалить существующие"
|
||||
" службы, или «Отмена», чтобы прервать это обновление."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports 64-bit Windows."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd поддерживает только 64-битные версии Windows."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports Windows 8.1 and above."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd поддерживает только Windows 8.1 и более новые."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Shutting down SABnzbd"
|
||||
msgstr ""
|
||||
msgstr "Завершение работы SABnzbd"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
# 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>, 2020\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Serbian (https://app.transifex.com/sabnzbd/teams/111101/sr/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\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 "Подржите пројекат, дајте добровољан прилог!"
|
||||
@@ -30,18 +34,21 @@ msgid ""
|
||||
"reinstall the SABnzbd service. \\n\\nClick `OK` to remove the existing "
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"Windows услуга за SABnzbd је измењена у верзији SABnzbd 3.0.0.\\nМораћете "
|
||||
"поново да инсталирате SABnzbd услугу.\\n\\nКликните „У реду“ да уклоните "
|
||||
"постојеће услуге или „Откажи“ да откажете ово ажурирање."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports 64-bit Windows."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd подржава само 64‑битни Windows."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports Windows 8.1 and above."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd подржава само Windows 8.1 и новије."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Shutting down SABnzbd"
|
||||
msgstr ""
|
||||
msgstr "Искључивање SABnzbd"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
# Petter Ramme, 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: Petter Ramme, 2024\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Swedish (https://app.transifex.com/sabnzbd/teams/111101/sv/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -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!"
|
||||
@@ -32,16 +36,16 @@ msgid ""
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"SABnzbd Windows tjänsten ändrades i SABnzbd 3.0.0.\\nSABnzbd tjänsten "
|
||||
"behöver installeras om.\\n\\Välj OK` för att ta bort den befintliga "
|
||||
"behöver installeras om.\\n\\nVälj OK` för att ta bort den befintliga "
|
||||
"tjänsten, eller välj `Cancel`för att avbryta uppdateringen."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports 64-bit Windows."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd stöder endast 64-bitars Windows."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports Windows 8.1 and above."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd stöder endast Windows 8.1 och senare."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Shutting down SABnzbd"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
# 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>, 2020\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2025\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/sabnzbd/teams/111101/zh_CN/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\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 "支持该项目,捐助!"
|
||||
@@ -30,18 +34,20 @@ msgid ""
|
||||
"reinstall the SABnzbd service. \\n\\nClick `OK` to remove the existing "
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"SABnzbd 的 Windows 服务在 SABnzbd 3.0.0 中发生了变化。\\n您需要重新安装 SABnzbd "
|
||||
"服务。\\n\\n点击“确定”以移除现有服务,或点击“取消”以取消此次升级。"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports 64-bit Windows."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd 仅支持 64 位 Windows。"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "SABnzbd only supports Windows 8.1 and above."
|
||||
msgstr ""
|
||||
msgstr "SABnzbd 仅支持 Windows 8.1 及更高版本。"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Shutting down SABnzbd"
|
||||
msgstr ""
|
||||
msgstr "正在关闭 SABnzbd"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
|
||||
@@ -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.3
|
||||
sabctools==8.2.5
|
||||
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.1.2
|
||||
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
|
||||
@@ -24,49 +24,51 @@ sgmllib3k==1.0.0
|
||||
portend==3.2.1
|
||||
chardet==5.2.0
|
||||
PySocks==1.7.1
|
||||
puremagic==1.29
|
||||
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.4
|
||||
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.10.18
|
||||
ujson==5.11.0
|
||||
orjson==3.11.4
|
||||
|
||||
# Windows system integration
|
||||
pywin32==310; sys_platform == 'win32'
|
||||
pywin32==311; sys_platform == 'win32'
|
||||
windows-toasts==1.3.1; sys_platform == 'win32'
|
||||
winrt-runtime==3.2.1; sys_platform == 'win32'
|
||||
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.0; 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.1; sys_platform == 'darwin'
|
||||
pyobjc-framework-Cocoa==12.1; 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 # Version-less for Python 3.9 and below
|
||||
markdown==3.10; python_version > '3.9'
|
||||
paho-mqtt==1.6.1 # Pinned, newer versions don't work with AppRise yet
|
||||
|
||||
# Requests Requirements
|
||||
charset_normalizer==3.4.2
|
||||
idna==3.10
|
||||
charset_normalizer==3.4.4
|
||||
idna==3.11
|
||||
urllib3==2.5.0
|
||||
certifi==2025.6.15
|
||||
certifi==2025.11.12
|
||||
oauthlib==3.3.1
|
||||
PyJWT==2.10.1
|
||||
blinker==1.9.0
|
||||
|
||||
403
sabnzbd/api.py
403
sabnzbd/api.py
@@ -28,7 +28,7 @@ import time
|
||||
import getpass
|
||||
import cherrypy
|
||||
from threading import Thread
|
||||
from typing import Tuple, Optional, List, Dict, Any, Union
|
||||
from typing import Optional, Any, Union
|
||||
|
||||
# For json.dumps, orjson is magnitudes faster than ujson, but it is harder to
|
||||
# compile due to Rust dependency. Since the output is the same, we support all modules.
|
||||
@@ -103,7 +103,7 @@ _MSG_NO_SUCH_CONFIG = "Config item does not exist"
|
||||
_MSG_CONFIG_LOCKED = "Configuration locked"
|
||||
|
||||
|
||||
def api_handler(kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def api_handler(kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API Dispatcher"""
|
||||
# Clean-up the arguments
|
||||
for vr in ("mode", "name", "value", "value2", "value3", "start", "limit", "search"):
|
||||
@@ -117,13 +117,13 @@ def api_handler(kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
return response
|
||||
|
||||
|
||||
def _api_get_config(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_get_config(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts keyword, section"""
|
||||
_, data = config.get_dconfig(kwargs.get("section"), kwargs.get("keyword"))
|
||||
return report(keyword="config", data=data)
|
||||
|
||||
|
||||
def _api_set_config(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_set_config(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts keyword, section"""
|
||||
if cfg.configlock():
|
||||
return report(_MSG_CONFIG_LOCKED)
|
||||
@@ -144,7 +144,7 @@ def _api_set_config(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> byte
|
||||
return report(keyword="config", data=data)
|
||||
|
||||
|
||||
def _api_set_config_default(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_set_config_default(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: Reset requested config variables back to defaults. Currently only for misc-section"""
|
||||
if cfg.configlock():
|
||||
return report(_MSG_CONFIG_LOCKED)
|
||||
@@ -159,7 +159,7 @@ def _api_set_config_default(name: str, kwargs: Dict[str, Union[str, List[str]]])
|
||||
return report()
|
||||
|
||||
|
||||
def _api_del_config(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_del_config(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts keyword, section"""
|
||||
if cfg.configlock():
|
||||
return report(_MSG_CONFIG_LOCKED)
|
||||
@@ -169,13 +169,13 @@ def _api_del_config(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> byte
|
||||
return report(_MSG_NOT_IMPLEMENTED)
|
||||
|
||||
|
||||
def _api_queue(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_queue(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: Dispatcher for mode=queue"""
|
||||
value = kwargs.get("value", "")
|
||||
return _api_queue_table.get(name, (_api_queue_default, 2))[0](value, kwargs)
|
||||
|
||||
|
||||
def _api_queue_delete(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_queue_delete(value: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts value"""
|
||||
if value.lower() == "all":
|
||||
removed = sabnzbd.NzbQueue.remove_all(kwargs.get("search"))
|
||||
@@ -188,7 +188,7 @@ def _api_queue_delete(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> b
|
||||
return report(_MSG_NO_VALUE)
|
||||
|
||||
|
||||
def _api_queue_delete_nzf(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_queue_delete_nzf(value: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts value(=nzo_id), value2(=nzf_ids)"""
|
||||
nzf_ids = clean_comma_separated_list(kwargs.get("value2"))
|
||||
if value and nzf_ids:
|
||||
@@ -198,7 +198,7 @@ def _api_queue_delete_nzf(value: str, kwargs: Dict[str, Union[str, List[str]]])
|
||||
return report(_MSG_NO_VALUE2)
|
||||
|
||||
|
||||
def _api_queue_rename(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_queue_rename(value: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts value(=old name), value2(=new name), value3(=password)"""
|
||||
value2 = kwargs.get("value2")
|
||||
value3 = kwargs.get("value3")
|
||||
@@ -209,18 +209,18 @@ def _api_queue_rename(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> b
|
||||
return report(_MSG_NO_VALUE2)
|
||||
|
||||
|
||||
def _api_queue_change_complete_action(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_queue_change_complete_action(value: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts value(=action)"""
|
||||
change_queue_complete_action(value)
|
||||
return report()
|
||||
|
||||
|
||||
def _api_queue_purge(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_queue_purge(value: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
removed = sabnzbd.NzbQueue.remove_all(kwargs.get("search"))
|
||||
return report(keyword="", data={"status": bool(removed), "nzo_ids": removed})
|
||||
|
||||
|
||||
def _api_queue_pause(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_queue_pause(value: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts value(=list of nzo_id)"""
|
||||
if items := clean_comma_separated_list(value):
|
||||
handled = sabnzbd.NzbQueue.pause_multiple_nzo(items)
|
||||
@@ -229,7 +229,7 @@ def _api_queue_pause(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> by
|
||||
return report(keyword="", data={"status": bool(handled), "nzo_ids": handled})
|
||||
|
||||
|
||||
def _api_queue_resume(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_queue_resume(value: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts value(=list of nzo_id)"""
|
||||
if items := clean_comma_separated_list(value):
|
||||
handled = sabnzbd.NzbQueue.resume_multiple_nzo(items)
|
||||
@@ -238,7 +238,7 @@ def _api_queue_resume(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> b
|
||||
return report(keyword="", data={"status": bool(handled), "nzo_ids": handled})
|
||||
|
||||
|
||||
def _api_queue_priority(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_queue_priority(value: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts value(=nzo_id), value2(=priority)"""
|
||||
nzo_ids = clean_comma_separated_list(value)
|
||||
priority = kwargs.get("value2")
|
||||
@@ -257,7 +257,7 @@ def _api_queue_priority(value: str, kwargs: Dict[str, Union[str, List[str]]]) ->
|
||||
return report(_MSG_NO_VALUE2)
|
||||
|
||||
|
||||
def _api_queue_sort(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_queue_sort(value: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts sort, dir"""
|
||||
sort = kwargs.get("sort", "")
|
||||
direction = kwargs.get("dir", "")
|
||||
@@ -268,7 +268,7 @@ def _api_queue_sort(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> byt
|
||||
return report(_MSG_NO_VALUE2)
|
||||
|
||||
|
||||
def _api_queue_default(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_queue_default(value: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts sort, dir, start, limit and search terms"""
|
||||
start = int_conv(kwargs.get("start"))
|
||||
limit = int_conv(kwargs.get("limit"))
|
||||
@@ -296,12 +296,12 @@ def _api_queue_default(value: str, kwargs: Dict[str, Union[str, List[str]]]) ->
|
||||
)
|
||||
|
||||
|
||||
def _api_translate(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_translate(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts value(=acronym)"""
|
||||
return report(keyword="value", data=T(kwargs.get("value", "")))
|
||||
|
||||
|
||||
def _api_addfile(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_addfile(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts name, pp, script, cat, priority, nzbname"""
|
||||
# Normal upload will send the nzb in a kw arg called name or nzbfile
|
||||
if not name or isinstance(name, str):
|
||||
@@ -322,7 +322,7 @@ def _api_addfile(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
return report(_MSG_NO_VALUE)
|
||||
|
||||
|
||||
def _api_retry(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_retry(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts name, value(=nzo_id), nzbfile(=optional NZB), password (optional)"""
|
||||
value = kwargs.get("value")
|
||||
# Normal upload will send the nzb in a kw arg called nzbfile
|
||||
@@ -337,7 +337,7 @@ def _api_retry(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
return report(_MSG_NO_ITEM)
|
||||
|
||||
|
||||
def _api_cancel_pp(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_cancel_pp(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts name, value(=nzo_ids)"""
|
||||
if nzo_ids := clean_comma_separated_list(kwargs.get("value")):
|
||||
if sabnzbd.PostProcessor.cancel_pp(nzo_ids):
|
||||
@@ -345,7 +345,7 @@ def _api_cancel_pp(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes
|
||||
return report(_MSG_NO_ITEM)
|
||||
|
||||
|
||||
def _api_addlocalfile(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_addlocalfile(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts name, pp, script, cat, priority, nzbname"""
|
||||
if name:
|
||||
if os.path.exists(name):
|
||||
@@ -372,7 +372,7 @@ def _api_addlocalfile(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> by
|
||||
return report(_MSG_NO_VALUE)
|
||||
|
||||
|
||||
def _api_switch(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_switch(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts value(=first id), value2(=second id)"""
|
||||
value = kwargs.get("value")
|
||||
value2 = kwargs.get("value2")
|
||||
@@ -384,7 +384,7 @@ def _api_switch(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
return report(_MSG_NO_VALUE2)
|
||||
|
||||
|
||||
def _api_change_cat(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_change_cat(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts value(=nzo_id), value2(=category)"""
|
||||
nzo_ids = clean_comma_separated_list(kwargs.get("value"))
|
||||
cat = kwargs.get("value2")
|
||||
@@ -397,7 +397,7 @@ def _api_change_cat(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> byte
|
||||
return report(_MSG_NO_VALUE)
|
||||
|
||||
|
||||
def _api_change_script(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_change_script(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts value(=nzo_id), value2(=script)"""
|
||||
nzo_ids = clean_comma_separated_list(kwargs.get("value"))
|
||||
script = kwargs.get("value2")
|
||||
@@ -410,7 +410,7 @@ def _api_change_script(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> b
|
||||
return report(_MSG_NO_VALUE)
|
||||
|
||||
|
||||
def _api_change_opts(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_change_opts(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts value(=nzo_id), value2(=pp)"""
|
||||
nzo_ids = clean_comma_separated_list(kwargs.get("value"))
|
||||
pp = kwargs.get("value2")
|
||||
@@ -420,7 +420,7 @@ def _api_change_opts(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> byt
|
||||
return report(_MSG_NO_ITEM)
|
||||
|
||||
|
||||
def _api_fullstatus(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_fullstatus(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: full history status"""
|
||||
status = build_status(
|
||||
calculate_performance=bool_conv(kwargs.get("calculate_performance")),
|
||||
@@ -429,19 +429,19 @@ def _api_fullstatus(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> byte
|
||||
return report(keyword="status", data=status)
|
||||
|
||||
|
||||
def _api_status(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_status(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: Dispatcher for mode=status, passing on the value"""
|
||||
value = kwargs.get("value", "")
|
||||
return _api_status_table.get(name, (_api_fullstatus, 2))[0](value, kwargs)
|
||||
|
||||
|
||||
def _api_unblock_server(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_unblock_server(value: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""Unblock a blocked server"""
|
||||
sabnzbd.Downloader.unblock(value)
|
||||
return report()
|
||||
|
||||
|
||||
def _api_delete_orphan(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_delete_orphan(value: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""Remove orphaned job"""
|
||||
if value:
|
||||
path = os.path.join(cfg.download_dir.get_path(), value)
|
||||
@@ -452,7 +452,7 @@ def _api_delete_orphan(value: str, kwargs: Dict[str, Union[str, List[str]]]) ->
|
||||
return report(_MSG_NO_ITEM)
|
||||
|
||||
|
||||
def _api_delete_all_orphan(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_delete_all_orphan(value: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""Remove all orphaned jobs"""
|
||||
paths = sabnzbd.NzbQueue.scan_jobs(all_jobs=False, action=False)
|
||||
for path in paths:
|
||||
@@ -460,7 +460,7 @@ def _api_delete_all_orphan(value: str, kwargs: Dict[str, Union[str, List[str]]])
|
||||
return report()
|
||||
|
||||
|
||||
def _api_add_orphan(value: str, kwargs: Dict[str, Union[str, List[str]]]):
|
||||
def _api_add_orphan(value: str, kwargs: dict[str, Union[str, list[str]]]):
|
||||
"""Add orphaned job"""
|
||||
if value:
|
||||
path = os.path.join(cfg.download_dir.get_path(), value)
|
||||
@@ -471,7 +471,7 @@ def _api_add_orphan(value: str, kwargs: Dict[str, Union[str, List[str]]]):
|
||||
return report(_MSG_NO_ITEM)
|
||||
|
||||
|
||||
def _api_add_all_orphan(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_add_all_orphan(value: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""Add all orphaned jobs"""
|
||||
paths = sabnzbd.NzbQueue.scan_jobs(all_jobs=False, action=False)
|
||||
for path in paths:
|
||||
@@ -479,9 +479,79 @@ def _api_add_all_orphan(value: str, kwargs: Dict[str, Union[str, List[str]]]) ->
|
||||
return report()
|
||||
|
||||
|
||||
def _api_history(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
"""API: accepts value(=nzo_id), start, limit, search, nzo_ids"""
|
||||
def _api_history(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""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,87 +561,41 @@ 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:
|
||||
def _api_get_files(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts value(=nzo_id)"""
|
||||
value = kwargs.get("value")
|
||||
if value:
|
||||
@@ -580,7 +604,7 @@ def _api_get_files(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes
|
||||
return report(_MSG_NO_VALUE)
|
||||
|
||||
|
||||
def _api_move_nzf_bulk(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_move_nzf_bulk(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts name(=top/up/down/bottom), value=(=nzo_id), nzf_ids, size (optional)"""
|
||||
nzo_id = kwargs.get("value")
|
||||
nzf_ids = clean_comma_separated_list(kwargs.get("nzf_ids"))
|
||||
@@ -606,7 +630,7 @@ def _api_move_nzf_bulk(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> b
|
||||
return report(_MSG_NO_VALUE)
|
||||
|
||||
|
||||
def _api_addurl(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_addurl(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts name, output, pp, script, cat, priority, nzbname"""
|
||||
pp = kwargs.get("pp")
|
||||
script = kwargs.get("script")
|
||||
@@ -624,24 +648,24 @@ def _api_addurl(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
return report(_MSG_NO_VALUE)
|
||||
|
||||
|
||||
def _api_pause(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_pause(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
sabnzbd.Scheduler.plan_resume(0)
|
||||
sabnzbd.Downloader.pause()
|
||||
return report()
|
||||
|
||||
|
||||
def _api_resume(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_resume(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
sabnzbd.Scheduler.plan_resume(0)
|
||||
sabnzbd.downloader.unpause_all()
|
||||
return report()
|
||||
|
||||
|
||||
def _api_shutdown(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_shutdown(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
sabnzbd.shutdown_program()
|
||||
return report()
|
||||
|
||||
|
||||
def _api_warnings(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_warnings(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts name, output"""
|
||||
if name == "clear":
|
||||
return report(keyword="warnings", data=sabnzbd.GUIHANDLER.clear())
|
||||
@@ -661,14 +685,17 @@ LOG_INI_HIDE_RE = re.compile(
|
||||
LOG_HASH_RE = re.compile(rb"([a-zA-Z\d]{25})", re.I)
|
||||
|
||||
|
||||
def _api_showlog(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_showlog(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""Fetch the INI and the log-data and add a message at the top"""
|
||||
log_data = b"--------------------------------\n\n"
|
||||
log_data += b"The log includes a copy of your sabnzbd.ini with\nall usernames, passwords and API-keys removed."
|
||||
log_data += b"\n\n--------------------------------\n"
|
||||
|
||||
with open(sabnzbd.LOGFILE, "rb") as f:
|
||||
log_data += f.read()
|
||||
if sabnzbd.LOGFILE and os.path.exists(sabnzbd.LOGFILE):
|
||||
with open(sabnzbd.LOGFILE, "rb") as f:
|
||||
log_data += f.read()
|
||||
else:
|
||||
log_data += b"\nFile log disabled or not found.\n\n"
|
||||
|
||||
with open(config.get_filename(), "rb") as f:
|
||||
log_data += f.read()
|
||||
@@ -691,19 +718,19 @@ def _api_showlog(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
return log_data
|
||||
|
||||
|
||||
def _api_get_cats(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_get_cats(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
return report(keyword="categories", data=list_cats(False))
|
||||
|
||||
|
||||
def _api_get_scripts(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_get_scripts(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
return report(keyword="scripts", data=list_scripts())
|
||||
|
||||
|
||||
def _api_version(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_version(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
return report(keyword="version", data=sabnzbd.__version__)
|
||||
|
||||
|
||||
def _api_auth(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_auth(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
key = kwargs.get("key", "")
|
||||
if not key:
|
||||
auth = "apikey"
|
||||
@@ -716,14 +743,14 @@ def _api_auth(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
return report(keyword="auth", data=auth)
|
||||
|
||||
|
||||
def _api_restart(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_restart(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
logging.info("Restart requested by API")
|
||||
# Do the shutdown async to still send goodbye to browser
|
||||
Thread(target=sabnzbd.trigger_restart, kwargs={"timeout": 1}).start()
|
||||
return report()
|
||||
|
||||
|
||||
def _api_restart_repair(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_restart_repair(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
logging.info("Queue repair requested by API")
|
||||
request_repair()
|
||||
# Do the shutdown async to still send goodbye to browser
|
||||
@@ -731,12 +758,12 @@ def _api_restart_repair(name: str, kwargs: Dict[str, Union[str, List[str]]]) ->
|
||||
return report()
|
||||
|
||||
|
||||
def _api_disconnect(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_disconnect(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
sabnzbd.Downloader.disconnect()
|
||||
return report()
|
||||
|
||||
|
||||
def _api_eval_sort(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_eval_sort(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: evaluate sorting expression"""
|
||||
sort_string = kwargs.get("sort_string", "")
|
||||
job_name = kwargs.get("job_name", "")
|
||||
@@ -748,28 +775,28 @@ def _api_eval_sort(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes
|
||||
return report(keyword="result", data=path)
|
||||
|
||||
|
||||
def _api_watched_now(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_watched_now(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
sabnzbd.DirScanner.scan()
|
||||
return report()
|
||||
|
||||
|
||||
def _api_resume_pp(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_resume_pp(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
sabnzbd.PostProcessor.paused = False
|
||||
return report()
|
||||
|
||||
|
||||
def _api_pause_pp(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_pause_pp(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
sabnzbd.PostProcessor.paused = True
|
||||
return report()
|
||||
|
||||
|
||||
def _api_rss_now(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_rss_now(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
# Run RSS scan async, because it can take a long time
|
||||
sabnzbd.Scheduler.force_rss()
|
||||
return report()
|
||||
|
||||
|
||||
def _api_retry_all(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_retry_all(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: Retry all failed items in History"""
|
||||
items = sabnzbd.api.build_history()[0]
|
||||
nzo_ids = []
|
||||
@@ -779,13 +806,13 @@ def _api_retry_all(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes
|
||||
return report(keyword="status", data=nzo_ids)
|
||||
|
||||
|
||||
def _api_reset_quota(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_reset_quota(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""Reset quota left"""
|
||||
sabnzbd.BPSMeter.reset_quota(force=True)
|
||||
return report()
|
||||
|
||||
|
||||
def _api_test_email(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_test_email(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: send a test email, return result"""
|
||||
logging.info("Sending test email")
|
||||
pack = {"download": ["action 1", "action 2"], "unpack": ["action 1", "action 2"]}
|
||||
@@ -807,67 +834,67 @@ def _api_test_email(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> byte
|
||||
return report(error=res)
|
||||
|
||||
|
||||
def _api_test_windows(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_test_windows(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: send a test to Windows, return result"""
|
||||
logging.info("Sending test notification")
|
||||
res = sabnzbd.notifier.send_windows("SABnzbd", T("Test Notification"), "other")
|
||||
return report(error=res)
|
||||
|
||||
|
||||
def _api_test_notif(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_test_notif(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: send a test to Notification Center, return result"""
|
||||
logging.info("Sending test notification")
|
||||
res = sabnzbd.notifier.send_notification_center("SABnzbd", T("Test Notification"), "other")
|
||||
return report(error=res)
|
||||
|
||||
|
||||
def _api_test_osd(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_test_osd(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: send a test OSD notification, return result"""
|
||||
logging.info("Sending OSD notification")
|
||||
res = sabnzbd.notifier.send_notify_osd("SABnzbd", T("Test Notification"))
|
||||
return report(error=res)
|
||||
|
||||
|
||||
def _api_test_prowl(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_test_prowl(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: send a test Prowl notification, return result"""
|
||||
logging.info("Sending Prowl notification")
|
||||
res = sabnzbd.notifier.send_prowl("SABnzbd", T("Test Notification"), "other", force=True, test=kwargs)
|
||||
return report(error=res)
|
||||
|
||||
|
||||
def _api_test_pushover(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_test_pushover(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: send a test Pushover notification, return result"""
|
||||
logging.info("Sending Pushover notification")
|
||||
res = sabnzbd.notifier.send_pushover("SABnzbd", T("Test Notification"), "other", force=True, test=kwargs)
|
||||
return report(error=res)
|
||||
|
||||
|
||||
def _api_test_pushbullet(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_test_pushbullet(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: send a test Pushbullet notification, return result"""
|
||||
logging.info("Sending Pushbullet notification")
|
||||
res = sabnzbd.notifier.send_pushbullet("SABnzbd", T("Test Notification"), "other", force=True, test=kwargs)
|
||||
return report(error=res)
|
||||
|
||||
|
||||
def _api_test_apprise(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_test_apprise(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: send a test Apprise notification, return result"""
|
||||
logging.info("Sending Apprise notification")
|
||||
res = sabnzbd.notifier.send_apprise("SABnzbd", T("Test Notification"), "other", force=True, test=kwargs)
|
||||
return report(error=res)
|
||||
|
||||
|
||||
def _api_test_nscript(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_test_nscript(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: execute a test notification script, return result"""
|
||||
logging.info("Executing notification script")
|
||||
res = sabnzbd.notifier.send_nscript("SABnzbd", T("Test Notification"), "other", force=True, test=kwargs)
|
||||
return report(error=res)
|
||||
|
||||
|
||||
def _api_undefined(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_undefined(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
return report(_MSG_NOT_IMPLEMENTED)
|
||||
|
||||
|
||||
def _api_browse(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_browse(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""Return tree of local path"""
|
||||
compact = bool_conv(kwargs.get("compact"))
|
||||
show_files = bool_conv(kwargs.get("show_files"))
|
||||
@@ -884,14 +911,14 @@ def _api_browse(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
return report(keyword="paths", data=paths)
|
||||
|
||||
|
||||
def _api_config(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_config(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: Dispatcher for "config" """
|
||||
if cfg.configlock():
|
||||
return report(_MSG_CONFIG_LOCKED)
|
||||
return _api_config_table.get(name, (_api_config_undefined, 2))[0](kwargs)
|
||||
|
||||
|
||||
def _api_config_speedlimit(kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_config_speedlimit(kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts value(=speed)"""
|
||||
value = kwargs.get("value")
|
||||
if not value:
|
||||
@@ -900,26 +927,26 @@ def _api_config_speedlimit(kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
return report()
|
||||
|
||||
|
||||
def _api_config_set_pause(kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_config_set_pause(kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts value(=pause interval)"""
|
||||
value = kwargs.get("value")
|
||||
sabnzbd.Scheduler.plan_resume(int_conv(value))
|
||||
return report()
|
||||
|
||||
|
||||
def _api_config_set_apikey(kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_config_set_apikey(kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
cfg.api_key.set(config.create_api_key())
|
||||
config.save_config()
|
||||
return report(keyword="apikey", data=cfg.api_key())
|
||||
|
||||
|
||||
def _api_config_set_nzbkey(kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_config_set_nzbkey(kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
cfg.nzb_key.set(config.create_api_key())
|
||||
config.save_config()
|
||||
return report(keyword="nzbkey", data=cfg.nzb_key())
|
||||
|
||||
|
||||
def _api_config_regenerate_certs(kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_config_regenerate_certs(kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
# Make sure we only over-write default locations
|
||||
result = False
|
||||
if (
|
||||
@@ -933,27 +960,27 @@ def _api_config_regenerate_certs(kwargs: Dict[str, Union[str, List[str]]]) -> by
|
||||
return report(data=result)
|
||||
|
||||
|
||||
def _api_config_test_server(kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_config_test_server(kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""API: accepts server-params"""
|
||||
result, msg = test_nntp_server_dict(kwargs)
|
||||
return report(data={"result": result, "message": msg})
|
||||
|
||||
|
||||
def _api_config_create_backup(kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_config_create_backup(kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
backup_file = config.create_config_backup()
|
||||
return report(data={"result": bool(backup_file), "message": backup_file})
|
||||
|
||||
|
||||
def _api_config_purge_log_files(kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_config_purge_log_files(kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
purge_log_files()
|
||||
return report()
|
||||
|
||||
|
||||
def _api_config_undefined(kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_config_undefined(kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
return report(_MSG_NOT_IMPLEMENTED)
|
||||
|
||||
|
||||
def _api_server_stats(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_server_stats(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
sum_t, sum_m, sum_w, sum_d = sabnzbd.BPSMeter.get_sums()
|
||||
stats = {"total": sum_t, "month": sum_m, "week": sum_w, "day": sum_d, "servers": {}}
|
||||
|
||||
@@ -972,7 +999,7 @@ def _api_server_stats(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> by
|
||||
return report(keyword="", data=stats)
|
||||
|
||||
|
||||
def _api_gc_stats(name: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
def _api_gc_stats(name: str, kwargs: dict[str, Union[str, list[str]]]) -> bytes:
|
||||
"""Function only intended for internal testing of the memory handling"""
|
||||
# Collect before we check
|
||||
gc.collect()
|
||||
@@ -1048,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),
|
||||
@@ -1072,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:
|
||||
@@ -1175,7 +1210,7 @@ class XmlOutputFactory:
|
||||
return text
|
||||
|
||||
|
||||
def handle_server_api(kwargs: Dict[str, Union[str, List[str]]]) -> str:
|
||||
def handle_server_api(kwargs: dict[str, Union[str, list[str]]]) -> str:
|
||||
"""Special handler for API-call 'set_config' [servers]"""
|
||||
name = kwargs.get("keyword")
|
||||
if not name:
|
||||
@@ -1193,7 +1228,7 @@ def handle_server_api(kwargs: Dict[str, Union[str, List[str]]]) -> str:
|
||||
return name
|
||||
|
||||
|
||||
def handle_sorter_api(kwargs: Dict[str, Union[str, List[str]]]) -> Optional[str]:
|
||||
def handle_sorter_api(kwargs: dict[str, Union[str, list[str]]]) -> Optional[str]:
|
||||
"""Special handler for API-call 'set_config' [sorters]"""
|
||||
name = kwargs.get("keyword")
|
||||
if not name:
|
||||
@@ -1209,7 +1244,7 @@ def handle_sorter_api(kwargs: Dict[str, Union[str, List[str]]]) -> Optional[str]
|
||||
return name
|
||||
|
||||
|
||||
def handle_rss_api(kwargs: Dict[str, Union[str, List[str]]]) -> Optional[str]:
|
||||
def handle_rss_api(kwargs: dict[str, Union[str, list[str]]]) -> Optional[str]:
|
||||
"""Special handler for API-call 'set_config' [rss]"""
|
||||
name = kwargs.get("keyword")
|
||||
if not name:
|
||||
@@ -1243,7 +1278,7 @@ def handle_rss_api(kwargs: Dict[str, Union[str, List[str]]]) -> Optional[str]:
|
||||
return name
|
||||
|
||||
|
||||
def handle_cat_api(kwargs: Dict[str, Union[str, List[str]]]) -> Optional[str]:
|
||||
def handle_cat_api(kwargs: dict[str, Union[str, list[str]]]) -> Optional[str]:
|
||||
"""Special handler for API-call 'set_config' [categories]"""
|
||||
name = kwargs.get("keyword")
|
||||
if not name:
|
||||
@@ -1260,7 +1295,7 @@ def handle_cat_api(kwargs: Dict[str, Union[str, List[str]]]) -> Optional[str]:
|
||||
return name
|
||||
|
||||
|
||||
def test_nntp_server_dict(kwargs: Dict[str, Union[str, List[str]]]) -> Tuple[bool, str]:
|
||||
def test_nntp_server_dict(kwargs: dict[str, Union[str, list[str]]]) -> tuple[bool, str]:
|
||||
"""Will connect (blocking) to the NNTP server and report back any errors"""
|
||||
host = kwargs.get("host", "").strip()
|
||||
port = int_conv(kwargs.get("port", 0))
|
||||
@@ -1409,11 +1444,11 @@ def test_nntp_server_dict(kwargs: Dict[str, Union[str, List[str]]]) -> Tuple[boo
|
||||
return return_status
|
||||
|
||||
|
||||
def build_status(calculate_performance: bool = False, skip_dashboard: bool = False) -> Dict[str, Any]:
|
||||
def build_status(calculate_performance: bool = False, skip_dashboard: bool = False) -> dict[str, Any]:
|
||||
# build up header full of basic information
|
||||
info = build_header(trans_functions=False)
|
||||
|
||||
info["logfile"] = clip_path(sabnzbd.LOGFILE)
|
||||
info["logfile"] = clip_path(sabnzbd.LOGFILE) if sabnzbd.LOGFILE else ""
|
||||
info["weblogfile"] = clip_path(sabnzbd.WEBLOGFILE)
|
||||
info["webdir"] = clip_path(info["webdir"])
|
||||
info["loglevel"] = str(cfg.log_level())
|
||||
@@ -1511,11 +1546,11 @@ def build_queue(
|
||||
start: int = 0,
|
||||
limit: int = 0,
|
||||
search: Optional[str] = None,
|
||||
categories: Optional[List[str]] = None,
|
||||
priorities: Optional[List[str]] = None,
|
||||
statuses: Optional[List[str]] = None,
|
||||
nzo_ids: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
categories: Optional[list[str]] = None,
|
||||
priorities: Optional[list[str]] = None,
|
||||
statuses: Optional[list[str]] = None,
|
||||
nzo_ids: Optional[list[str]] = None,
|
||||
) -> dict[str, Any]:
|
||||
info = build_header(for_template=False)
|
||||
(
|
||||
queue_bytes_total,
|
||||
@@ -1610,6 +1645,9 @@ def build_queue(
|
||||
else:
|
||||
slot["avg_age"] = calc_age(nzo.avg_date)
|
||||
|
||||
# Add timestamp when the item was added to the queue
|
||||
slot["time_added"] = nzo.time_added
|
||||
|
||||
slotinfo.append(slot)
|
||||
n += 1
|
||||
|
||||
@@ -1621,7 +1659,7 @@ def build_queue(
|
||||
return info
|
||||
|
||||
|
||||
def fast_queue() -> Tuple[bool, int, float, str]:
|
||||
def fast_queue() -> tuple[bool, int, float, str]:
|
||||
"""Return paused, bytes_left, bpsnow, time_left"""
|
||||
bytes_left = sabnzbd.sabnzbd.NzbQueue.remaining()
|
||||
paused = sabnzbd.Downloader.paused
|
||||
@@ -1630,7 +1668,7 @@ def fast_queue() -> Tuple[bool, int, float, str]:
|
||||
return paused, bytes_left, bpsnow, time_left
|
||||
|
||||
|
||||
def build_file_list(nzo_id: str) -> List[Dict[str, Any]]:
|
||||
def build_file_list(nzo_id: str) -> list[dict[str, Any]]:
|
||||
"""Build file lists for specified job"""
|
||||
jobs = []
|
||||
nzo = sabnzbd.sabnzbd.NzbQueue.get_nzo(nzo_id)
|
||||
@@ -1704,7 +1742,7 @@ def retry_job(
|
||||
return None
|
||||
|
||||
|
||||
def del_job_files(job_paths: List[str]):
|
||||
def del_job_files(job_paths: list[str]):
|
||||
"""Remove files of each path in the list"""
|
||||
for path in job_paths:
|
||||
if path and clip_path(path).lower().startswith(cfg.download_dir.get_clipped_path().lower()):
|
||||
@@ -1747,7 +1785,7 @@ def clear_trans_cache():
|
||||
sabnzbd.WEBUI_READY = True
|
||||
|
||||
|
||||
def build_header(webdir: str = "", for_template: bool = True, trans_functions: bool = True) -> Dict[str, Any]:
|
||||
def build_header(webdir: str = "", for_template: bool = True, trans_functions: bool = True) -> dict[str, Any]:
|
||||
"""Build the basic header"""
|
||||
header = {}
|
||||
|
||||
@@ -1814,10 +1852,10 @@ def build_history(
|
||||
limit: int = 1000000,
|
||||
archive: bool = False,
|
||||
search: Optional[str] = None,
|
||||
categories: Optional[List[str]] = None,
|
||||
statuses: Optional[List[str]] = None,
|
||||
nzo_ids: Optional[List[str]] = None,
|
||||
) -> Tuple[List[Dict[str, Any]], int, int]:
|
||||
categories: Optional[list[str]] = None,
|
||||
statuses: Optional[list[str]] = None,
|
||||
nzo_ids: Optional[list[str]] = None,
|
||||
) -> tuple[list[dict[str, Any]], int, int]:
|
||||
"""Combine the jobs still in post-processing and the database history"""
|
||||
if not archive:
|
||||
# Grab any items that are active or queued in postproc
|
||||
@@ -1893,9 +1931,15 @@ def build_history(
|
||||
return items, postproc_queue_size, total_items
|
||||
|
||||
|
||||
def add_active_history(postproc_queue: List[NzbObject], items: List[Dict[str, Any]]):
|
||||
def add_active_history(postproc_queue: list[NzbObject], items: list[dict[str, Any]]):
|
||||
"""Get the active history queue and add it to the existing items list"""
|
||||
nzo_ids = set([nzo["nzo_id"] for nzo in items])
|
||||
|
||||
for nzo in postproc_queue:
|
||||
# Skip already in history
|
||||
if nzo.nzo_id in nzo_ids:
|
||||
continue
|
||||
|
||||
# This output has to be the same as fetch_history!
|
||||
item = {
|
||||
"completed": int(time.time()),
|
||||
@@ -1929,6 +1973,7 @@ def add_active_history(postproc_queue: List[NzbObject], items: List[Dict[str, An
|
||||
"loaded": nzo.pp_active,
|
||||
"retry": False,
|
||||
"archive": False,
|
||||
"time_added": nzo.time_added,
|
||||
}
|
||||
# Add stage information, in the correct order
|
||||
for stage in STAGES:
|
||||
@@ -1945,7 +1990,7 @@ def calc_timeleft(bytesleft: float, bps: float) -> str:
|
||||
return format_time_left(int(bytesleft / bps))
|
||||
|
||||
|
||||
def list_cats(default: bool = True) -> List[str]:
|
||||
def list_cats(default: bool = True) -> list[str]:
|
||||
"""Return list of (ordered) categories,
|
||||
when default==False use '*' for Default category
|
||||
"""
|
||||
@@ -1974,7 +2019,7 @@ def plural_to_single(kw, def_kw=""):
|
||||
return def_kw
|
||||
|
||||
|
||||
def del_from_section(kwargs: Dict[str, Union[str, List[str]]]) -> bool:
|
||||
def del_from_section(kwargs: dict[str, Union[str, list[str]]]) -> bool:
|
||||
"""Remove keyword in section"""
|
||||
section = kwargs.get("section", "")
|
||||
if section in ("sorters", "servers", "rss", "categories"):
|
||||
|
||||
@@ -22,7 +22,7 @@ sabnzbd.articlecache - Article cache handling
|
||||
import logging
|
||||
import threading
|
||||
import struct
|
||||
from typing import Dict, Collection
|
||||
from typing import Collection
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.decorators import synchronized
|
||||
@@ -39,7 +39,7 @@ class ArticleCache:
|
||||
self.__cache_limit_org = 0
|
||||
self.__cache_limit = 0
|
||||
self.__cache_size = 0
|
||||
self.__article_table: Dict[Article, bytes] = {} # Dict of buffered articles
|
||||
self.__article_table: dict[Article, bytes] = {} # Dict of buffered articles
|
||||
|
||||
self.assembler_write_trigger: int = 1
|
||||
|
||||
|
||||
@@ -25,10 +25,11 @@ import logging
|
||||
import re
|
||||
from threading import Thread
|
||||
import ctypes
|
||||
from typing import Tuple, Optional, List
|
||||
from typing import Optional
|
||||
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,
|
||||
@@ -38,17 +39,17 @@ from sabnzbd.filesystem import (
|
||||
has_unwanted_extension,
|
||||
get_basename,
|
||||
)
|
||||
from sabnzbd.constants import Status, GIGI, MAX_ASSEMBLER_QUEUE
|
||||
from sabnzbd.constants import Status, GIGI, DEF_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):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.queue: queue.Queue[Tuple[Optional[NzbObject], Optional[NzbFile], Optional[bool]]] = queue.Queue()
|
||||
self.max_queue_size: int = cfg.assembler_max_queue_size()
|
||||
self.queue: queue.Queue[tuple[Optional[NzbObject], Optional[NzbFile], Optional[bool]]] = queue.Queue()
|
||||
|
||||
def stop(self):
|
||||
self.queue.put((None, None, None))
|
||||
@@ -57,7 +58,7 @@ class Assembler(Thread):
|
||||
self.queue.put((nzo, nzf, file_done))
|
||||
|
||||
def queue_level(self) -> float:
|
||||
return self.queue.qsize() / MAX_ASSEMBLER_QUEUE
|
||||
return self.queue.qsize() / self.max_queue_size
|
||||
|
||||
def run(self):
|
||||
while 1:
|
||||
@@ -249,7 +250,7 @@ RE_SUBS = re.compile(r"\W+sub|subs|subpack|subtitle|subtitles(?![a-z])", re.I)
|
||||
SAFE_EXTS = (".mkv", ".mp4", ".avi", ".wmv", ".mpg", ".webm")
|
||||
|
||||
|
||||
def is_cloaked(nzo: NzbObject, path: str, names: List[str]) -> bool:
|
||||
def is_cloaked(nzo: NzbObject, path: str, names: list[str]) -> bool:
|
||||
"""Return True if this is likely to be a cloaked encrypted post"""
|
||||
fname = get_basename(get_filename(path.lower()))
|
||||
for name in names:
|
||||
@@ -278,7 +279,7 @@ def is_cloaked(nzo: NzbObject, path: str, names: List[str]) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def check_encrypted_and_unwanted_files(nzo: NzbObject, filepath: str) -> Tuple[bool, Optional[str]]:
|
||||
def check_encrypted_and_unwanted_files(nzo: NzbObject, filepath: str) -> tuple[bool, Optional[str]]:
|
||||
"""Combines check for unwanted and encrypted files to save on CPU and IO"""
|
||||
encrypted = False
|
||||
unwanted = None
|
||||
@@ -295,7 +296,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 +323,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:
|
||||
|
||||
@@ -22,7 +22,7 @@ sabnzbd.bpsmeter - bpsmeter
|
||||
import time
|
||||
import logging
|
||||
import re
|
||||
from typing import List, Dict, Optional
|
||||
from typing import Optional
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.constants import BYTES_FILE_NAME, KIBI
|
||||
@@ -122,6 +122,7 @@ class BPSMeter:
|
||||
"q_hour",
|
||||
"q_minute",
|
||||
"quota_enabled",
|
||||
"quota_notifications_sent",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
@@ -131,20 +132,20 @@ class BPSMeter:
|
||||
self.speed_log_time = t
|
||||
self.last_update = t
|
||||
self.bps = 0.0
|
||||
self.bps_list: List[int] = []
|
||||
self.bps_list: list[int] = []
|
||||
|
||||
self.server_bps: Dict[str, float] = {}
|
||||
self.cached_amount: Dict[str, int] = {}
|
||||
self.server_bps: dict[str, float] = {}
|
||||
self.cached_amount: dict[str, int] = {}
|
||||
self.sum_cached_amount: int = 0
|
||||
self.day_total: Dict[str, int] = {}
|
||||
self.week_total: Dict[str, int] = {}
|
||||
self.month_total: Dict[str, int] = {}
|
||||
self.grand_total: Dict[str, int] = {}
|
||||
self.day_total: dict[str, int] = {}
|
||||
self.week_total: dict[str, int] = {}
|
||||
self.month_total: dict[str, int] = {}
|
||||
self.grand_total: dict[str, int] = {}
|
||||
|
||||
self.timeline_total: Dict[str, Dict[str, int]] = {}
|
||||
self.timeline_total: dict[str, dict[str, int]] = {}
|
||||
|
||||
self.article_stats_tried: Dict[str, Dict[str, int]] = {}
|
||||
self.article_stats_failed: Dict[str, Dict[str, int]] = {}
|
||||
self.article_stats_tried: dict[str, dict[str, int]] = {}
|
||||
self.article_stats_failed: dict[str, dict[str, int]] = {}
|
||||
|
||||
self.delayed_assembler: int = 0
|
||||
|
||||
@@ -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:
|
||||
@@ -383,7 +382,7 @@ class BPSMeter:
|
||||
|
||||
# Always trim the list to the max-length
|
||||
if len(self.bps_list) > BPS_LIST_MAX:
|
||||
self.bps_list = self.bps_list[len(self.bps_list) - BPS_LIST_MAX :]
|
||||
self.bps_list = self.bps_list[-BPS_LIST_MAX:]
|
||||
|
||||
def get_sums(self):
|
||||
"""return tuple of grand, month, week, day totals"""
|
||||
@@ -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
|
||||
|
||||
@@ -25,7 +25,7 @@ import re
|
||||
import argparse
|
||||
import socket
|
||||
import ipaddress
|
||||
from typing import List, Tuple, Union
|
||||
from typing import Union
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.config import (
|
||||
@@ -52,12 +52,13 @@ from sabnzbd.constants import (
|
||||
DEF_STD_WEB_COLOR,
|
||||
DEF_HTTPS_CERT_FILE,
|
||||
DEF_HTTPS_KEY_FILE,
|
||||
DEF_MAX_ASSEMBLER_QUEUE,
|
||||
)
|
||||
from sabnzbd.filesystem import same_directory, real_path, is_valid_script, is_network_path
|
||||
|
||||
# Validators currently only are made for string/list-of-strings
|
||||
# and return those on success or an error message.
|
||||
ValidateResult = Union[Tuple[None, str], Tuple[None, List[str]], Tuple[str, None]]
|
||||
ValidateResult = Union[tuple[None, str], tuple[None, list[str]], tuple[str, None]]
|
||||
|
||||
|
||||
##############################################################################
|
||||
@@ -122,21 +123,21 @@ def supported_unrar_parameters(value: str) -> ValidateResult:
|
||||
return None, value
|
||||
|
||||
|
||||
def all_lowercase(value: Union[str, List]) -> Tuple[None, Union[str, List]]:
|
||||
def all_lowercase(value: Union[str, list]) -> tuple[None, Union[str, list]]:
|
||||
"""Lowercase and strip everything!"""
|
||||
if isinstance(value, list):
|
||||
return None, [item.lower().strip() for item in value]
|
||||
return None, value.lower().strip()
|
||||
|
||||
|
||||
def lower_case_ext(value: Union[str, List]) -> Tuple[None, Union[str, List]]:
|
||||
def lower_case_ext(value: Union[str, list]) -> tuple[None, Union[str, list]]:
|
||||
"""Generate lower case extension(s), without dot"""
|
||||
if isinstance(value, list):
|
||||
return None, [item.lower().strip(" .") for item in value]
|
||||
return None, value.lower().strip(" .")
|
||||
|
||||
|
||||
def validate_single_tag(value: List[str]) -> Tuple[None, List[str]]:
|
||||
def validate_single_tag(value: list[str]) -> tuple[None, list[str]]:
|
||||
"""Don't split single indexer tags like "TV > HD"
|
||||
into ['TV', '>', 'HD']
|
||||
"""
|
||||
@@ -146,7 +147,7 @@ def validate_single_tag(value: List[str]) -> Tuple[None, List[str]]:
|
||||
return None, value
|
||||
|
||||
|
||||
def validate_url_base(value: str) -> Tuple[None, str]:
|
||||
def validate_url_base(value: str) -> tuple[None, str]:
|
||||
"""Strips the right slash and adds starting slash, if not present"""
|
||||
if value and isinstance(value, str):
|
||||
if not value.startswith("/"):
|
||||
@@ -158,7 +159,7 @@ def validate_url_base(value: str) -> Tuple[None, str]:
|
||||
RE_VAL = re.compile(r"[^@ ]+@[^.@ ]+\.[^.@ ]")
|
||||
|
||||
|
||||
def validate_email(value: Union[List, str]) -> ValidateResult:
|
||||
def validate_email(value: Union[list, str]) -> ValidateResult:
|
||||
if email_endjob() or email_full() or email_rss():
|
||||
if isinstance(value, list):
|
||||
values = value
|
||||
@@ -285,7 +286,7 @@ def validate_download_vs_complete_dir(root: str, value: str, default: str):
|
||||
return validate_safedir(root, value, default)
|
||||
|
||||
|
||||
def validate_scriptdir_not_appdir(root: str, value: str, default: str) -> Tuple[None, str]:
|
||||
def validate_scriptdir_not_appdir(root: str, value: str, default: str) -> tuple[None, str]:
|
||||
"""Warn users to not use the Program Files folder for their scripts"""
|
||||
# Need to add separator so /mnt/sabnzbd and /mnt/sabnzbd-data are not detected as equal
|
||||
if value and same_directory(sabnzbd.DIR_PROG, os.path.join(root, value)):
|
||||
@@ -298,7 +299,7 @@ def validate_scriptdir_not_appdir(root: str, value: str, default: str) -> Tuple[
|
||||
return None, value
|
||||
|
||||
|
||||
def validate_default_if_empty(root: str, value: str, default: str) -> Tuple[None, str]:
|
||||
def validate_default_if_empty(root: str, value: str, default: str) -> tuple[None, str]:
|
||||
"""If value is empty, return default"""
|
||||
if value:
|
||||
return None, value
|
||||
@@ -505,7 +506,7 @@ no_penalties = OptionBool("misc", "no_penalties", False)
|
||||
x_frame_options = OptionBool("misc", "x_frame_options", True)
|
||||
allow_old_ssl_tls = OptionBool("misc", "allow_old_ssl_tls", False)
|
||||
enable_season_sorting = OptionBool("misc", "enable_season_sorting", True)
|
||||
verify_xff_header = OptionBool("misc", "verify_xff_header", False)
|
||||
verify_xff_header = OptionBool("misc", "verify_xff_header", True)
|
||||
|
||||
# Text values
|
||||
rss_odd_titles = OptionList("misc", "rss_odd_titles", ["nzbindex.nl/", "nzbindex.com/", "nzbclub.com/"])
|
||||
@@ -527,10 +528,12 @@ local_ranges = OptionList("misc", "local_ranges", protect=True)
|
||||
max_url_retries = OptionNumber("misc", "max_url_retries", 10, minval=1)
|
||||
downloader_sleep_time = OptionNumber("misc", "downloader_sleep_time", 10, minval=0)
|
||||
receive_threads = OptionNumber("misc", "receive_threads", 2, minval=1)
|
||||
assembler_max_queue_size = OptionNumber("misc", "assembler_max_queue_size", DEF_MAX_ASSEMBLER_QUEUE, minval=1)
|
||||
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 +561,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 +578,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 +595,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 +613,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 +635,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 +654,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 +679,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 +704,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)
|
||||
|
||||
@@ -28,7 +28,7 @@ import time
|
||||
import uuid
|
||||
import io
|
||||
import zipfile
|
||||
from typing import List, Dict, Any, Callable, Optional, Union, Tuple
|
||||
from typing import Any, Callable, Optional, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import configobj
|
||||
@@ -101,14 +101,14 @@ class Option:
|
||||
def get_string(self) -> str:
|
||||
return str(self.get())
|
||||
|
||||
def get_dict(self, for_public_api: bool = False) -> Dict[str, Any]:
|
||||
def get_dict(self, for_public_api: bool = False) -> dict[str, Any]:
|
||||
"""Return value as a dictionary.
|
||||
Will not show non-public options if needed for the API"""
|
||||
if not self.__public and for_public_api:
|
||||
return {}
|
||||
return {self.__keyword: self.get()}
|
||||
|
||||
def set_dict(self, values: Dict[str, Any]):
|
||||
def set_dict(self, values: dict[str, Any]):
|
||||
"""Set value based on dictionary"""
|
||||
if not self.__protect:
|
||||
try:
|
||||
@@ -307,7 +307,7 @@ class OptionList(Option):
|
||||
self,
|
||||
section: str,
|
||||
keyword: str,
|
||||
default_val: Union[str, List, None] = None,
|
||||
default_val: Union[str, list, None] = None,
|
||||
validation: Optional[Callable] = None,
|
||||
add: bool = True,
|
||||
public: bool = True,
|
||||
@@ -318,7 +318,7 @@ class OptionList(Option):
|
||||
default_val = []
|
||||
super().__init__(section, keyword, default_val, add=add, public=public, protect=protect)
|
||||
|
||||
def set(self, value: Union[str, List]) -> Optional[str]:
|
||||
def set(self, value: Union[str, list]) -> Optional[str]:
|
||||
"""Set the list given a comma-separated string or a list"""
|
||||
error = None
|
||||
if value is not None:
|
||||
@@ -341,7 +341,7 @@ class OptionList(Option):
|
||||
"""Return the default list as a comma-separated string"""
|
||||
return ", ".join(self.default)
|
||||
|
||||
def __call__(self) -> List[str]:
|
||||
def __call__(self) -> list[str]:
|
||||
"""get() replacement"""
|
||||
return self.get()
|
||||
|
||||
@@ -406,7 +406,7 @@ class OptionPassword(Option):
|
||||
return "*" * 10
|
||||
return ""
|
||||
|
||||
def get_dict(self, for_public_api: bool = False) -> Dict[str, str]:
|
||||
def get_dict(self, for_public_api: bool = False) -> dict[str, str]:
|
||||
"""Return value a dictionary"""
|
||||
if for_public_api:
|
||||
return {self.keyword: self.get_stars()}
|
||||
@@ -454,7 +454,7 @@ class ConfigServer:
|
||||
self.set_dict(values)
|
||||
add_to_database("servers", self.__name, self)
|
||||
|
||||
def set_dict(self, values: Dict[str, Any]):
|
||||
def set_dict(self, values: dict[str, Any]):
|
||||
"""Set one or more fields, passed as dictionary"""
|
||||
# Replace usage_at_start value with most recent statistics if the user changes the quota value
|
||||
# Only when we are updating it from the Config
|
||||
@@ -491,7 +491,7 @@ class ConfigServer:
|
||||
if not self.displayname():
|
||||
self.displayname.set(self.__name)
|
||||
|
||||
def get_dict(self, for_public_api: bool = False) -> Dict[str, Any]:
|
||||
def get_dict(self, for_public_api: bool = False) -> dict[str, Any]:
|
||||
"""Return a dictionary with all attributes"""
|
||||
output_dict = {}
|
||||
output_dict["name"] = self.__name
|
||||
@@ -531,7 +531,7 @@ class ConfigServer:
|
||||
class ConfigCat:
|
||||
"""Class defining a single category"""
|
||||
|
||||
def __init__(self, name: str, values: Dict[str, Any]):
|
||||
def __init__(self, name: str, values: dict[str, Any]):
|
||||
self.__name = clean_section_name(name)
|
||||
name = "categories," + self.__name
|
||||
|
||||
@@ -545,7 +545,7 @@ class ConfigCat:
|
||||
self.set_dict(values)
|
||||
add_to_database("categories", self.__name, self)
|
||||
|
||||
def set_dict(self, values: Dict[str, Any]):
|
||||
def set_dict(self, values: dict[str, Any]):
|
||||
"""Set one or more fields, passed as dictionary"""
|
||||
for kw in ("order", "pp", "script", "dir", "newzbin", "priority"):
|
||||
try:
|
||||
@@ -554,7 +554,7 @@ class ConfigCat:
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
def get_dict(self, for_public_api: bool = False) -> Dict[str, Any]:
|
||||
def get_dict(self, for_public_api: bool = False) -> dict[str, Any]:
|
||||
"""Return a dictionary with all attributes"""
|
||||
output_dict = {}
|
||||
output_dict["name"] = self.__name
|
||||
@@ -589,7 +589,7 @@ class ConfigSorter:
|
||||
self.set_dict(values)
|
||||
add_to_database("sorters", self.__name, self)
|
||||
|
||||
def set_dict(self, values: Dict[str, Any]):
|
||||
def set_dict(self, values: dict[str, Any]):
|
||||
"""Set one or more fields, passed as dictionary"""
|
||||
for kw in ("order", "min_size", "multipart_label", "sort_string", "sort_cats", "sort_type", "is_active"):
|
||||
try:
|
||||
@@ -598,7 +598,7 @@ class ConfigSorter:
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
def get_dict(self, for_public_api: bool = False) -> Dict[str, Any]:
|
||||
def get_dict(self, for_public_api: bool = False) -> dict[str, Any]:
|
||||
"""Return a dictionary with all attributes"""
|
||||
output_dict = {}
|
||||
output_dict["name"] = self.__name
|
||||
@@ -639,7 +639,7 @@ class OptionFilters(Option):
|
||||
return
|
||||
self.set(lst)
|
||||
|
||||
def update(self, pos: int, value: Tuple):
|
||||
def update(self, pos: int, value: tuple):
|
||||
"""Update filter 'pos' definition, value is a list
|
||||
Append if 'pos' outside list
|
||||
"""
|
||||
@@ -659,14 +659,14 @@ class OptionFilters(Option):
|
||||
return
|
||||
self.set(lst)
|
||||
|
||||
def get_dict(self, for_public_api: bool = False) -> Dict[str, str]:
|
||||
def get_dict(self, for_public_api: bool = False) -> dict[str, str]:
|
||||
"""Return filter list as a dictionary with keys 'filter[0-9]+'"""
|
||||
output_dict = {}
|
||||
for n, rss_filter in enumerate(self.get()):
|
||||
output_dict[f"filter{n}"] = rss_filter
|
||||
return output_dict
|
||||
|
||||
def set_dict(self, values: Dict[str, Any]):
|
||||
def set_dict(self, values: dict[str, Any]):
|
||||
"""Create filter list from dictionary with keys 'filter[0-9]+'"""
|
||||
filters = []
|
||||
# We don't know how many filters there are, so just assume all values are filters
|
||||
@@ -677,7 +677,7 @@ class OptionFilters(Option):
|
||||
if filters:
|
||||
self.set(filters)
|
||||
|
||||
def __call__(self) -> List[List[str]]:
|
||||
def __call__(self) -> list[list[str]]:
|
||||
"""get() replacement"""
|
||||
return self.get()
|
||||
|
||||
@@ -701,7 +701,7 @@ class ConfigRSS:
|
||||
self.set_dict(values)
|
||||
add_to_database("rss", self.__name, self)
|
||||
|
||||
def set_dict(self, values: Dict[str, Any]):
|
||||
def set_dict(self, values: dict[str, Any]):
|
||||
"""Set one or more fields, passed as dictionary"""
|
||||
for kw in ("uri", "cat", "pp", "script", "priority", "enable"):
|
||||
try:
|
||||
@@ -711,7 +711,7 @@ class ConfigRSS:
|
||||
continue
|
||||
self.filters.set_dict(values)
|
||||
|
||||
def get_dict(self, for_public_api: bool = False) -> Dict[str, Any]:
|
||||
def get_dict(self, for_public_api: bool = False) -> dict[str, Any]:
|
||||
"""Return a dictionary with all attributes"""
|
||||
output_dict = {}
|
||||
output_dict["name"] = self.__name
|
||||
@@ -755,7 +755,7 @@ AllConfigTypes = Union[
|
||||
ConfigRSS,
|
||||
ConfigServer,
|
||||
]
|
||||
CFG_DATABASE: Dict[str, Dict[str, AllConfigTypes]] = {}
|
||||
CFG_DATABASE: dict[str, dict[str, AllConfigTypes]] = {}
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
@@ -1103,7 +1103,7 @@ def restore_config_backup(config_backup_data: bytes):
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def get_servers() -> Dict[str, ConfigServer]:
|
||||
def get_servers() -> dict[str, ConfigServer]:
|
||||
global CFG_DATABASE
|
||||
try:
|
||||
return CFG_DATABASE["servers"]
|
||||
@@ -1112,7 +1112,7 @@ def get_servers() -> Dict[str, ConfigServer]:
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def get_sorters() -> Dict[str, ConfigSorter]:
|
||||
def get_sorters() -> dict[str, ConfigSorter]:
|
||||
global CFG_DATABASE
|
||||
try:
|
||||
return CFG_DATABASE["sorters"]
|
||||
@@ -1120,7 +1120,7 @@ def get_sorters() -> Dict[str, ConfigSorter]:
|
||||
return {}
|
||||
|
||||
|
||||
def get_ordered_sorters() -> List[Dict]:
|
||||
def get_ordered_sorters() -> list[dict]:
|
||||
"""Return sorters as an ordered list"""
|
||||
database_sorters = get_sorters()
|
||||
|
||||
@@ -1131,7 +1131,7 @@ def get_ordered_sorters() -> List[Dict]:
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def get_categories() -> Dict[str, ConfigCat]:
|
||||
def get_categories() -> dict[str, ConfigCat]:
|
||||
"""Return link to categories section.
|
||||
This section will always contain special category '*'
|
||||
"""
|
||||
@@ -1163,7 +1163,7 @@ def get_category(cat: str = "*") -> ConfigCat:
|
||||
return cats["*"]
|
||||
|
||||
|
||||
def get_ordered_categories() -> List[Dict]:
|
||||
def get_ordered_categories() -> list[dict]:
|
||||
"""Return list-copy of categories section that's ordered
|
||||
by user's ordering including Default-category
|
||||
"""
|
||||
@@ -1183,7 +1183,7 @@ def get_ordered_categories() -> List[Dict]:
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def get_rss() -> Dict[str, ConfigRSS]:
|
||||
def get_rss() -> dict[str, ConfigRSS]:
|
||||
global CFG_DATABASE
|
||||
try:
|
||||
# We have to remove non-separator commas by detecting if they are valid URL's
|
||||
|
||||
@@ -50,7 +50,7 @@ RENAMES_FILE = "__renames__"
|
||||
ATTRIB_FILE = "SABnzbd_attrib"
|
||||
REPAIR_REQUEST = "repair-all.sab"
|
||||
|
||||
SABCTOOLS_VERSION_REQUIRED = "8.2.5"
|
||||
SABCTOOLS_VERSION_REQUIRED = "8.2.6"
|
||||
|
||||
DB_HISTORY_VERSION = 1
|
||||
DB_HISTORY_NAME = "history%s.db" % DB_HISTORY_VERSION
|
||||
@@ -97,8 +97,8 @@ CONFIG_BACKUP_HTTPS = { # "basename": "associated setting"
|
||||
}
|
||||
|
||||
# Constants affecting download performance
|
||||
MAX_ASSEMBLER_QUEUE = 12
|
||||
SOFT_QUEUE_LIMIT = 0.5
|
||||
DEF_MAX_ASSEMBLER_QUEUE = 12
|
||||
SOFT_ASSEMBLER_QUEUE_LIMIT = 0.5
|
||||
# Percentage of cache to use before adding file to assembler
|
||||
ASSEMBLER_WRITE_THRESHOLD = 5
|
||||
NNTP_BUFFER_SIZE = int(800 * KIBI)
|
||||
@@ -180,6 +180,7 @@ class DuplicateStatus:
|
||||
|
||||
class AddNzbFileResult:
|
||||
RETRY = "Retry" # File could not be read
|
||||
ERROR = "Error" # Rejected as duplicate, by pre-queue script or other failure to process file
|
||||
ERROR = "Error" # Rejected as duplicate or other failure to process file
|
||||
PREQUEUE_REJECTED = "Pre-queue rejected" # Rejected by pre-queue script
|
||||
OK = "OK" # Added to queue
|
||||
NO_FILES_FOUND = "No files found" # Malformed or might not be an NZB file
|
||||
|
||||
@@ -27,7 +27,7 @@ import sys
|
||||
import threading
|
||||
import sqlite3
|
||||
from sqlite3 import Connection, Cursor
|
||||
from typing import Optional, List, Sequence, Dict, Any, Tuple, Union
|
||||
from typing import Optional, Sequence, Any
|
||||
|
||||
import sabnzbd
|
||||
import sabnzbd.cfg
|
||||
@@ -110,6 +110,10 @@ class HistoryDB:
|
||||
_ = self.execute("PRAGMA user_version = 4;") and self.execute(
|
||||
"ALTER TABLE history ADD COLUMN archive INTEGER;"
|
||||
)
|
||||
if version < 5:
|
||||
_ = self.execute("PRAGMA user_version = 5;") and self.execute(
|
||||
"ALTER TABLE history ADD COLUMN time_added INTEGER;"
|
||||
)
|
||||
|
||||
HistoryDB.startup_done = True
|
||||
|
||||
@@ -187,11 +191,12 @@ class HistoryDB:
|
||||
"md5sum" TEXT,
|
||||
"password" TEXT,
|
||||
"duplicate_key" TEXT,
|
||||
"archive" INTEGER
|
||||
"archive" INTEGER,
|
||||
"time_added" INTEGER
|
||||
)
|
||||
"""
|
||||
)
|
||||
self.execute("PRAGMA user_version = 4;")
|
||||
self.execute("PRAGMA user_version = 5;")
|
||||
|
||||
def close(self):
|
||||
"""Close database connection"""
|
||||
@@ -227,7 +232,12 @@ class HistoryDB:
|
||||
logging.info("Removing all jobs with status=%s", status)
|
||||
self.execute("""DELETE FROM history WHERE name LIKE ? AND status = ?""", (search, status))
|
||||
|
||||
def get_failed_paths(self, search: Optional[str] = None) -> List[str]:
|
||||
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)
|
||||
fetch_ok = self.execute(
|
||||
@@ -293,8 +303,8 @@ class HistoryDB:
|
||||
self.execute(
|
||||
"""INSERT INTO history (completed, name, nzb_name, category, pp, script, report,
|
||||
url, status, nzo_id, storage, path, script_log, script_line, download_time, postproc_time, stage_log,
|
||||
downloaded, fail_message, url_info, bytes, duplicate_key, md5sum, password)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
downloaded, fail_message, url_info, bytes, duplicate_key, md5sum, password, time_added)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
t,
|
||||
)
|
||||
logging.info("Added job %s to history", nzo.final_name)
|
||||
@@ -305,10 +315,10 @@ class HistoryDB:
|
||||
limit: Optional[int] = None,
|
||||
archive: Optional[bool] = None,
|
||||
search: Optional[str] = None,
|
||||
categories: Optional[List[str]] = None,
|
||||
statuses: Optional[List[str]] = None,
|
||||
nzo_ids: Optional[List[str]] = None,
|
||||
) -> Tuple[List[Dict[str, Any]], int]:
|
||||
categories: Optional[list[str]] = None,
|
||||
statuses: Optional[list[str]] = None,
|
||||
nzo_ids: Optional[list[str]] = None,
|
||||
) -> tuple[list[dict[str, Any]], int]:
|
||||
"""Return records for specified jobs"""
|
||||
command_args = [convert_search(search)]
|
||||
|
||||
@@ -387,7 +397,7 @@ class HistoryDB:
|
||||
total = self.cursor.fetchone()["COUNT(*)"]
|
||||
return total > 0
|
||||
|
||||
def get_history_size(self) -> Tuple[int, int, int]:
|
||||
def get_history_size(self) -> tuple[int, int, int]:
|
||||
"""Returns the total size of the history and
|
||||
amounts downloaded in the last month and week
|
||||
"""
|
||||
@@ -447,7 +457,7 @@ class HistoryDB:
|
||||
return path
|
||||
return path
|
||||
|
||||
def get_other(self, nzo_id: str) -> Tuple[str, str, str, str, str]:
|
||||
def get_other(self, nzo_id: str) -> tuple[str, str, str, str, str]:
|
||||
"""Return additional data for job `nzo_id`"""
|
||||
if self.execute("""SELECT * FROM history WHERE nzo_id = ?""", (nzo_id,)):
|
||||
try:
|
||||
@@ -540,10 +550,11 @@ def build_history_info(nzo, workdir_complete: str, postproc_time: int, script_ou
|
||||
nzo.duplicate_key,
|
||||
nzo.md5sum,
|
||||
nzo.correct_password,
|
||||
nzo.time_added,
|
||||
)
|
||||
|
||||
|
||||
def unpack_history_info(item: sqlite3.Row) -> Dict[str, Any]:
|
||||
def unpack_history_info(item: sqlite3.Row) -> dict[str, Any]:
|
||||
"""Expands the single line stage_log from the DB
|
||||
into a python dictionary for use in the history display
|
||||
"""
|
||||
|
||||
@@ -64,22 +64,52 @@ def NzbQueueLocker(func: Callable):
|
||||
return call_func
|
||||
|
||||
|
||||
def cache_maintainer(clear_time: int):
|
||||
def conditional_cache(cache_time: int):
|
||||
"""
|
||||
A function decorator that clears functools.cache or functools.lru_cache clear_time seconds
|
||||
:param clear_time: In seconds, how often to clear cache (only checks when called)
|
||||
A decorator that caches function results for a specified time, but only if the result is not empty.
|
||||
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
|
||||
"""
|
||||
|
||||
def inner(func):
|
||||
def decorator(func):
|
||||
cache = {}
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
if hasattr(func, "next_clear"):
|
||||
if time.time() > func.next_clear or kwargs.get("force"):
|
||||
func.cache_clear()
|
||||
func.next_clear = time.time() + clear_time
|
||||
else:
|
||||
func.next_clear = time.time() + clear_time
|
||||
return func(*args, **kwargs)
|
||||
current_time = time.time()
|
||||
|
||||
# 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)
|
||||
|
||||
# Allow force kward to skip cache
|
||||
if not kwargs.get("force"):
|
||||
# Check if we have a valid cached result
|
||||
if key in cache:
|
||||
cached_result, timestamp = cache[key]
|
||||
if current_time - timestamp < cache_time:
|
||||
return cached_result
|
||||
# Cache entry expired, remove it
|
||||
del cache[key]
|
||||
|
||||
# Call the original function
|
||||
result = func(*args, **kwargs)
|
||||
|
||||
# Only cache non-empty results
|
||||
# This excludes None, [], {}, "", 0, False, etc.
|
||||
if result:
|
||||
cache[key] = (result, current_time)
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
return inner
|
||||
return decorator
|
||||
|
||||
@@ -38,14 +38,13 @@ from sabnzbd.par2file import is_par2_file, parse_par2_file
|
||||
import sabnzbd.utils.file_extension as file_extension
|
||||
from sabnzbd.misc import match_str
|
||||
from sabnzbd.constants import IGNORED_MOVIE_FOLDERS
|
||||
from typing import List
|
||||
|
||||
# Files to exclude and minimal file size for renaming
|
||||
EXCLUDED_FILE_EXTS = (".vob", ".rar", ".par2", ".mts", ".m2ts", ".cpi", ".clpi", ".mpl", ".mpls", ".bdm", ".bdmv")
|
||||
MIN_FILE_SIZE = 10 * 1024 * 1024
|
||||
|
||||
|
||||
def decode_par2(parfile: str) -> List[str]:
|
||||
def decode_par2(parfile: str) -> list[str]:
|
||||
"""Parse a par2 file and rename files listed in the par2 to their real name. Return list of generated files"""
|
||||
# Check if really a par2 file
|
||||
if not is_par2_file(parfile):
|
||||
@@ -77,7 +76,7 @@ def decode_par2(parfile: str) -> List[str]:
|
||||
return new_files
|
||||
|
||||
|
||||
def recover_par2_names(filelist: List[str]) -> List[str]:
|
||||
def recover_par2_names(filelist: list[str]) -> list[str]:
|
||||
"""Find par2 files and use them for renaming"""
|
||||
# Check that files exists
|
||||
filelist = [f for f in filelist if os.path.isfile(f)]
|
||||
@@ -168,7 +167,7 @@ def is_probably_obfuscated(myinputfilename: str) -> bool:
|
||||
return True # default is obfuscated
|
||||
|
||||
|
||||
def get_biggest_file(filelist: List[str]) -> str:
|
||||
def get_biggest_file(filelist: list[str]) -> str:
|
||||
"""Returns biggest file if that file is much bigger than the other files
|
||||
If only one file exists, return that. If no file, return None
|
||||
Note: the files in filelist must exist, because their sizes on disk are checked"""
|
||||
@@ -190,7 +189,7 @@ def get_biggest_file(filelist: List[str]) -> str:
|
||||
return None
|
||||
|
||||
|
||||
def deobfuscate(nzo, filelist: List[str], usefulname: str) -> List[str]:
|
||||
def deobfuscate(nzo, filelist: list[str], usefulname: str) -> list[str]:
|
||||
"""
|
||||
For files in filelist:
|
||||
1. if a file has no meaningful extension, add it (for example ".txt" or ".png")
|
||||
@@ -321,7 +320,7 @@ def without_extension(fullpathfilename: str) -> str:
|
||||
return os.path.splitext(fullpathfilename)[0]
|
||||
|
||||
|
||||
def deobfuscate_subtitles(nzo, filelist: List[str]):
|
||||
def deobfuscate_subtitles(nzo, filelist: list[str]):
|
||||
"""
|
||||
input:
|
||||
nzo, so we can update result via set_unpack_info()
|
||||
|
||||
@@ -25,18 +25,19 @@ import subprocess
|
||||
import time
|
||||
import threading
|
||||
import logging
|
||||
from typing import Optional, Dict, List, Tuple
|
||||
from typing import Optional
|
||||
|
||||
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
|
||||
@@ -61,11 +62,11 @@ class DirectUnpacker(threading.Thread):
|
||||
self.rarfile_nzf: Optional[NzbFile] = None
|
||||
self.cur_setname: Optional[str] = None
|
||||
self.cur_volume: int = 0
|
||||
self.total_volumes: Dict[str, int] = {}
|
||||
self.total_volumes: dict[str, int] = {}
|
||||
self.unpack_time: float = 0.0
|
||||
|
||||
self.success_sets: Dict[str, Tuple[List[str], List[str]]] = {}
|
||||
self.next_sets: List[NzbFile] = []
|
||||
self.success_sets: dict[str, tuple[list[str], list[str]]] = {}
|
||||
self.next_sets: list[NzbFile] = []
|
||||
|
||||
self.duplicate_lines: int = 0
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -128,7 +128,7 @@ class DirScanner(threading.Thread):
|
||||
|
||||
def get_suspected_files(
|
||||
self, folder: str, catdir: Optional[str] = None
|
||||
) -> Generator[Tuple[str, Optional[str], Optional[os.stat_result]], None, None]:
|
||||
) -> Generator[tuple[str, Optional[str], Optional[os.stat_result]], None, None]:
|
||||
"""Generator listing possible paths to NZB files"""
|
||||
|
||||
if catdir is None:
|
||||
@@ -222,17 +222,15 @@ class DirScanner(threading.Thread):
|
||||
|
||||
async def scan_async(self, dirscan_dir: str):
|
||||
"""Do one scan of the watched folder"""
|
||||
# On Python 3.8 we first need an event loop before we can create a asyncio.Lock
|
||||
if not self.lock:
|
||||
with DIR_SCANNER_LOCK:
|
||||
self.lock = asyncio.Lock()
|
||||
with DIR_SCANNER_LOCK:
|
||||
self.lock = asyncio.Lock()
|
||||
|
||||
async with self.lock:
|
||||
if sabnzbd.PAUSED_ALL:
|
||||
return
|
||||
|
||||
files: Set[str] = set()
|
||||
futures: Set[asyncio.Task] = set()
|
||||
files: set[str] = set()
|
||||
futures: set[asyncio.Task] = set()
|
||||
|
||||
for path, catdir, stat_tuple in self.get_suspected_files(dirscan_dir):
|
||||
files.add(path)
|
||||
|
||||
@@ -27,7 +27,7 @@ import sys
|
||||
import ssl
|
||||
import time
|
||||
from datetime import date
|
||||
from typing import List, Dict, Optional, Union, Set
|
||||
from typing import Optional, Union
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.decorators import synchronized, NzbQueueLocker, DOWNLOADER_CV, DOWNLOADER_LOCK
|
||||
@@ -35,8 +35,8 @@ from sabnzbd.newswrapper import NewsWrapper, NNTPPermanentError
|
||||
import sabnzbd.config as config
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.misc import from_units, helpful_warning, int_conv, MultiAddQueue
|
||||
from sabnzbd.happyeyeballs import happyeyeballs, AddrInfo
|
||||
from sabnzbd.constants import SOFT_QUEUE_LIMIT
|
||||
from sabnzbd.get_addrinfo import get_fastest_addrinfo, AddrInfo
|
||||
from sabnzbd.constants import SOFT_ASSEMBLER_QUEUE_LIMIT
|
||||
|
||||
|
||||
# Timeout penalty in minutes for each cause
|
||||
@@ -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
|
||||
@@ -135,9 +135,9 @@ class Server:
|
||||
self.username: Optional[str] = username
|
||||
self.password: Optional[str] = password
|
||||
|
||||
self.busy_threads: Set[NewsWrapper] = set()
|
||||
self.busy_threads: set[NewsWrapper] = set()
|
||||
self.next_busy_threads_check: float = 0
|
||||
self.idle_threads: Set[NewsWrapper] = set()
|
||||
self.idle_threads: set[NewsWrapper] = set()
|
||||
self.next_article_search: float = 0
|
||||
self.active: bool = True
|
||||
self.bad_cons: int = 0
|
||||
@@ -148,7 +148,7 @@ class Server:
|
||||
self.request: bool = False # True if a getaddrinfo() request is pending
|
||||
self.have_body: bool = True # Assume server has "BODY", until proven otherwise
|
||||
self.have_stat: bool = True # Assume server has "STAT", until proven otherwise
|
||||
self.article_queue: List[sabnzbd.nzbstuff.Article] = []
|
||||
self.article_queue: list[sabnzbd.nzbstuff.Article] = []
|
||||
|
||||
# Skip during server testing
|
||||
if threads:
|
||||
@@ -206,7 +206,7 @@ class Server:
|
||||
self.article_queue = []
|
||||
|
||||
def request_addrinfo(self):
|
||||
"""Launch async request to resolve server address and perform Happy Eyeballs.
|
||||
"""Launch async request to resolve server address and select the fastest.
|
||||
In some situations this can be slow and result in delayed starts and timeouts on connections.
|
||||
Because of this, the results will be cached in the server object."""
|
||||
if not self.request:
|
||||
@@ -214,7 +214,7 @@ class Server:
|
||||
Thread(target=self.request_addrinfo_blocking).start()
|
||||
|
||||
def request_addrinfo_blocking(self):
|
||||
"""Blocking attempt to run getaddrinfo() and Happy Eyeballs for specified server"""
|
||||
"""Blocking attempt to run getaddrinfo() and address selection for specified server"""
|
||||
logging.debug("Retrieving server address information for %s", self)
|
||||
|
||||
# Disable IPV6 if desired
|
||||
@@ -222,7 +222,7 @@ class Server:
|
||||
if not cfg.ipv6_servers():
|
||||
family = socket.AF_INET
|
||||
|
||||
self.addrinfo = happyeyeballs(self.host, self.port, self.timeout, family)
|
||||
self.addrinfo = get_fastest_addrinfo(self.host, self.port, self.timeout, family)
|
||||
if not self.addrinfo:
|
||||
self.bad_cons += self.threads
|
||||
# Notify next call to maybe_block_server
|
||||
@@ -290,10 +290,10 @@ class Downloader(Thread):
|
||||
|
||||
self.force_disconnect: bool = False
|
||||
|
||||
self.read_fds: Dict[int, NewsWrapper] = {}
|
||||
self.read_fds: dict[int, NewsWrapper] = {}
|
||||
|
||||
self.servers: List[Server] = []
|
||||
self.timers: Dict[str, List[float]] = {}
|
||||
self.servers: list[Server] = []
|
||||
self.timers: dict[str, list[float]] = {}
|
||||
|
||||
for server in config.get_servers():
|
||||
self.init_server(None, server)
|
||||
@@ -451,6 +451,15 @@ class Downloader(Thread):
|
||||
self.bandwidth_perc = 0
|
||||
self.bandwidth_limit = 0
|
||||
|
||||
# Increase limits for faster connections
|
||||
if limit > from_units("150M"):
|
||||
if cfg.receive_threads() == cfg.receive_threads.default:
|
||||
cfg.receive_threads.set(4)
|
||||
logging.info("Receive threads set to 4")
|
||||
if cfg.assembler_max_queue_size() == cfg.assembler_max_queue_size.default:
|
||||
cfg.assembler_max_queue_size.set(30)
|
||||
logging.info("Assembler max_queue_size set to 30")
|
||||
|
||||
def sleep_time_set(self):
|
||||
self.sleep_time = cfg.downloader_sleep_time() * 0.0001
|
||||
logging.debug("Sleep time: %f seconds", self.sleep_time)
|
||||
@@ -490,7 +499,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()
|
||||
@@ -685,7 +694,7 @@ class Downloader(Thread):
|
||||
except Exception:
|
||||
logging.error(T("Fatal error in Downloader"), exc_info=True)
|
||||
|
||||
def process_nw_worker(self, read_fds: Dict[int, NewsWrapper], nw_queue: MultiAddQueue):
|
||||
def process_nw_worker(self, read_fds: dict[int, NewsWrapper], nw_queue: MultiAddQueue):
|
||||
"""Worker for the daemon thread to process results.
|
||||
Wrapped in try/except because in case of an exception, logging
|
||||
might get lost and the queue.join() would block forever."""
|
||||
@@ -753,7 +762,7 @@ class Downloader(Thread):
|
||||
|
||||
elif nw.status_code == 223:
|
||||
article_done = True
|
||||
logging.debug("Article <%s> is present", article.article)
|
||||
logging.debug("Article <%s> is present on %s", article.article, nw.server.host)
|
||||
|
||||
elif nw.status_code in (411, 423, 430, 451):
|
||||
article_done = True
|
||||
@@ -768,9 +777,15 @@ class Downloader(Thread):
|
||||
|
||||
elif nw.status_code == 500:
|
||||
if article.nzf.nzo.precheck:
|
||||
# Assume "STAT" command is not supported
|
||||
server.have_stat = False
|
||||
logging.debug("Server %s does not support STAT", server.host)
|
||||
# Did we try "STAT" already?
|
||||
if not server.have_stat:
|
||||
# Hopless server, just discard
|
||||
logging.info("Server %s does not support STAT or HEAD, precheck not possible", server.host)
|
||||
article_done = True
|
||||
else:
|
||||
# Assume "STAT" command is not supported
|
||||
server.have_stat = False
|
||||
logging.debug("Server %s does not support STAT, trying HEAD", server.host)
|
||||
else:
|
||||
# Assume "BODY" command is not supported
|
||||
server.have_body = False
|
||||
@@ -826,8 +841,8 @@ class Downloader(Thread):
|
||||
|
||||
def check_assembler_levels(self):
|
||||
"""Check the Assembler queue to see if we need to delay, depending on queue size"""
|
||||
if (assembler_level := sabnzbd.Assembler.queue_level()) > SOFT_QUEUE_LIMIT:
|
||||
time.sleep(min((assembler_level - SOFT_QUEUE_LIMIT) / 4, 0.15))
|
||||
if (assembler_level := sabnzbd.Assembler.queue_level()) > SOFT_ASSEMBLER_QUEUE_LIMIT:
|
||||
time.sleep(min((assembler_level - SOFT_ASSEMBLER_QUEUE_LIMIT) / 4, 0.15))
|
||||
sabnzbd.BPSMeter.delayed_assembler += 1
|
||||
logged_counter = 0
|
||||
|
||||
@@ -870,7 +885,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 +1158,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,8 +33,7 @@ import fnmatch
|
||||
import stat
|
||||
import ctypes
|
||||
import random
|
||||
import functools
|
||||
from typing import Union, List, Tuple, Any, Dict, Optional, BinaryIO
|
||||
from typing import Union, Any, Optional, BinaryIO
|
||||
|
||||
try:
|
||||
import win32api
|
||||
@@ -44,7 +43,7 @@ except ImportError:
|
||||
pass
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.decorators import synchronized, cache_maintainer
|
||||
from sabnzbd.decorators import synchronized, conditional_cache
|
||||
from sabnzbd.constants import (
|
||||
FUTURE_Q_FOLDER,
|
||||
JOB_ADMIN,
|
||||
@@ -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
|
||||
@@ -296,10 +295,10 @@ def sanitize_and_trim_path(path: str) -> str:
|
||||
if sabnzbd.WINDOWS:
|
||||
if path.startswith("\\\\?\\UNC\\"):
|
||||
new_path = "\\\\?\\UNC\\"
|
||||
path = path[8:]
|
||||
path = path.removeprefix("\\\\?\\UNC\\")
|
||||
elif path.startswith("\\\\?\\"):
|
||||
new_path = "\\\\?\\"
|
||||
path = path[4:]
|
||||
path = path.removeprefix("\\\\?\\")
|
||||
|
||||
path = path.replace("\\", "/")
|
||||
parts = path.split("/")
|
||||
@@ -315,7 +314,7 @@ def sanitize_and_trim_path(path: str) -> str:
|
||||
return os.path.abspath(os.path.normpath(new_path))
|
||||
|
||||
|
||||
def sanitize_files(folder: Optional[str] = None, filelist: Optional[List[str]] = None) -> List[str]:
|
||||
def sanitize_files(folder: Optional[str] = None, filelist: Optional[list[str]] = None) -> list[str]:
|
||||
"""Sanitize each file in the folder or list of filepaths, return list of new names"""
|
||||
logging.info("Checking if any resulting filenames need to be sanitized")
|
||||
if folder:
|
||||
@@ -331,7 +330,7 @@ def sanitize_files(folder: Optional[str] = None, filelist: Optional[List[str]] =
|
||||
return output_filelist
|
||||
|
||||
|
||||
def strip_extensions(name: str, ext_to_remove: Tuple[str, ...] = (".nzb", ".par", ".par2")):
|
||||
def strip_extensions(name: str, ext_to_remove: tuple[str, ...] = (".nzb", ".par", ".par2")) -> str:
|
||||
"""Strip extensions from a filename, without sanitizing the filename"""
|
||||
name_base, ext = os.path.splitext(name)
|
||||
while ext.lower() in ext_to_remove:
|
||||
@@ -379,7 +378,7 @@ def real_path(loc: str, path: str) -> str:
|
||||
|
||||
def create_real_path(
|
||||
name: str, loc: str, path: str, apply_permissions: bool = False, writable: bool = True
|
||||
) -> Tuple[bool, str, Optional[str]]:
|
||||
) -> tuple[bool, str, Optional[str]]:
|
||||
"""When 'path' is relative, create join of 'loc' and 'path'
|
||||
When 'path' is absolute, create normalized path
|
||||
'name' is used for logging.
|
||||
@@ -485,7 +484,7 @@ TS_RE = re.compile(r"\.(\d+)\.(ts$)", re.I)
|
||||
|
||||
def build_filelists(
|
||||
workdir: Optional[str], workdir_complete: Optional[str] = None, check_both: bool = False, check_rar: bool = True
|
||||
) -> Tuple[List[str], List[str], List[str], List[str]]:
|
||||
) -> tuple[list[str], list[str], list[str], list[str]]:
|
||||
"""Build filelists, if workdir_complete has files, ignore workdir.
|
||||
Optionally scan both directories.
|
||||
Optionally test content to establish RAR-ness
|
||||
@@ -536,7 +535,7 @@ def safe_fnmatch(f: str, pattern: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def globber(path: str, pattern: str = "*") -> List[str]:
|
||||
def globber(path: str, pattern: str = "*") -> list[str]:
|
||||
"""Return matching base file/folder names in folder `path`"""
|
||||
# Cannot use glob.glob() because it doesn't support Windows long name notation
|
||||
if os.path.exists(path):
|
||||
@@ -544,7 +543,7 @@ def globber(path: str, pattern: str = "*") -> List[str]:
|
||||
return []
|
||||
|
||||
|
||||
def globber_full(path: str, pattern: str = "*") -> List[str]:
|
||||
def globber_full(path: str, pattern: str = "*") -> list[str]:
|
||||
"""Return matching full file/folder names in folder `path`"""
|
||||
# Cannot use glob.glob() because it doesn't support Windows long name notation
|
||||
if os.path.exists(path):
|
||||
@@ -573,7 +572,7 @@ def is_valid_script(basename: str) -> bool:
|
||||
return basename in list_scripts(default=False, none=False)
|
||||
|
||||
|
||||
def list_scripts(default: bool = False, none: bool = True) -> List[str]:
|
||||
def list_scripts(default: bool = False, none: bool = True) -> list[str]:
|
||||
"""Return a list of script names, optionally with 'Default' added"""
|
||||
lst = []
|
||||
path = sabnzbd.cfg.script_dir.get_path()
|
||||
@@ -614,7 +613,7 @@ def make_script_path(script: str) -> Optional[str]:
|
||||
return script_path
|
||||
|
||||
|
||||
def get_admin_path(name: str, future: bool):
|
||||
def get_admin_path(name: str, future: bool) -> str:
|
||||
"""Return news-style full path to job-admin folder of names job
|
||||
or else the old cache path
|
||||
"""
|
||||
@@ -661,7 +660,7 @@ def set_permissions(path: str, recursive: bool = True):
|
||||
UNWANTED_FILE_PERMISSIONS = stat.S_ISUID | stat.S_ISGID | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
||||
|
||||
|
||||
def removexbits(path: str, custom_permissions: int = None):
|
||||
def removexbits(path: str, custom_permissions: Optional[int] = None):
|
||||
"""Remove all the x-bits from files, respecting current or custom permissions"""
|
||||
if os.path.isfile(path):
|
||||
# Use custom permissions as base
|
||||
@@ -784,7 +783,7 @@ def get_unique_filename(path: str) -> str:
|
||||
|
||||
|
||||
@synchronized(DIR_LOCK)
|
||||
def listdir_full(input_dir: str, recursive: bool = True) -> List[str]:
|
||||
def listdir_full(input_dir: str, recursive: bool = True) -> list[str]:
|
||||
"""List all files in dirs and sub-dirs"""
|
||||
filelist = []
|
||||
for root, dirs, files in os.walk(input_dir):
|
||||
@@ -798,7 +797,7 @@ def listdir_full(input_dir: str, recursive: bool = True) -> List[str]:
|
||||
|
||||
|
||||
@synchronized(DIR_LOCK)
|
||||
def move_to_path(path: str, new_path: str) -> Tuple[bool, Optional[str]]:
|
||||
def move_to_path(path: str, new_path: str) -> tuple[bool, Optional[str]]:
|
||||
"""Move a file to a new path, optionally give unique filename
|
||||
Return (ok, new_path)
|
||||
"""
|
||||
@@ -991,51 +990,7 @@ 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]:
|
||||
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
|
||||
x = "x"
|
||||
@@ -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:
|
||||
@@ -1072,10 +1023,9 @@ def diskspace_base(dir_to_check: str) -> Tuple[float, float]:
|
||||
return 20.0, 10.0
|
||||
|
||||
|
||||
@cache_maintainer(clear_time=10)
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def diskspace(force: bool = False) -> Dict[str, Tuple[float, float]]:
|
||||
"""Wrapper to keep results cached by cache_maintainer
|
||||
@conditional_cache(cache_time=10)
|
||||
def diskspace(force: bool = False) -> dict[str, tuple[float, float]]:
|
||||
"""Wrapper to keep results cached by conditional_cache
|
||||
If called with force=True, the wrapper will clear the results"""
|
||||
return {
|
||||
"download_dir": diskspace_base(sabnzbd.cfg.download_dir.get_path()),
|
||||
@@ -1083,7 +1033,7 @@ def diskspace(force: bool = False) -> Dict[str, Tuple[float, float]]:
|
||||
}
|
||||
|
||||
|
||||
def get_new_id(prefix, folder, check_list=None):
|
||||
def get_new_id(prefix: str, folder: str, check_list: Optional[list] = None) -> str:
|
||||
"""Return unique prefixed admin identifier within folder
|
||||
optionally making sure that id is not in the check_list.
|
||||
"""
|
||||
@@ -1104,7 +1054,7 @@ def get_new_id(prefix, folder, check_list=None):
|
||||
raise IOError
|
||||
|
||||
|
||||
def save_data(data, _id, path, do_pickle=True, silent=False):
|
||||
def save_data(data: Any, _id: str, path: str, do_pickle: bool = True, silent: bool = False):
|
||||
"""Save data to a diskfile"""
|
||||
if not silent:
|
||||
logging.debug("[%s] Saving data for %s in %s", sabnzbd.misc.caller_name(), _id, path)
|
||||
@@ -1131,7 +1081,7 @@ def save_data(data, _id, path, do_pickle=True, silent=False):
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
def load_data(data_id, path, remove=True, do_pickle=True, silent=False):
|
||||
def load_data(data_id: str, path: str, remove: bool = True, do_pickle: bool = True, silent: bool = False) -> Any:
|
||||
"""Read data from disk file"""
|
||||
path = os.path.join(path, data_id)
|
||||
|
||||
@@ -1179,7 +1129,7 @@ def save_admin(data: Any, data_id: str):
|
||||
save_data(data, data_id, sabnzbd.cfg.admin_dir.get_path())
|
||||
|
||||
|
||||
def load_admin(data_id: str, remove=False, silent=False) -> Any:
|
||||
def load_admin(data_id: str, remove: bool = False, silent: bool = False) -> Any:
|
||||
"""Read data in admin folder in specified format"""
|
||||
logging.debug("[%s] Loading data for %s", sabnzbd.misc.caller_name(), data_id)
|
||||
return load_data(data_id, sabnzbd.cfg.admin_dir.get_path(), remove=remove, silent=silent)
|
||||
@@ -1246,7 +1196,7 @@ def purge_log_files():
|
||||
logging.debug("Finished puring log files")
|
||||
|
||||
|
||||
def directory_is_writable_with_file(mydir, myfilename):
|
||||
def directory_is_writable_with_file(mydir: str, myfilename: str) -> bool:
|
||||
filename = os.path.join(mydir, myfilename)
|
||||
if os.path.exists(filename):
|
||||
try:
|
||||
@@ -1291,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
|
||||
)
|
||||
@@ -1299,7 +1253,7 @@ def check_filesystem_capabilities(test_dir: str) -> bool:
|
||||
return allgood
|
||||
|
||||
|
||||
def get_win_drives() -> List[str]:
|
||||
def get_win_drives() -> list[str]:
|
||||
"""Return list of detected drives, adapted from:
|
||||
http://stackoverflow.com/questions/827371/is-there-a-way-to-list-all-the-available-drive-letters-in-python/827490
|
||||
"""
|
||||
@@ -1327,7 +1281,7 @@ PATHBROWSER_JUNKFOLDERS = (
|
||||
)
|
||||
|
||||
|
||||
def pathbrowser(path: str, show_hidden: bool = False, show_files: bool = False) -> List[Dict[str, str]]:
|
||||
def pathbrowser(path: str, show_hidden: bool = False, show_files: bool = False) -> list[dict[str, str]]:
|
||||
"""Returns a list of dictionaries with the folders and folders contained at the given path
|
||||
Give the empty string as the path to list the contents of the root path
|
||||
under Unix this means "/", on Windows this will be a list of drive letters
|
||||
|
||||
@@ -16,31 +16,24 @@
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
"""
|
||||
sabnzbd.happyeyeballs - Python implementation of RFC 6555 / Happy Eyeballs: find the quickest IPv4/IPv6 connection
|
||||
sabnzbd.get_addrinfo - Concurrent IP address testing: find the fastest IPv4/IPv6 connection
|
||||
"""
|
||||
|
||||
# Python implementation of RFC 6555/8305 (Happy Eyeballs): find the quickest IPv4/IPv6 connection
|
||||
# See https://tools.ietf.org/html/rfc6555
|
||||
# See https://tools.ietf.org/html/rfc8305
|
||||
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
import queue
|
||||
import functools
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple, Union, Optional
|
||||
from more_itertools import roundrobin
|
||||
from typing import Union, Optional
|
||||
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.constants import DEF_NETWORKING_TIMEOUT
|
||||
from sabnzbd.decorators import cache_maintainer
|
||||
from sabnzbd.decorators import conditional_cache
|
||||
|
||||
# How long to delay between connection attempts? The RFC suggests 250ms, but this is
|
||||
# quite long and might give us a slow host that just happened to be on top of the list.
|
||||
# The absolute minimum specified in RFC 8305 is 10ms, so we use that.
|
||||
CONNECTION_ATTEMPT_DELAY = 0.01
|
||||
# How often to check for connection results
|
||||
CONNECTION_RESULT_CHECK = 0.1 # 100ms
|
||||
|
||||
# While providers are afraid to add IPv6 to their standard hostnames
|
||||
# we map a number of well known hostnames to their IPv6 alternatives.
|
||||
@@ -68,9 +61,10 @@ class AddrInfo:
|
||||
type: socket.SocketKind
|
||||
proto: int
|
||||
canonname: str
|
||||
sockaddr: Union[Tuple[str, int], Tuple[str, int, int, int]]
|
||||
sockaddr: Union[tuple[str, int], tuple[str, int, int, int]]
|
||||
ipaddress: str = ""
|
||||
port: int = 0
|
||||
connection_time: float = 0.0
|
||||
|
||||
def __post_init__(self):
|
||||
# For easy access
|
||||
@@ -90,25 +84,26 @@ def family_type(family) -> str:
|
||||
|
||||
|
||||
# Called by each thread
|
||||
def do_socket_connect(result_queue: queue.Queue, addrinfo: AddrInfo, timeout: int):
|
||||
"""Connect to the ip, and put the result into the queue"""
|
||||
def do_socket_connect(results_list: list, addrinfo: AddrInfo, timeout: int):
|
||||
"""Connect to the ip, and add the result with timing info to the shared list"""
|
||||
try:
|
||||
start = time.time()
|
||||
s = socket.socket(addrinfo.family, addrinfo.type)
|
||||
s.settimeout(timeout)
|
||||
try:
|
||||
s.connect(addrinfo.sockaddr)
|
||||
result_queue.put(addrinfo)
|
||||
addrinfo.connection_time = time.time() - start
|
||||
results_list.append(addrinfo)
|
||||
logging.debug(
|
||||
"Happy Eyeballs connected to %s (%s, port=%d) in %dms",
|
||||
"Connected to %s (%s, port=%d) in %dms",
|
||||
addrinfo.ipaddress,
|
||||
addrinfo.canonname,
|
||||
addrinfo.port,
|
||||
1000 * (time.time() - start),
|
||||
1000 * addrinfo.connection_time,
|
||||
)
|
||||
except socket.error:
|
||||
logging.debug(
|
||||
"Happy Eyeballs failed to connect to %s (%s, port=%d) in %dms",
|
||||
"Failed to connect to %s (%s, port=%d) in %dms",
|
||||
addrinfo.ipaddress,
|
||||
addrinfo.canonname,
|
||||
addrinfo.port,
|
||||
@@ -120,28 +115,28 @@ def do_socket_connect(result_queue: queue.Queue, addrinfo: AddrInfo, timeout: in
|
||||
pass
|
||||
|
||||
|
||||
@cache_maintainer(clear_time=10)
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def happyeyeballs(
|
||||
@conditional_cache(cache_time=60)
|
||||
def get_fastest_addrinfo(
|
||||
host: str,
|
||||
port: int,
|
||||
timeout: int = DEF_NETWORKING_TIMEOUT,
|
||||
family=socket.AF_UNSPEC,
|
||||
) -> Optional[AddrInfo]:
|
||||
"""Return the fastest result of getaddrinfo() based on RFC 6555/8305 (Happy Eyeballs),
|
||||
including IPv6 addresses if desired. Returns None in case no addresses were returned
|
||||
by getaddrinfo or if no connection could be made to any of the addresses.
|
||||
If family is specified, only that family is tried"""
|
||||
"""Return the fastest result of getaddrinfo() by testing all IP addresses concurrently.
|
||||
Tests all available IP addresses simultaneously (alternating IPv4/6) in separate threads and returns the
|
||||
connection with the shortest response time after CONNECTION_CHECK interval.
|
||||
Returns None in case no addresses were returned by getaddrinfo or if no connection
|
||||
could be made to any of the addresses. If family is specified, only that family is tried"""
|
||||
try:
|
||||
# See if we can add a IPv6 alternative
|
||||
# See if we can add an IPv6 alternative
|
||||
check_hosts = [host]
|
||||
if cfg.ipv6_staging() and host in IPV6_MAPPING:
|
||||
check_hosts.append(IPV6_MAPPING[host])
|
||||
logging.info("Added IPv6 alternative %s for host %s", IPV6_MAPPING[host], host)
|
||||
|
||||
last_canonname = ""
|
||||
ipv4_addrinfo = []
|
||||
ipv6_addrinfo = []
|
||||
last_canonname = ""
|
||||
for check_host in check_hosts:
|
||||
try:
|
||||
for addrinfo in socket.getaddrinfo(
|
||||
@@ -178,39 +173,44 @@ def happyeyeballs(
|
||||
len(ipv6_addrinfo),
|
||||
)
|
||||
|
||||
# To optimize success, the RFC states to alternate between trying the
|
||||
# IPv6 and IPv4 results, starting with IPv6 since it is the preferred method.
|
||||
result_queue: queue.Queue[AddrInfo] = queue.Queue()
|
||||
addr_tried = 0
|
||||
result: Optional[AddrInfo] = None
|
||||
if not ipv4_addrinfo and not ipv6_addrinfo:
|
||||
raise ConnectionError("No usable IP addresses found for %s" % ", ".join(check_hosts))
|
||||
|
||||
# Try IPv6 and IPv4 alternating since there is delay in starting threads
|
||||
successful_connections = []
|
||||
threads = []
|
||||
for addrinfo in roundrobin(ipv6_addrinfo, ipv4_addrinfo):
|
||||
threading.Thread(target=do_socket_connect, args=(result_queue, addrinfo, timeout), daemon=True).start()
|
||||
addr_tried += 1
|
||||
try:
|
||||
result = result_queue.get(timeout=CONNECTION_ATTEMPT_DELAY)
|
||||
break
|
||||
except queue.Empty:
|
||||
# Start a thread for the next address in the list if the previous
|
||||
# connection attempt did not complete in time or if it wasn't a success
|
||||
continue
|
||||
thread = threading.Thread(
|
||||
target=do_socket_connect,
|
||||
args=(successful_connections, addrinfo, timeout),
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
threads.append(thread)
|
||||
|
||||
# If we had no results, we might just need to give it more time
|
||||
if not result:
|
||||
try:
|
||||
# Reduce waiting time by time already spent
|
||||
result = result_queue.get(timeout=timeout - addr_tried * CONNECTION_ATTEMPT_DELAY)
|
||||
except queue.Empty:
|
||||
raise ConnectionError("No usable IP addresses found for %s" % ", ".join(check_hosts))
|
||||
# Wait for the first successful connection
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
time.sleep(CONNECTION_RESULT_CHECK)
|
||||
# Check if we have any successful connections
|
||||
if successful_connections:
|
||||
# Return the fastest connection
|
||||
fastest_addrinfo = min(successful_connections, key=lambda result: result.connection_time)
|
||||
logging.info(
|
||||
"Fastest connection to %s (port=%d, %s): %s (%s) in %dms (out of %d results)",
|
||||
host,
|
||||
port,
|
||||
family_type(family),
|
||||
fastest_addrinfo.ipaddress,
|
||||
fastest_addrinfo.canonname,
|
||||
1000 * fastest_addrinfo.connection_time,
|
||||
len(successful_connections),
|
||||
)
|
||||
return fastest_addrinfo
|
||||
|
||||
# If no connections succeeded within timeout
|
||||
raise ConnectionError("No usable IP addresses found for %s" % ", ".join(check_hosts))
|
||||
|
||||
logging.info(
|
||||
"Quickest IP address for %s (port=%d, %s): %s (%s)",
|
||||
host,
|
||||
port,
|
||||
family_type(family),
|
||||
result.ipaddress,
|
||||
result.canonname,
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logging.debug("Failed Happy Eyeballs lookup: %s", e)
|
||||
logging.debug("Failed IP address lookup: %s", e)
|
||||
return None
|
||||
@@ -32,7 +32,7 @@ import socks
|
||||
import sabnzbd
|
||||
import sabnzbd.cfg
|
||||
from sabnzbd.encoding import ubtou
|
||||
from sabnzbd.happyeyeballs import happyeyeballs, family_type
|
||||
from sabnzbd.get_addrinfo import get_fastest_addrinfo, family_type
|
||||
from sabnzbd.constants import DEF_NETWORKING_SHORT_TIMEOUT
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -109,7 +121,7 @@ def public_ip(family: int = socket.AF_UNSPEC) -> Optional[str]:
|
||||
Reports the client's public IP address (IPv4 or IPv6, if specified by family), as reported by selftest host
|
||||
"""
|
||||
start = time.time()
|
||||
if resolvehostaddress := happyeyeballs(
|
||||
if resolvehostaddress := get_fastest_addrinfo(
|
||||
sabnzbd.cfg.selftest_host(),
|
||||
port=443,
|
||||
timeout=DEF_NETWORKING_SHORT_TIMEOUT,
|
||||
|
||||
@@ -34,7 +34,7 @@ import copy
|
||||
from random import randint
|
||||
from xml.sax.saxutils import escape
|
||||
from Cheetah.Template import Template
|
||||
from typing import Optional, Callable, Union, Any, Dict, List
|
||||
from typing import Optional, Callable, Union, Any
|
||||
from guessit.api import properties as guessit_properties
|
||||
|
||||
import sabnzbd
|
||||
@@ -55,7 +55,7 @@ from sabnzbd.misc import (
|
||||
get_cpu_name,
|
||||
clean_comma_separated_list,
|
||||
)
|
||||
from sabnzbd.happyeyeballs import happyeyeballs
|
||||
from sabnzbd.get_addrinfo import get_fastest_addrinfo
|
||||
from sabnzbd.filesystem import (
|
||||
real_path,
|
||||
globber,
|
||||
@@ -264,7 +264,7 @@ def check_hostname():
|
||||
COOKIE_SECRET = str(randint(1000, 100000) * os.getpid())
|
||||
|
||||
|
||||
def remote_ip_from_xff(xff_ips: List[str]) -> str:
|
||||
def remote_ip_from_xff(xff_ips: list[str]) -> str:
|
||||
# Per MDN docs, the first non-local/non-trusted IP (rtl) is our "client"
|
||||
# However, it's possible that all IPs are local/trusted, so we may also
|
||||
# return the first ip in the list as it "should" be the client
|
||||
@@ -310,9 +310,6 @@ def set_login_cookie(remove=False, remember_me=False):
|
||||
if remove:
|
||||
cherrypy.response.cookie["login_cookie"]["expires"] = 0
|
||||
cherrypy.response.cookie["login_salt"]["expires"] = 0
|
||||
else:
|
||||
# Notify about new login
|
||||
notifier.send_notification(T("User logged in"), T("User logged in to the web interface"), "new_login")
|
||||
|
||||
|
||||
def check_login_cookie():
|
||||
@@ -402,7 +399,7 @@ def check_apikey(kwargs):
|
||||
return _MSG_APIKEY_INCORRECT
|
||||
|
||||
|
||||
def template_filtered_response(file: str, search_list: Dict[str, Any]):
|
||||
def template_filtered_response(file: str, search_list: dict[str, Any]):
|
||||
"""Wrapper for Cheetah response"""
|
||||
# We need a copy, because otherwise source-dicts might be modified
|
||||
search_list_copy = copy.deepcopy(search_list)
|
||||
@@ -561,7 +558,7 @@ class Wizard:
|
||||
info["password"] = ""
|
||||
info["connections"] = ""
|
||||
info["ssl"] = 1
|
||||
info["ssl_verify"] = 2
|
||||
info["ssl_verify"] = 3
|
||||
else:
|
||||
# Sort servers to get the first enabled one
|
||||
server_names = sorted(
|
||||
@@ -679,6 +676,8 @@ class LoginPage:
|
||||
set_login_cookie(remember_me=kwargs.get("remember_me", False))
|
||||
# Log the success
|
||||
logging.info("Successful login from %s", cherrypy.request.remote_label)
|
||||
# Notify about new login
|
||||
notifier.send_notification(T("User logged in"), T("User logged in to the web interface"), "new_login")
|
||||
# Redirect
|
||||
raise Raiser("/")
|
||||
elif kwargs.get("username") or kwargs.get("password"):
|
||||
@@ -907,11 +906,13 @@ SPECIAL_VALUE_LIST = (
|
||||
"max_foldername_length",
|
||||
"url_base",
|
||||
"receive_threads",
|
||||
"assembler_max_queue_size",
|
||||
"switchinterval",
|
||||
"direct_unpack_threads",
|
||||
"selftest_host",
|
||||
"ssdp_broadcast_interval",
|
||||
"unrar_parameters",
|
||||
"outgoing_nntp_ip",
|
||||
)
|
||||
SPECIAL_LIST_LIST = (
|
||||
"rss_odd_titles",
|
||||
@@ -1174,7 +1175,7 @@ def handle_server(kwargs, root=None, new_svr=False):
|
||||
kwargs["connections"] = "1"
|
||||
|
||||
if kwargs.get("enable") == "1":
|
||||
if not happyeyeballs(
|
||||
if not get_fastest_addrinfo(
|
||||
host, int_conv(port), int_conv(kwargs.get("timeout"), default=DEF_NETWORKING_TEST_TIMEOUT)
|
||||
):
|
||||
return badParameterResponse(T('Server address "%s:%s" is not valid.') % (host, port), ajax)
|
||||
@@ -2029,6 +2030,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",
|
||||
@@ -2045,6 +2047,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",
|
||||
@@ -2061,6 +2064,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",
|
||||
@@ -2078,6 +2082,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",
|
||||
@@ -2097,6 +2102,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",
|
||||
@@ -2117,6 +2123,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",
|
||||
@@ -2141,6 +2148,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",
|
||||
@@ -2164,6 +2173,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",
|
||||
|
||||
@@ -26,12 +26,11 @@ import socket
|
||||
import ssl
|
||||
import time
|
||||
import threading
|
||||
from typing import Dict
|
||||
|
||||
import sabctools
|
||||
import sabnzbd
|
||||
from sabnzbd.constants import DEF_NETWORKING_SHORT_TIMEOUT
|
||||
from sabnzbd.happyeyeballs import happyeyeballs, family_type
|
||||
from sabnzbd.get_addrinfo import get_fastest_addrinfo, family_type
|
||||
|
||||
TEST_HOSTNAME = "sabnzbd.org"
|
||||
TEST_PORT = 443
|
||||
@@ -44,7 +43,7 @@ NR_CONNECTIONS = 5
|
||||
TIME_LIMIT = 3
|
||||
|
||||
|
||||
def internetspeed_worker(secure_sock: ssl.SSLSocket, socket_speed: Dict[ssl.SSLSocket, float]):
|
||||
def internetspeed_worker(secure_sock: ssl.SSLSocket, socket_speed: dict[ssl.SSLSocket, float]):
|
||||
"""Worker to perform the requests in parallel"""
|
||||
secure_sock.sendall(TEST_REQUEST.encode())
|
||||
empty_buffer = memoryview(sabctools.bytearray_malloc(BUFFER_SIZE))
|
||||
@@ -88,8 +87,8 @@ def internetspeed_interal(family: int = socket.AF_UNSPEC) -> float:
|
||||
context.verify_flags &= ~ssl.VERIFY_X509_STRICT
|
||||
|
||||
try:
|
||||
if not (addrinfo := happyeyeballs(TEST_HOSTNAME, TEST_PORT, DEF_NETWORKING_SHORT_TIMEOUT, family)):
|
||||
# no addrinfo from happyeyeballs, so no connection was possible
|
||||
if not (addrinfo := get_fastest_addrinfo(TEST_HOSTNAME, TEST_PORT, DEF_NETWORKING_SHORT_TIMEOUT, family)):
|
||||
# no addrinfo from get_fastest_addrinfo, so no connection was possible
|
||||
return 0.0 # no speed at all
|
||||
|
||||
for _ in range(NR_CONNECTIONS):
|
||||
|
||||
120
sabnzbd/misc.py
120
sabnzbd/misc.py
@@ -24,7 +24,6 @@ import platform
|
||||
import ssl
|
||||
import sys
|
||||
import logging
|
||||
import functools
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import re
|
||||
@@ -39,9 +38,10 @@ 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
|
||||
from typing import Union, Tuple, Any, AnyStr, Optional, Collection
|
||||
|
||||
import sabnzbd
|
||||
import sabnzbd.getipaddress
|
||||
@@ -55,7 +55,7 @@ from sabnzbd.constants import (
|
||||
)
|
||||
import sabnzbd.config as config
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.decorators import cache_maintainer
|
||||
from sabnzbd.decorators import conditional_cache
|
||||
from sabnzbd.encoding import ubtou, platform_btou
|
||||
from sabnzbd.filesystem import userxbit, make_script_path, remove_file
|
||||
|
||||
@@ -178,7 +178,7 @@ def is_none(inp: Any) -> bool:
|
||||
return not inp or (isinstance(inp, str) and inp.lower() == "none")
|
||||
|
||||
|
||||
def clean_comma_separated_list(inp: Any) -> List[str]:
|
||||
def clean_comma_separated_list(inp: Any) -> list[str]:
|
||||
"""Return a list of stripped values from a string or list, empty ones removed"""
|
||||
result_ids = []
|
||||
if isinstance(inp, str):
|
||||
@@ -190,7 +190,7 @@ def clean_comma_separated_list(inp: Any) -> List[str]:
|
||||
return result_ids
|
||||
|
||||
|
||||
def cmp(x, y):
|
||||
def cmp(x: Any, y: Any) -> int:
|
||||
"""
|
||||
Replacement for built-in function cmp that was removed in Python 3
|
||||
|
||||
@@ -217,7 +217,7 @@ def cat_pp_script_sanitizer(
|
||||
cat: Optional[str] = None,
|
||||
pp: Optional[Union[int, str]] = None,
|
||||
script: Optional[str] = None,
|
||||
) -> Tuple[Optional[Union[int, str]], Optional[str], Optional[str]]:
|
||||
) -> tuple[Optional[Union[int, str]], Optional[str], Optional[str]]:
|
||||
"""Basic sanitizer from outside input to a bit more predictable values"""
|
||||
# * and Default are valid values
|
||||
if safe_lower(cat) in ("", "none"):
|
||||
@@ -234,7 +234,7 @@ def cat_pp_script_sanitizer(
|
||||
return cat, pp, script
|
||||
|
||||
|
||||
def name_to_cat(fname, cat=None):
|
||||
def name_to_cat(fname: str, cat: Optional[str] = None) -> tuple[str, Optional[str]]:
|
||||
"""Retrieve category from file name, but only if "cat" is None."""
|
||||
if cat is None and fname.startswith("{{"):
|
||||
n = fname.find("}}")
|
||||
@@ -246,7 +246,9 @@ def name_to_cat(fname, cat=None):
|
||||
return fname, cat
|
||||
|
||||
|
||||
def cat_to_opts(cat, pp=None, script=None, priority=None) -> Tuple[str, int, str, int]:
|
||||
def cat_to_opts(
|
||||
cat: Optional[str], pp: Optional[int] = None, script: Optional[str] = None, priority: Optional[int] = None
|
||||
) -> tuple[str, int, str, int]:
|
||||
"""Derive options from category, if options not already defined.
|
||||
Specified options have priority over category-options.
|
||||
If no valid category is given, special category '*' will supply default values
|
||||
@@ -279,7 +281,7 @@ def cat_to_opts(cat, pp=None, script=None, priority=None) -> Tuple[str, int, str
|
||||
return cat, pp, script, priority
|
||||
|
||||
|
||||
def pp_to_opts(pp: Optional[int]) -> Tuple[bool, bool, bool]:
|
||||
def pp_to_opts(pp: Optional[int]) -> tuple[bool, bool, bool]:
|
||||
"""Convert numeric processing options to (repair, unpack, delete)"""
|
||||
# Convert the pp to an int
|
||||
pp = int_conv(pp)
|
||||
@@ -331,12 +333,12 @@ _wildcard_to_regex = {
|
||||
}
|
||||
|
||||
|
||||
def wildcard_to_re(text):
|
||||
def wildcard_to_re(text: str) -> str:
|
||||
"""Convert plain wildcard string (with '*' and '?') to regex."""
|
||||
return "".join([_wildcard_to_regex.get(ch, ch) for ch in text])
|
||||
|
||||
|
||||
def convert_filter(text):
|
||||
def convert_filter(text: str) -> Optional[re.Pattern]:
|
||||
"""Return compiled regex.
|
||||
If string starts with re: it's a real regex
|
||||
else quote all regex specials, replace '*' by '.*'
|
||||
@@ -353,7 +355,7 @@ def convert_filter(text):
|
||||
return None
|
||||
|
||||
|
||||
def cat_convert(cat):
|
||||
def cat_convert(cat: Optional[str]) -> Optional[str]:
|
||||
"""Convert indexer's category/group-name to user categories.
|
||||
If no match found, but indexer-cat equals user-cat, then return user-cat
|
||||
If no match found, but the indexer-cat starts with the user-cat, return user-cat
|
||||
@@ -397,7 +399,7 @@ _SERVICE_KEY = "SYSTEM\\CurrentControlSet\\services\\"
|
||||
_SERVICE_PARM = "CommandLine"
|
||||
|
||||
|
||||
def get_serv_parms(service):
|
||||
def get_serv_parms(service: str) -> list[str]:
|
||||
"""Get the service command line parameters from Registry"""
|
||||
service_parms = []
|
||||
try:
|
||||
@@ -416,7 +418,7 @@ def get_serv_parms(service):
|
||||
return service_parms
|
||||
|
||||
|
||||
def set_serv_parms(service, args):
|
||||
def set_serv_parms(service: str, args: list) -> bool:
|
||||
"""Set the service command line parameters in Registry"""
|
||||
serv = []
|
||||
for arg in args:
|
||||
@@ -444,7 +446,7 @@ def get_from_url(url: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def convert_version(text):
|
||||
def convert_version(text: str) -> tuple[int, bool]:
|
||||
"""Convert version string to numerical value and a testversion indicator"""
|
||||
version = 0
|
||||
test = True
|
||||
@@ -551,7 +553,7 @@ def check_latest_version():
|
||||
)
|
||||
|
||||
|
||||
def upload_file_to_sabnzbd(url, fp):
|
||||
def upload_file_to_sabnzbd(url: str, fp: str):
|
||||
"""Function for uploading nzbs to a running SABnzbd instance"""
|
||||
try:
|
||||
fp = urllib.parse.quote_plus(fp)
|
||||
@@ -644,7 +646,7 @@ def to_units(val: Union[int, float], postfix="") -> str:
|
||||
return f"{sign}{val:.{decimals}f}{units}"
|
||||
|
||||
|
||||
def caller_name(skip=2):
|
||||
def caller_name(skip: int = 2) -> str:
|
||||
"""Get a name of a caller in the format module.method
|
||||
Originally used: https://gist.github.com/techtonik/2151727
|
||||
Adapted for speed by using sys calls directly
|
||||
@@ -682,7 +684,7 @@ def exit_sab(value: int):
|
||||
os._exit(value)
|
||||
|
||||
|
||||
def split_host(srv):
|
||||
def split_host(srv: Optional[str]) -> tuple[Optional[str], Optional[int]]:
|
||||
"""Split host:port notation, allowing for IPV6"""
|
||||
if not srv:
|
||||
return None, None
|
||||
@@ -704,7 +706,7 @@ def split_host(srv):
|
||||
return out[0], port
|
||||
|
||||
|
||||
def get_cache_limit():
|
||||
def get_cache_limit() -> str:
|
||||
"""Depending on OS, calculate cache limits.
|
||||
In ArticleCache it will make sure we stay
|
||||
within system limits for 32/64 bit
|
||||
@@ -742,7 +744,7 @@ def get_cache_limit():
|
||||
return ""
|
||||
|
||||
|
||||
def get_windows_memory():
|
||||
def get_windows_memory() -> int:
|
||||
"""Use ctypes to extract available memory"""
|
||||
|
||||
class MEMORYSTATUSEX(ctypes.Structure):
|
||||
@@ -768,15 +770,14 @@ def get_windows_memory():
|
||||
return stat.ullTotalPhys
|
||||
|
||||
|
||||
def get_macos_memory():
|
||||
def get_macos_memory() -> float:
|
||||
"""Use system-call to extract total memory on macOS"""
|
||||
system_output = run_command(["sysctl", "hw.memsize"])
|
||||
return float(system_output.split()[1])
|
||||
|
||||
|
||||
@cache_maintainer(clear_time=3600)
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def get_cpu_name():
|
||||
@conditional_cache(cache_time=3600)
|
||||
def get_cpu_name() -> Optional[str]:
|
||||
"""Find the CPU name (which needs a different method per OS), and return it
|
||||
If none found, return platform.platform()"""
|
||||
|
||||
@@ -876,7 +877,7 @@ def on_cleanup_list(filename: str, skip_nzb: bool = False) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def memory_usage():
|
||||
def memory_usage() -> Optional[str]:
|
||||
try:
|
||||
# Probably only works on Linux because it uses /proc/<pid>/statm
|
||||
with open("/proc/%d/statm" % os.getpid()) as t:
|
||||
@@ -898,7 +899,7 @@ except Exception:
|
||||
_HAVE_STATM = _PAGE_SIZE and memory_usage()
|
||||
|
||||
|
||||
def loadavg():
|
||||
def loadavg() -> str:
|
||||
"""Return 1, 5 and 15 minute load average of host or "" if not supported"""
|
||||
p = ""
|
||||
if not sabnzbd.WINDOWS and not sabnzbd.MACOS:
|
||||
@@ -973,7 +974,7 @@ def bool_conv(value: Any) -> bool:
|
||||
return bool(int_conv(value))
|
||||
|
||||
|
||||
def create_https_certificates(ssl_cert, ssl_key):
|
||||
def create_https_certificates(ssl_cert: str, ssl_key: str) -> bool:
|
||||
"""Create self-signed HTTPS certificates and store in paths 'ssl_cert' and 'ssl_key'"""
|
||||
try:
|
||||
from sabnzbd.utils.certgen import generate_key, generate_local_cert
|
||||
@@ -989,7 +990,7 @@ def create_https_certificates(ssl_cert, ssl_key):
|
||||
return True
|
||||
|
||||
|
||||
def get_all_passwords(nzo) -> List[str]:
|
||||
def get_all_passwords(nzo) -> list[str]:
|
||||
"""Get all passwords, from the NZB, meta and password file. In case a working password is
|
||||
already known, try it first."""
|
||||
passwords = []
|
||||
@@ -1052,7 +1053,7 @@ def is_sample(filename: str) -> bool:
|
||||
return bool(re.search(RE_SAMPLE, filename))
|
||||
|
||||
|
||||
def find_on_path(targets):
|
||||
def find_on_path(targets: Union[str, tuple[str, ...]]) -> Optional[str]:
|
||||
"""Search the PATH for a program and return full path"""
|
||||
if sabnzbd.WINDOWS:
|
||||
paths = os.getenv("PATH").split(";")
|
||||
@@ -1171,7 +1172,7 @@ def is_local_addr(ip: str) -> bool:
|
||||
return is_lan_addr(ip)
|
||||
|
||||
|
||||
def ip_extract() -> List[str]:
|
||||
def ip_extract() -> list[str]:
|
||||
"""Return list of IP addresses of this system"""
|
||||
ips = []
|
||||
program = find_on_path("ip")
|
||||
@@ -1216,7 +1217,7 @@ def get_base_url(url: str) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def match_str(text: AnyStr, matches: Tuple[AnyStr, ...]) -> Optional[AnyStr]:
|
||||
def match_str(text: AnyStr, matches: tuple[AnyStr, ...]) -> Optional[AnyStr]:
|
||||
"""Return first matching element of list 'matches' in 'text', otherwise None"""
|
||||
text = text.lower()
|
||||
for match in matches:
|
||||
@@ -1225,7 +1226,7 @@ def match_str(text: AnyStr, matches: Tuple[AnyStr, ...]) -> Optional[AnyStr]:
|
||||
return None
|
||||
|
||||
|
||||
def recursive_html_escape(input_dict_or_list: Union[Dict[str, Any], List], exclude_items: Tuple[str, ...] = ()):
|
||||
def recursive_html_escape(input_dict_or_list: Union[dict[str, Any], list], exclude_items: tuple[str, ...] = ()):
|
||||
"""Recursively update the input_dict in-place with html-safe values"""
|
||||
if isinstance(input_dict_or_list, (dict, list)):
|
||||
if isinstance(input_dict_or_list, dict):
|
||||
@@ -1246,7 +1247,7 @@ def recursive_html_escape(input_dict_or_list: Union[Dict[str, Any], List], exclu
|
||||
raise ValueError("Expected dict or str, got %s" % type(input_dict_or_list))
|
||||
|
||||
|
||||
def list2cmdline_unrar(lst: List[str]) -> str:
|
||||
def list2cmdline_unrar(lst: list[str]) -> str:
|
||||
"""convert list to a unrar.exe-compatible command string
|
||||
Unrar uses "" instead of \" to escape the double quote"""
|
||||
nlst = []
|
||||
@@ -1260,7 +1261,9 @@ def list2cmdline_unrar(lst: List[str]) -> str:
|
||||
return " ".join(nlst)
|
||||
|
||||
|
||||
def build_and_run_command(command: List[str], windows_unrar_command: bool = False, text_mode: bool = True, **kwargs):
|
||||
def build_and_run_command(
|
||||
command: list[str], windows_unrar_command: bool = False, text_mode: bool = True, **kwargs
|
||||
) -> subprocess.Popen:
|
||||
"""Builds and then runs command with necessary flags and optional
|
||||
IONice and Nice commands. Optional Popen arguments can be supplied.
|
||||
On Windows we need to run our own list2cmdline for Unrar.
|
||||
@@ -1327,7 +1330,7 @@ def build_and_run_command(command: List[str], windows_unrar_command: bool = Fals
|
||||
return subprocess.Popen(command, **popen_kwargs)
|
||||
|
||||
|
||||
def run_command(cmd: List[str], **kwargs):
|
||||
def run_command(cmd: list[str], **kwargs) -> str:
|
||||
"""Run simple external command and return output as a string."""
|
||||
with build_and_run_command(cmd, **kwargs) as p:
|
||||
txt = p.stdout.read()
|
||||
@@ -1360,7 +1363,7 @@ def set_socks5_proxy():
|
||||
socket.socket = socks.socksocket
|
||||
|
||||
|
||||
def set_https_verification(value):
|
||||
def set_https_verification(value: bool) -> bool:
|
||||
"""Set HTTPS-verification state while returning current setting
|
||||
False = disable verification
|
||||
"""
|
||||
@@ -1382,7 +1385,7 @@ def request_repair():
|
||||
pass
|
||||
|
||||
|
||||
def check_repair_request():
|
||||
def check_repair_request() -> bool:
|
||||
"""Return True if repair request found, remove afterwards"""
|
||||
path = os.path.join(cfg.admin_dir.get_path(), REPAIR_REQUEST)
|
||||
if os.path.exists(path):
|
||||
@@ -1515,8 +1518,8 @@ def convert_sorter_settings():
|
||||
min_size: Union[str|int] = "50M"
|
||||
multipart_label: Optional[str] = ""
|
||||
sort_string: str
|
||||
sort_cats: List[str]
|
||||
sort_type: List[int]
|
||||
sort_cats: list[str]
|
||||
sort_type: list[int]
|
||||
is_active: bool = 1
|
||||
}
|
||||
|
||||
@@ -1576,7 +1579,7 @@ def convert_sorter_settings():
|
||||
def convert_history_retention():
|
||||
"""Convert single-option to the split history retention setting"""
|
||||
if "d" in cfg.history_retention():
|
||||
days_to_keep = int_conv(cfg.history_retention().strip()[:-1])
|
||||
days_to_keep = int_conv(cfg.history_retention().strip().removesuffix("d"))
|
||||
cfg.history_retention_option.set("days-delete")
|
||||
cfg.history_retention_number.set(days_to_keep)
|
||||
else:
|
||||
@@ -1586,3 +1589,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) -> list[str]:
|
||||
"""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
|
||||
from typing import Tuple, List, BinaryIO, Optional, Dict, Any, Union, Set
|
||||
import rarfile
|
||||
from typing import BinaryIO, Optional, Any, Union, Callable
|
||||
|
||||
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
|
||||
@@ -105,14 +106,14 @@ def find_programs(curdir: str):
|
||||
|
||||
if sabnzbd.MACOS:
|
||||
if sabnzbd.MACOSARM64:
|
||||
# M1 (ARM64) versions
|
||||
sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, "macos/par2/arm64/par2")
|
||||
# ARM64 version of unrar
|
||||
sabnzbd.newsunpack.RAR_COMMAND = check(curdir, "macos/unrar/arm64/unrar")
|
||||
else:
|
||||
# Regular x64 versions
|
||||
sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, "macos/par2/par2")
|
||||
# Regular x64 version
|
||||
sabnzbd.newsunpack.RAR_COMMAND = check(curdir, "macos/unrar/unrar")
|
||||
# The 7zip binary is universal2
|
||||
|
||||
# The par2 and 7zip binary are universal2
|
||||
sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, "macos/par2/par2")
|
||||
sabnzbd.newsunpack.SEVENZIP_COMMAND = check(curdir, "macos/7zip/7zz")
|
||||
|
||||
if sabnzbd.WINDOWS:
|
||||
@@ -199,7 +200,7 @@ ENV_NZO_FIELDS = [
|
||||
|
||||
def external_processing(
|
||||
extern_proc: str, nzo: NzbObject, complete_dir: str, nicename: str, status: int
|
||||
) -> Tuple[str, int]:
|
||||
) -> tuple[str, int]:
|
||||
"""Run a user postproc script, return console output and exit value"""
|
||||
failure_url = nzo.nzo_info.get("failure", "")
|
||||
# Items can be bool or null, causing POpen to fail
|
||||
@@ -261,12 +262,12 @@ def unpacker(
|
||||
nzo: NzbObject,
|
||||
workdir_complete: str,
|
||||
one_folder: bool,
|
||||
joinables: List[str] = [],
|
||||
rars: List[str] = [],
|
||||
sevens: List[str] = [],
|
||||
ts: List[str] = [],
|
||||
joinables: list[str] = [],
|
||||
rars: list[str] = [],
|
||||
sevens: list[str] = [],
|
||||
ts: list[str] = [],
|
||||
depth: int = 0,
|
||||
) -> Tuple[Union[int, bool], List[str]]:
|
||||
) -> tuple[Union[int, bool], list[str]]:
|
||||
"""Do a recursive unpack from all archives in 'download_path' to 'workdir_complete'"""
|
||||
if depth > 2:
|
||||
# Prevent going to deep down the rabbit-hole
|
||||
@@ -358,7 +359,7 @@ def unpacker(
|
||||
##############################################################################
|
||||
# Filejoin Functions
|
||||
##############################################################################
|
||||
def match_ts(file: str) -> Tuple[str, int]:
|
||||
def match_ts(file: str) -> tuple[str, int]:
|
||||
"""Return True if file is a joinable TS file"""
|
||||
match = TS_RE.search(file)
|
||||
if not match:
|
||||
@@ -373,7 +374,7 @@ def match_ts(file: str) -> Tuple[str, int]:
|
||||
return setname, num
|
||||
|
||||
|
||||
def clean_up_joinables(names: List[str]):
|
||||
def clean_up_joinables(names: list[str]):
|
||||
"""Remove joinable files and their .1 backups"""
|
||||
for name in names:
|
||||
if os.path.exists(name):
|
||||
@@ -402,7 +403,7 @@ def get_seq_number(name: str) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def file_join(nzo: NzbObject, workdir_complete: str, joinables: List[str]) -> Tuple[bool, List[str]]:
|
||||
def file_join(nzo: NzbObject, workdir_complete: str, joinables: list[str]) -> tuple[bool, list[str]]:
|
||||
"""Join and joinable files in 'workdir' to 'workdir_complete' and
|
||||
when successful, delete originals
|
||||
"""
|
||||
@@ -493,7 +494,7 @@ def file_join(nzo: NzbObject, workdir_complete: str, joinables: List[str]) -> Tu
|
||||
##############################################################################
|
||||
# (Un)Rar Functions
|
||||
##############################################################################
|
||||
def rar_unpack(nzo: NzbObject, workdir_complete: str, one_folder: bool, rars: List[str]) -> Tuple[int, List[str]]:
|
||||
def rar_unpack(nzo: NzbObject, workdir_complete: str, one_folder: bool, rars: list[str]) -> tuple[int, list[str]]:
|
||||
"""Unpack multiple sets 'rars' of RAR files from 'download_path' to 'workdir_complete.
|
||||
When 'delete' is set, originals will be deleted.
|
||||
When 'one_folder' is set, all files will be in a single folder
|
||||
@@ -615,7 +616,7 @@ def rar_unpack(nzo: NzbObject, workdir_complete: str, one_folder: bool, rars: Li
|
||||
|
||||
def rar_extract(
|
||||
rarfile_path: str, numrars: int, one_folder: bool, nzo: NzbObject, setname: str, extraction_path: str
|
||||
) -> Tuple[int, List[str], List[str]]:
|
||||
) -> tuple[int, list[str], list[str]]:
|
||||
"""Unpack single rar set 'rarfile' to 'extraction_path',
|
||||
with password tries
|
||||
Return fail==0(ok)/fail==1(error)/fail==2(wrong password)/fail==3(crc-error), new_files, rars
|
||||
@@ -641,21 +642,20 @@ def rar_extract(
|
||||
|
||||
def rar_extract_core(
|
||||
rarfile_path: str, numrars: int, one_folder: bool, nzo: NzbObject, setname: str, extraction_path: str, password: str
|
||||
) -> Tuple[int, List[str], List[str]]:
|
||||
) -> tuple[int, list[str], list[str]]:
|
||||
"""Unpack single rar set 'rarfile_path' to 'extraction_path'
|
||||
Return fail==0(ok)/fail==1(error)/fail==2(wrong password)/fail==3(crc-error), new_files, rars
|
||||
"""
|
||||
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
|
||||
@@ -872,7 +866,7 @@ def rar_extract_core(
|
||||
##############################################################################
|
||||
# 7Zip Functions
|
||||
##############################################################################
|
||||
def unseven(nzo: NzbObject, workdir_complete: str, one_folder: bool, sevens: List[str]):
|
||||
def unseven(nzo: NzbObject, workdir_complete: str, one_folder: bool, sevens: list[str]) -> tuple[bool, list[str]]:
|
||||
"""Unpack multiple sets '7z' of 7Zip files from 'download_path' to 'workdir_complete.
|
||||
When 'delete' is set, originals will be deleted.
|
||||
"""
|
||||
@@ -920,7 +914,7 @@ def unseven(nzo: NzbObject, workdir_complete: str, one_folder: bool, sevens: Lis
|
||||
|
||||
def seven_extract(
|
||||
nzo: NzbObject, seven_path: str, seven_set: str, extraction_path: str, one_folder: bool
|
||||
) -> Tuple[int, List[str]]:
|
||||
) -> tuple[int, list[str]]:
|
||||
"""Unpack single set 'sevenset' to 'extraction_path', with password tries
|
||||
Return fail==0(ok)/fail==1(error)/fail==2(wrong password), new_files, sevens
|
||||
"""
|
||||
@@ -944,7 +938,7 @@ def seven_extract(
|
||||
|
||||
def seven_extract_core(
|
||||
nzo: NzbObject, seven_path: str, extraction_path: str, seven_set: str, one_folder: bool, password: str
|
||||
) -> Tuple[int, List[str]]:
|
||||
) -> tuple[int, list[str]]:
|
||||
"""Unpack single 7Z set 'sevenset' to 'extraction_path'
|
||||
Return fail==0(ok)/fail==1(error)/fail==2(wrong password), new_files, message
|
||||
"""
|
||||
@@ -1010,7 +1004,7 @@ def seven_extract_core(
|
||||
##############################################################################
|
||||
# PAR2 Functions
|
||||
##############################################################################
|
||||
def par2_repair(nzo: NzbObject, setname: str) -> Tuple[bool, bool]:
|
||||
def par2_repair(nzo: NzbObject, setname: str) -> tuple[bool, bool]:
|
||||
"""Try to repair a set, return readd and correctness"""
|
||||
# Check which of the files exists
|
||||
for new_par in nzo.extrapars[setname]:
|
||||
@@ -1123,8 +1117,8 @@ def par2_repair(nzo: NzbObject, setname: str) -> Tuple[bool, bool]:
|
||||
|
||||
|
||||
def par2cmdline_verify(
|
||||
parfile: str, nzo: NzbObject, setname: str, joinables: List[str]
|
||||
) -> Tuple[bool, bool, List[str], List[str]]:
|
||||
parfile: str, nzo: NzbObject, setname: str, joinables: list[str]
|
||||
) -> tuple[bool, bool, list[str], list[str]]:
|
||||
"""Run par2 on par-set"""
|
||||
used_joinables = []
|
||||
used_for_repair = []
|
||||
@@ -1409,7 +1403,7 @@ def par2cmdline_verify(
|
||||
return finished, readd, used_joinables, used_for_repair
|
||||
|
||||
|
||||
def create_env(nzo: Optional[NzbObject] = None, extra_env_fields: Dict[str, Any] = {}) -> Optional[Dict[str, Any]]:
|
||||
def create_env(nzo: Optional[NzbObject] = None, extra_env_fields: dict[str, Any] = {}) -> Optional[dict[str, Any]]:
|
||||
"""Modify the environment for pp-scripts with extra information
|
||||
macOS: Return copy of environment without PYTHONPATH and PYTHONHOME
|
||||
other: return None
|
||||
@@ -1466,7 +1460,7 @@ def create_env(nzo: Optional[NzbObject] = None, extra_env_fields: Dict[str, Any]
|
||||
return env
|
||||
|
||||
|
||||
def rar_volumelist(rarfile_path: str, password: str, known_volumes: List[str]) -> List[str]:
|
||||
def rar_volumelist(rarfile_path: str, password: str, known_volumes: list[str]) -> list[str]:
|
||||
"""List volumes that are part of this rarset
|
||||
and merge them with parsed paths list, removing duplicates.
|
||||
We assume RarFile is right and use parsed paths as backup.
|
||||
@@ -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:
|
||||
@@ -1522,7 +1516,7 @@ def quick_check_set(setname: str, nzo: NzbObject) -> bool:
|
||||
result = True
|
||||
nzf_list = nzo.finished_files
|
||||
renames = {}
|
||||
found_paths: Set[str] = set()
|
||||
found_paths: set[str] = set()
|
||||
|
||||
# Files to ignore
|
||||
ignore_ext = cfg.quick_check_ext_ignore()
|
||||
@@ -1596,7 +1590,7 @@ def quick_check_set(setname: str, nzo: NzbObject) -> bool:
|
||||
return result
|
||||
|
||||
|
||||
def unrar_check(rar: str) -> Tuple[int, bool]:
|
||||
def unrar_check(rar: str) -> tuple[int, bool]:
|
||||
"""Return version number of unrar, where "5.01" returns 501
|
||||
Also return whether an original version is found
|
||||
(version, original)
|
||||
@@ -1684,7 +1678,7 @@ def is_sfv_file(myfile: str) -> bool:
|
||||
return sfv_info_line_counter >= 1
|
||||
|
||||
|
||||
def sfv_check(sfvs: List[str], nzo: NzbObject) -> bool:
|
||||
def sfv_check(sfvs: list[str], nzo: NzbObject) -> bool:
|
||||
"""Verify files using SFV files"""
|
||||
# Update status
|
||||
nzo.status = Status.VERIFYING
|
||||
@@ -1768,7 +1762,7 @@ def sfv_check(sfvs: List[str], nzo: NzbObject) -> bool:
|
||||
return result
|
||||
|
||||
|
||||
def parse_sfv(sfv_filename):
|
||||
def parse_sfv(sfv_filename: str) -> dict[str, bytes]:
|
||||
"""Parse SFV file and return dictionary of crc32's and filenames"""
|
||||
results = {}
|
||||
with open(sfv_filename, mode="rb") as sfv_list:
|
||||
@@ -1793,12 +1787,12 @@ def add_time_left(perc: float, start_time: Optional[float] = None, time_used: Op
|
||||
return ""
|
||||
|
||||
|
||||
def pre_queue(nzo: NzbObject, pp, cat):
|
||||
def pre_queue(nzo: NzbObject, pp: str, cat: str) -> list[Any]:
|
||||
"""Run pre-queue script (if any) and process results.
|
||||
pp and cat are supplied separate since they can change.
|
||||
"""
|
||||
|
||||
def fix(p):
|
||||
def fix(p: Any) -> str:
|
||||
# If added via API, some items can still be "None" (as a string)
|
||||
if is_none(p):
|
||||
return ""
|
||||
@@ -1892,7 +1886,7 @@ class SevenZip:
|
||||
if not is_sevenfile(self.path):
|
||||
raise TypeError("File is not a 7zip file")
|
||||
|
||||
def namelist(self) -> List[str]:
|
||||
def namelist(self) -> list[str]:
|
||||
"""Return list of names in 7Zip"""
|
||||
names = []
|
||||
command = [SEVENZIP_COMMAND, "l", "-p", "-y", "-slt", "-sccUTF-8", self.path]
|
||||
@@ -1915,6 +1909,6 @@ class SevenZip:
|
||||
p.wait()
|
||||
return data
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
"""Close file"""
|
||||
pass
|
||||
|
||||
@@ -32,7 +32,7 @@ import sabnzbd
|
||||
import sabnzbd.cfg
|
||||
from sabnzbd.constants import DEF_NETWORKING_TIMEOUT, NNTP_BUFFER_SIZE, NTTP_MAX_BUFFER_SIZE
|
||||
from sabnzbd.encoding import utob, ubtou
|
||||
from sabnzbd.happyeyeballs import AddrInfo
|
||||
from sabnzbd.get_addrinfo import AddrInfo
|
||||
from sabnzbd.decorators import synchronized, DOWNLOADER_LOCK
|
||||
from sabnzbd.misc import int_conv
|
||||
|
||||
@@ -178,7 +178,7 @@ class NewsWrapper:
|
||||
self.nntp.sock.sendall(command)
|
||||
self.reset_data_buffer()
|
||||
|
||||
def recv_chunk(self) -> Tuple[int, bool, bool]:
|
||||
def recv_chunk(self) -> tuple[int, bool, bool]:
|
||||
"""Receive data, return #bytes, end-of-line, end-of-article"""
|
||||
# Resize the buffer in the extremely unlikely case that it got full
|
||||
if self.data_position == len(self.data):
|
||||
@@ -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?
|
||||
|
||||
@@ -31,7 +31,7 @@ import http.client
|
||||
import json
|
||||
import apprise
|
||||
from threading import Thread
|
||||
from typing import Optional, Dict, Union
|
||||
from typing import Optional, Union
|
||||
|
||||
import sabnzbd
|
||||
import sabnzbd.cfg
|
||||
@@ -41,12 +41,14 @@ from sabnzbd.misc import build_and_run_command, int_conv
|
||||
from sabnzbd.newsunpack import create_env
|
||||
|
||||
if sabnzbd.WINDOWS:
|
||||
windows_major_version = int_conv(platform.version().split(".")[0])
|
||||
|
||||
try:
|
||||
from win32comext.shell import shell
|
||||
from windows_toasts import InteractableWindowsToaster, Toast, ToastActivatedEventArgs, ToastButton
|
||||
|
||||
# Only Windows 10 and above are supported
|
||||
if int_conv(platform.version().split(".")[0]) < 10:
|
||||
if windows_major_version < 10:
|
||||
raise OSError
|
||||
|
||||
# Set a custom AUMID to display the right icon, it is written to the registry by the installer
|
||||
@@ -54,7 +56,7 @@ if sabnzbd.WINDOWS:
|
||||
_HAVE_WINDOWS_TOASTER = True
|
||||
except Exception:
|
||||
# This needs to work on Windows releases
|
||||
if hasattr(sys, "frozen"):
|
||||
if windows_major_version >= 10 and hasattr(sys, "frozen"):
|
||||
raise
|
||||
|
||||
# Sending toasts on non-supported platforms results in segfaults
|
||||
@@ -87,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
|
||||
@@ -157,7 +160,7 @@ def send_notification(
|
||||
msg: str,
|
||||
notification_type: str,
|
||||
job_cat: Optional[str] = None,
|
||||
actions: Optional[Dict[str, str]] = None,
|
||||
actions: Optional[dict[str, str]] = None,
|
||||
):
|
||||
"""Send Notification message"""
|
||||
logging.info("Sending notification: %s - %s (type=%s, job_cat=%s)", title, msg, notification_type, job_cat)
|
||||
@@ -240,7 +243,7 @@ def send_notify_osd(title, message):
|
||||
return error
|
||||
|
||||
|
||||
def send_notification_center(title: str, msg: str, notification_type: str, actions: Optional[Dict[str, str]] = None):
|
||||
def send_notification_center(title: str, msg: str, notification_type: str, actions: Optional[dict[str, str]] = None):
|
||||
"""Send message to macOS Notification Center.
|
||||
Only 1 button is possible on macOS!"""
|
||||
logging.debug("Sending macOS notification")
|
||||
@@ -321,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
|
||||
@@ -526,7 +531,7 @@ def send_nscript(title, msg, notification_type, force=False, test=None):
|
||||
return ""
|
||||
|
||||
|
||||
def send_windows(title: str, msg: str, notification_type: str, actions: Optional[Dict[str, str]] = None):
|
||||
def send_windows(title: str, msg: str, notification_type: str, actions: Optional[dict[str, str]] = None):
|
||||
"""Send Windows notifications, either fancy with buttons (Windows 10+) or basic ones"""
|
||||
# Skip any notifications if ran as a Windows Service, it can result in crashes
|
||||
if sabnzbd.WIN_SERVICE:
|
||||
|
||||
@@ -30,7 +30,7 @@ import zipfile
|
||||
import tempfile
|
||||
|
||||
import cherrypy._cpreqbody
|
||||
from typing import Optional, Dict, Any, Union, List, Tuple
|
||||
from typing import Optional, Any, Union
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd import nzbstuff
|
||||
@@ -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(
|
||||
@@ -151,12 +152,12 @@ def process_nzb_archive_file(
|
||||
priority: Optional[Union[int, str]] = None,
|
||||
nzbname: Optional[str] = None,
|
||||
reuse: Optional[str] = None,
|
||||
nzo_info: Optional[Dict[str, Any]] = None,
|
||||
nzo_info: Optional[dict[str, Any]] = None,
|
||||
url: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
nzo_id: Optional[str] = None,
|
||||
dup_check: bool = True,
|
||||
) -> Tuple[AddNzbFileResult, List[str]]:
|
||||
) -> tuple[AddNzbFileResult, list[str]]:
|
||||
"""Analyse archive and create job(s).
|
||||
Accepts archive files with ONLY nzb/nfo/folder files in it.
|
||||
"""
|
||||
@@ -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:
|
||||
@@ -218,8 +219,12 @@ def process_nzb_archive_file(
|
||||
nzo_id=nzo_id,
|
||||
dup_check=dup_check,
|
||||
)
|
||||
except (sabnzbd.nzbstuff.NzbEmpty, sabnzbd.nzbstuff.NzbRejected):
|
||||
# Empty or fully rejected
|
||||
except (
|
||||
sabnzbd.nzbstuff.NzbEmpty,
|
||||
sabnzbd.nzbstuff.NzbRejected,
|
||||
sabnzbd.nzbstuff.NzbPreQueueRejected,
|
||||
):
|
||||
# Empty or fully rejected (including pre-queue rejections)
|
||||
pass
|
||||
except sabnzbd.nzbstuff.NzbRejectToHistory as err:
|
||||
# Duplicate or unwanted extension directed to history
|
||||
@@ -266,12 +271,12 @@ def process_single_nzb(
|
||||
priority: Optional[Union[int, str]] = None,
|
||||
nzbname: Optional[str] = None,
|
||||
reuse: Optional[str] = None,
|
||||
nzo_info: Optional[Dict[str, Any]] = None,
|
||||
nzo_info: Optional[dict[str, Any]] = None,
|
||||
url: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
nzo_id: Optional[str] = None,
|
||||
dup_check: bool = True,
|
||||
) -> Tuple[AddNzbFileResult, List[str]]:
|
||||
) -> tuple[AddNzbFileResult, list[str]]:
|
||||
"""Analyze file and create a job from it
|
||||
Supports NZB, NZB.BZ2, NZB.GZ and GZ.NZB-in-disguise
|
||||
"""
|
||||
@@ -329,8 +334,11 @@ def process_single_nzb(
|
||||
# Malformed or might not be an NZB file
|
||||
result = AddNzbFileResult.NO_FILES_FOUND
|
||||
except sabnzbd.nzbstuff.NzbRejected:
|
||||
# Rejected as duplicate or by pre-queue script
|
||||
# Rejected as duplicate
|
||||
result = AddNzbFileResult.ERROR
|
||||
except sabnzbd.nzbstuff.NzbPreQueueRejected:
|
||||
# Rejected by pre-queue script - should be silently ignored for URL fetches
|
||||
result = AddNzbFileResult.PREQUEUE_REJECTED
|
||||
except sabnzbd.nzbstuff.NzbRejectToHistory as err:
|
||||
# Duplicate or unwanted extension directed to history
|
||||
sabnzbd.NzbQueue.fail_to_history(err.nzo)
|
||||
|
||||
@@ -23,7 +23,7 @@ import os
|
||||
import logging
|
||||
import time
|
||||
import cherrypy._cpreqbody
|
||||
from typing import List, Dict, Union, Tuple, Optional
|
||||
from typing import Union, Optional
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.nzbstuff import NzbObject, Article
|
||||
@@ -57,8 +57,8 @@ class NzbQueue:
|
||||
|
||||
def __init__(self):
|
||||
self.__top_only: bool = cfg.top_only()
|
||||
self.__nzo_list: List[NzbObject] = []
|
||||
self.__nzo_table: Dict[str, NzbObject] = {}
|
||||
self.__nzo_list: list[NzbObject] = []
|
||||
self.__nzo_table: dict[str, NzbObject] = {}
|
||||
|
||||
def read_queue(self, repair: int):
|
||||
"""Read queue from disk, supporting repair modes
|
||||
@@ -121,7 +121,7 @@ class NzbQueue:
|
||||
pass
|
||||
|
||||
@NzbQueueLocker
|
||||
def scan_jobs(self, all_jobs: bool = False, action: bool = True) -> List[str]:
|
||||
def scan_jobs(self, all_jobs: bool = False, action: bool = True) -> list[str]:
|
||||
"""Scan "incomplete" for missing folders,
|
||||
'all' is True: Include active folders
|
||||
'action' is True, do the recovery action
|
||||
@@ -246,7 +246,9 @@ class NzbQueue:
|
||||
def set_top_only(self, value):
|
||||
self.__top_only = value
|
||||
|
||||
def change_opts(self, nzo_ids: List[str], pp: int) -> int:
|
||||
@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
|
||||
|
||||
def change_script(self, nzo_ids: List[str], script: str) -> int:
|
||||
@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
|
||||
|
||||
def change_cat(self, nzo_ids: List[str], cat: str) -> int:
|
||||
@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)
|
||||
@@ -379,7 +387,7 @@ class NzbQueue:
|
||||
return nzo
|
||||
|
||||
@NzbQueueLocker
|
||||
def remove_multiple(self, nzo_ids: List[str], delete_all_data=True) -> List[str]:
|
||||
def remove_multiple(self, nzo_ids: list[str], delete_all_data=True) -> list[str]:
|
||||
"""Remove multiple jobs from the queue. Also triggers duplicate handling
|
||||
and downloader-disconnect, so intended for external use only!"""
|
||||
removed = []
|
||||
@@ -397,7 +405,7 @@ class NzbQueue:
|
||||
return removed
|
||||
|
||||
@NzbQueueLocker
|
||||
def remove_all(self, search: Optional[str] = None) -> List[str]:
|
||||
def remove_all(self, search: Optional[str] = None) -> list[str]:
|
||||
"""Remove NZO's that match the search-pattern"""
|
||||
nzo_ids = []
|
||||
search = safe_lower(search)
|
||||
@@ -406,7 +414,7 @@ class NzbQueue:
|
||||
nzo_ids.append(nzo_id)
|
||||
return self.remove_multiple(nzo_ids)
|
||||
|
||||
def remove_nzfs(self, nzo_id: str, nzf_ids: List[str]) -> List[str]:
|
||||
def remove_nzfs(self, nzo_id: str, nzf_ids: list[str]) -> list[str]:
|
||||
removed = []
|
||||
if nzo_id in self.__nzo_table:
|
||||
nzo = self.__nzo_table[nzo_id]
|
||||
@@ -433,14 +441,16 @@ class NzbQueue:
|
||||
logging.info("Removed NZFs %s from job %s", removed, nzo.final_name)
|
||||
return removed
|
||||
|
||||
def pause_multiple_nzo(self, nzo_ids: List[str]) -> List[str]:
|
||||
def pause_multiple_nzo(self, nzo_ids: list[str]) -> list[str]:
|
||||
handled = []
|
||||
for nzo_id in nzo_ids:
|
||||
self.pause_nzo(nzo_id)
|
||||
handled.append(nzo_id)
|
||||
return handled
|
||||
|
||||
def pause_nzo(self, nzo_id: str) -> List[str]:
|
||||
@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]
|
||||
@@ -449,7 +459,7 @@ class NzbQueue:
|
||||
handled.append(nzo_id)
|
||||
return handled
|
||||
|
||||
def resume_multiple_nzo(self, nzo_ids: List[str]) -> List[str]:
|
||||
def resume_multiple_nzo(self, nzo_ids: list[str]) -> list[str]:
|
||||
handled = []
|
||||
for nzo_id in nzo_ids:
|
||||
self.resume_nzo(nzo_id)
|
||||
@@ -457,7 +467,7 @@ class NzbQueue:
|
||||
return handled
|
||||
|
||||
@NzbQueueLocker
|
||||
def resume_nzo(self, nzo_id: str) -> List[str]:
|
||||
def resume_nzo(self, nzo_id: str) -> list[str]:
|
||||
handled = []
|
||||
if nzo_id in self.__nzo_table:
|
||||
nzo = self.__nzo_table[nzo_id]
|
||||
@@ -467,7 +477,7 @@ class NzbQueue:
|
||||
return handled
|
||||
|
||||
@NzbQueueLocker
|
||||
def switch(self, item_id_1: str, item_id_2: str) -> Tuple[int, int]:
|
||||
def switch(self, item_id_1: str, item_id_2: str) -> tuple[int, int]:
|
||||
try:
|
||||
# Allow an index as second parameter, easier for some skins
|
||||
i = int(item_id_2)
|
||||
@@ -522,24 +532,24 @@ class NzbQueue:
|
||||
return -1, nzo1.priority
|
||||
|
||||
@NzbQueueLocker
|
||||
def move_nzf_up_bulk(self, nzo_id: str, nzf_ids: List[str], size: int):
|
||||
def move_nzf_up_bulk(self, nzo_id: str, nzf_ids: list[str], size: int):
|
||||
if nzo_id in self.__nzo_table:
|
||||
for _ in range(size):
|
||||
self.__nzo_table[nzo_id].move_up_bulk(nzf_ids)
|
||||
|
||||
@NzbQueueLocker
|
||||
def move_nzf_top_bulk(self, nzo_id: str, nzf_ids: List[str]):
|
||||
def move_nzf_top_bulk(self, nzo_id: str, nzf_ids: list[str]):
|
||||
if nzo_id in self.__nzo_table:
|
||||
self.__nzo_table[nzo_id].move_top_bulk(nzf_ids)
|
||||
|
||||
@NzbQueueLocker
|
||||
def move_nzf_down_bulk(self, nzo_id: str, nzf_ids: List[str], size: int):
|
||||
def move_nzf_down_bulk(self, nzo_id: str, nzf_ids: list[str], size: int):
|
||||
if nzo_id in self.__nzo_table:
|
||||
for _ in range(size):
|
||||
self.__nzo_table[nzo_id].move_down_bulk(nzf_ids)
|
||||
|
||||
@NzbQueueLocker
|
||||
def move_nzf_bottom_bulk(self, nzo_id: str, nzf_ids: List[str]):
|
||||
def move_nzf_bottom_bulk(self, nzo_id: str, nzf_ids: list[str]):
|
||||
if nzo_id in self.__nzo_table:
|
||||
self.__nzo_table[nzo_id].move_bottom_bulk(nzf_ids)
|
||||
|
||||
@@ -660,7 +670,7 @@ class NzbQueue:
|
||||
return -1
|
||||
|
||||
@NzbQueueLocker
|
||||
def set_priority(self, nzo_ids: List[str], priority: int) -> int:
|
||||
def set_priority(self, nzo_ids: list[str], priority: int) -> int:
|
||||
try:
|
||||
n = -1
|
||||
for nzo_id in nzo_ids:
|
||||
@@ -682,7 +692,7 @@ class NzbQueue:
|
||||
return False
|
||||
return False
|
||||
|
||||
def get_articles(self, server: Server, servers: List[Server], fetch_limit: int) -> List[Article]:
|
||||
def get_articles(self, server: Server, servers: list[Server], fetch_limit: int) -> list[Article]:
|
||||
"""Get next article for jobs in the queue
|
||||
Not locked for performance, since it only reads the queue
|
||||
"""
|
||||
@@ -758,10 +768,9 @@ class NzbQueue:
|
||||
nzo.removed_from_queue = True
|
||||
if nzo.precheck:
|
||||
nzo.save_to_disk()
|
||||
# Check result
|
||||
enough, _ = nzo.check_availability_ratio()
|
||||
if enough:
|
||||
# Enough data present, do real download
|
||||
# If not enough data is present, fail flag will be set (also used by postproc)
|
||||
if not nzo.fail_msg:
|
||||
# Send back for real download
|
||||
self.send_back(nzo)
|
||||
return
|
||||
else:
|
||||
@@ -792,13 +801,13 @@ class NzbQueue:
|
||||
def queue_info(
|
||||
self,
|
||||
search: Optional[str] = None,
|
||||
categories: Optional[List[str]] = None,
|
||||
priorities: Optional[List[str]] = None,
|
||||
statuses: Optional[List[str]] = None,
|
||||
nzo_ids: Optional[List[str]] = None,
|
||||
categories: Optional[list[str]] = None,
|
||||
priorities: Optional[list[str]] = None,
|
||||
statuses: Optional[list[str]] = None,
|
||||
nzo_ids: Optional[list[str]] = None,
|
||||
start: int = 0,
|
||||
limit: int = 0,
|
||||
) -> Tuple[int, int, int, List[NzbObject], int, int]:
|
||||
) -> tuple[int, int, int, list[NzbObject], int, int]:
|
||||
"""Return list of queued jobs, optionally filtered and limited by start and limit.
|
||||
Not locked for performance, only reads the queue
|
||||
"""
|
||||
@@ -924,7 +933,7 @@ class NzbQueue:
|
||||
# Don't use nzo.resume() to avoid resetting job warning flags
|
||||
nzo.status = Status.QUEUED
|
||||
|
||||
def get_urls(self) -> List[Tuple[str, NzbObject]]:
|
||||
def get_urls(self) -> list[tuple[str, NzbObject]]:
|
||||
"""Return list of future-types needing URL"""
|
||||
lst = []
|
||||
for nzo_id in self.__nzo_table:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user