mirror of
https://github.com/sabnzbd/sabnzbd.git
synced 2026-02-28 20:46:36 -05:00
Compare commits
24 Commits
master
...
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 |
3
.github/renovate.json
vendored
3
.github/renovate.json
vendored
@@ -23,7 +23,8 @@
|
||||
"jaraco.collections",
|
||||
"sabctools",
|
||||
"paho-mqtt",
|
||||
"werkzeug"
|
||||
"werkzeug",
|
||||
"tavern"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
|
||||
24
.github/workflows/build_release.yml
vendored
24
.github/workflows/build_release.yml
vendored
@@ -31,13 +31,13 @@ jobs:
|
||||
id: windows_binary
|
||||
run: python builder/package.py binary
|
||||
- name: Upload Windows standalone binary (unsigned)
|
||||
uses: actions/upload-artifact@v4
|
||||
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@v1
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: "signed"
|
||||
- name: Upload Windows standalone binary (signed)
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
with:
|
||||
name: Windows standalone binary (signed)
|
||||
@@ -57,13 +57,13 @@ jobs:
|
||||
- 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@v1
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
output-artifact-directory: "signed"
|
||||
- name: Upload Windows installer (signed)
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Windows installer (signed)
|
||||
path: "signed/*-win-setup.exe"
|
||||
@@ -140,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
|
||||
@@ -153,7 +153,7 @@ 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
|
||||
@@ -196,7 +196,7 @@ jobs:
|
||||
timeout 10s snap run sabnzbd --help || true
|
||||
sudo snap remove sabnzbd
|
||||
- name: Upload snap
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Snap package (${{ matrix.linux_arch }})
|
||||
path: ${{ steps.snapcraft.outputs.snap }}
|
||||
@@ -223,15 +223,15 @@ jobs:
|
||||
cache: pip
|
||||
cache-dependency-path: "builder/release-requirements.txt"
|
||||
- name: Download Source distribution artifact
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: Source distribution
|
||||
- name: Download macOS artifact
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: macOS binary
|
||||
- name: Download Windows artifacts
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
pattern: ${{ (contains(github.ref, 'refs/tags/')) && '*signed*' || '*Windows*' }}
|
||||
merge-multiple: true
|
||||
|
||||
6
.github/workflows/integration_testing.yml
vendored
6
.github/workflows/integration_testing.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
builder/SABnzbd.spec
|
||||
tests
|
||||
--line-length=120
|
||||
--target-version=py38
|
||||
--target-version=py39
|
||||
--check
|
||||
--diff
|
||||
|
||||
@@ -31,12 +31,12 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" ]
|
||||
name: ["Linux"]
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- name: macOS
|
||||
os: macos-13
|
||||
os: macos-latest
|
||||
python-version: "3.14"
|
||||
- name: Windows
|
||||
os: windows-2022
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
27
README.mkd
27
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
|
||||
|
||||
|
||||
28
SABnzbd.py
28
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"""
|
||||
@@ -540,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."):
|
||||
|
||||
@@ -28,7 +28,6 @@ import urllib.request
|
||||
import urllib.error
|
||||
import configobj
|
||||
import packaging.version
|
||||
from typing import List
|
||||
|
||||
from constants import (
|
||||
RELEASE_VERSION,
|
||||
@@ -70,7 +69,7 @@ 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, **kwargs):
|
||||
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, **kwargs)
|
||||
output, _ = process.communicate()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@ pyinstaller==6.16.0
|
||||
packaging==25.0
|
||||
pyinstaller-hooks-contrib==2025.9
|
||||
altgraph==0.17.4
|
||||
wrapt==2.0.0
|
||||
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.6.0; sys_platform == 'darwin'
|
||||
PyNaCl==1.6.1; sys_platform == 'darwin'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.container-full-width .container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main-navbar {
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<url type="faq">https://sabnzbd.org/wiki/faq</url>
|
||||
<url type="contact">https://sabnzbd.org/live-chat.html</url>
|
||||
<releases>
|
||||
<release version="4.6.0" date="2025-12-24" type="stable"/>
|
||||
<release version="4.5.5" date="2025-10-24" type="stable"/>
|
||||
<release version="4.5.4" date="2025-10-22" type="stable"/>
|
||||
<release version="4.5.3" date="2025-08-25" type="stable"/>
|
||||
|
||||
@@ -3885,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
|
||||
|
||||
@@ -4080,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
|
||||
|
||||
228
po/main/da.po
228
po/main/da.po
@@ -360,11 +360,11 @@ msgstr "Kvota"
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Quota limit warning (%d%%)"
|
||||
msgstr ""
|
||||
msgstr "Advarsel om kvotegrænse (%d%%)"
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Downloading resumed after quota reset"
|
||||
msgstr ""
|
||||
msgstr "Download genoptaget efter nulstilling af kvote"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/interface.py
|
||||
msgid "Incorrect parameter"
|
||||
@@ -1461,7 +1461,7 @@ msgstr "Før-kø script job markeret som mislykkedet"
|
||||
#. Warning message
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Unwanted Extension in file %s (%s)"
|
||||
msgstr ""
|
||||
msgstr "Uønsket filtype i fil %s (%s)"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "Aborted, cannot be completed"
|
||||
@@ -1478,7 +1478,7 @@ msgstr "DUPLIKERE"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ALTERNATIVE"
|
||||
msgstr ""
|
||||
msgstr "ALTERNATIV"
|
||||
|
||||
#: sabnzbd/nzbstuff.py
|
||||
msgid "ENCRYPTED"
|
||||
@@ -1717,7 +1717,7 @@ msgstr "Efterbehandling mislykkedes for %s (%s)"
|
||||
|
||||
#: sabnzbd/postproc.py
|
||||
msgid "Post-processing was aborted"
|
||||
msgstr ""
|
||||
msgstr "Efterbehandling blev afbrudt"
|
||||
|
||||
#: sabnzbd/postproc.py
|
||||
msgid "Download Failed"
|
||||
@@ -1771,12 +1771,12 @@ msgstr "RAR filer kunne ikke bekræfte"
|
||||
|
||||
#: sabnzbd/postproc.py
|
||||
msgid "Trying RAR renamer"
|
||||
msgstr ""
|
||||
msgstr "Forsøger RAR-omdøbning"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/postproc.py
|
||||
msgid "No matching earlier rar file for %s"
|
||||
msgstr ""
|
||||
msgstr "Ingen matchende tidligere rar-fil for %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/postproc.py
|
||||
@@ -1801,7 +1801,7 @@ msgstr "Fejl ved lukning af system"
|
||||
#. Error message
|
||||
#: sabnzbd/powersup.py
|
||||
msgid "Received a DBus exception %s"
|
||||
msgstr ""
|
||||
msgstr "Modtog en DBus-undtagelse %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/rss.py
|
||||
@@ -2177,7 +2177,7 @@ msgstr "Denne måned"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Selected date range"
|
||||
msgstr ""
|
||||
msgstr "Valgt datointerval"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Today"
|
||||
@@ -2272,7 +2272,7 @@ msgstr "Forum"
|
||||
#. Main menu item
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Live Chat"
|
||||
msgstr ""
|
||||
msgstr "Live chat"
|
||||
|
||||
#. Main menu item
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -2421,7 +2421,7 @@ msgstr "Forsøg igen"
|
||||
#. History page button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Mark as Completed & Remove Temporary Files"
|
||||
msgstr ""
|
||||
msgstr "Markér som fuldført og fjern midlertidige filer"
|
||||
|
||||
#. Queue page table, script selection menu
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -2436,7 +2436,7 @@ msgstr "Fjern alt fra køen?"
|
||||
#. Delete confirmation popup
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Are you sure you want to remove these jobs?"
|
||||
msgstr ""
|
||||
msgstr "Er du sikker på, at du vil fjerne disse jobs?"
|
||||
|
||||
#. Queue page button
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -2461,7 +2461,7 @@ msgstr "Fjern NZB & slet filer"
|
||||
#. Checkbox if job should be added to Archive
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Permanently delete (skip archive)"
|
||||
msgstr ""
|
||||
msgstr "Slet permanent (spring arkiv over)"
|
||||
|
||||
#. Caption for missing articles in Queue
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -2484,7 +2484,7 @@ msgstr "Nulstil kvota nu"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Archive"
|
||||
msgstr ""
|
||||
msgstr "Arkiv"
|
||||
|
||||
#. Button/link hiding History job details
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -2509,7 +2509,7 @@ msgstr "Vis Alt"
|
||||
#. Button showing all archived jobs
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Show Archive"
|
||||
msgstr ""
|
||||
msgstr "Vis arkiv"
|
||||
|
||||
#. History table header - Size of the download quota
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -2560,6 +2560,8 @@ msgid ""
|
||||
"Disconnect all active connections to usenet servers. Connections will be "
|
||||
"reopened after a few seconds if there are items in the queue."
|
||||
msgstr ""
|
||||
"Afbryd alle aktive forbindelser til usenet-servere. Forbindelser genåbnes "
|
||||
"efter få sekunder, hvis der er elementer i køen."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "This will send a test email to your account."
|
||||
@@ -2750,6 +2752,8 @@ msgid ""
|
||||
"Speed up repairs by installing par2cmdline-turbo, it is available for many "
|
||||
"platforms."
|
||||
msgstr ""
|
||||
"Sæt fart på reparationer ved at installere par2cmdline-turbo, det er "
|
||||
"tilgængeligt for mange platforme."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Version"
|
||||
@@ -2825,6 +2829,8 @@ msgid ""
|
||||
"If the SABnzbd Host or Port is exposed to the internet, your current "
|
||||
"settings allow full external access to the SABnzbd interface."
|
||||
msgstr ""
|
||||
"Hvis SABnzbd-værten eller porten er eksponeret på internettet, tillader dine"
|
||||
" nuværende indstillinger fuld ekstern adgang til SABnzbd-grænsefladen."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Security"
|
||||
@@ -2935,6 +2941,10 @@ msgid ""
|
||||
"the Completed Download Folder.<br>Recurring backups can be configured on the"
|
||||
" Scheduling page."
|
||||
msgstr ""
|
||||
"Opret en sikkerhedskopi af konfigurationsfilen og databaser i "
|
||||
"sikkerhedskopimappen.<br>Hvis sikkerhedskopimappen ikke er indstillet, "
|
||||
"oprettes sikkerhedskopien i den fuldførte downloadmappe.<br>Tilbagevendende "
|
||||
"sikkerhedskopier kan konfigureres på planlægningssiden."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Cleanup List"
|
||||
@@ -3049,6 +3059,8 @@ msgstr "Eksterne internetadgang"
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "You can set access rights for systems outside your local network."
|
||||
msgstr ""
|
||||
"Du kan indstille adgangsrettigheder for systemer uden for dit lokale "
|
||||
"netværk."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "No access"
|
||||
@@ -3152,6 +3164,9 @@ msgid ""
|
||||
" again.<br />Applies to both the Temporary and Complete Download Folder.<br "
|
||||
"/>Checked every few minutes."
|
||||
msgstr ""
|
||||
"Download genoptages automatisk, hvis den minimale ledige plads er "
|
||||
"tilgængelig igen.<br />Gælder for både den midlertidige og den fuldførte "
|
||||
"downloadmappe.<br />Kontrolleres hvert par minutter."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Permissions for completed downloads"
|
||||
@@ -3237,6 +3252,9 @@ msgid ""
|
||||
"stored.<br />If left empty, the backup will be created in the Completed "
|
||||
"Download Folder."
|
||||
msgstr ""
|
||||
"Placering, hvor sikkerhedskopier af konfigurationsfilen og databaser "
|
||||
"gemmes.<br />Hvis den efterlades tom, oprettes sikkerhedskopien i den "
|
||||
"fuldførte downloadmappe."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "<i>Data will <b>not</b> be moved. Requires SABnzbd restart!</i>"
|
||||
@@ -3254,7 +3272,7 @@ msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Purge Logs"
|
||||
msgstr ""
|
||||
msgstr "Ryd logfiler"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ".nzb Backup Folder"
|
||||
@@ -3318,6 +3336,8 @@ msgid ""
|
||||
"turned off, all jobs will be marked as Completed even if they are "
|
||||
"incomplete."
|
||||
msgstr ""
|
||||
"Udpak kun og kør scripts på jobs, der bestod verifikationsstadiet. Hvis "
|
||||
"slået fra, markeres alle jobs som fuldført, selvom de er ufuldstændige."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Action when encrypted RAR is downloaded"
|
||||
@@ -3330,19 +3350,19 @@ msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Identical download detection"
|
||||
msgstr ""
|
||||
msgstr "Identisk downloaddetektering"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Detect identical downloads based on name or NZB contents."
|
||||
msgstr ""
|
||||
msgstr "Detektér identiske downloads baseret på navn eller NZB-indhold."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Smart duplicate detection"
|
||||
msgstr ""
|
||||
msgstr "Smart dubletdetektering"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Detect duplicates based on analysis of the filename."
|
||||
msgstr ""
|
||||
msgstr "Detektér dubletter baseret på analyse af filnavnet."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Allow proper releases"
|
||||
@@ -3353,6 +3373,8 @@ msgid ""
|
||||
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
|
||||
"the download name."
|
||||
msgstr ""
|
||||
"Spring smart dubletdetektering over, hvis PROPER, REAL eller REPACK "
|
||||
"registreres i downloadnavnet."
|
||||
|
||||
#. Four way switch for duplicates
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -3371,7 +3393,7 @@ msgstr "Mislykkes job (flyt til historik)"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Abort post-processing"
|
||||
msgstr ""
|
||||
msgstr "Afbryd efterbehandling"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Action when unwanted extension detected"
|
||||
@@ -3379,7 +3401,7 @@ msgstr "Aktion når uønsket extension er fundet"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Action when an unwanted extension is detected"
|
||||
msgstr ""
|
||||
msgstr "Handling når en uønsket filtype registreres"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Unwanted extensions"
|
||||
@@ -3387,17 +3409,19 @@ msgstr "Uønsket extension"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Blacklist"
|
||||
msgstr ""
|
||||
msgstr "Sortliste"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Whitelist"
|
||||
msgstr ""
|
||||
msgstr "Hvidliste"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Select a mode and list all (un)wanted extensions. For example: <b>exe</b> or"
|
||||
" <b>exe, com</b>"
|
||||
msgstr ""
|
||||
"Vælg en tilstand og angiv alle (u)ønskede filtypeendelser. For eksempel: "
|
||||
"<b>exe</b> eller <b>exe, com</b>"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Enable SFV-based checks"
|
||||
@@ -3473,15 +3497,15 @@ msgstr "Afbryd fra usenet-serverne når køen er tom eller sat på pause."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Automatically sort queue"
|
||||
msgstr ""
|
||||
msgstr "Sortér kø automatisk"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Automatically sort jobs in the queue when a new job is added."
|
||||
msgstr ""
|
||||
msgstr "Sortér automatisk jobs i køen, når et nyt job tilføjes."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "The queue will resort every 30 seconds if % downloaded is selected."
|
||||
msgstr ""
|
||||
msgstr "Køen vil sortere hver 30. sekund, hvis % downloadet er valgt."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Propagation delay"
|
||||
@@ -3514,11 +3538,11 @@ msgstr "Erstat mellemrum med understreg i mappenavn."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Replace underscores in folder name"
|
||||
msgstr ""
|
||||
msgstr "Erstat understreger i mappenavn"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Replace underscores with dots in folder names."
|
||||
msgstr ""
|
||||
msgstr "Erstat understreger med punktummer i mappenavne."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Replace dots in Foldername"
|
||||
@@ -3570,19 +3594,23 @@ msgstr "Fjern efter download"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Deobfuscate final filenames"
|
||||
msgstr ""
|
||||
msgstr "Afslør endelige filnavne"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"If filenames of (large) files in the final folder look obfuscated or "
|
||||
"meaningless they will be renamed to the job name."
|
||||
msgstr ""
|
||||
"Hvis filnavne på (store) filer i den endelige mappe ser slørede eller "
|
||||
"meningsløse ud, omdøbes de til jobnavnet."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Additionally, attempts to set the correct file extension based on the file "
|
||||
"signature if the extension is not present or meaningless."
|
||||
msgstr ""
|
||||
"Forsøger derudover at indstille den korrekte filendelse baseret på "
|
||||
"filsignaturen, hvis endelsen ikke er til stede eller meningsløs."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "HTTPS certificate verification"
|
||||
@@ -3597,11 +3625,11 @@ msgstr ""
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "SOCKS5 Proxy"
|
||||
msgstr ""
|
||||
msgstr "SOCKS5-proxy"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use the specified SOCKS5 proxy for all outgoing connections."
|
||||
msgstr ""
|
||||
msgstr "Brug den angivne SOCKS5-proxy til alle udgående forbindelser."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Server"
|
||||
@@ -3714,11 +3742,11 @@ msgstr "Tidsudløb"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Account expiration date"
|
||||
msgstr ""
|
||||
msgstr "Kontoudløbsdato"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Warn 5 days in advance of account expiration date."
|
||||
msgstr ""
|
||||
msgstr "Advar 5 dage før kontoudløbsdato."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
@@ -3726,6 +3754,9 @@ msgid ""
|
||||
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
|
||||
"when quota is spent."
|
||||
msgstr ""
|
||||
"Kvote for denne server, talt fra det tidspunkt, den indstilles. I bytes, "
|
||||
"efterfulgt eventuelt af K,M,G.<br />Kontrolleres hvert par minutter. Besked "
|
||||
"sendes, når kvoten er brugt."
|
||||
|
||||
#. Server's retention time in days
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -3756,6 +3787,13 @@ msgid ""
|
||||
"used. - Disabled: no certification verification. This is not secure at all, "
|
||||
"anyone could intercept your connection. "
|
||||
msgstr ""
|
||||
"Når SSL er aktiveret: - Streng: gennemtving fuld certifikatverifikation. "
|
||||
"Dette er den mest sikre indstilling. - Medium: verificér at certifikatet er "
|
||||
"gyldigt og matcher serveradressen, men tillad lokalt injicerede certifikater"
|
||||
" (f.eks. af firewall eller virusscanner). - Minimal: verificér at "
|
||||
"certifikatet er gyldigt. Dette er ikke sikkert, ethvert gyldigt certifikat "
|
||||
"kan bruges. - Deaktiveret: ingen certifikatverifikation. Dette er slet ikke "
|
||||
"sikkert, enhver kan opfange din forbindelse."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Disabled"
|
||||
@@ -3767,7 +3805,7 @@ msgstr "Minimal"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Medium"
|
||||
msgstr ""
|
||||
msgstr "Medium"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Strict"
|
||||
@@ -3781,13 +3819,15 @@ msgstr "0 er højeste prioritet, 100 er den laveste prioritet"
|
||||
#. Server required tickbox
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Required"
|
||||
msgstr ""
|
||||
msgstr "Påkrævet"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"In case of connection failures, the download queue will be paused for a few "
|
||||
"minutes instead of skipping this server"
|
||||
msgstr ""
|
||||
"I tilfælde af forbindelsesfejl vil downloadkøen blive sat på pause i et par "
|
||||
"minutter i stedet for at springe denne server over"
|
||||
|
||||
#. Server optional tickbox
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -3833,11 +3873,11 @@ msgstr "Personlige notater"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Article availability"
|
||||
msgstr ""
|
||||
msgstr "Artikeltilgængelighed"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "%f% available of %d requested articles"
|
||||
msgstr ""
|
||||
msgstr "%f% tilgængelige af %d anmodede artikler"
|
||||
|
||||
#. Config->Scheduling
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -3898,12 +3938,12 @@ msgstr "Anvend filtre"
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
msgstr "Redigér"
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
msgstr "Næste scanning kl."
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -3985,6 +4025,8 @@ msgid ""
|
||||
"If only the <em>Default</em> category is selected, notifications are enabled"
|
||||
" for jobs in all categories."
|
||||
msgstr ""
|
||||
"Hvis kun kategorien <em>Standard</em> er valgt, er beskeder aktiveret for "
|
||||
"jobs i alle kategorier."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Email Notification On Job Completion"
|
||||
@@ -4161,20 +4203,20 @@ msgstr "Enhed(er) som meddelelse skal sendes til"
|
||||
#. Pushover settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Emergency retry"
|
||||
msgstr ""
|
||||
msgstr "Nødforsøg"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "How often (in seconds) the same notification will be sent"
|
||||
msgstr ""
|
||||
msgstr "Hvor ofte (i sekunder) samme besked vil blive sendt"
|
||||
|
||||
#. Pushover settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Emergency expire"
|
||||
msgstr ""
|
||||
msgstr "Nødudløb"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "How many seconds your notification will continue to be retried"
|
||||
msgstr ""
|
||||
msgstr "Hvor mange sekunder din besked fortsætter med at blive forsøgt"
|
||||
|
||||
#. Header for Pushbullet notification section
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -4217,19 +4259,30 @@ msgid "Enable Apprise notifications"
|
||||
msgstr "Aktiver Apprise-notifikationer"
|
||||
|
||||
#: 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 ""
|
||||
"Send notifikationer via Apprise til næsten enhver notifikationstjeneste"
|
||||
"Send beskeder direkte til enhver beskedtjeneste, du bruger.<br>For eksempel:"
|
||||
" Slack, Discord, Telegram eller enhver tjeneste fra over 100 understøttede "
|
||||
"tjenester!"
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgstr "Standard Apprise-URL'er"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr "Brug standard Apprise-URL'er"
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgstr "Brug komma og/eller mellemrum for at angive flere URL'er."
|
||||
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 definerer tjenesteforbindelsesoplysninger ved hjælp af "
|
||||
"URL'er.<br>Læs Apprise-wikien om, hvordan man definerer URL'en for hver "
|
||||
"tjeneste.<br>Brug komma og/eller mellemrum til at identificere mere end én "
|
||||
"URL."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
@@ -4408,15 +4461,15 @@ msgstr "Sorteringsstreng"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Multi-part Label"
|
||||
msgstr ""
|
||||
msgstr "Fler-dels-etiket"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Show folder"
|
||||
msgstr ""
|
||||
msgstr "Vis mappe"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Season folder"
|
||||
msgstr ""
|
||||
msgstr "Sæsonmappe"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "In folders"
|
||||
@@ -4432,7 +4485,7 @@ msgstr "Job Navn som Filnavn"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Series"
|
||||
msgstr ""
|
||||
msgstr "Serier"
|
||||
|
||||
#. Note for title expression in Sorting that does case adjustment
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -4445,31 +4498,31 @@ msgstr "Forarbejdede resultat"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Any property"
|
||||
msgstr ""
|
||||
msgstr "Enhver egenskab"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "property"
|
||||
msgstr ""
|
||||
msgstr "egenskab"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "GuessIt Property"
|
||||
msgstr ""
|
||||
msgstr "GuessIt-egenskab"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "GuessIt.Property"
|
||||
msgstr ""
|
||||
msgstr "GuessIt.Egenskab"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "GuessIt_Property"
|
||||
msgstr ""
|
||||
msgstr "GuessIt_Egenskab"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Minimum Filesize"
|
||||
msgstr ""
|
||||
msgstr "Minimum filstørrelse"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Affected Job Types"
|
||||
msgstr ""
|
||||
msgstr "Berørte jobtyper"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "All"
|
||||
@@ -4477,15 +4530,15 @@ msgstr "Alle"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Series with air dates"
|
||||
msgstr ""
|
||||
msgstr "Serier med sendetidspunkter"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Movies"
|
||||
msgstr ""
|
||||
msgstr "Film"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Other / Unknown"
|
||||
msgstr ""
|
||||
msgstr "Andet / Ukendt"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
@@ -4497,34 +4550,43 @@ msgid ""
|
||||
"applied.</p><p>More options are available when Advanced Settings is "
|
||||
"checked.<br/>Detailed information can be found on the Wiki.</p>"
|
||||
msgstr ""
|
||||
"<p>Brug sorteringsværktøjer til automatisk at organisere dine fuldførte "
|
||||
"downloads. For eksempel, placer alle episoder fra en serie i en "
|
||||
"sæsonspecifik mappe. Eller placer film i en mappe opkaldt efter "
|
||||
"filmen.</p><p>Sorteringsværktøjer afprøves i den rækkefølge, de vises, og "
|
||||
"kan omarrangeres ved at trække og slippe.<br/>Den første aktive sortering, "
|
||||
"der matcher både den berørte kategori og jobtype, anvendes.</p><p>Flere "
|
||||
"muligheder er tilgængelige, når Avancerede indstillinger er "
|
||||
"markeret.<br/>Detaljeret information kan findes på Wiki'en.</p>"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Add Sorter"
|
||||
msgstr ""
|
||||
msgstr "Tilføj sortering"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Remove Sorter"
|
||||
msgstr ""
|
||||
msgstr "Fjern sortering"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Test Data"
|
||||
msgstr ""
|
||||
msgstr "Testdata"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Quick start"
|
||||
msgstr ""
|
||||
msgstr "Hurtig start"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Move and rename all episodes in the \"tv\" category to a show-specific "
|
||||
"folder"
|
||||
msgstr ""
|
||||
"Flyt og omdøb alle episoder i kategorien \"tv\" til en programspecifik mappe"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
"Move and rename all movies in the \"movies\" category to a movie-specific "
|
||||
"folder"
|
||||
msgstr ""
|
||||
msgstr "Flyt og omdøb alle film i kategorien \"movies\" til en filmspecifik mappe"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
@@ -4635,11 +4697,11 @@ msgstr "Datoformat"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Extra queue columns"
|
||||
msgstr ""
|
||||
msgstr "Ekstra køkolonner"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Extra history columns"
|
||||
msgstr ""
|
||||
msgstr "Ekstra historikkolonner"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "page"
|
||||
@@ -4690,6 +4752,8 @@ msgid ""
|
||||
"Are you sure you want to delete all folders in your Temporary Download "
|
||||
"Folder? This cannot be undone!"
|
||||
msgstr ""
|
||||
"Er du sikker på, at du vil slette alle mapper i din midlertidige "
|
||||
"downloadmappe? Dette kan ikke fortrydes!"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Fetch NZB from URL"
|
||||
@@ -4728,6 +4792,8 @@ msgid ""
|
||||
"When you Retry a job, 'Duplicate Detection' and 'Abort jobs that cannot be "
|
||||
"completed' are disabled."
|
||||
msgstr ""
|
||||
"Når du genforsøger et job, er 'Dubletdetektering' og 'Afbryd jobs, der ikke "
|
||||
"kan fuldføres' deaktiveret."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "View Script Log"
|
||||
@@ -4735,7 +4801,7 @@ msgstr "Vis scriptlog"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Renaming the job will abort Direct Unpack."
|
||||
msgstr ""
|
||||
msgstr "Omdøbning af jobbet vil afbryde direkte udpakning."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
@@ -4759,7 +4825,7 @@ msgstr "Kompakt layout"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Always use full screen width"
|
||||
msgstr ""
|
||||
msgstr "Brug altid fuld skærmbredde"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Tabbed layout <br/>(separate queue and history)"
|
||||
@@ -4779,11 +4845,11 @@ msgstr "Bekræft Historik-fjernelse"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr ""
|
||||
msgstr "Tastaturgenveje"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Shift+Arrow key: Browse Queue and History pages"
|
||||
msgstr ""
|
||||
msgstr "Shift+piletast: Gennemse Kø- og Historiksider"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "How long or untill when do you want to pause? (in English!)"
|
||||
@@ -4806,10 +4872,12 @@ msgid ""
|
||||
"All usernames, passwords and API-keys are automatically removed from the log"
|
||||
" and the included copy of your settings."
|
||||
msgstr ""
|
||||
"Alle brugernavne, adgangskoder og API-nøgler fjernes automatisk fra loggen "
|
||||
"og den inkluderede kopi af dine indstillinger."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Sort by % downloaded <small>Most→Least</small>"
|
||||
msgstr ""
|
||||
msgstr "Sortér efter % downloadet <small>Mest→Mindst</small>"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Sort by Age <small>Oldest→Newest</small>"
|
||||
@@ -4944,11 +5012,11 @@ msgstr "Start guide"
|
||||
#. Tooltip for disabled Next button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Click on Test Server before continuing"
|
||||
msgstr ""
|
||||
msgstr "Klik på Test server før du fortsætter"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Restore backup"
|
||||
msgstr ""
|
||||
msgstr "Gendan sikkerhedskopi"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
@@ -4965,7 +5033,7 @@ msgstr ""
|
||||
#. Error message
|
||||
#: sabnzbd/sorting.py
|
||||
msgid "Failed to rename %s to %s"
|
||||
msgstr ""
|
||||
msgstr "Kunne ikke omdøbe %s til %s"
|
||||
|
||||
#. Error message
|
||||
#: sabnzbd/sorting.py
|
||||
|
||||
@@ -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"
|
||||
@@ -58,6 +58,8 @@ 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
|
||||
@@ -384,11 +386,11 @@ msgstr "Kontingent"
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Quota limit warning (%d%%)"
|
||||
msgstr ""
|
||||
msgstr "Warnung zur Kontingentgrenze (%d%%)"
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Downloading resumed after quota reset"
|
||||
msgstr ""
|
||||
msgstr "Download nach Kontingentzurücksetzung fortgesetzt"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/interface.py
|
||||
msgid "Incorrect parameter"
|
||||
@@ -2478,7 +2480,7 @@ msgstr "Erneut versuchen"
|
||||
#. History page button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Mark as Completed & Remove Temporary Files"
|
||||
msgstr ""
|
||||
msgstr "Als abgeschlossen markieren und temporäre Dateien entfernen"
|
||||
|
||||
#. Queue page table, script selection menu
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -3861,6 +3863,9 @@ msgid ""
|
||||
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
|
||||
"when quota is spent."
|
||||
msgstr ""
|
||||
"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
|
||||
@@ -4367,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 ""
|
||||
@@ -4856,6 +4869,8 @@ 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"
|
||||
@@ -5115,7 +5130,7 @@ msgstr "Assistenten starten"
|
||||
#. Tooltip for disabled Next button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Click on Test Server before continuing"
|
||||
msgstr ""
|
||||
msgstr "Klicken Sie auf \"Server testen\", bevor Sie fortfahren"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Restore backup"
|
||||
|
||||
@@ -373,11 +373,11 @@ msgstr "Cuota"
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Quota limit warning (%d%%)"
|
||||
msgstr ""
|
||||
msgstr "Advertencia de límite de cuota (%d%%)"
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Downloading resumed after quota reset"
|
||||
msgstr ""
|
||||
msgstr "Descarga reanudada después de reiniciar la cuota"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/interface.py
|
||||
msgid "Incorrect parameter"
|
||||
@@ -2469,7 +2469,7 @@ msgstr "Reintentar"
|
||||
#. History page button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Mark as Completed & Remove Temporary Files"
|
||||
msgstr ""
|
||||
msgstr "Marcar como completado y eliminar archivos temporales"
|
||||
|
||||
#. Queue page table, script selection menu
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -3831,6 +3831,9 @@ msgid ""
|
||||
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
|
||||
"when quota is spent."
|
||||
msgstr ""
|
||||
"Cuota para este servidor, contada desde el momento en que se establece. En "
|
||||
"bytes, opcionalmente seguido de K,M,G.<br />Comprobado cada pocos minutos. "
|
||||
"Se envía una notificación cuando se agota la cuota."
|
||||
|
||||
#. Server's retention time in days
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -4338,20 +4341,29 @@ msgid "Enable Apprise notifications"
|
||||
msgstr "Habilitar notificaciones 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 ""
|
||||
"Enviar notificaciones usando Apprise a casi cualquier servicio de "
|
||||
"notificación"
|
||||
"Envíe notificaciones directamente a cualquier servicio de notificaciones que"
|
||||
" utilice.<br>Por ejemplo: Slack, Discord, Telegram o cualquier servicio de "
|
||||
"más de 100 servicios compatibles."
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Default Apprise URLs"
|
||||
msgstr "URLs predeterminadas de Apprise"
|
||||
msgid "Use default Apprise URLs"
|
||||
msgstr "Usar URLs de Apprise predeterminadas"
|
||||
|
||||
#. Apprise settings
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Use a comma and/or space to identify more than one URL."
|
||||
msgstr "Use una coma y/o espacio para identificar más de una 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 define la información de conexión del servicio mediante URLs.<br>Lea"
|
||||
" el wiki de Apprise para saber cómo definir la URL de cada servicio.<br>Use "
|
||||
"una coma y/o espacio para identificar más de una URL."
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid ""
|
||||
@@ -4828,6 +4840,8 @@ msgid ""
|
||||
"Are you sure you want to delete all folders in your Temporary Download "
|
||||
"Folder? This cannot be undone!"
|
||||
msgstr ""
|
||||
"¿Está seguro de que desea eliminar todas las carpetas en su carpeta de "
|
||||
"descargas temporales? ¡Esto no se puede deshacer!"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Fetch NZB from URL"
|
||||
@@ -5087,7 +5101,7 @@ msgstr "Iniciar Asistente"
|
||||
#. Tooltip for disabled Next button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Click on Test Server before continuing"
|
||||
msgstr ""
|
||||
msgstr "Haga clic en Probar servidor antes de continuar"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Restore backup"
|
||||
|
||||
@@ -4175,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
|
||||
|
||||
@@ -4353,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 ""
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2023
|
||||
# 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, 2025\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"
|
||||
@@ -42,7 +42,7 @@ msgstr "לא ניתן למצוא תבניות רשת: %s, מנסה תבנית ת
|
||||
msgid ""
|
||||
"Unable to link to OpenSSL, optimized SSL connection functions will not be "
|
||||
"used."
|
||||
msgstr ""
|
||||
msgstr "לא ניתן לקשר ל-OpenSSL, פונקציות חיבור SSL מותאמות לא יהיו בשימוש."
|
||||
|
||||
#. Error message
|
||||
#: SABnzbd.py
|
||||
@@ -210,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."
|
||||
@@ -343,11 +347,11 @@ msgstr "מכסה"
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Quota limit warning (%d%%)"
|
||||
msgstr ""
|
||||
msgstr "אזהרת מגבלת מכסה (%d%%)"
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Downloading resumed after quota reset"
|
||||
msgstr ""
|
||||
msgstr "ההורדה התחדשה לאחר איפוס מכסה"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/interface.py
|
||||
msgid "Incorrect parameter"
|
||||
@@ -411,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
|
||||
@@ -487,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"
|
||||
@@ -1235,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]"
|
||||
@@ -1315,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
|
||||
@@ -2387,7 +2393,7 @@ msgstr "נסה שוב"
|
||||
#. History page button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Mark as Completed & Remove Temporary Files"
|
||||
msgstr ""
|
||||
msgstr "סמן כהושלם והסר קבצים זמניים"
|
||||
|
||||
#. Queue page table, script selection menu
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -2933,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"
|
||||
@@ -2942,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"
|
||||
@@ -3688,6 +3694,8 @@ msgid ""
|
||||
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
|
||||
"when quota is spent."
|
||||
msgstr ""
|
||||
"מכסה לשרת זה, נספרת מהרגע שהיא נקבעה. בבייטים, באופן אופציונלי ניתן להוסיף "
|
||||
"K,M,G.<br />נבדקת כל כמה דקות. הודעה נשלחת כאשר המכסה מוצתה."
|
||||
|
||||
#. Server's retention time in days
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -3718,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"
|
||||
@@ -3729,7 +3742,7 @@ msgstr "מזערי"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Medium"
|
||||
msgstr ""
|
||||
msgstr "בינוני"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Strict"
|
||||
@@ -4181,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 ""
|
||||
@@ -4655,6 +4678,8 @@ 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"
|
||||
@@ -4913,7 +4938,7 @@ msgstr "התחל אשף"
|
||||
#. Tooltip for disabled Next button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Click on Test Server before continuing"
|
||||
msgstr ""
|
||||
msgstr "לחץ על בדיקת שרת לפני המשך"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Restore backup"
|
||||
|
||||
@@ -42,6 +42,8 @@ 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
|
||||
@@ -369,11 +371,11 @@ msgstr "Quota"
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Quota limit warning (%d%%)"
|
||||
msgstr ""
|
||||
msgstr "Avviso limite quota (%d%%)"
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Downloading resumed after quota reset"
|
||||
msgstr ""
|
||||
msgstr "Download ripreso dopo il ripristino della quota"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/interface.py
|
||||
msgid "Incorrect parameter"
|
||||
@@ -2442,7 +2444,7 @@ msgstr "Riprova"
|
||||
#. History page button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Mark as Completed & Remove Temporary Files"
|
||||
msgstr ""
|
||||
msgstr "Segna come completato e rimuovi i file temporanei"
|
||||
|
||||
#. Queue page table, script selection menu
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -3800,6 +3802,9 @@ msgid ""
|
||||
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
|
||||
"when quota is spent."
|
||||
msgstr ""
|
||||
"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
|
||||
@@ -4304,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 ""
|
||||
@@ -4792,6 +4808,8 @@ 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"
|
||||
@@ -5052,7 +5070,7 @@ msgstr "Avvia procedura guidata"
|
||||
#. Tooltip for disabled Next button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Click on Test Server before continuing"
|
||||
msgstr ""
|
||||
msgstr "Fai clic su Prova server prima di continuare"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Restore backup"
|
||||
|
||||
@@ -4154,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
|
||||
|
||||
@@ -44,6 +44,8 @@ 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
|
||||
@@ -366,11 +368,11 @@ msgstr "Quotum"
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Quota limit warning (%d%%)"
|
||||
msgstr ""
|
||||
msgstr "Waarschuwing quotumlimiet (%d%%)"
|
||||
|
||||
#: sabnzbd/bpsmeter.py
|
||||
msgid "Downloading resumed after quota reset"
|
||||
msgstr ""
|
||||
msgstr "Downloaden hervat na quotumreset"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/interface.py
|
||||
msgid "Incorrect parameter"
|
||||
@@ -2445,7 +2447,7 @@ msgstr "Opnieuw"
|
||||
#. History page button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Mark as Completed & Remove Temporary Files"
|
||||
msgstr ""
|
||||
msgstr "Markeer als voltooid en verwijder tijdelijke bestanden"
|
||||
|
||||
#. Queue page table, script selection menu
|
||||
#: sabnzbd/skintext.py
|
||||
@@ -3802,6 +3804,9 @@ msgid ""
|
||||
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
|
||||
"when quota is spent."
|
||||
msgstr ""
|
||||
"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
|
||||
@@ -4306,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 ""
|
||||
@@ -4789,6 +4805,8 @@ 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"
|
||||
@@ -5048,7 +5066,7 @@ msgstr "Wizard starten"
|
||||
#. Tooltip for disabled Next button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Click on Test Server before continuing"
|
||||
msgstr ""
|
||||
msgstr "Klik op Test server voordat u doorgaat"
|
||||
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Restore backup"
|
||||
|
||||
@@ -4166,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
|
||||
|
||||
@@ -4177,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
|
||||
|
||||
@@ -4198,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
|
||||
|
||||
@@ -4162,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
|
||||
|
||||
@@ -4140,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
|
||||
|
||||
@@ -4153,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
|
||||
|
||||
@@ -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"
|
||||
@@ -4295,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 ""
|
||||
|
||||
390
po/main/zh_CN.po
390
po/main/zh_CN.po
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ cffi==2.0.0
|
||||
pycparser==2.23
|
||||
feedparser==6.0.12
|
||||
configobj==5.0.9
|
||||
cheroot==11.0.0
|
||||
cheroot==11.1.2
|
||||
six==1.17.0
|
||||
cherrypy==18.10.0
|
||||
jaraco.functools==4.3.0
|
||||
@@ -37,7 +37,7 @@ 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.11.0
|
||||
orjson==3.11.3
|
||||
orjson==3.11.4
|
||||
|
||||
# Windows system integration
|
||||
pywin32==311; sys_platform == 'win32'
|
||||
@@ -50,8 +50,8 @@ winrt-Windows.UI.Notifications==3.2.1; sys_platform == 'win32'
|
||||
typing_extensions==4.15.0; sys_platform == 'win32'
|
||||
|
||||
# macOS system calls
|
||||
pyobjc-core==12.0; sys_platform == 'darwin'
|
||||
pyobjc-framework-Cocoa==12.0; 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'
|
||||
@@ -60,14 +60,15 @@ notify2==0.3.1; sys_platform != 'win32' and sys_platform != 'darwin'
|
||||
requests==2.32.5
|
||||
requests-oauthlib==2.0.0
|
||||
PyYAML==6.0.3
|
||||
markdown==3.9
|
||||
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.4
|
||||
idna==3.11
|
||||
urllib3==2.5.0
|
||||
certifi==2025.10.5
|
||||
certifi==2025.11.12
|
||||
oauthlib==3.3.1
|
||||
PyJWT==2.10.1
|
||||
blinker==1.9.0
|
||||
|
||||
208
sabnzbd/api.py
208
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,13 +479,13 @@ 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:
|
||||
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:
|
||||
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
|
||||
@@ -531,7 +531,7 @@ def _api_history_delete(value: str, kwargs: Dict[str, Union[str, List[str]]]) ->
|
||||
return report(_MSG_NO_VALUE)
|
||||
|
||||
|
||||
def _api_history_mark_as_completed(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
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()
|
||||
@@ -550,7 +550,7 @@ def _api_history_mark_as_completed(value: str, kwargs: Dict[str, Union[str, List
|
||||
return report(_MSG_NO_VALUE)
|
||||
|
||||
|
||||
def _api_history_default(value: str, kwargs: Dict[str, Union[str, List[str]]]) -> bytes:
|
||||
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"))
|
||||
@@ -595,7 +595,7 @@ def _api_history_default(value: str, kwargs: Dict[str, Union[str, List[str]]]) -
|
||||
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:
|
||||
@@ -604,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"))
|
||||
@@ -630,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")
|
||||
@@ -648,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())
|
||||
@@ -685,7 +685,7 @@ 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."
|
||||
@@ -718,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"
|
||||
@@ -743,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
|
||||
@@ -758,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", "")
|
||||
@@ -775,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 = []
|
||||
@@ -806,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"]}
|
||||
@@ -834,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"))
|
||||
@@ -911,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:
|
||||
@@ -927,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 (
|
||||
@@ -960,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": {}}
|
||||
|
||||
@@ -999,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()
|
||||
@@ -1210,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:
|
||||
@@ -1228,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:
|
||||
@@ -1244,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:
|
||||
@@ -1278,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:
|
||||
@@ -1295,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))
|
||||
@@ -1444,7 +1444,7 @@ 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)
|
||||
|
||||
@@ -1546,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,
|
||||
@@ -1659,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
|
||||
@@ -1668,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)
|
||||
@@ -1742,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()):
|
||||
@@ -1785,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 = {}
|
||||
|
||||
@@ -1852,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
|
||||
@@ -1931,7 +1931,7 @@ 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])
|
||||
|
||||
@@ -1990,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
|
||||
"""
|
||||
@@ -2019,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,7 +25,7 @@ import logging
|
||||
import re
|
||||
from threading import Thread
|
||||
import ctypes
|
||||
from typing import Tuple, Optional, List
|
||||
from typing import Optional
|
||||
import rarfile
|
||||
|
||||
import sabnzbd
|
||||
@@ -39,7 +39,7 @@ 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
|
||||
@@ -48,7 +48,8 @@ import sabnzbd.par2file as par2file
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -132,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
|
||||
|
||||
@@ -382,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"""
|
||||
|
||||
@@ -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,6 +528,7 @@ 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -237,7 +237,7 @@ class HistoryDB:
|
||||
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]:
|
||||
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(
|
||||
@@ -315,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)]
|
||||
|
||||
@@ -397,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
|
||||
"""
|
||||
@@ -457,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:
|
||||
@@ -554,7 +554,7 @@ def build_history_info(nzo, workdir_complete: str, postproc_time: int, script_ou
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
@@ -70,7 +70,7 @@ def conditional_cache(cache_time: int):
|
||||
Empty results (None, empty collections, empty strings, False, 0) are not cached.
|
||||
If a keyword argument of `force=True` is used, the cache is skipped.
|
||||
|
||||
Unhashable types (such as List) can not be used as an input to the wrapped function in the current implementation!
|
||||
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
|
||||
"""
|
||||
|
||||
@@ -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,7 +25,7 @@ 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
|
||||
@@ -62,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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -36,7 +36,7 @@ import sabnzbd.config as config
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.misc import from_units, helpful_warning, int_conv, MultiAddQueue
|
||||
from sabnzbd.get_addrinfo import get_fastest_addrinfo, AddrInfo
|
||||
from sabnzbd.constants import SOFT_QUEUE_LIMIT
|
||||
from sabnzbd.constants import SOFT_ASSEMBLER_QUEUE_LIMIT
|
||||
|
||||
|
||||
# Timeout penalty in minutes for each cause
|
||||
@@ -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:
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import fnmatch
|
||||
import stat
|
||||
import ctypes
|
||||
import random
|
||||
from typing import Union, List, Tuple, Any, Dict, Optional, BinaryIO
|
||||
from typing import Union, Any, Optional, BinaryIO
|
||||
|
||||
try:
|
||||
import win32api
|
||||
@@ -295,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("/")
|
||||
@@ -314,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:
|
||||
@@ -330,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:
|
||||
@@ -378,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.
|
||||
@@ -484,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
|
||||
@@ -535,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):
|
||||
@@ -543,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):
|
||||
@@ -572,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()
|
||||
@@ -613,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
|
||||
"""
|
||||
@@ -660,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
|
||||
@@ -783,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):
|
||||
@@ -797,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)
|
||||
"""
|
||||
@@ -990,7 +990,7 @@ def remove_all(path: str, pattern: str = "*", keep_folder: bool = False, recursi
|
||||
##############################################################################
|
||||
# Diskfree
|
||||
##############################################################################
|
||||
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"
|
||||
@@ -1024,7 +1024,7 @@ def diskspace_base(dir_to_check: str) -> Tuple[float, float]:
|
||||
|
||||
|
||||
@conditional_cache(cache_time=10)
|
||||
def diskspace(force: bool = False) -> Dict[str, Tuple[float, float]]:
|
||||
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 {
|
||||
@@ -1033,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.
|
||||
"""
|
||||
@@ -1054,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)
|
||||
@@ -1081,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)
|
||||
|
||||
@@ -1129,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)
|
||||
@@ -1196,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:
|
||||
@@ -1253,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
|
||||
"""
|
||||
@@ -1281,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
|
||||
|
||||
@@ -26,7 +26,7 @@ import logging
|
||||
import functools
|
||||
from dataclasses import dataclass
|
||||
from more_itertools import roundrobin
|
||||
from typing import Tuple, Union, Optional
|
||||
from typing import Union, Optional
|
||||
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.constants import DEF_NETWORKING_TIMEOUT
|
||||
@@ -61,7 +61,7 @@ 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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -399,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)
|
||||
@@ -558,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(
|
||||
@@ -906,6 +906,7 @@ SPECIAL_VALUE_LIST = (
|
||||
"max_foldername_length",
|
||||
"url_base",
|
||||
"receive_threads",
|
||||
"assembler_max_queue_size",
|
||||
"switchinterval",
|
||||
"direct_unpack_threads",
|
||||
"selftest_host",
|
||||
|
||||
@@ -26,7 +26,6 @@ import socket
|
||||
import ssl
|
||||
import time
|
||||
import threading
|
||||
from typing import Dict
|
||||
|
||||
import sabctools
|
||||
import sabnzbd
|
||||
@@ -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))
|
||||
|
||||
@@ -41,7 +41,7 @@ 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
|
||||
@@ -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,14 +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])
|
||||
|
||||
|
||||
@conditional_cache(cache_time=3600)
|
||||
def get_cpu_name():
|
||||
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()"""
|
||||
|
||||
@@ -875,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:
|
||||
@@ -897,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:
|
||||
@@ -972,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
|
||||
@@ -988,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 = []
|
||||
@@ -1051,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(";")
|
||||
@@ -1170,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")
|
||||
@@ -1215,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:
|
||||
@@ -1224,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):
|
||||
@@ -1245,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 = []
|
||||
@@ -1259,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.
|
||||
@@ -1326,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()
|
||||
@@ -1359,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
|
||||
"""
|
||||
@@ -1381,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):
|
||||
@@ -1514,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
|
||||
}
|
||||
|
||||
@@ -1575,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:
|
||||
@@ -1615,7 +1619,7 @@ class SABRarFile(rarfile.RarFile):
|
||||
self._file_parser._info_list.append(rar_obj)
|
||||
self._file_parser._info_map[rar_obj.filename.rstrip("/")] = rar_obj
|
||||
|
||||
def filelist(self):
|
||||
def filelist(self) -> list[str]:
|
||||
"""Return list of filenames in archive."""
|
||||
return [f.filename for f in self.infolist() if not f.isdir()]
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import io
|
||||
import shutil
|
||||
import functools
|
||||
import rarfile
|
||||
from typing import Tuple, List, BinaryIO, Optional, Dict, Any, Union, Set
|
||||
from typing import BinaryIO, Optional, Any, Union, Callable
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.encoding import correct_unknown_encoding, ubtou
|
||||
@@ -200,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
|
||||
@@ -262,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
|
||||
@@ -359,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:
|
||||
@@ -374,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):
|
||||
@@ -403,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
|
||||
"""
|
||||
@@ -494,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
|
||||
@@ -616,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
|
||||
@@ -642,7 +642,7 @@ 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
|
||||
"""
|
||||
@@ -866,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.
|
||||
"""
|
||||
@@ -914,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
|
||||
"""
|
||||
@@ -938,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
|
||||
"""
|
||||
@@ -1004,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]:
|
||||
@@ -1117,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 = []
|
||||
@@ -1403,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
|
||||
@@ -1460,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.
|
||||
@@ -1516,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()
|
||||
@@ -1590,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)
|
||||
@@ -1678,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
|
||||
@@ -1762,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:
|
||||
@@ -1787,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 ""
|
||||
@@ -1886,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]
|
||||
@@ -1909,6 +1909,6 @@ class SevenZip:
|
||||
p.wait()
|
||||
return data
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
"""Close file"""
|
||||
pass
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -160,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)
|
||||
@@ -243,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")
|
||||
@@ -531,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
|
||||
@@ -152,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.
|
||||
"""
|
||||
@@ -271,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
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
@@ -247,7 +247,7 @@ class NzbQueue:
|
||||
self.__top_only = value
|
||||
|
||||
@NzbQueueLocker
|
||||
def change_opts(self, nzo_ids: List[str], pp: int) -> int:
|
||||
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:
|
||||
@@ -257,7 +257,7 @@ class NzbQueue:
|
||||
return result
|
||||
|
||||
@NzbQueueLocker
|
||||
def change_script(self, nzo_ids: List[str], script: str) -> int:
|
||||
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):
|
||||
@@ -269,7 +269,7 @@ class NzbQueue:
|
||||
return result
|
||||
|
||||
@NzbQueueLocker
|
||||
def change_cat(self, nzo_ids: List[str], cat: str) -> int:
|
||||
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:
|
||||
@@ -387,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 = []
|
||||
@@ -405,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)
|
||||
@@ -414,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]
|
||||
@@ -441,7 +441,7 @@ 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)
|
||||
@@ -449,7 +449,7 @@ class NzbQueue:
|
||||
return handled
|
||||
|
||||
@NzbQueueLocker
|
||||
def pause_nzo(self, nzo_id: str) -> List[str]:
|
||||
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:
|
||||
@@ -459,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)
|
||||
@@ -467,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]
|
||||
@@ -477,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)
|
||||
@@ -532,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)
|
||||
|
||||
@@ -670,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:
|
||||
@@ -692,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
|
||||
"""
|
||||
@@ -768,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:
|
||||
@@ -802,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
|
||||
"""
|
||||
@@ -934,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:
|
||||
|
||||
@@ -26,7 +26,7 @@ import datetime
|
||||
import threading
|
||||
import functools
|
||||
import difflib
|
||||
from typing import List, Dict, Any, Tuple, Optional, Union, BinaryIO, Set
|
||||
from typing import Any, Optional, Union, BinaryIO
|
||||
|
||||
# SABnzbd modules
|
||||
import sabnzbd
|
||||
@@ -122,14 +122,14 @@ class TryList:
|
||||
|
||||
def __init__(self):
|
||||
# Sets are faster than lists
|
||||
self.try_list: Set[Server] = set()
|
||||
self.try_list: set[Server] = set()
|
||||
|
||||
def server_in_try_list(self, server: Server) -> bool:
|
||||
"""Return whether specified server has been tried"""
|
||||
with TRYLIST_LOCK:
|
||||
return server in self.try_list
|
||||
|
||||
def all_servers_in_try_list(self, all_servers: Set[Server]) -> bool:
|
||||
def all_servers_in_try_list(self, all_servers: set[Server]) -> bool:
|
||||
"""Check if all servers have been tried"""
|
||||
with TRYLIST_LOCK:
|
||||
return all_servers.issubset(self.try_list)
|
||||
@@ -155,7 +155,7 @@ class TryList:
|
||||
"""Save the servers"""
|
||||
return set(server.id for server in self.try_list)
|
||||
|
||||
def __setstate__(self, servers_ids: List[str]):
|
||||
def __setstate__(self, servers_ids: list[str]):
|
||||
self.try_list = set()
|
||||
for server in sabnzbd.Downloader.servers:
|
||||
if server.id in servers_ids:
|
||||
@@ -222,7 +222,7 @@ class Article(TryList):
|
||||
self.nzf.reset_try_list()
|
||||
self.nzf.nzo.reset_try_list()
|
||||
|
||||
def get_article(self, server: Server, servers: List[Server]):
|
||||
def get_article(self, server: Server, servers: list[Server]):
|
||||
"""Return article when appropriate for specified server"""
|
||||
if self.fetcher or self.server_in_try_list(server):
|
||||
return None
|
||||
@@ -347,8 +347,8 @@ class NzbFile(TryList):
|
||||
self.setname: Optional[str] = None
|
||||
|
||||
# Articles are removed from "articles" after being fetched
|
||||
self.articles: List[Article] = []
|
||||
self.decodetable: List[Article] = []
|
||||
self.articles: list[Article] = []
|
||||
self.decodetable: list[Article] = []
|
||||
|
||||
self.bytes: int = file_bytes
|
||||
self.bytes_left: int = file_bytes
|
||||
@@ -427,7 +427,7 @@ class NzbFile(TryList):
|
||||
else:
|
||||
self.crc32 = sabctools.crc32_combine(self.crc32, crc32, length)
|
||||
|
||||
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 articles to be downloaded"""
|
||||
articles = []
|
||||
for article in self.articles:
|
||||
@@ -614,7 +614,7 @@ class NzbObject(TryList):
|
||||
password: Optional[str] = None,
|
||||
nzbname: Optional[str] = None,
|
||||
status: str = Status.QUEUED,
|
||||
nzo_info: Optional[Dict[str, Any]] = None,
|
||||
nzo_info: Optional[dict[str, Any]] = None,
|
||||
reuse: Optional[str] = None,
|
||||
nzo_id: Optional[str] = None,
|
||||
dup_check: bool = True,
|
||||
@@ -677,7 +677,7 @@ class NzbObject(TryList):
|
||||
|
||||
# Bookkeeping values
|
||||
self.meta = {}
|
||||
self.servercount: Dict[str, int] = {} # Dict to keep bytes per server
|
||||
self.servercount: dict[str, int] = {} # Dict to keep bytes per server
|
||||
self.direct_unpacker: Optional[sabnzbd.directunpacker.DirectUnpacker] = None # The DirectUnpacker instance
|
||||
self.bytes: int = 0 # Original bytesize
|
||||
self.bytes_par2: int = 0 # Bytes available for repair
|
||||
@@ -686,15 +686,15 @@ class NzbObject(TryList):
|
||||
self.bytes_missing: int = 0 # Bytes missing
|
||||
self.bad_articles: int = 0 # How many bad (non-recoverable) articles
|
||||
|
||||
self.extrapars: Dict[str, List[NzbFile]] = {} # Holds the extra parfile names for all sets
|
||||
self.par2packs: Dict[str, Dict[str, FilePar2Info]] = {} # Holds the par2info for each file in each set
|
||||
self.md5of16k: Dict[bytes, str] = {} # Holds the md5s of the first-16k of all files in the NZB (hash: name)
|
||||
self.extrapars: dict[str, list[NzbFile]] = {} # Holds the extra parfile names for all sets
|
||||
self.par2packs: dict[str, dict[str, FilePar2Info]] = {} # Holds the par2info for each file in each set
|
||||
self.md5of16k: dict[bytes, str] = {} # Holds the md5s of the first-16k of all files in the NZB (hash: name)
|
||||
|
||||
self.files: List[NzbFile] = [] # List of all NZFs
|
||||
self.files_table: Dict[str, NzbFile] = {} # Dictionary of NZFs indexed using NZF_ID
|
||||
self.renames: Dict[str, str] = {} # Dictionary of all renamed files
|
||||
self.files: list[NzbFile] = [] # List of all NZFs
|
||||
self.files_table: dict[str, NzbFile] = {} # Dictionary of NZFs indexed using NZF_ID
|
||||
self.renames: dict[str, str] = {} # Dictionary of all renamed files
|
||||
|
||||
self.finished_files: List[NzbFile] = [] # List of all finished NZFs
|
||||
self.finished_files: list[NzbFile] = [] # List of all finished NZFs
|
||||
|
||||
# The current status of the nzo eg:
|
||||
# Queued, Downloading, Repairing, Unpacking, Failed, Complete
|
||||
@@ -703,9 +703,9 @@ class NzbObject(TryList):
|
||||
self.avg_bps_freq = 0
|
||||
self.avg_bps_total = 0
|
||||
|
||||
self.first_articles: List[Article] = []
|
||||
self.first_articles: list[Article] = []
|
||||
self.first_articles_count = 0
|
||||
self.saved_articles: Set[Article] = set()
|
||||
self.saved_articles: set[Article] = set()
|
||||
self.nzo_id: Optional[str] = None
|
||||
|
||||
self.duplicate: Optional[str] = None
|
||||
@@ -727,11 +727,11 @@ class NzbObject(TryList):
|
||||
# Store one line responses for filejoin/par2/unrar here for history display
|
||||
self.action_line = ""
|
||||
# Store the results from various filejoin/par2/unrar stages
|
||||
self.unpack_info: Dict[str, List[str]] = {}
|
||||
self.unpack_info: dict[str, list[str]] = {}
|
||||
# Stores one line containing the last failure
|
||||
self.fail_msg = ""
|
||||
# Stores various info about the nzo to be
|
||||
self.nzo_info: Dict[str, Any] = nzo_info or {}
|
||||
self.nzo_info: dict[str, Any] = nzo_info or {}
|
||||
|
||||
self.next_save = None
|
||||
self.save_timeout = None
|
||||
@@ -1522,7 +1522,7 @@ class NzbObject(TryList):
|
||||
if hasattr(self, "direct_unpacker") and self.direct_unpacker:
|
||||
self.direct_unpacker.abort()
|
||||
|
||||
def check_availability_ratio(self) -> Tuple[bool, float]:
|
||||
def check_availability_ratio(self) -> tuple[bool, float]:
|
||||
"""Determine if we are still meeting the required ratio"""
|
||||
availability_ratio = req_ratio = cfg.req_completion_rate()
|
||||
|
||||
@@ -1625,7 +1625,7 @@ class NzbObject(TryList):
|
||||
self.nzo_info[bad_article_type] += 1
|
||||
self.bad_articles += 1
|
||||
|
||||
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]:
|
||||
articles = []
|
||||
nzf_remove_list = []
|
||||
|
||||
@@ -1679,7 +1679,7 @@ class NzbObject(TryList):
|
||||
return articles
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
def move_top_bulk(self, nzf_ids: List[str]):
|
||||
def move_top_bulk(self, nzf_ids: list[str]):
|
||||
self.cleanup_nzf_ids(nzf_ids)
|
||||
if nzf_ids:
|
||||
target = list(range(len(nzf_ids)))
|
||||
@@ -1899,7 +1899,7 @@ class NzbObject(TryList):
|
||||
logging.debug("Saving attributes %s for %s", attribs, self.final_name)
|
||||
save_data(attribs, ATTRIB_FILE, self.admin_path, silent=True)
|
||||
|
||||
def load_attribs(self) -> Tuple[Optional[str], Optional[int], Optional[str]]:
|
||||
def load_attribs(self) -> tuple[Optional[str], Optional[int], Optional[str]]:
|
||||
"""Load saved attributes and return them to be parsed"""
|
||||
attribs = load_data(ATTRIB_FILE, self.admin_path, remove=False)
|
||||
logging.debug("Loaded attributes %s for %s", attribs, self.final_name)
|
||||
@@ -1922,7 +1922,7 @@ class NzbObject(TryList):
|
||||
return attribs["cat"], attribs["pp"], attribs["script"]
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
def build_pos_nzf_table(self, nzf_ids: List[str]) -> Dict[int, NzbFile]:
|
||||
def build_pos_nzf_table(self, nzf_ids: list[str]) -> dict[int, NzbFile]:
|
||||
pos_nzf_table = {}
|
||||
for nzf_id in nzf_ids:
|
||||
if nzf_id in self.files_table:
|
||||
@@ -1933,7 +1933,7 @@ class NzbObject(TryList):
|
||||
return pos_nzf_table
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
def cleanup_nzf_ids(self, nzf_ids: List[str]):
|
||||
def cleanup_nzf_ids(self, nzf_ids: list[str]):
|
||||
for nzf_id in nzf_ids[:]:
|
||||
if nzf_id in self.files_table:
|
||||
if self.files_table[nzf_id] not in self.files:
|
||||
@@ -2150,7 +2150,7 @@ def create_work_name(name: str) -> str:
|
||||
return name.strip()
|
||||
|
||||
|
||||
def scan_password(name: str) -> Tuple[str, Optional[str]]:
|
||||
def scan_password(name: str) -> tuple[str, Optional[str]]:
|
||||
"""Get password (if any) from the title"""
|
||||
if "http://" in name or "https://" in name:
|
||||
return name, None
|
||||
|
||||
@@ -25,7 +25,7 @@ import re
|
||||
import struct
|
||||
import sabctools
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional, Tuple
|
||||
from typing import Optional
|
||||
|
||||
from sabnzbd.constants import MEBI
|
||||
from sabnzbd.encoding import correct_unknown_encoding
|
||||
@@ -71,7 +71,7 @@ def is_par2_file(filepath: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def analyse_par2(name: str, filepath: Optional[str] = None) -> Tuple[str, int, int]:
|
||||
def analyse_par2(name: str, filepath: Optional[str] = None) -> tuple[str, int, int]:
|
||||
"""Check if file is a par2-file and determine vol/block
|
||||
return setname, vol, block
|
||||
setname is empty when not a par2 file
|
||||
@@ -103,7 +103,7 @@ def analyse_par2(name: str, filepath: Optional[str] = None) -> Tuple[str, int, i
|
||||
return setname, vol, block
|
||||
|
||||
|
||||
def parse_par2_file(fname: str, md5of16k: Dict[bytes, str]) -> Tuple[str, Dict[str, FilePar2Info]]:
|
||||
def parse_par2_file(fname: str, md5of16k: dict[bytes, str]) -> tuple[str, dict[str, FilePar2Info]]:
|
||||
"""Get the hash table and the first-16k hash table from a PAR2 file
|
||||
Return as dictionary, indexed on names or hashes for the first-16 table
|
||||
The input md5of16k is modified in place and thus not returned!
|
||||
|
||||
@@ -27,7 +27,7 @@ import re
|
||||
import gc
|
||||
import queue
|
||||
import rarfile
|
||||
from typing import List, Optional, Tuple
|
||||
from typing import Optional
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.newsunpack import (
|
||||
@@ -107,7 +107,7 @@ class PostProcessor(Thread):
|
||||
super().__init__()
|
||||
|
||||
# This history queue is simply used to log what active items to display in the web_ui
|
||||
self.history_queue: List[NzbObject] = []
|
||||
self.history_queue: list[NzbObject] = []
|
||||
self.load()
|
||||
|
||||
# Fast-queue for jobs already finished by DirectUnpack
|
||||
@@ -195,7 +195,7 @@ class PostProcessor(Thread):
|
||||
self.slow_queue.put(None)
|
||||
self.fast_queue.put(None)
|
||||
|
||||
def cancel_pp(self, nzo_ids: List[str]) -> Optional[bool]:
|
||||
def cancel_pp(self, nzo_ids: list[str]) -> Optional[bool]:
|
||||
"""Abort Direct Unpack and change the status, so that the PP is canceled"""
|
||||
result = None
|
||||
for nzo in self.history_queue:
|
||||
@@ -220,10 +220,10 @@ class PostProcessor(Thread):
|
||||
def get_queue(
|
||||
self,
|
||||
search: Optional[str] = None,
|
||||
categories: Optional[List[str]] = None,
|
||||
statuses: Optional[List[str]] = None,
|
||||
nzo_ids: Optional[List[str]] = None,
|
||||
) -> List[NzbObject]:
|
||||
categories: Optional[list[str]] = None,
|
||||
statuses: Optional[list[str]] = None,
|
||||
nzo_ids: Optional[list[str]] = None,
|
||||
) -> list[NzbObject]:
|
||||
"""Return list of NZOs that still need to be processed.
|
||||
Optionally filtered by the search terms"""
|
||||
re_search = None
|
||||
@@ -693,7 +693,7 @@ def process_job(nzo: NzbObject) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def prepare_extraction_path(nzo: NzbObject) -> Tuple[str, str, Sorter, bool, Optional[str]]:
|
||||
def prepare_extraction_path(nzo: NzbObject) -> tuple[str, str, Sorter, bool, Optional[str]]:
|
||||
"""Based on the information that we have, generate
|
||||
the extraction path and create the directory.
|
||||
Separated so it can be called from DirectUnpacker
|
||||
@@ -757,7 +757,7 @@ def prepare_extraction_path(nzo: NzbObject) -> Tuple[str, str, Sorter, bool, Opt
|
||||
return tmp_workdir_complete, workdir_complete, file_sorter, not create_job_dir, marker_file
|
||||
|
||||
|
||||
def parring(nzo: NzbObject) -> Tuple[bool, bool]:
|
||||
def parring(nzo: NzbObject) -> tuple[bool, bool]:
|
||||
"""Perform par processing. Returns: (par_error, re_add)"""
|
||||
logging.info("Starting verification and repair of %s", nzo.final_name)
|
||||
par_error = False
|
||||
@@ -876,7 +876,7 @@ def try_sfv_check(nzo: NzbObject) -> Optional[bool]:
|
||||
return True
|
||||
|
||||
|
||||
def try_rar_check(nzo: NzbObject, rars: List[str]) -> bool:
|
||||
def try_rar_check(nzo: NzbObject, rars: list[str]) -> bool:
|
||||
"""Attempt to verify set using the RARs
|
||||
Return True if verified, False when failed
|
||||
When setname is '', all RAR files will be used, otherwise only the matching one
|
||||
@@ -1221,7 +1221,7 @@ def remove_samples(path: str):
|
||||
logging.info("Skipping sample-removal, false-positive")
|
||||
|
||||
|
||||
def rename_and_collapse_folder(oldpath: str, newpath: str, files: List[str]) -> List[str]:
|
||||
def rename_and_collapse_folder(oldpath: str, newpath: str, files: list[str]) -> list[str]:
|
||||
"""Rename folder, collapsing when there's just a single subfolder
|
||||
oldpath --> newpath OR oldpath/subfolder --> newpath
|
||||
Modify list of filenames accordingly
|
||||
@@ -1273,7 +1273,7 @@ def del_marker(path: str):
|
||||
logging.info("Traceback: ", exc_info=True)
|
||||
|
||||
|
||||
def remove_from_list(name: Optional[str], lst: List[str]):
|
||||
def remove_from_list(name: Optional[str], lst: list[str]):
|
||||
if name:
|
||||
for n in range(len(lst)):
|
||||
if lst[n].endswith(name):
|
||||
|
||||
@@ -686,10 +686,15 @@ SKIN_TEXT = {
|
||||
"explain-pushbullet_device": TT("Device to which message should be sent"), #: Pushbullet settings
|
||||
"opt-apprise_enable": TT("Enable Apprise notifications"), #: Apprise settings
|
||||
"explain-apprise_enable": TT(
|
||||
"Send notifications using Apprise to almost any notification service"
|
||||
"Send notifications directly to any notification service you use.<br>"
|
||||
"For example: Slack, Discord, Telegram, or any service from over 100 supported services!"
|
||||
), #: Apprise settings
|
||||
"opt-apprise_urls": TT("Use default Apprise URLs"), #: Apprise settings
|
||||
"explain-apprise_urls": TT(
|
||||
"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."
|
||||
), #: Apprise settings
|
||||
"opt-apprise_urls": TT("Default Apprise URLs"), #: Apprise settings
|
||||
"explain-apprise_urls": TT("Use a comma and/or space to identify more than one URL."), #: Apprise settings
|
||||
"explain-apprise_extra_urls": TT(
|
||||
"Override the default URLs for specific notification types below, if desired."
|
||||
), #: Apprise settings
|
||||
|
||||
@@ -25,7 +25,7 @@ import re
|
||||
import guessit
|
||||
from rebulk.match import MatchesDict
|
||||
from string import whitespace, punctuation
|
||||
from typing import Optional, Union, List, Tuple, Dict
|
||||
from typing import Optional, Union
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.filesystem import (
|
||||
@@ -179,7 +179,7 @@ class Sorter:
|
||||
self.get_showdescriptions()
|
||||
self.get_date()
|
||||
|
||||
def format_series_numbers(self, numbers: Union[int, List[int]], info_name: str):
|
||||
def format_series_numbers(self, numbers: Union[int, list[int]], info_name: str):
|
||||
"""Format the numbers in both plain and alternative (zero-padded) format and set as showinfo"""
|
||||
# Guessit returns multiple episodes or seasons as a list of integers, single values as int
|
||||
if isinstance(numbers, int):
|
||||
@@ -283,7 +283,7 @@ class Sorter:
|
||||
if ends_in_file(sort_string):
|
||||
extension = True
|
||||
if sort_string.endswith(".%ext"):
|
||||
sort_string = sort_string[:-5] # Strip '.%ext' off the end; other %ext may remain in sort_string
|
||||
sort_string = sort_string.removesuffix(".%ext") # Strip '.%ext' off the end; other %ext may remain
|
||||
if self.is_season_pack:
|
||||
# Create a record of the filename part of the sort_string
|
||||
_, self.season_pack_setname = os.path.split(sort_string)
|
||||
@@ -417,7 +417,7 @@ class Sorter:
|
||||
# The normpath function translates "" to "." which results in an incorrect path
|
||||
return os.path.normpath(path) if path else path
|
||||
|
||||
def _rename_season_pack(self, files: List[str], base_path: str, all_job_files: List[str] = []) -> bool:
|
||||
def _rename_season_pack(self, files: list[str], base_path: str, all_job_files: list[str] = []) -> bool:
|
||||
success = False
|
||||
for f in files:
|
||||
f_name, f_ext = os.path.splitext(os.path.basename(f))
|
||||
@@ -476,7 +476,7 @@ class Sorter:
|
||||
)
|
||||
return success
|
||||
|
||||
def _rename_sequential(self, sequential_files: Dict[str, str], base_path: str) -> bool:
|
||||
def _rename_sequential(self, sequential_files: dict[str, str], base_path: str) -> bool:
|
||||
success = False
|
||||
for index, f in sequential_files.items():
|
||||
filepath = self._to_filepath(f, base_path)
|
||||
@@ -515,7 +515,7 @@ class Sorter:
|
||||
and os.stat(filepath).st_size >= self.rename_limit
|
||||
)
|
||||
|
||||
def rename(self, files: List[str], base_path: str) -> Tuple[str, bool]:
|
||||
def rename(self, files: list[str], base_path: str) -> tuple[str, bool]:
|
||||
if not self.rename_files:
|
||||
return move_to_parent_directory(base_path)
|
||||
|
||||
@@ -607,7 +607,7 @@ def ends_in_file(path: str) -> bool:
|
||||
return bool(RE_ENDEXT.search(path) or RE_ENDFN.search(path))
|
||||
|
||||
|
||||
def move_to_parent_directory(workdir: str) -> Tuple[str, bool]:
|
||||
def move_to_parent_directory(workdir: str) -> tuple[str, bool]:
|
||||
"""Move all files under 'workdir' into 'workdir/..'"""
|
||||
# Determine 'folder'/..
|
||||
workdir = os.path.abspath(os.path.normpath(workdir))
|
||||
@@ -658,7 +658,7 @@ def guess_what(name: str) -> MatchesDict:
|
||||
|
||||
if digit_fix:
|
||||
# Unfix the title
|
||||
guess["title"] = guess.get("title", "")[len(digit_fix) :]
|
||||
guess["title"] = guess.get("title", "").removeprefix(digit_fix)
|
||||
|
||||
# Handle weird anime episode notation, that results in the episode number ending up as the episode title
|
||||
if (
|
||||
@@ -696,7 +696,7 @@ def guess_what(name: str) -> MatchesDict:
|
||||
return guess
|
||||
|
||||
|
||||
def path_subst(path: str, mapping: List[Tuple[str, str]]) -> str:
|
||||
def path_subst(path: str, mapping: list[tuple[str, str]]) -> str:
|
||||
"""Replace the sort string elements in the path with the real values provided by the mapping;
|
||||
non-elements are copied verbatim."""
|
||||
# Added ugly hack to prevent %ext from being masked by %e
|
||||
@@ -719,7 +719,7 @@ def path_subst(path: str, mapping: List[Tuple[str, str]]) -> str:
|
||||
|
||||
def get_titles(
|
||||
nzo: Optional[NzbObject], guess: Optional[MatchesDict], jobname: str, titleing: bool = False
|
||||
) -> Tuple[str, str, str]:
|
||||
) -> tuple[str, str, str]:
|
||||
"""Get the title from NZB metadata or jobname, and return it in various formats. Formatting
|
||||
mostly deals with working around quirks of Python's str.title(). NZB metadata is used as-is,
|
||||
further processing done only for info obtained from guessit or the jobname."""
|
||||
@@ -779,7 +779,7 @@ def replace_word(word_input: str, one: str, two: str) -> str:
|
||||
return word_input
|
||||
|
||||
|
||||
def get_descriptions(nzo: Optional[NzbObject], guess: Optional[MatchesDict]) -> Tuple[str, str, str]:
|
||||
def get_descriptions(nzo: Optional[NzbObject], guess: Optional[MatchesDict]) -> tuple[str, str, str]:
|
||||
"""Try to get an episode title or similar description from the NZB metadata or jobname, e.g.
|
||||
'Download This' in Show.S01E23.Download.This.1080p.HDTV.x264 and return multiple formats"""
|
||||
ep_name = None
|
||||
@@ -836,7 +836,7 @@ def strip_path_elements(path: str) -> str:
|
||||
return "\\\\" + path if is_unc else path
|
||||
|
||||
|
||||
def rename_similar(folder: str, skip_ext: str, name: str, skipped_files: Optional[List[str]] = None):
|
||||
def rename_similar(folder: str, skip_ext: str, name: str, skipped_files: Optional[list[str]] = None):
|
||||
"""Rename all other files in the 'folder' hierarchy after 'name'
|
||||
and move them to the root of 'folder'.
|
||||
Files having extension 'skip_ext' will be moved, but not renamed.
|
||||
@@ -921,7 +921,7 @@ def eval_sort(sort_string: str, job_name: str, multipart_label: str = "") -> Opt
|
||||
return sorted_path
|
||||
|
||||
|
||||
def check_for_multiple(files: List[str]) -> Optional[Dict[str, str]]:
|
||||
def check_for_multiple(files: list[str]) -> Optional[dict[str, str]]:
|
||||
"""Return a dictionary of a single set of files that look like parts of
|
||||
a multi-part post. Takes a limited set of indicators from guessit into
|
||||
consideration and only accepts numerical sequences. The files argument
|
||||
|
||||
@@ -32,7 +32,7 @@ from http.client import IncompleteRead, HTTPResponse
|
||||
from mailbox import Message
|
||||
from threading import Thread
|
||||
import base64
|
||||
from typing import Tuple, Optional, Union, List, Dict, Any
|
||||
from typing import Optional, Union, Any
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.constants import (
|
||||
@@ -57,7 +57,7 @@ from sabnzbd.nzbstuff import NzbObject, NzbRejected, NzbRejectToHistory
|
||||
class URLGrabber(Thread):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.queue: queue.Queue[Tuple[Optional[str], Optional[NzbObject]]] = queue.Queue()
|
||||
self.queue: queue.Queue[tuple[Optional[str], Optional[NzbObject]]] = queue.Queue()
|
||||
self.shutdown = False
|
||||
|
||||
def add(self, url: str, future_nzo: NzbObject, when: Optional[int] = None):
|
||||
@@ -417,9 +417,9 @@ def add_url(
|
||||
priority: Optional[Union[int, str]] = None,
|
||||
nzbname: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
nzo_info: Optional[Dict[str, Any]] = None,
|
||||
nzo_info: Optional[dict[str, Any]] = None,
|
||||
dup_check: bool = True,
|
||||
) -> Tuple[AddNzbFileResult, List[str]]:
|
||||
) -> tuple[AddNzbFileResult, list[str]]:
|
||||
"""Add NZB based on a URL, attributes optional"""
|
||||
if not url.lower().startswith("http"):
|
||||
return AddNzbFileResult.NO_FILES_FOUND, []
|
||||
|
||||
@@ -7,10 +7,9 @@ Functions to check if the path filesystem uses FAT
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
from typing import List
|
||||
|
||||
|
||||
def getcmdoutput(cmd: List[str]) -> List[str]:
|
||||
def getcmdoutput(cmd: list[str]) -> list[str]:
|
||||
"""execute cmd, and return a list of output lines"""
|
||||
subprocess_kwargs = {
|
||||
"bufsize": 0,
|
||||
|
||||
@@ -7,7 +7,7 @@ import sys
|
||||
import logging
|
||||
import time
|
||||
|
||||
_DUMP_DATA_SIZE = 10 * 1024 * 1024
|
||||
BUFFERSIZE = 16 * 1024 * 1024
|
||||
|
||||
|
||||
def diskspeedmeasure(dirname: str) -> float:
|
||||
@@ -16,39 +16,57 @@ def diskspeedmeasure(dirname: str) -> float:
|
||||
Then divide bytes written by time passed
|
||||
In case of problems (ie non-writable dir or file), return 0.0
|
||||
"""
|
||||
dump_data = os.urandom(_DUMP_DATA_SIZE)
|
||||
start = time.time()
|
||||
maxtime = 0.5 # sec
|
||||
maxtime = 1 # sec
|
||||
total_written = 0
|
||||
filename = os.path.join(dirname, "outputTESTING.txt")
|
||||
|
||||
# Prepare the whole buffer now for better write performance later
|
||||
# This is done before timing starts to exclude buffer creation from measurement
|
||||
buffer = os.urandom(BUFFERSIZE)
|
||||
|
||||
try:
|
||||
# Use low-level I/O
|
||||
try:
|
||||
fp_testfile = os.open(filename, os.O_CREAT | os.O_WRONLY | os.O_BINARY, 0o777)
|
||||
except AttributeError:
|
||||
fp_testfile = os.open(filename, os.O_CREAT | os.O_WRONLY, 0o777)
|
||||
fp_testfile = os.open(
|
||||
filename,
|
||||
os.O_CREAT | os.O_WRONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_SYNC", 0),
|
||||
0o777,
|
||||
)
|
||||
|
||||
overall_start = time.perf_counter()
|
||||
maxtime = overall_start + 1
|
||||
total_time = 0.0
|
||||
|
||||
# Start looping
|
||||
total_time = 0.0
|
||||
while total_time < maxtime:
|
||||
start = time.time()
|
||||
os.write(fp_testfile, dump_data)
|
||||
for i in range(1, 5):
|
||||
# Stop writing next buffer block, if time exceeds limit
|
||||
if time.perf_counter() >= maxtime:
|
||||
break
|
||||
# Prepare the data chunk outside of timing
|
||||
data_chunk = buffer * (i**2)
|
||||
|
||||
# Only measure the actual write and sync operations
|
||||
write_start = time.perf_counter()
|
||||
total_written += os.write(fp_testfile, data_chunk)
|
||||
os.fsync(fp_testfile)
|
||||
total_time += time.time() - start
|
||||
total_written += _DUMP_DATA_SIZE
|
||||
total_time += time.perf_counter() - write_start
|
||||
|
||||
# Have to use low-level close
|
||||
os.close(fp_testfile)
|
||||
# Remove the file
|
||||
os.remove(filename)
|
||||
|
||||
except OSError:
|
||||
# Could not write, so ... report 0.0
|
||||
logging.debug("Failed to measure disk speed on %s", dirname)
|
||||
return 0.0
|
||||
|
||||
megabyte_per_second = round(total_written / total_time / 1024 / 1024, 1)
|
||||
logging.debug("Disk speed of %s = %.2f MB/s (in %.2f seconds)", dirname, megabyte_per_second, time.time() - start)
|
||||
logging.debug(
|
||||
"Disk speed of %s = %.2f MB/s (in %.2f seconds)",
|
||||
dirname,
|
||||
megabyte_per_second,
|
||||
time.perf_counter() - overall_start,
|
||||
)
|
||||
return megabyte_per_second
|
||||
|
||||
|
||||
@@ -68,7 +86,7 @@ if __name__ == "__main__":
|
||||
try:
|
||||
SPEED = max(diskspeedmeasure(DIRNAME), diskspeedmeasure(DIRNAME))
|
||||
if SPEED:
|
||||
print("Disk writing speed: %.2f Mbytes per second" % SPEED)
|
||||
print("Disk writing speed: %.2f MBytes per second" % SPEED)
|
||||
else:
|
||||
print("No measurement possible. Check that directory is writable.")
|
||||
except Exception:
|
||||
|
||||
@@ -8,7 +8,6 @@ Note: extension always contains a leading dot
|
||||
import puremagic
|
||||
import os
|
||||
import sys
|
||||
from typing import List, Tuple
|
||||
from sabnzbd.filesystem import get_ext, RAR_RE
|
||||
import sabnzbd.cfg as cfg
|
||||
|
||||
@@ -260,7 +259,7 @@ ALL_EXT = tuple(set(POPULAR_EXT + DOWNLOAD_EXT))
|
||||
ALL_EXT = tuple(["." + i for i in ALL_EXT])
|
||||
|
||||
|
||||
def all_extensions() -> Tuple[str, ...]:
|
||||
def all_extensions() -> tuple[str, ...]:
|
||||
"""returns tuple with ALL (standard + userdef) extensions (including leading dot in extension)"""
|
||||
user_defined_extensions = tuple(["." + i for i in cfg.ext_rename_ignore()])
|
||||
return ALL_EXT + user_defined_extensions
|
||||
@@ -272,7 +271,7 @@ def has_popular_extension(file_path: str) -> bool:
|
||||
return file_extension in all_extensions() or RAR_RE.match(file_extension)
|
||||
|
||||
|
||||
def all_possible_extensions(file_path: str) -> List[str]:
|
||||
def all_possible_extensions(file_path: str) -> list[str]:
|
||||
"""returns a list with all possible extensions (with leading dot) for given file_path as reported by puremagic"""
|
||||
extension_list = []
|
||||
for i in puremagic.magic_file(file_path):
|
||||
|
||||
@@ -161,6 +161,7 @@ class SysTrayIconThread(Thread):
|
||||
pass
|
||||
|
||||
def restart(self, hwnd, msg, wparam, lparam):
|
||||
self.notify_id = None
|
||||
self.refresh_icon()
|
||||
return True
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
# You MUST use double quotes (so " and not ')
|
||||
# Do not forget to update the appdata file for every major release!
|
||||
|
||||
__version__ = "4.6.0-dev"
|
||||
__version__ = "4.6.0Alpha1"
|
||||
__baseline__ = "unknown"
|
||||
|
||||
@@ -23,7 +23,6 @@ from tests.testhelper import *
|
||||
import shutil
|
||||
import zipfile
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
import sabnzbd.cfg
|
||||
from sabnzbd.constants import (
|
||||
@@ -87,7 +86,7 @@ class TestConfig:
|
||||
return zip_buffer.getvalue()
|
||||
|
||||
@staticmethod
|
||||
def create_and_verify_backup(admin_dir: str, must_haves: List[str]):
|
||||
def create_and_verify_backup(admin_dir: str, must_haves: list[str]):
|
||||
# Create the backup
|
||||
config_backup_path = config.create_config_backup()
|
||||
assert os.path.exists(config_backup_path)
|
||||
|
||||
@@ -68,7 +68,10 @@ class TestWiki:
|
||||
config_diff = {}
|
||||
for url in ("general", "switches", "special"):
|
||||
config_tree = lxml.html.fromstring(
|
||||
requests.get("http://%s:%s/config/%s/" % (SAB_HOST, SAB_PORT, url)).content
|
||||
requests.get(
|
||||
"http://%s:%s/config/%s/" % (SAB_HOST, SAB_PORT, url),
|
||||
headers={"User-Agent": "SABnzbd/%s" % sabnzbd.__version__},
|
||||
).content
|
||||
)
|
||||
# Have to remove some decorating stuff and empty values
|
||||
config_labels = [
|
||||
@@ -79,7 +82,10 @@ class TestWiki:
|
||||
# Parse the version info to get the right Wiki version
|
||||
version = re.search(r"(\d+\.\d+)\.(\d+)([a-zA-Z]*)(\d*)", sabnzbd.__version__).group(1)
|
||||
wiki_tree = lxml.html.fromstring(
|
||||
requests.get("https://sabnzbd.org/wiki/configuration/%s/%s" % (version, url)).content
|
||||
requests.get(
|
||||
"https://sabnzbd.org/wiki/configuration/%s/%s" % (version, url),
|
||||
headers={"User-Agent": "SABnzbd/%s" % sabnzbd.__version__},
|
||||
).content
|
||||
)
|
||||
|
||||
# Special-page needs different label locator
|
||||
|
||||
@@ -25,7 +25,6 @@ import sys
|
||||
|
||||
from math import ceil
|
||||
from random import sample
|
||||
from typing import List
|
||||
|
||||
from tavern.core import run
|
||||
from warnings import warn
|
||||
@@ -172,7 +171,7 @@ class ApiTestFunctions:
|
||||
self._get_api_json("queue", extra_args={"name": "purge", "del_files": del_files})
|
||||
assert len(self._get_api_json("queue")["queue"]["slots"]) == 0
|
||||
|
||||
def _get_files(self, nzo_id: str) -> List[str]:
|
||||
def _get_files(self, nzo_id: str) -> list[str]:
|
||||
files_json = self._get_api_json("get_files", extra_args={"value": nzo_id})
|
||||
assert "files" in files_json
|
||||
return [file["nzf_id"] for file in files_json["files"]]
|
||||
|
||||
@@ -76,7 +76,7 @@ def get_local_ip(protocol_version: IPProtocolVersion) -> Optional[str]:
|
||||
sending any traffic but already prefills what would be the sender ip address.
|
||||
"""
|
||||
s: Optional[socket.socket] = None
|
||||
address_to_connect_to: Optional[Tuple[str, int]] = None
|
||||
address_to_connect_to: Optional[tuple[str, int]] = None
|
||||
if protocol_version == IPProtocolVersion.IPV4:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
# Google DNS IPv4
|
||||
|
||||
@@ -22,7 +22,7 @@ import io
|
||||
import os
|
||||
import time
|
||||
from http.client import RemoteDisconnected
|
||||
from typing import BinaryIO, Optional, Dict, List
|
||||
from typing import BinaryIO, Optional
|
||||
|
||||
import pytest
|
||||
from random import choice, randint
|
||||
@@ -149,13 +149,13 @@ def get_api_result(mode, host=SAB_HOST, port=SAB_PORT, extra_arguments={}):
|
||||
return r.text
|
||||
|
||||
|
||||
def create_nzb(nzb_dir: str, metadata: Optional[Dict[str, str]] = None) -> str:
|
||||
def create_nzb(nzb_dir: str, metadata: Optional[dict[str, str]] = None) -> str:
|
||||
"""Create NZB from directory using SABNews"""
|
||||
nzb_dir_full = os.path.join(SAB_DATA_DIR, nzb_dir)
|
||||
return tests.sabnews.create_nzb(nzb_dir=nzb_dir_full, metadata=metadata)
|
||||
|
||||
|
||||
def create_and_read_nzb_fp(nzbdir: str, metadata: Optional[Dict[str, str]] = None) -> BinaryIO:
|
||||
def create_and_read_nzb_fp(nzbdir: str, metadata: Optional[dict[str, str]] = None) -> BinaryIO:
|
||||
"""Create NZB, return data and delete file"""
|
||||
# Create NZB-file to import
|
||||
nzb_path = create_nzb(nzbdir, metadata)
|
||||
@@ -332,7 +332,7 @@ class DownloadFlowBasics(SABnzbdBaseTest):
|
||||
self.selenium_wrapper(self.driver.find_element, By.CSS_SELECTOR, ".btn.btn-success").click()
|
||||
self.no_page_crash()
|
||||
|
||||
def download_nzb(self, nzb_dir: str, file_output: List[str], dir_name_as_job_name: bool = False):
|
||||
def download_nzb(self, nzb_dir: str, file_output: list[str], dir_name_as_job_name: bool = False):
|
||||
# Verify if the server was setup before we start
|
||||
self.is_server_configured()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user