mirror of
https://github.com/sabnzbd/sabnzbd.git
synced 2026-01-06 06:28:45 -05:00
Compare commits
73 Commits
4.1.x
...
4.2.0Alpha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
210f254f63 | ||
|
|
ecdccda1ce | ||
|
|
ed66ac91e0 | ||
|
|
e571165c15 | ||
|
|
1513664b5f | ||
|
|
0132d81c43 | ||
|
|
8d32da8b27 | ||
|
|
b5fbc8af86 | ||
|
|
d0166b5a5c | ||
|
|
ada77d6970 | ||
|
|
9f8758b242 | ||
|
|
5ca629ebea | ||
|
|
f9f3820652 | ||
|
|
08e61ecf19 | ||
|
|
d15f0cafce | ||
|
|
1b85253940 | ||
|
|
b329ff007e | ||
|
|
f6918d598a | ||
|
|
0cdfdd82d4 | ||
|
|
de3649dba4 | ||
|
|
9ba975ac44 | ||
|
|
2b0ea92da8 | ||
|
|
b79a1e973d | ||
|
|
1be4cf986d | ||
|
|
18c4226b90 | ||
|
|
07a5ba6857 | ||
|
|
6252d02498 | ||
|
|
11cf8c5397 | ||
|
|
1f3f4a4c85 | ||
|
|
5bfe5967db | ||
|
|
476fa25a12 | ||
|
|
792bd20fa2 | ||
|
|
26f3cd064e | ||
|
|
0556a84cbc | ||
|
|
090871625a | ||
|
|
12dedb7cff | ||
|
|
d4187e93b2 | ||
|
|
1beb1aafd8 | ||
|
|
67c4703bab | ||
|
|
d850c9c6e3 | ||
|
|
38e07b0859 | ||
|
|
ea10785160 | ||
|
|
16803b9f17 | ||
|
|
b9a0cf3f76 | ||
|
|
71ff6b14da | ||
|
|
a98b3c7e85 | ||
|
|
7259c25ece | ||
|
|
5e7154530b | ||
|
|
d501cc0a23 | ||
|
|
45606285ec | ||
|
|
a5e860a60f | ||
|
|
d93333f9ef | ||
|
|
3bd68b630a | ||
|
|
97c93a0858 | ||
|
|
8b15fe0d6a | ||
|
|
2d22a5f5b9 | ||
|
|
be63fbaada | ||
|
|
dc6b338266 | ||
|
|
9e36971151 | ||
|
|
9dc08d16b6 | ||
|
|
182a5412a5 | ||
|
|
cb15c79e4b | ||
|
|
06e6e81779 | ||
|
|
938b833954 | ||
|
|
596f069e46 | ||
|
|
e16a7f06d6 | ||
|
|
2947f2c2ff | ||
|
|
0d33039b72 | ||
|
|
682f8227fd | ||
|
|
dc1675073d | ||
|
|
d71f4eb802 | ||
|
|
e55756469d | ||
|
|
3764b705a8 |
30
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
30
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Bug report
|
||||
description: >
|
||||
Did you discover a bug in SABnzbd? Report it here!
|
||||
If you are not 100% certain this is a bug please go to our forums, Reddit or Discord server first.
|
||||
labels:
|
||||
- Bug
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: SABnzbd version
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Operating system
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Using Docker image
|
||||
options:
|
||||
- linuxserver
|
||||
- hotio
|
||||
- Other
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: Include error logs directly or link to extended logs on https://pastebin.com/
|
||||
validations:
|
||||
required: true
|
||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Support forum
|
||||
url: https://forums.sabnzbd.org/
|
||||
about: Support questions can be asked on our forums, Reddit or Discord server.
|
||||
- name: Discord
|
||||
url: https://discord.gg/KQzDe7fvNU
|
||||
about: Support questions can be asked on our forums, Reddit or Discord server.
|
||||
- name: Reddit - r/sabnzbd
|
||||
url: https://www.reddit.com/r/sabnzbd
|
||||
about: Support questions can be asked on our forums, Reddit or Discord server.
|
||||
10
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
10
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
name: Feature request
|
||||
description: What new feature would you like to have added to SABnzbd?
|
||||
labels:
|
||||
- Feature request
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
validations:
|
||||
required: true
|
||||
5
.github/renovate.json
vendored
5
.github/renovate.json
vendored
@@ -8,12 +8,12 @@
|
||||
"before 8am on Monday"
|
||||
],
|
||||
"ignorePaths": [
|
||||
"tests/**",
|
||||
".github/workflows/**"
|
||||
],
|
||||
"pip_requirements": {
|
||||
"fileMatch": [
|
||||
"requirements.txt",
|
||||
"tests/requirements.txt",
|
||||
"builder/requirements.txt",
|
||||
"builder/release-requirements.txt",
|
||||
"builder/osx/requirements.txt"
|
||||
@@ -21,7 +21,8 @@
|
||||
},
|
||||
"ignoreDeps": [
|
||||
"jaraco.text",
|
||||
"sabctools"
|
||||
"sabctools",
|
||||
"werkzeug"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
|
||||
21
.github/workflows/build_release.yml
vendored
21
.github/workflows/build_release.yml
vendored
@@ -13,10 +13,10 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python 3.11 (64bit)
|
||||
- name: Set up Python 3.12 (64bit)
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
architecture: "x64"
|
||||
- name: Cache Python virtualenv (64bit)
|
||||
uses: syphar/restore-virtualenv@v1.3
|
||||
@@ -63,8 +63,8 @@ jobs:
|
||||
run: |
|
||||
python --version
|
||||
python -m pip install --upgrade pip wheel
|
||||
pip install --upgrade -r requirements.txt
|
||||
pip install --upgrade -r builder/requirements.txt
|
||||
pip install --upgrade -r requirements.txt --no-dependencies
|
||||
pip install --upgrade -r builder/requirements.txt --no-dependencies
|
||||
- name: Build Windows standalone binary (32bit and legacy)
|
||||
run: python builder/package.py binary
|
||||
- name: Upload Windows standalone binary (32bit and legacy)
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
# We need the official Python, because the GA ones only support newer macOS versions
|
||||
# The deployment target is picked up by the Python build tools automatically
|
||||
# If updated, make sure to also set LSMinimumSystemVersion in SABnzbd.spec
|
||||
PYTHON_VERSION: "3.11.5"
|
||||
PYTHON_VERSION: "3.12.0"
|
||||
MACOSX_DEPLOYMENT_TARGET: "10.9"
|
||||
# We need to force compile for universal2 support
|
||||
CFLAGS: -arch x86_64 -arch arm64
|
||||
@@ -112,19 +112,18 @@ jobs:
|
||||
# We have to manually take a few steps:
|
||||
# 1. Because building cryptography is hard, and we cannot force pip to fetch universal2 version we
|
||||
# first install the x86 version (and it's dependencies) and then manually fetch the universal2 build
|
||||
# https://github.com/pyca/cryptography/issues/5918
|
||||
# 2. Due to PyObjC we cannot run pip on the main requirements without installing dependencies
|
||||
# 3. We need to build the PyInstaller bootloader:
|
||||
# https://github.com/pypa/pip/issues/5453
|
||||
# 2. We need to build the PyInstaller bootloader:
|
||||
# https://github.com/pyinstaller/pyinstaller/issues/6235
|
||||
if: steps.cache-virtualenv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
python3 --version
|
||||
pip3 install --upgrade pip wheel
|
||||
|
||||
pip3 install --upgrade -r requirements.txt --no-binary cffi
|
||||
|
||||
pip3 install --upgrade -r requirements.txt --no-binary cffi --no-dependencies
|
||||
|
||||
pip3 uninstall cryptography -y
|
||||
pip3 download -r builder/osx/requirements.txt --platform macosx_10_12_universal2 --only-binary :all: --no-deps --dest .
|
||||
pip3 download -r builder/osx/requirements.txt --platform macosx_10_12_universal2 --only-binary :all: --no-dependencies --dest .
|
||||
pip3 install -r builder/osx/requirements.txt --no-cache-dir --no-index --find-links .
|
||||
|
||||
PYINSTALLER_COMPILE_BOOTLOADER=1 pip3 install --upgrade -r builder/requirements.txt --no-binary pyinstaller --no-dependencies
|
||||
|
||||
6
.github/workflows/integration_testing.yml
vendored
6
.github/workflows/integration_testing.yml
vendored
@@ -31,18 +31,18 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
python-architecture: ["x64"]
|
||||
name: ["Linux"]
|
||||
os: [ubuntu-20.04]
|
||||
include:
|
||||
- name: macOS
|
||||
os: macos-latest
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
python-architecture: "x64"
|
||||
- name: Windows
|
||||
os: windows-latest
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
python-architecture: "x64"
|
||||
- name: Windows (32bit)
|
||||
os: windows-latest
|
||||
|
||||
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
- name: Push/pull Transifex translations
|
||||
if: env.TX_TOKEN
|
||||
# Add --translation to the push command in order to update Transifex using local translation edits
|
||||
# However, this prevents modifying existing translations in Transifex as they will be overwritten by the push!
|
||||
# However, this prevents modifying existing translations in Transifex as they will be overwritten by the push!
|
||||
run: |
|
||||
curl -o- https://raw.githubusercontent.com/transifex/cli/master/install.sh | bash
|
||||
./tx push --source
|
||||
./tx push --source
|
||||
./tx pull --all --force
|
||||
- name: Compile translations to validate them
|
||||
run: |
|
||||
|
||||
49
README.mkd
49
README.mkd
@@ -1,37 +1,24 @@
|
||||
Release Notes - SABnzbd 4.1.0
|
||||
Release Notes - SABnzbd 4.2.0 Alpha 2
|
||||
=========================================================
|
||||
|
||||
## Changes since 4.0.3
|
||||
- Added a dark mode for the Config, Login, and Wizard pages.
|
||||
- Added multi-select to the History.
|
||||
- Show the number of items in post-processing when in Tabbed mode.
|
||||
- Added option `verify_xff_header` to include `X-Forwarded-For` when
|
||||
validating if connections should be accepted when using a proxy.
|
||||
- Added option to purge log files from the Folders Config page.
|
||||
- Moved `Server IP address selection` and `On failure, try
|
||||
alternative NZB` to Special settings.
|
||||
- Special setting `ipv6_servers` changed to on/off.
|
||||
- Only use 7zip to unpack `.zip` files.
|
||||
- Windows: Added option `enable_multipar` to use par2cmdline-turbo
|
||||
instead of Multipar for verification and repair. It is faster,
|
||||
but on Windows it can fail on special (UTF8) filenames.
|
||||
- macOS: Switched to par2cmdline-turbo for verification and repair.
|
||||
- Linux: Detect more recent versions of 7zip.
|
||||
- Windows: Use `All Users` locations during installation of shortcuts.
|
||||
- Windows/macOS: Updated Python to 3.11.5, 7Zip to 23.01 and
|
||||
UnRar to 6.23. All these updates include security fixes.
|
||||
## Changes since 4.1.0
|
||||
- Numerous smaller performance improvements were made.
|
||||
- Reduced recursive unpacking to 2 levels, instead of 5.
|
||||
- IPv6 addresses are preferred during server address selection.
|
||||
- Stricter check if `Complete Folder` is inside `Download Folder`.
|
||||
- Windows: Reduced size of installer.
|
||||
- Windows/macOS: Updated to Python 3.12.
|
||||
|
||||
## Bugfixes since 4.0.3
|
||||
- Series duplicate detection did not detect duplicates.
|
||||
- Sorting would append `.1` to some filenames.
|
||||
- If a paused queue contained items with `Force` priority,
|
||||
items with a lower priority would also be downloaded.
|
||||
- Not all API-keys were removed during log-sanitization.
|
||||
- In certain situations, not all data would be written to disk.
|
||||
- Folder names could be sanitized too eagerly.
|
||||
- Some articles would fail to decode.
|
||||
- QuickCheck could wrongly rename files with identical content.
|
||||
- Warning about `Scripts Folder` location was triggered incorrectly.
|
||||
## Bugfixes since 4.1.0
|
||||
- Multi-select in the queue was broken for some users.
|
||||
- Prevent crash during saving of configuration.
|
||||
- Removing a failed download from the history could break active downloads.
|
||||
|
||||
## Upgrade notices
|
||||
- Direct upgrade is possible from version 3.0.0 and newer.
|
||||
Upgrading from older versions will require `Queue repair`.
|
||||
- Downgrading from version 4.2.0 or newer to 3.7.2 or older will
|
||||
require `Queue repair` due to changes in the internal data format.
|
||||
|
||||
## Known problems and solutions
|
||||
- Read the file "ISSUES.txt"
|
||||
|
||||
27
SABnzbd.py
27
SABnzbd.py
@@ -752,26 +752,9 @@ def commandline_handler():
|
||||
serv_opts = [os.path.normpath(os.path.abspath(sys.argv[0]))]
|
||||
upload_nzbs = []
|
||||
|
||||
# macOS binary: get rid of the weird -psn_0_123456 parameter
|
||||
for arg in sys.argv:
|
||||
if arg.startswith("-psn_"):
|
||||
sys.argv.remove(arg)
|
||||
break
|
||||
|
||||
# Ugly hack to remove the extra "SABnzbd*" parameter the Windows binary
|
||||
# gets when it's restarted
|
||||
if len(sys.argv) > 1 and "sabnzbd" in sys.argv[1].lower() and not sys.argv[1].startswith("-"):
|
||||
slice_start = 2
|
||||
else:
|
||||
slice_start = 1
|
||||
|
||||
# Prepend options from env-variable to options
|
||||
info = os.environ.get("SABnzbd", "").split()
|
||||
info.extend(sys.argv[slice_start:])
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(
|
||||
info,
|
||||
sys.argv[1:],
|
||||
"phdvncwl:s:f:t:b:2:",
|
||||
[
|
||||
"pause",
|
||||
@@ -854,7 +837,7 @@ def main():
|
||||
|
||||
autobrowser = None
|
||||
autorestarted = False
|
||||
sabnzbd.MY_FULLNAME = sys.argv[0]
|
||||
sabnzbd.MY_FULLNAME = __file__
|
||||
sabnzbd.MY_NAME = os.path.basename(sabnzbd.MY_FULLNAME)
|
||||
fork = False
|
||||
pause = False
|
||||
@@ -922,6 +905,7 @@ def main():
|
||||
exit_sab(1)
|
||||
elif opt == "--console":
|
||||
console_logging = True
|
||||
sabnzbd.RESTART_ARGS.append(opt)
|
||||
elif opt in ("-v", "--version"):
|
||||
print_version()
|
||||
exit_sab(0)
|
||||
@@ -966,7 +950,7 @@ def main():
|
||||
org_dir = os.getcwd()
|
||||
|
||||
# Need console logging if requested, for SABnzbd.py and SABnzbd-console.exe
|
||||
console_logging = console_logging or sabnzbd.MY_NAME.lower().find("-console") > 0 or not hasattr(sys, "frozen")
|
||||
console_logging = console_logging or sys.executable.endswith("console.exe") or not hasattr(sys, "frozen")
|
||||
console_logging = console_logging and not sabnzbd.DAEMON
|
||||
|
||||
LOGLEVELS = (logging.FATAL, logging.WARNING, logging.INFO, logging.DEBUG)
|
||||
@@ -1596,9 +1580,8 @@ def main():
|
||||
if hasattr(sys, "frozen"):
|
||||
if sabnzbd.MACOS:
|
||||
# On macOS restart of app instead of embedded python
|
||||
my_name = sabnzbd.MY_FULLNAME.replace("/Contents/MacOS/SABnzbd", "")
|
||||
my_args = " ".join(sys.argv[1:])
|
||||
cmd = 'kill -9 %s && open "%s" --args %s' % (os.getpid(), my_name, my_args)
|
||||
cmd = 'kill -9 %s && open "%s" --args %s' % (os.getpid(), sys.executable, my_args)
|
||||
logging.info("Launching: %s", cmd)
|
||||
os.system(cmd)
|
||||
elif sabnzbd.WIN_SERVICE:
|
||||
|
||||
@@ -96,7 +96,7 @@ pyi_analysis = Analysis(
|
||||
["SABnzbd.py"],
|
||||
datas=extra_pyinstaller_files,
|
||||
hiddenimports=extra_hiddenimports,
|
||||
excludes=["ujson", "FixTk", "tcl", "tk", "_tkinter", "tkinter", "Tkinter"],
|
||||
excludes=["ujson", "FixTk", "tcl", "tk", "_tkinter", "tkinter", "Tkinter", "pydoc", "pydoc_data.topics"],
|
||||
)
|
||||
|
||||
pyz = PYZ(pyi_analysis.pure, pyi_analysis.zipped_data)
|
||||
@@ -113,10 +113,10 @@ exe = EXE(
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name="SABnzbd",
|
||||
upx=True,
|
||||
console=False,
|
||||
append_pkg=False,
|
||||
icon="icons/sabnzbd.ico",
|
||||
contents_directory=".",
|
||||
version=version_info,
|
||||
target_arch="universal2",
|
||||
entitlements_file="builder/osx/entitlements.plist",
|
||||
@@ -134,9 +134,9 @@ if sys.platform == "win32":
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name="SABnzbd-console",
|
||||
upx=True,
|
||||
append_pkg=False,
|
||||
icon="icons/sabnzbd.ico",
|
||||
contents_directory=".",
|
||||
version=version_info,
|
||||
)
|
||||
|
||||
@@ -145,7 +145,6 @@ if sys.platform == "win32":
|
||||
pyi_analysis.binaries,
|
||||
pyi_analysis.zipfiles,
|
||||
pyi_analysis.datas,
|
||||
upx=True,
|
||||
name="SABnzbd-console",
|
||||
)
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Special requirements for macOS universal2 binary release
|
||||
# This way dependabot can auto-update them
|
||||
cryptography==41.0.4
|
||||
cryptography==41.0.5
|
||||
@@ -58,11 +58,16 @@ def safe_remove(path):
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def delete_files_glob(name):
|
||||
"""Delete one file or set of files from wild-card spec"""
|
||||
for f in glob.glob(name):
|
||||
if os.path.exists(f):
|
||||
os.remove(f)
|
||||
def delete_files_glob(glob_pattern: str, allow_no_matches: bool = False):
|
||||
"""Delete one file or set of files from wild-card spec.
|
||||
We expect to match at least 1 file, to force expected behavior"""
|
||||
if files_to_remove := glob.glob(glob_pattern):
|
||||
for path in files_to_remove:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
else:
|
||||
if not allow_no_matches:
|
||||
raise FileNotFoundError(f"No files found that match '{glob_pattern}'")
|
||||
|
||||
|
||||
def run_external_command(command: List[str], print_output: bool = True):
|
||||
@@ -211,8 +216,10 @@ if __name__ == "__main__":
|
||||
|
||||
# Check what architecture we are on
|
||||
RELEASE_BINARY = RELEASE_BINARY_32
|
||||
BUILDING_64BIT = False
|
||||
if platform.architecture()[0] == "64bit":
|
||||
RELEASE_BINARY = RELEASE_BINARY_64
|
||||
BUILDING_64BIT = True
|
||||
|
||||
# Remove any leftovers
|
||||
safe_remove(RELEASE_BINARY)
|
||||
@@ -224,17 +231,20 @@ if __name__ == "__main__":
|
||||
safe_remove("dist/SABnzbd-console")
|
||||
|
||||
# Remove unwanted DLL's
|
||||
delete_files_glob("dist/SABnzbd/api-ms-win*.dll")
|
||||
delete_files_glob("dist/SABnzbd/mfc140u.dll")
|
||||
delete_files_glob("dist/SABnzbd/ucrtbase.dll")
|
||||
shutil.rmtree("dist/SABnzbd/Pythonwin")
|
||||
if BUILDING_64BIT:
|
||||
# These are only present on 64bit (Python 3.9+)
|
||||
delete_files_glob("dist/SABnzbd/api-ms-win*.dll", allow_no_matches=True)
|
||||
delete_files_glob("dist/SABnzbd/ucrtbase.dll", allow_no_matches=True)
|
||||
|
||||
# Remove other files we don't need
|
||||
delete_files_glob("dist/SABnzbd/win32ui.pyd")
|
||||
delete_files_glob("dist/SABnzbd/winxpgui.pyd")
|
||||
# Remove 32bit external executables
|
||||
delete_files_glob("dist/SABnzbd/win/par2/par2.exe")
|
||||
delete_files_glob("dist/SABnzbd/win/multipar/par2j.exe")
|
||||
delete_files_glob("dist/SABnzbd/win/unrar/UnRAR.exe")
|
||||
|
||||
if "installer" in sys.argv:
|
||||
# Needs to be run on 64 bit
|
||||
if RELEASE_BINARY != RELEASE_BINARY_64:
|
||||
if not BUILDING_64BIT:
|
||||
raise RuntimeError("Installer should be created on 64bit Python")
|
||||
|
||||
# Compile NSIS translations
|
||||
@@ -243,11 +253,6 @@ if __name__ == "__main__":
|
||||
shutil.copyfile("builder/win/NSIS_Installer.nsi", "NSIS_Installer.nsi")
|
||||
run_external_command([sys.executable, "tools/make_mo.py", "nsis"])
|
||||
|
||||
# Remove 32bit external executables
|
||||
delete_files_glob("dist/SABnzbd/win/par2/par2.exe")
|
||||
delete_files_glob("dist/SABnzbd/win/multipar/par2j.exe")
|
||||
delete_files_glob("dist/SABnzbd/win/unrar/UnRAR.exe")
|
||||
|
||||
# Run NSIS to build installer
|
||||
run_external_command(
|
||||
[
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
PyGithub==1.59.1
|
||||
PyGithub==2.1.1
|
||||
praw==7.7.1
|
||||
@@ -19,7 +19,6 @@ import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import github
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
# Basic build requirements
|
||||
# Note that not all sub-dependencies are listed, but only ones we know could cause trouble
|
||||
pyinstaller==5.13.2
|
||||
pyinstaller-hooks-contrib==2023.8
|
||||
altgraph==0.17.3
|
||||
pyinstaller==6.1.0
|
||||
packaging==23.2
|
||||
pyinstaller-hooks-contrib==2023.10
|
||||
altgraph==0.17.4
|
||||
wrapt==1.15.0
|
||||
setuptools==68.2.2
|
||||
certifi
|
||||
|
||||
# orjson does not support 32bit Windows, exclude it based on Python-version
|
||||
# This way we also test ujson on Python 3.8 in the CI-tests
|
||||
orjson==3.9.7; python_version > '3.8'
|
||||
# Required on 32bit Windows, exclude it based on Python-version
|
||||
importlib_metadata==6.8.0; python_version < '3.10'
|
||||
importlib_resources==6.1.0; python_version < '3.10'
|
||||
zipp==3.17.0; python_version < '3.10'
|
||||
|
||||
# orjson does not support 32bit Windows, also exclude based on Python-version
|
||||
orjson==3.9.9; python_version > '3.8'
|
||||
|
||||
# For the Windows build
|
||||
pefile==2023.2.7; sys_platform == 'win32'
|
||||
@@ -18,6 +23,6 @@ pywin32-ctypes==0.2.2; sys_platform == 'win32'
|
||||
# For the macOS build
|
||||
dmgbuild==1.6.1; sys_platform == 'darwin'
|
||||
mac-alias==2.2.2; sys_platform == 'darwin'
|
||||
macholib==1.16.2; sys_platform == 'darwin'
|
||||
macholib==1.16.3; sys_platform == 'darwin'
|
||||
ds-store==1.3.1; sys_platform == 'darwin'
|
||||
PyNaCl==1.5.0; sys_platform == 'darwin'
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
update: function(event, ui) {
|
||||
jQuery('.Categories form.sorting-row').each(function(index, elm) {
|
||||
// Update order of all elements
|
||||
if(index != elm.order.value) {
|
||||
if(index !== elm.order.value) {
|
||||
elm.order.value = index
|
||||
// Submit changed order
|
||||
var data = {}
|
||||
|
||||
@@ -267,7 +267,7 @@
|
||||
jQuery(document).ready(function(){
|
||||
// Show the message about translating when it's non-English
|
||||
function hideOrShowTranslate() {
|
||||
if(jQuery('#language').val() == 'en') {
|
||||
if(jQuery('#language').val() === 'en') {
|
||||
jQuery('.alert-translate').hide()
|
||||
} else {
|
||||
jQuery('.alert-translate').show()
|
||||
@@ -290,7 +290,7 @@ jQuery(document).ready(function(){
|
||||
// So when exposed to internet and no password, no external limit or no username/password
|
||||
var safeCheck = jQuery('#host, #inet_exposure, #${pid}_wu, #${pid}_wp')
|
||||
function checkSafety() {
|
||||
if(jQuery('#host').val() != 'localhost' && jQuery('#host').val() != '127.0.0.1') {
|
||||
if(jQuery('#host').val() !== 'localhost' && jQuery('#host').val() !== '127.0.0.1') {
|
||||
// No limitation on local-network
|
||||
if(jQuery('#inet_exposure').val() > 3) {
|
||||
// And no username and password?
|
||||
@@ -375,7 +375,7 @@ jQuery(document).ready(function(){
|
||||
})
|
||||
|
||||
// Only allow re-generate if default certs
|
||||
if(jQuery('#https_cert').val() != '$def_https_cert_file') {
|
||||
if(jQuery('#https_cert').val() !== '$def_https_cert_file') {
|
||||
jQuery('.generate_cert').attr('disabled', 'disabled')
|
||||
}
|
||||
|
||||
|
||||
@@ -616,7 +616,7 @@ jQuery(document).ready(function(){
|
||||
|
||||
// Only the Accept filter needs all the options
|
||||
jQuery('form[action="upd_rss_filter"]').find('select[name="filter_type"]').change(function() {
|
||||
jQuery(this).parent().parent().find('select:not([name="filter_type"])').attr('disabled', jQuery(this).val() != "A" && jQuery(this).val() != "S")
|
||||
jQuery(this).parent().parent().find('select:not([name="filter_type"])').attr('disabled', jQuery(this).val() !== "A" && jQuery(this).val() !== "S")
|
||||
})
|
||||
// Trigger on-load for all
|
||||
jQuery('.disabled_options_rule').find('td select:not([name="filter_type"])').attr('disabled', true)
|
||||
|
||||
@@ -117,7 +117,7 @@ else:
|
||||
jQuery('#arguments').val((jQuery(this).find('option:selected').data('action')))
|
||||
|
||||
// Is it speedlimit?
|
||||
if(jQuery(this).find('option:selected').val() == 'speedlimit') {
|
||||
if(jQuery(this).find('option:selected').val() === 'speedlimit') {
|
||||
jQuery('#hidden_arguments').show()
|
||||
jQuery('#hidden_arguments input').attr('placeholder', 'Bytes/s, "1M" = 1 MB/s, "500K" = 500 KB/s')
|
||||
} else {
|
||||
|
||||
@@ -525,13 +525,13 @@
|
||||
var portBox = jQuery(this).parent().parent().find('[name="port"]')
|
||||
if(this.checked) {
|
||||
// Enabled SSL change port when not already a custom port
|
||||
if(portBox.val() == '119') {
|
||||
if(portBox.val() === '119') {
|
||||
portBox.val('563')
|
||||
portBox.addClass('port-highlight')
|
||||
}
|
||||
} else {
|
||||
// Remove SSL port
|
||||
if(portBox.val() == '563') {
|
||||
if(portBox.val() === '563') {
|
||||
portBox.val('119')
|
||||
portBox.addClass('port-highlight')
|
||||
}
|
||||
@@ -615,14 +615,14 @@
|
||||
function receiveMessage(event) {
|
||||
// Check origin of message for security reasons
|
||||
if(event.origin === 'https://sabnzbd.org') {
|
||||
if(event.data == 'show_server') {
|
||||
if(event.data === 'show_server') {
|
||||
jQuery('.Servers .server-frame').show()
|
||||
jQuery('.Servers .server-frame a').click(function () {
|
||||
localStorage.setItem("server-frame-hide-$version", "hide")
|
||||
jQuery('.Servers .server-frame').hide()
|
||||
})
|
||||
}
|
||||
if(event.data == 'hide_server') {
|
||||
if(event.data === 'hide_server') {
|
||||
// Hide and don't load anymore untill the next release
|
||||
jQuery('.Servers .server-frame').hide()
|
||||
localStorage.setItem("server-frame-hide-$version", "hide")
|
||||
|
||||
@@ -493,7 +493,7 @@
|
||||
update: function(event, ui) {
|
||||
jQuery('.Sorting form.sorting-row').each(function(index, elm) {
|
||||
// Update order of all elements
|
||||
if(index != elm.order.value) {
|
||||
if(index !== elm.order.value) {
|
||||
elm.order.value = index
|
||||
// Submit changed order
|
||||
var data = {}
|
||||
|
||||
@@ -346,7 +346,7 @@ jQuery(document).ready(function() {
|
||||
var retention_select = jQuery('#history_retention_select').val()
|
||||
var retention_number = jQuery('#history_retention_number')
|
||||
// Keep all or keep none
|
||||
if(retention_select == "0" || retention_select == "-1") {
|
||||
if(retention_select === "0" || retention_select === "-1") {
|
||||
retention_number.hide()
|
||||
retention_number.val('')
|
||||
retention_number.attr('placeholder', '')
|
||||
|
||||
@@ -109,13 +109,13 @@
|
||||
// Remove start
|
||||
self.currentBrowserPath = self.currentBrowserPath.replace(self.element.data('initialdir')+folderSeperator, '');
|
||||
// If it's identical to the initial dir the replacement won't work
|
||||
if(self.currentBrowserPath == self.element.data('initialdir')) {
|
||||
if(self.currentBrowserPath === self.element.data('initialdir')) {
|
||||
self.currentBrowserPath = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Changed?
|
||||
if(self.element.val() != self.currentBrowserPath) {
|
||||
if(self.element.val() !== self.currentBrowserPath) {
|
||||
self.element.val(self.currentBrowserPath);
|
||||
formHasChanged = true;
|
||||
}
|
||||
@@ -157,7 +157,7 @@
|
||||
var list = $('<div class="list-group">').appendTo(self.fileBrowserDialog);
|
||||
$.each(data.paths, function (i, entry) {
|
||||
// Title for first one
|
||||
if(i == 0) {
|
||||
if(i === 0) {
|
||||
self.fileBrowserDialog.prepend($('<h4>').text(entry.current_path))
|
||||
return
|
||||
}
|
||||
@@ -166,7 +166,7 @@
|
||||
self.browse(entry.path, endpoint); }
|
||||
).text(entry.name);
|
||||
// Back image
|
||||
if(entry.name == '..') {
|
||||
if(entry.name === '..') {
|
||||
$('<span class="glyphicon glyphicon-arrow-left"></span> ').prependTo(link);
|
||||
} else {
|
||||
$('<span class="glyphicon glyphicon-folder-open"></span> ').prependTo(link);
|
||||
@@ -239,8 +239,8 @@ function do_restart() {
|
||||
$('.main-restarting').show()
|
||||
|
||||
// What template
|
||||
var switchedHTTPS = ($('#enable_https').is(':checked') == ($('#enable_https').data('original') === undefined))
|
||||
var portsUnchanged = ($('#port').val() == $('#port').data('original')) && ($('#https_port').val() == $('#https_port').data('original'))
|
||||
var switchedHTTPS = ($('#enable_https').is(':checked') === ($('#enable_https').data('original') === undefined))
|
||||
var portsUnchanged = ($('#port').val() === $('#port').data('original')) && ($('#https_port').val() === $('#https_port').data('original'))
|
||||
|
||||
// Are we on settings page or did nothing change?
|
||||
if(!$('body').hasClass('General') || (!switchedHTTPS && portsUnchanged)) {
|
||||
@@ -248,7 +248,7 @@ function do_restart() {
|
||||
var urlTotal = window.location.origin + urlBase
|
||||
} else {
|
||||
// Protocol and port depend on http(s) setting
|
||||
if($('#enable_https').is(':checked') && (window.location.protocol == 'https:' || !$('#https_port').val())) {
|
||||
if($('#enable_https').is(':checked') && (window.location.protocol === 'https:' || !$('#https_port').val())) {
|
||||
// Https on and we visited this page from HTTPS
|
||||
var urlProtocol = 'https:';
|
||||
var urlPort = $('#https_port').val() ? $('#https_port').val() : $('#port').val();
|
||||
@@ -297,7 +297,7 @@ function do_restart() {
|
||||
|
||||
// Exception if we go from HTTPS to HTTP
|
||||
// (this is not allowed by browsers and all of the above will be ignored)
|
||||
if(window.location.protocol != urlProtocol) {
|
||||
if(window.location.protocol !== urlProtocol) {
|
||||
// Saftey redirect after 20 sec
|
||||
setTimeout(function() {
|
||||
location.href = urlTotal;
|
||||
@@ -429,7 +429,7 @@ $(document).ready(function () {
|
||||
$('.advanced-settings').toggle()
|
||||
addRowColor()
|
||||
})
|
||||
if(localStorage.getItem('advanced-settings') == 'true') {
|
||||
if(localStorage.getItem('advanced-settings') === 'true') {
|
||||
$('.advanced-settings').show()
|
||||
$('#advanced-settings-button').prop('checked', true)
|
||||
addRowColor()
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<td class="history-completedon row-wrap-text" data-bind="text: completedOn(), attr: { 'data-timestamp': completed }" onclick="showDetails(this)"></td>
|
||||
<td class="delete">
|
||||
<label data-bind="visible: parent.isMultiEditing()">
|
||||
<input type="checkbox" name="multiedit" title="$T('Glitter-multiSelect')" data-bind="click: parent.parent.addMultiEdit, attr: { 'id': 'multiedit_' + id } " />
|
||||
<input type="checkbox" name="multiedit" title="$T('Glitter-multiSelect')" data-bind="click: parent.addMultiEdit, attr: { 'id': 'multiedit_' + id } " />
|
||||
</label>
|
||||
<div class="dropdown" data-bind="visible: !parent.isMultiEditing()">
|
||||
<a href="#" data-toggle="dropdown" data-bind="click: updateAllHistoryInfo">
|
||||
@@ -136,11 +136,11 @@
|
||||
<div data-bind="visible: history.isMultiEditing()">
|
||||
<span class="label label-default" data-bind="text: history.multiEditItems().length">0</span>
|
||||
<label for="multiedit-checkall-history">
|
||||
<input type="checkbox" name="multieditCheckAll" id="multiedit-checkall-history" title="$T('Glitter-checkAll')" data-bind="click: checkAllJobs" data-tooltip="true" data-placement="top" />
|
||||
<input type="checkbox" name="multieditCheckAll" id="multiedit-checkall-history" title="$T('Glitter-checkAll')" data-bind="click: history.checkAllJobs" data-tooltip="true" data-placement="top" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<a href="#" class="hover-button" data-bind="visible: history.isMultiEditing(), click: doMultiDelete">
|
||||
<a href="#" class="hover-button" data-bind="visible: history.isMultiEditing(), click: history.doMultiDelete">
|
||||
<span class="glyphicon glyphicon-trash"></span>
|
||||
</a>
|
||||
<a href="#modal-purge-history" class="hover-button" title="$T('purgeHist')" data-bind="visible: !history.isMultiEditing()" data-toggle="modal" data-tooltip="true" data-placement="left">
|
||||
|
||||
@@ -225,15 +225,20 @@
|
||||
</div>
|
||||
<div class="row" data-bind="visible: serverssl">
|
||||
<div class="col-sm-6">$T('srv-ssl')</div>
|
||||
<div class="col-sm-6">
|
||||
<span class="glyphicon glyphicon-ok"></span> <span data-bind="text: serversslinfo"></span>
|
||||
<div class="col-sm-6 col-dot-overflow">
|
||||
<span class="glyphicon glyphicon-ok"></span>
|
||||
<span data-bind="text: serversslinfo"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6"># $T('connections')</div>
|
||||
<div class="col-sm-6">
|
||||
<span data-bind="text: serverconnections().length"></span> /
|
||||
<span data-bind="text: servertotalconn"></span>
|
||||
<span data-bind="text: servertotalconn"></span><br>
|
||||
<!-- ko if: serveripaddress() -->
|
||||
<span data-bind="text: servercanonname"></span><br>
|
||||
<span data-bind="text: serveripaddress"></span>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -249,11 +254,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" data-bind="visible: !isFinite(serveractiveconn())">
|
||||
<div class="row" data-bind="visible: serverwarning()">
|
||||
<div class="col-sm-12">
|
||||
<div class="alert alert-warning">
|
||||
<span class="glyphicon glyphicon-info-sign"></span>
|
||||
<span data-bind="text: serveractiveconn()"></span>
|
||||
<span data-bind="text: serverwarning()"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
<td class="timeleft row-wrap-text" data-bind="text: statusText"></td>
|
||||
<td class="delete">
|
||||
<label data-bind="visible: parent.isMultiEditing()">
|
||||
<input type="checkbox" name="multiedit" title="$T('Glitter-multiSelect')" data-bind="click: parent.parent.addMultiEdit, attr: { 'id': 'multiedit_' + id } " />
|
||||
<input type="checkbox" name="multiedit" title="$T('Glitter-multiSelect')" data-bind="click: parent.addMultiEdit, attr: { 'id': 'multiedit_' + id } " />
|
||||
</label>
|
||||
<!-- ko if: !isGrabbing() -->
|
||||
<div class="dropdown" data-bind="visible: !parent.isMultiEditing()">
|
||||
@@ -172,9 +172,9 @@
|
||||
<form class="multioperations-selector" data-bind="visible: (hasQueue() && queue.isMultiEditing())" style="display: none;">
|
||||
<div class="add-nzb-inputbox add-nzb-inputbox-small add-nzb-inputbox-options">
|
||||
<label for="multiedit-checkall-queue">
|
||||
<input type="checkbox" name="multieditCheckAll" id="multiedit-checkall-queue" title="$T('Glitter-checkAll')" data-bind="click: checkAllJobs" data-tooltip="true" data-placement="top" />
|
||||
<input type="checkbox" name="multieditCheckAll" id="multiedit-checkall-queue" title="$T('Glitter-checkAll')" data-bind="click: queue.checkAllJobs" data-tooltip="true" data-placement="top" />
|
||||
</label>
|
||||
<a href="#" class="hover-button" data-bind="click: doMultiDelete">
|
||||
<a href="#" class="hover-button" data-bind="click: queue.doMultiDelete">
|
||||
<span class="glyphicon glyphicon-trash"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -58,11 +58,11 @@ function convertHTMLtoText(htmltxt) {
|
||||
// Function to re-write 0:09:21=>9:21, 0:10:10=>10:10, 0:00:30=>0:30
|
||||
function rewriteTime(timeString) {
|
||||
// Remove "0:0" from start
|
||||
if(timeString.substring(0,3) == '0:0') {
|
||||
if(timeString.substring(0,3) === '0:0') {
|
||||
timeString = timeString.substring(3)
|
||||
}
|
||||
// Remove "0:" from start
|
||||
else if(timeString.substring(0,2) == '0:') {
|
||||
else if(timeString.substring(0,2) === '0:') {
|
||||
timeString = timeString.substring(2)
|
||||
}
|
||||
return timeString
|
||||
@@ -71,13 +71,13 @@ function rewriteTime(timeString) {
|
||||
// How to display the date-time?
|
||||
function displayDateTime(inDate, outFormat, inFormat) {
|
||||
// What input?
|
||||
if(inDate == '') {
|
||||
if(inDate === '') {
|
||||
var theMoment = moment()
|
||||
} else {
|
||||
var theMoment = moment.utc(inDate, inFormat)
|
||||
}
|
||||
// Special format or regular format?
|
||||
if(outFormat == 'fromNow') {
|
||||
if(outFormat === 'fromNow') {
|
||||
return theMoment.fromNow()
|
||||
} else {
|
||||
return theMoment.local().format(outFormat)
|
||||
@@ -155,7 +155,7 @@ function setCheckAllState(checkSelector, rangeSelector) {
|
||||
var nrChecks = allChecks.filter(":checked");
|
||||
if(nrChecks.length === 0) {
|
||||
$(checkSelector).prop({'checked': false, 'indeterminate': false})
|
||||
} else if(nrChecks.length == allChecks.length) {
|
||||
} else if(nrChecks.length === allChecks.length) {
|
||||
$(checkSelector).prop({'checked': true, 'indeterminate': false})
|
||||
} else {
|
||||
$(checkSelector).prop({'checked': false, 'indeterminate': true})
|
||||
|
||||
@@ -57,7 +57,7 @@ function Fileslisting(parent) {
|
||||
$.each(response.files, function(index, slot) {
|
||||
// Existing or updating?
|
||||
var existingItem = ko.utils.arrayFirst(self.fileItems(), function(i) {
|
||||
return i.nzf_id() == slot.nzf_id;
|
||||
return i.nzf_id() === slot.nzf_id;
|
||||
});
|
||||
|
||||
if(existingItem) {
|
||||
@@ -76,7 +76,7 @@ function Fileslisting(parent) {
|
||||
}
|
||||
|
||||
// Check if we show/hide completed
|
||||
if(localStorageGetItem('showCompletedFiles') == 'No') {
|
||||
if(localStorageGetItem('showCompletedFiles') === 'No') {
|
||||
$('.item-files-table tr.files-done').hide();
|
||||
$('#filelist-showcompleted').removeClass('hover-button')
|
||||
}
|
||||
@@ -217,8 +217,8 @@ function FileslistingModel(parent, data) {
|
||||
self.nzf_id = ko.observable(data.nzf_id);
|
||||
self.file_age = ko.observable(data.age);
|
||||
self.mb = ko.observable(data.mb);
|
||||
self.canselect = ko.observable(data.status != "finished" && data.status != "queued");
|
||||
self.isdone = ko.observable(data.status == "finished");
|
||||
self.canselect = ko.observable(data.status !== "finished" && data.status !== "queued");
|
||||
self.isdone = ko.observable(data.status === "finished");
|
||||
self.percentage = ko.observable(self.isdone() ? fixPercentages(100) : fixPercentages((100 - (data.mbleft / data.mb * 100)).toFixed(0)));
|
||||
|
||||
// Update internally
|
||||
@@ -227,8 +227,8 @@ function FileslistingModel(parent, data) {
|
||||
self.nzf_id(data.nzf_id)
|
||||
self.file_age(data.age)
|
||||
self.mb(data.mb)
|
||||
self.canselect(data.status != "finished" && data.status != "queued")
|
||||
self.isdone(data.status == "finished")
|
||||
self.canselect(data.status !== "finished" && data.status !== "queued")
|
||||
self.isdone(data.status === "finished")
|
||||
// Data is given in MB, would always show 0% for small files even if completed
|
||||
self.percentage(self.isdone() ? fixPercentages(100) : fixPercentages((100 - (data.mbleft / data.mb * 100)).toFixed(0)))
|
||||
}
|
||||
@@ -266,7 +266,7 @@ function paginationModel(parent) {
|
||||
// Return object for adding
|
||||
return {
|
||||
page: pageNr,
|
||||
isCurrent: pageNr == self.currentPage(),
|
||||
isCurrent: pageNr === self.currentPage(),
|
||||
isDots: false,
|
||||
onclick: function(data) {
|
||||
self.moveToPage(data.page);
|
||||
@@ -356,7 +356,7 @@ function paginationModel(parent) {
|
||||
}
|
||||
|
||||
// Change of number of pages?
|
||||
if(newNrPages != self.nrPages()) {
|
||||
if(newNrPages !== self.nrPages()) {
|
||||
// Update
|
||||
self.nrPages(newNrPages);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ function HistoryListModel(parent) {
|
||||
var newItems = [];
|
||||
$.each(data.slots, function(index, slot) {
|
||||
var existingItem = ko.utils.arrayFirst(self.historyItems(), function(i) {
|
||||
return i.historyStatus.nzo_id() == slot.nzo_id;
|
||||
return i.historyStatus.nzo_id() === slot.nzo_id;
|
||||
});
|
||||
// Set index in the results
|
||||
slot.index = index
|
||||
@@ -59,7 +59,7 @@ function HistoryListModel(parent) {
|
||||
});
|
||||
|
||||
// Remove all items
|
||||
if(itemIds.length == self.paginationLimit()) {
|
||||
if(itemIds.length === self.paginationLimit()) {
|
||||
// Replace it, so only 1 Knockout DOM-update!
|
||||
self.historyItems(newItems);
|
||||
newItems = [];
|
||||
@@ -68,7 +68,7 @@ function HistoryListModel(parent) {
|
||||
$.each(itemIds, function() {
|
||||
var id = this.toString();
|
||||
self.historyItems.remove(ko.utils.arrayFirst(self.historyItems(), function(i) {
|
||||
return i.historyStatus.nzo_id() == id;
|
||||
return i.historyStatus.nzo_id() === id;
|
||||
}));
|
||||
});
|
||||
}
|
||||
@@ -82,7 +82,7 @@ function HistoryListModel(parent) {
|
||||
if(self.parent.queue.multiEditItems().length > 0) {
|
||||
$.each(newItems, function() {
|
||||
var currentItem = this;
|
||||
self.parent.queue.multiEditItems.remove(function(inList) { return inList.id == currentItem.id; })
|
||||
self.parent.queue.multiEditItems.remove(function(inList) { return inList.id === currentItem.id; })
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -148,7 +148,7 @@ function HistoryListModel(parent) {
|
||||
// Searching in history (rate-limited in declaration)
|
||||
self.searchTerm.subscribe(function() {
|
||||
// Go back to page 1
|
||||
if(self.pagination.currentPage() != 1) {
|
||||
if(self.pagination.currentPage() !== 1) {
|
||||
// This forces a refresh
|
||||
self.pagination.moveToPage(1);
|
||||
} else {
|
||||
@@ -160,13 +160,13 @@ function HistoryListModel(parent) {
|
||||
// Clear searchterm
|
||||
self.clearSearchTerm = function(data, event) {
|
||||
// Was it escape key or click?
|
||||
if(event.type == 'mousedown' || (event.keyCode && event.keyCode == 27)) {
|
||||
if(event.type === 'mousedown' || (event.keyCode && event.keyCode === 27)) {
|
||||
// Set the loader so it doesn't flicker and then switch
|
||||
self.isLoading(true)
|
||||
self.searchTerm('');
|
||||
}
|
||||
// Was it click and the field is empty? Then we focus on the field
|
||||
if(event.type == 'mousedown' && self.searchTerm() == '') {
|
||||
if(event.type === 'mousedown' && self.searchTerm() === '') {
|
||||
$(event.target).parents('.search-box').find('input[type="text"]').focus()
|
||||
return;
|
||||
}
|
||||
@@ -209,22 +209,22 @@ function HistoryListModel(parent) {
|
||||
var del_files, value;
|
||||
|
||||
// Purge failed
|
||||
if(whatToRemove == 'history-purge-failed') {
|
||||
if(whatToRemove === 'history-purge-failed') {
|
||||
del_files = 0;
|
||||
value = 'failed';
|
||||
}
|
||||
// Also remove files
|
||||
if(whatToRemove == 'history-purgeremove-failed') {
|
||||
if(whatToRemove === 'history-purgeremove-failed') {
|
||||
del_files = 1;
|
||||
value = 'failed';
|
||||
}
|
||||
// Remove completed
|
||||
if(whatToRemove == 'history-purge-completed') {
|
||||
if(whatToRemove === 'history-purge-completed') {
|
||||
del_files = 0;
|
||||
value = 'completed';
|
||||
}
|
||||
// Remove the ones on this page
|
||||
if(whatToRemove == 'history-purge-page') {
|
||||
if(whatToRemove === 'history-purge-page') {
|
||||
// List all the ID's
|
||||
var strIDs = '';
|
||||
$.each(self.historyItems(), function(index) {
|
||||
@@ -264,8 +264,119 @@ function HistoryListModel(parent) {
|
||||
self.showMultiEdit = function() {
|
||||
self.isMultiEditing(!self.isMultiEditing())
|
||||
self.multiEditItems.removeAll();
|
||||
$('.history-table input[name="multiedit"], #history-options #multiedit-checkall').prop({'checked': false, 'indeterminate': false})
|
||||
$('.history-table input[name="multiedit"], #multiedit-checkall-history').prop({'checked': false, 'indeterminate': false})
|
||||
}
|
||||
|
||||
// Add to the list
|
||||
self.addMultiEdit = function(item, event) {
|
||||
// Is it a shift-click?
|
||||
if(event.shiftKey) {
|
||||
checkShiftRange('.history-table input[name="multiedit"]');
|
||||
}
|
||||
|
||||
// Add or remove from the list?
|
||||
if(event.currentTarget.checked) {
|
||||
// Add item
|
||||
self.multiEditItems.push(item);
|
||||
} else {
|
||||
// Go over them all to know which one to remove
|
||||
self.multiEditItems.remove(function(inList) { return inList.id == item.id; })
|
||||
}
|
||||
|
||||
// Update check-all buton state
|
||||
setCheckAllState('#multiedit-checkall-history', '.history-table input[name="multiedit"]')
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check all
|
||||
self.checkAllJobs = function(item, event) {
|
||||
// Get which ones we care about
|
||||
var allChecks = $('.history-table input[name="multiedit"]').filter(':not(:disabled):visible');
|
||||
|
||||
// We need to re-evaltuate the state of this check-all
|
||||
// Otherwise the 'inderterminate' will be overwritten by the click event!
|
||||
setCheckAllState('#multiedit-checkall-history', '.history-table input[name="multiedit"]')
|
||||
|
||||
// Now we can check what happend
|
||||
// For when some are checked, or all are checked (but not partly)
|
||||
if(event.target.indeterminate || (event.target.checked && !event.target.indeterminate)) {
|
||||
var allActive = allChecks.filter(":checked")
|
||||
// First remove the from the list
|
||||
if(allActive.length == self.multiEditItems().length) {
|
||||
// Just remove all
|
||||
self.multiEditItems.removeAll();
|
||||
// Remove the check
|
||||
allActive.prop('checked', false)
|
||||
} else {
|
||||
// Remove them seperate
|
||||
allActive.each(function() {
|
||||
// Go over them all to know which one to remove
|
||||
var item = ko.dataFor(this)
|
||||
self.multiEditItems.remove(function(inList) { return inList.id == item.id; })
|
||||
// Remove the check of this one
|
||||
this.checked = false;
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// None are checked, so check and add them all
|
||||
allChecks.prop('checked', true)
|
||||
allChecks.each(function() { self.multiEditItems.push(ko.dataFor(this)) })
|
||||
event.target.checked = true
|
||||
}
|
||||
// Set state of all the check-all's
|
||||
setCheckAllState('#multiedit-checkall-history', '.history-table input[name="multiedit"]')
|
||||
return true;
|
||||
}
|
||||
|
||||
// Delete all selected
|
||||
self.doMultiDelete = function() {
|
||||
// Anything selected?
|
||||
if(self.multiEditItems().length < 1) return;
|
||||
|
||||
// Need confirm
|
||||
if(!self.parent.confirmDeleteHistory() || confirm(glitterTranslate.removeDown)) {
|
||||
// List all the ID's
|
||||
var strIDs = '';
|
||||
$.each(self.multiEditItems(), function(index) {
|
||||
strIDs = strIDs + this.id + ',';
|
||||
})
|
||||
|
||||
// Show notification
|
||||
showNotification('.main-notification-box-removing-multiple', 0, self.multiEditItems().length)
|
||||
|
||||
// Remove
|
||||
callAPI({
|
||||
mode: 'history',
|
||||
name: 'delete',
|
||||
del_files: 1,
|
||||
value: strIDs
|
||||
}).then(function(response) {
|
||||
if(response.status) {
|
||||
// Make sure the queue doesnt flicker and then fade-out
|
||||
// Make sure no flickering (if there are more items left) and then remove
|
||||
self.isLoading(self.totalItems() > 1)
|
||||
self.parent.refresh();
|
||||
// Empty it
|
||||
self.multiEditItems.removeAll();
|
||||
// Hide notification
|
||||
hideNotification()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// On change of page we need to check all those that were in the list!
|
||||
self.historyItems.subscribe(function() {
|
||||
// We need to wait until the unit is actually finished rendering
|
||||
setTimeout(function() {
|
||||
$.each(self.multiEditItems(), function(index) {
|
||||
$('#multiedit_' + this.id).prop('checked', true);
|
||||
})
|
||||
|
||||
// Update check-all buton state
|
||||
setCheckAllState('#multiedit-checkall-history', '.history-table input[name="multiedit"]')
|
||||
}, 100)
|
||||
}, null, "arrayChange")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -316,14 +427,14 @@ function HistoryModel(parent, data) {
|
||||
|
||||
// Waiting?
|
||||
self.processingWaiting = ko.pureComputed(function() {
|
||||
return(self.status() == 'Queued')
|
||||
return(self.status() === 'Queued')
|
||||
})
|
||||
|
||||
// Processing or done?
|
||||
self.processingDownload = ko.pureComputed(function() {
|
||||
var status = self.status();
|
||||
// When we can cancel
|
||||
if (status === 'Extracting' || status === 'Verifying' || status == 'Repairing' || status === 'Running') {
|
||||
if (status === 'Extracting' || status === 'Verifying' || status === 'Repairing' || status === 'Running') {
|
||||
return 2
|
||||
}
|
||||
// These cannot be cancelled
|
||||
@@ -357,7 +468,7 @@ function HistoryModel(parent, data) {
|
||||
try {
|
||||
// Extract the Download section
|
||||
var downloadLog = ko.utils.arrayFirst(self.historyStatus.stage_log(), function(item) {
|
||||
return item.name() == 'Download'
|
||||
return item.name() === 'Download'
|
||||
});
|
||||
// Extract the speed
|
||||
return downloadLog.actions()[0].match(/(\S*\s\S+)(?=<br\/>)/)[0]
|
||||
@@ -366,7 +477,7 @@ function HistoryModel(parent, data) {
|
||||
return;
|
||||
case 'category':
|
||||
// Exception for *
|
||||
if(self.historyStatus.category() == "*")
|
||||
if(self.historyStatus.category() === "*")
|
||||
return glitterTranslate.defaultText
|
||||
return self.historyStatus.category();
|
||||
case 'size':
|
||||
@@ -437,7 +548,7 @@ function HistoryModel(parent, data) {
|
||||
// Confirm?
|
||||
if(!self.parent.parent.confirmDeleteHistory() || confirm(glitterTranslate.deleteMsg + ":\n" + item.historyStatus.name() + "\n\n" + glitterTranslate.removeDow1)) {
|
||||
// Are we still processing and it can be stopped?
|
||||
if(item.processingDownload() == 2) {
|
||||
if(item.processingDownload() === 2) {
|
||||
callAPI({
|
||||
mode: 'cancel_pp',
|
||||
value: self.id
|
||||
@@ -455,6 +566,7 @@ function HistoryModel(parent, data) {
|
||||
// Make sure no flickering (if there are more items left) and then remove
|
||||
self.parent.isLoading(self.parent.totalItems() > 1)
|
||||
self.parent.historyItems.remove(self);
|
||||
self.parent.multiEditItems.remove(function(inList) { return inList.id === self.id; })
|
||||
self.parent.parent.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@ function ViewModel() {
|
||||
var speedLimitNumber = Math.round(speedLimitNumberFull * 10) / 10;
|
||||
|
||||
// Fix it for lower than 1MB/s
|
||||
if (bandwithLimitText == 'M' && speedLimitNumber < 1) {
|
||||
if (bandwithLimitText === 'M' && speedLimitNumber < 1) {
|
||||
bandwithLimitText = 'K';
|
||||
speedLimitNumber = Math.round(speedLimitNumberFull * 1024);
|
||||
}
|
||||
@@ -133,100 +133,6 @@ function ViewModel() {
|
||||
return parseInt(self.nrWarnings()) + self.allMessages().length;
|
||||
})
|
||||
|
||||
self.updateCheckAllButtonState = function(section) {
|
||||
setCheckAllState(`#multiedit-checkall-${section}`, `.${section}-table input[name="multiedit"]`)
|
||||
}
|
||||
|
||||
// Add queue or history item to multi-edit list
|
||||
self.addMultiEdit = function(item, event) {
|
||||
// The parent model is either the queue or history
|
||||
const model = this.parent;
|
||||
const section = model.queueItems ? 'queue' : 'history';
|
||||
|
||||
if(event.shiftKey) {
|
||||
checkShiftRange(`.${section}-table input[name="multiedit"]`);
|
||||
}
|
||||
|
||||
if(event.currentTarget.checked) {
|
||||
model.multiEditItems.push(item);
|
||||
|
||||
// History is not editable
|
||||
// Only the queue will fire the multi-edit update
|
||||
model.doMultiEditUpdate?.();
|
||||
} else {
|
||||
model.multiEditItems.remove(function(inList) { return inList.id == item.id; })
|
||||
}
|
||||
|
||||
self.updateCheckAllButtonState(section);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check all queue or history items
|
||||
self.checkAllJobs = function(item, event) {
|
||||
const section = event.currentTarget.closest('.multioperations-selector').id === 'history-options' ? 'history' : 'queue';
|
||||
const model = section === 'history' ? self.history : self.queue;
|
||||
|
||||
const allChecks = $(`.${section}-table input[name="multiedit"]`).filter(':not(:disabled):visible');
|
||||
|
||||
self.updateCheckAllButtonState(section);
|
||||
|
||||
if(event.target.indeterminate || (event.target.checked && !event.target.indeterminate)) {
|
||||
const allActive = allChecks.filter(":checked")
|
||||
if(allActive.length === model.multiEditItems().length) {
|
||||
model.multiEditItems.removeAll();
|
||||
allActive.prop('checked', false)
|
||||
} else {
|
||||
allActive.each(function() {
|
||||
var item = ko.dataFor(this)
|
||||
model.multiEditItems.remove(function(inList) { return inList.id === item.id; })
|
||||
this.checked = false;
|
||||
})
|
||||
}
|
||||
} else {
|
||||
allChecks.prop('checked', true)
|
||||
allChecks.each(function() { model.multiEditItems.push(ko.dataFor(this)) })
|
||||
event.target.checked = true
|
||||
|
||||
model.multiEditUpdate?.();
|
||||
}
|
||||
|
||||
self.updateCheckAllButtonState(section);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Delete all selected queue or history items
|
||||
self.doMultiDelete = function(item, event) {
|
||||
const section = event.currentTarget.closest('.multioperations-selector').id === 'history-options' ? 'history' : 'queue';
|
||||
const model = section === 'history' ? self.history : self.queue;
|
||||
|
||||
// Anything selected?
|
||||
if(model.multiEditItems().length < 1) return;
|
||||
|
||||
if(!self.confirmDeleteHistory() || confirm(glitterTranslate.removeDown)) {
|
||||
let strIDs = '';
|
||||
$.each(model.multiEditItems(), function() {
|
||||
strIDs = strIDs + this.id + ',';
|
||||
})
|
||||
|
||||
showNotification('.main-notification-box-removing-multiple', 0, model.multiEditItems().length)
|
||||
|
||||
callAPI({
|
||||
mode: section,
|
||||
name: 'delete',
|
||||
del_files: 1,
|
||||
value: strIDs
|
||||
}).then(function(response) {
|
||||
if(response.status) {
|
||||
// Make sure the history doesnt flicker and then fade-out
|
||||
model.isLoading(true)
|
||||
self.refresh()
|
||||
model.multiEditItems.removeAll();
|
||||
hideNotification()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Update main queue
|
||||
self.updateQueue = function(response) {
|
||||
// Block in case off dragging
|
||||
@@ -244,7 +150,7 @@ function ViewModel() {
|
||||
/***
|
||||
Possible login failure?
|
||||
***/
|
||||
if (response.hasOwnProperty('error') && response.error == 'Missing authentication') {
|
||||
if (response.hasOwnProperty('error') && response.error === 'Missing authentication') {
|
||||
// Restart
|
||||
document.location = document.location;
|
||||
}
|
||||
@@ -265,7 +171,7 @@ function ViewModel() {
|
||||
self.diskSpaceLeft1(response.queue.diskspace1_norm)
|
||||
|
||||
// Same sizes? Then it's all 1 disk!
|
||||
if (response.queue.diskspace1 != response.queue.diskspace2) {
|
||||
if (response.queue.diskspace1 !== response.queue.diskspace2) {
|
||||
self.diskSpaceLeft2(response.queue.diskspace2_norm)
|
||||
} else {
|
||||
self.diskSpaceLeft2('')
|
||||
@@ -290,7 +196,7 @@ function ViewModel() {
|
||||
Spark line
|
||||
***/
|
||||
// Break the speed if empty queue
|
||||
if (response.queue.sizeleft == '0 B') {
|
||||
if (response.queue.sizeleft === '0 B') {
|
||||
response.queue.kbpersec = 0;
|
||||
response.queue.speed = '0';
|
||||
}
|
||||
@@ -309,9 +215,9 @@ function ViewModel() {
|
||||
self.speedHistory.push(parseInt(response.queue.kbpersec));
|
||||
|
||||
// Is sparkline visible? Not on small mobile devices..
|
||||
if ($('.sparkline-container').css('display') != 'none') {
|
||||
if ($('.sparkline-container').css('display') !== 'none') {
|
||||
// Make sparkline
|
||||
if (self.speedHistory.length == 1) {
|
||||
if (self.speedHistory.length === 1) {
|
||||
// We only use speedhistory from SAB if we use global settings
|
||||
// Otherwise SAB doesn't know the refresh rate
|
||||
if (!self.useGlobalOptions()) {
|
||||
@@ -346,7 +252,7 @@ function ViewModel() {
|
||||
Speedlimit
|
||||
***/
|
||||
// Nothing or 0 means 100%
|
||||
if(response.queue.speedlimit == '' || response.queue.speedlimit == '0') {
|
||||
if(response.queue.speedlimit === '' || response.queue.speedlimit === '0') {
|
||||
self.speedLimitInt(100)
|
||||
} else {
|
||||
self.speedLimitInt(parseInt(response.queue.speedlimit));
|
||||
@@ -369,7 +275,7 @@ function ViewModel() {
|
||||
|
||||
// Paused main queue
|
||||
if (self.downloadsPaused()) {
|
||||
if (response.queue.pause_int == '0') {
|
||||
if (response.queue.pause_int === '0') {
|
||||
timeString = glitterTranslate.paused;
|
||||
} else {
|
||||
var pauseSplit = response.queue.pause_int.split(/:/);
|
||||
@@ -438,7 +344,7 @@ function ViewModel() {
|
||||
.done(self.updateQueue)
|
||||
.fail(function(response) {
|
||||
// Catch the failure of authorization error
|
||||
if (response.status == 401) {
|
||||
if (response.status === 401) {
|
||||
// Stop refresh and reload
|
||||
clearInterval(self.interval)
|
||||
location.reload();
|
||||
@@ -488,7 +394,7 @@ function ViewModel() {
|
||||
api_request[keyword] = parsed_query[keyword]
|
||||
}
|
||||
// Special case for priority, dirty replace of string by numeric value
|
||||
if (keyword == "priority" && api_request["priority"]) {
|
||||
if (keyword === "priority" && api_request["priority"]) {
|
||||
for (const prio_name in self.queue.priorityName) {
|
||||
api_request["priority"] = api_request["priority"].replace(prio_name, self.queue.priorityName[prio_name])
|
||||
|
||||
@@ -606,7 +512,7 @@ function ViewModel() {
|
||||
// Update the warnings
|
||||
self.nrWarnings.subscribe(function(newValue) {
|
||||
// Really any change?
|
||||
if (newValue == self.allWarnings().length) return;
|
||||
if (newValue === self.allWarnings().length) return;
|
||||
|
||||
// Get all warnings
|
||||
callAPI({
|
||||
@@ -628,7 +534,7 @@ function ViewModel() {
|
||||
type: glitterTranslate.status[warning.type].slice(0, -1),
|
||||
text: convertHTMLtoText(warning.text).replace(/ /g, '\u00A0').replace(/(?:\r\n|\r|\n)/g, '<br />'),
|
||||
timestamp: warning.time,
|
||||
css: (warning.type == "ERROR" ? "danger" : warning.type == "WARNING" ? "warning" : "info"),
|
||||
css: (warning.type === "ERROR" ? "danger" : warning.type === "WARNING" ? "warning" : "info"),
|
||||
clear: self.clearWarnings
|
||||
};
|
||||
self.allWarnings.push(warningData)
|
||||
@@ -648,7 +554,7 @@ function ViewModel() {
|
||||
// Clear messages
|
||||
self.clearMessages = function(whatToRemove) {
|
||||
// Remove specifc type of messages
|
||||
self.allMessages.remove(function(item) { return item.index == whatToRemove });
|
||||
self.allMessages.remove(function(item) { return item.index === whatToRemove });
|
||||
// Now so we don't show again today
|
||||
localStorageSetItem(whatToRemove, Date.now())
|
||||
}
|
||||
@@ -659,7 +565,7 @@ function ViewModel() {
|
||||
if (!self.speedLimitInt()) return;
|
||||
|
||||
// Update
|
||||
if (self.speedLimitInt() != newValue) {
|
||||
if (self.speedLimitInt() !== newValue) {
|
||||
callAPI({
|
||||
mode: "config",
|
||||
name: "speedlimit",
|
||||
@@ -732,11 +638,17 @@ function ViewModel() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Disable the buttons to prevent multiple uploads
|
||||
let submit_buttons = $(form).find("input[type='submit']")
|
||||
submit_buttons.attr("disabled", true)
|
||||
|
||||
// Upload file using the method we also use for drag-and-drop
|
||||
if ($(form.nzbFile)[0].files[0]) {
|
||||
self.addNZBFromFile($(form.nzbFile)[0].files);
|
||||
// Hide modal, upload will reset the form
|
||||
$("#modal-add-nzb").modal("hide");
|
||||
// Re-enable the buttons
|
||||
submit_buttons.attr("disabled", false)
|
||||
} else if ($(form.nzbURL).val()) {
|
||||
// Or add URL
|
||||
var theCall = {
|
||||
@@ -750,7 +662,7 @@ function ViewModel() {
|
||||
}
|
||||
|
||||
// Optional, otherwise they get mis-labeled if left empty
|
||||
if ($('#modal-add-nzb select[name="Category"]').val() != '*') theCall.cat = $('#modal-add-nzb select[name="Category"]').val()
|
||||
if ($('#modal-add-nzb select[name="Category"]').val() !== '*') theCall.cat = $('#modal-add-nzb select[name="Category"]').val()
|
||||
if ($('#modal-add-nzb select[name="Processing"]').val()) theCall.pp = $('#modal-add-nzb select[name="Category"]').val()
|
||||
|
||||
// Add
|
||||
@@ -760,6 +672,7 @@ function ViewModel() {
|
||||
$("#modal-add-nzb").modal("hide");
|
||||
form.reset()
|
||||
$('#nzbname').val('')
|
||||
submit_buttons.attr("disabled", false)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -779,7 +692,7 @@ function ViewModel() {
|
||||
fileindex++
|
||||
|
||||
// Check if it's maybe a folder, we can't handle those
|
||||
if (!file.type && file.size % 4096 == 0) return;
|
||||
if (!file.type && file.size % 4096 === 0) return;
|
||||
|
||||
// Add notification
|
||||
showNotification('.main-notification-box-uploading', 0, fileindex)
|
||||
@@ -795,7 +708,7 @@ function ViewModel() {
|
||||
data.append("apikey", apiKey);
|
||||
|
||||
// Optional, otherwise they get mis-labeled if left empty
|
||||
if ($('#modal-add-nzb select[name="Category"]').val() != '*') data.append("cat", $('#modal-add-nzb select[name="Category"]').val());
|
||||
if ($('#modal-add-nzb select[name="Category"]').val() !== '*') data.append("cat", $('#modal-add-nzb select[name="Category"]').val());
|
||||
if ($('#modal-add-nzb select[name="Processing"]').val()) data.append("pp", $('#modal-add-nzb select[name="Processing"]').val());
|
||||
|
||||
// Add this one
|
||||
@@ -827,10 +740,10 @@ function ViewModel() {
|
||||
// Load status info
|
||||
self.loadStatusInfo = function(item, event) {
|
||||
// Full refresh? Only on click and for the status-screen
|
||||
var statusFullRefresh = (event != undefined) && $('#options-status').hasClass('active');
|
||||
var statusFullRefresh = (event !== undefined) && $('#options-status').hasClass('active');
|
||||
|
||||
// Measure performance? Takes a while
|
||||
var statusPerformance = (event != undefined) && $(event.currentTarget).hasClass('diskspeed-button');
|
||||
var statusPerformance = (event !== undefined) && $(event.currentTarget).hasClass('diskspeed-button');
|
||||
|
||||
// Make it spin if the user requested it otherwise we don't,
|
||||
// because browsers use a lot of CPU for the animation
|
||||
@@ -870,43 +783,7 @@ function ViewModel() {
|
||||
}
|
||||
|
||||
// Update the servers
|
||||
if (self.statusInfo.servers().length != data.status.servers.length) {
|
||||
// Empty them, in case of update
|
||||
self.statusInfo.servers([])
|
||||
|
||||
// Initial add
|
||||
$.each(data.status.servers, function() {
|
||||
self.statusInfo.servers.push({
|
||||
'servername': ko.observable(this.servername),
|
||||
'serveroptional': ko.observable(this.serveroptional),
|
||||
'serverpriority': ko.observable(this.serverpriority),
|
||||
'servertotalconn': ko.observable(this.servertotalconn),
|
||||
'serverssl': ko.observable(this.serverssl),
|
||||
'serversslinfo': ko.observable(this.serversslinfo),
|
||||
'serveractiveconn': ko.observable(this.serveractiveconn),
|
||||
'servererror': ko.observable(this.servererror),
|
||||
'serveractive': ko.observable(this.serveractive),
|
||||
'serverconnections': ko.observableArray(this.serverconnections),
|
||||
'serverbps': ko.observable(this.serverbps)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// Update
|
||||
$.each(data.status.servers, function(index) {
|
||||
var activeServer = self.statusInfo.servers()[index];
|
||||
activeServer.servername(this.servername),
|
||||
activeServer.serveroptional(this.serveroptional),
|
||||
activeServer.serverpriority(this.serverpriority),
|
||||
activeServer.servertotalconn(this.servertotalconn),
|
||||
activeServer.serverssl(this.serverssl),
|
||||
activeServer.serversslinfo(this.serversslinfo),
|
||||
activeServer.serveractiveconn(this.serveractiveconn),
|
||||
activeServer.servererror(this.servererror),
|
||||
activeServer.serveractive(this.serveractive),
|
||||
activeServer.serverconnections(this.serverconnections),
|
||||
activeServer.serverbps(this.serverbps)
|
||||
})
|
||||
}
|
||||
ko.mapping.fromJS(data.status.servers, {}, self.statusInfo.servers)
|
||||
|
||||
// Add tooltips to possible new items
|
||||
if (!isMobile) $('#modal-options [data-tooltip="true"]').tooltip({ trigger: 'hover', container: 'body' })
|
||||
@@ -922,7 +799,7 @@ function ViewModel() {
|
||||
var nzbSize = $(event.target).data('size')
|
||||
|
||||
// Maybe it was a click on the icon?
|
||||
if (nzbSize == undefined) {
|
||||
if (nzbSize === undefined) {
|
||||
nzbSize = $(event.target.parentElement).data('size')
|
||||
}
|
||||
|
||||
@@ -1004,7 +881,7 @@ function ViewModel() {
|
||||
$('#options-orphans [data-tooltip="true"]').tooltip('hide')
|
||||
|
||||
// Show notification on delete
|
||||
if ($(htmlElement.currentTarget).data('action') == 'delete_orphan') {
|
||||
if ($(htmlElement.currentTarget).data('action') === 'delete_orphan') {
|
||||
showNotification('.main-notification-box-removing', 1000)
|
||||
} else {
|
||||
// Adding back to queue
|
||||
@@ -1218,7 +1095,7 @@ function ViewModel() {
|
||||
// Reformat and set categories
|
||||
self.queue.categoriesList($.map(response.config.categories, function(cat) {
|
||||
// Default?
|
||||
if(cat.name == '*') return { catValue: '*', catText: glitterTranslate.defaultText };
|
||||
if(cat.name === '*') return { catValue: '*', catText: glitterTranslate.defaultText };
|
||||
return { catValue: cat.name, catText: cat.name };
|
||||
}))
|
||||
|
||||
@@ -1230,7 +1107,7 @@ function ViewModel() {
|
||||
// Reformat script-list
|
||||
self.queue.scriptsList($.map(script_response.scripts, function(script) {
|
||||
// None?
|
||||
if(script == 'None') return { scriptValue: 'None', scriptText: glitterTranslate.noneText };
|
||||
if(script === 'None') return { scriptValue: 'None', scriptText: glitterTranslate.noneText };
|
||||
return { scriptValue: script, scriptText: script };
|
||||
}))
|
||||
self.queue.scriptsListLoaded(true)
|
||||
@@ -1314,7 +1191,7 @@ function ViewModel() {
|
||||
// Orphaned folders? If user clicked away we check again in 5 days
|
||||
if (self.statusInfo.folders().length >= 3 && orphanMsg) {
|
||||
// Check if not already there
|
||||
if (!ko.utils.arrayFirst(self.allMessages(), function(item) { return item.index == 'OrphanedMsg' })) {
|
||||
if (!ko.utils.arrayFirst(self.allMessages(), function(item) { return item.index === 'OrphanedMsg' })) {
|
||||
self.allMessages.push({
|
||||
index: 'OrphanedMsg',
|
||||
type: glitterTranslate.status['INFO'],
|
||||
@@ -1326,7 +1203,7 @@ function ViewModel() {
|
||||
} else {
|
||||
// Remove any message, if it was there
|
||||
self.allMessages.remove(function(item) {
|
||||
return item.index == 'OrphanedMsg';
|
||||
return item.index === 'OrphanedMsg';
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -75,7 +75,7 @@ function QueueListModel(parent) {
|
||||
$.each(data.slots, function() {
|
||||
var item = this;
|
||||
var existingItem = ko.utils.arrayFirst(self.queueItems(), function(i) {
|
||||
return i.id == item.nzo_id;
|
||||
return i.id === item.nzo_id;
|
||||
});
|
||||
|
||||
if(existingItem) {
|
||||
@@ -88,7 +88,7 @@ function QueueListModel(parent) {
|
||||
});
|
||||
|
||||
// Remove all items if there's any
|
||||
if(itemIds.length == self.paginationLimit()) {
|
||||
if(itemIds.length === self.paginationLimit()) {
|
||||
// Replace it, so only 1 Knockout DOM-update!
|
||||
self.queueItems(newItems);
|
||||
newItems = [];
|
||||
@@ -97,7 +97,7 @@ function QueueListModel(parent) {
|
||||
$.each(itemIds, function() {
|
||||
var id = this.toString();
|
||||
self.queueItems.remove(ko.utils.arrayFirst(self.queueItems(), function(i) {
|
||||
return i.id == id;
|
||||
return i.id === id;
|
||||
}));
|
||||
});
|
||||
}
|
||||
@@ -171,7 +171,7 @@ function QueueListModel(parent) {
|
||||
// Searching in queue (rate-limited in decleration)
|
||||
self.searchTerm.subscribe(function() {
|
||||
// Go back to page 1
|
||||
if(self.pagination.currentPage() != 1) {
|
||||
if(self.pagination.currentPage() !== 1) {
|
||||
// This forces a refresh
|
||||
self.pagination.moveToPage(1);
|
||||
} else {
|
||||
@@ -183,12 +183,12 @@ function QueueListModel(parent) {
|
||||
// Clear searchterm
|
||||
self.clearSearchTerm = function(data, event) {
|
||||
// Was it escape key or click?
|
||||
if(event.type == 'mousedown' || (event.keyCode && event.keyCode == 27)) {
|
||||
if(event.type === 'mousedown' || (event.keyCode && event.keyCode === 27)) {
|
||||
self.isLoading(true)
|
||||
self.searchTerm('');
|
||||
}
|
||||
// Was it click and the field is empty? Then we focus on the field
|
||||
if(event.type == 'mousedown' && self.searchTerm() == '') {
|
||||
if(event.type === 'mousedown' && self.searchTerm() === '') {
|
||||
$(event.target).parents('.search-box').find('input[type="text"]').focus()
|
||||
return;
|
||||
}
|
||||
@@ -255,7 +255,7 @@ function QueueListModel(parent) {
|
||||
// Reset form and remove all checked ones
|
||||
$form[0].reset();
|
||||
self.multiEditItems.removeAll();
|
||||
$('.queue-table input[name="multiedit"], .queue #multiedit-checkall').prop({'checked': false, 'indeterminate': false})
|
||||
$('.queue-table input[name="multiedit"], #multiedit-checkall-queue').prop({'checked': false, 'indeterminate': false})
|
||||
|
||||
// Is the multi-edit in view?
|
||||
if(($form.offset().top + $form.outerHeight(true)) > ($(window).scrollTop()+$(window).height())) {
|
||||
@@ -266,6 +266,72 @@ function QueueListModel(parent) {
|
||||
}
|
||||
}
|
||||
|
||||
// Add to the list
|
||||
self.addMultiEdit = function(item, event) {
|
||||
// Is it a shift-click?
|
||||
if(event.shiftKey) {
|
||||
checkShiftRange('.queue-table input[name="multiedit"]');
|
||||
}
|
||||
|
||||
// Add or remove from the list?
|
||||
if(event.currentTarget.checked) {
|
||||
// Add item
|
||||
self.multiEditItems.push(item);
|
||||
// Update them all
|
||||
self.doMultiEditUpdate();
|
||||
} else {
|
||||
// Go over them all to know which one to remove
|
||||
self.multiEditItems.remove(function(inList) { return inList.id == item.id; })
|
||||
}
|
||||
|
||||
// Update check-all buton state
|
||||
setCheckAllState('#multiedit-checkall-queue', '.queue-table input[name="multiedit"]')
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check all
|
||||
self.checkAllJobs = function(item, event) {
|
||||
// Get which ones we care about
|
||||
var allChecks = $('.queue-table input[name="multiedit"]').filter(':not(:disabled):visible');
|
||||
|
||||
// We need to re-evaltuate the state of this check-all
|
||||
// Otherwise the 'inderterminate' will be overwritten by the click event!
|
||||
setCheckAllState('#multiedit-checkall-queue', '.queue-table input[name="multiedit"]')
|
||||
|
||||
// Now we can check what happend
|
||||
// For when some are checked, or all are checked (but not partly)
|
||||
if(event.target.indeterminate || (event.target.checked && !event.target.indeterminate)) {
|
||||
var allActive = allChecks.filter(":checked")
|
||||
// First remove the from the list
|
||||
if(allActive.length == self.multiEditItems().length) {
|
||||
// Just remove all
|
||||
self.multiEditItems.removeAll();
|
||||
// Remove the check
|
||||
allActive.prop('checked', false)
|
||||
} else {
|
||||
// Remove them seperate
|
||||
allActive.each(function() {
|
||||
// Go over them all to know which one to remove
|
||||
var item = ko.dataFor(this)
|
||||
self.multiEditItems.remove(function(inList) { return inList.id == item.id; })
|
||||
// Remove the check of this one
|
||||
this.checked = false;
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// None are checked, so check and add them all
|
||||
allChecks.prop('checked', true)
|
||||
allChecks.each(function() { self.multiEditItems.push(ko.dataFor(this)) })
|
||||
event.target.checked = true
|
||||
|
||||
// Now we fire the update
|
||||
self.doMultiEditUpdate()
|
||||
}
|
||||
// Set state of all the check-all's
|
||||
setCheckAllState('#multiedit-checkall-queue', '.queue-table input[name="multiedit"]')
|
||||
return true;
|
||||
}
|
||||
|
||||
// Do the actual multi-update immediatly
|
||||
self.doMultiEditUpdate = function() {
|
||||
// Anything selected?
|
||||
@@ -286,14 +352,14 @@ function QueueListModel(parent) {
|
||||
|
||||
// All non-category updates need to only happen after a category update
|
||||
function nonCatUpdates() {
|
||||
if(newScript != '') {
|
||||
if(newScript !== '') {
|
||||
callAPI({
|
||||
mode: 'change_script',
|
||||
value: strIDs,
|
||||
value2: newScript
|
||||
})
|
||||
}
|
||||
if(newPrior != '') {
|
||||
if(newPrior !== '') {
|
||||
callAPI({
|
||||
mode: 'queue',
|
||||
name: 'priority',
|
||||
@@ -301,7 +367,7 @@ function QueueListModel(parent) {
|
||||
value2: newPrior
|
||||
})
|
||||
}
|
||||
if(newProc != '') {
|
||||
if(newProc !== '') {
|
||||
callAPI({
|
||||
mode: 'change_opts',
|
||||
value: strIDs,
|
||||
@@ -318,13 +384,13 @@ function QueueListModel(parent) {
|
||||
|
||||
// Wat a little and do the refresh
|
||||
// Only if anything changed!
|
||||
if(newStatus || newProc != '' || newPrior != '' || newScript != '' || newCat != '') {
|
||||
if(newStatus || newProc !== '' || newPrior !== '' || newScript !== '' || newCat !== '') {
|
||||
setTimeout(parent.refresh, 100)
|
||||
}
|
||||
}
|
||||
|
||||
// What is changed?
|
||||
if(newCat != '') {
|
||||
if(newCat !== '') {
|
||||
callAPI({
|
||||
mode: 'change_cat',
|
||||
value: strIDs,
|
||||
@@ -336,6 +402,42 @@ function QueueListModel(parent) {
|
||||
|
||||
}
|
||||
|
||||
// Delete all selected
|
||||
self.doMultiDelete = function() {
|
||||
// Anything selected?
|
||||
if(self.multiEditItems().length < 1) return;
|
||||
|
||||
// Need confirm
|
||||
if(!self.parent.confirmDeleteQueue() || confirm(glitterTranslate.removeDown)) {
|
||||
// List all the ID's
|
||||
var strIDs = '';
|
||||
$.each(self.multiEditItems(), function(index) {
|
||||
strIDs = strIDs + this.id + ',';
|
||||
})
|
||||
|
||||
// Show notification
|
||||
showNotification('.main-notification-box-removing-multiple', 0, self.multiEditItems().length)
|
||||
|
||||
// Remove
|
||||
callAPI({
|
||||
mode: 'queue',
|
||||
name: 'delete',
|
||||
del_files: 1,
|
||||
value: strIDs
|
||||
}).then(function(response) {
|
||||
if(response.status) {
|
||||
// Make sure the queue doesnt flicker and then fade-out
|
||||
self.isLoading(true)
|
||||
self.parent.refresh()
|
||||
// Empty it
|
||||
self.multiEditItems.removeAll();
|
||||
// Hide notification
|
||||
hideNotification()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// On change of page we need to check all those that were in the list!
|
||||
self.queueItems.subscribe(function() {
|
||||
// We need to wait until the unit is actually finished rendering
|
||||
@@ -345,7 +447,7 @@ function QueueListModel(parent) {
|
||||
})
|
||||
|
||||
// Update check-all buton state
|
||||
setCheckAllState('.queue #multiedit-checkall', '.queue-table input[name="multiedit"]')
|
||||
setCheckAllState('#multiedit-checkall-queue', '.queue-table input[name="multiedit"]')
|
||||
}, 100)
|
||||
}, null, "arrayChange")
|
||||
}
|
||||
@@ -365,8 +467,8 @@ function QueueModel(parent, data) {
|
||||
self.index = ko.observable(data.index);
|
||||
self.status = ko.observable(data.status);
|
||||
self.labels = ko.observableArray(data.labels);
|
||||
self.isGrabbing = ko.observable(data.status == 'Grabbing' || data.avg_age == '-')
|
||||
self.isFetchingBlocks = data.status == 'Fetching' || data.priority == 'Repair' // No need to update
|
||||
self.isGrabbing = ko.observable(data.status === 'Grabbing' || data.avg_age === '-')
|
||||
self.isFetchingBlocks = data.status === 'Fetching' || data.priority === 'Repair' // No need to update
|
||||
self.totalMB = ko.observable(parseFloat(data.mb));
|
||||
self.remainingMB = ko.observable(parseFloat(data.mbleft))
|
||||
self.missingMB = ko.observable(parseFloat(data.mbmissing))
|
||||
@@ -377,7 +479,7 @@ function QueueModel(parent, data) {
|
||||
self.priority = ko.observable(parent.priorityName[data.priority]);
|
||||
self.script = ko.observable(data.script);
|
||||
self.unpackopts = ko.observable(parseInt(data.unpackopts)) // UnpackOpts fails if not parseInt'd!
|
||||
self.pausedStatus = ko.observable(data.status == 'Paused');
|
||||
self.pausedStatus = ko.observable(data.status === 'Paused');
|
||||
self.timeLeft = ko.observable(data.timeleft);
|
||||
|
||||
// Initially empty
|
||||
@@ -388,7 +490,7 @@ function QueueModel(parent, data) {
|
||||
// Color of the progress bar
|
||||
self.progressColor = ko.computed(function() {
|
||||
// Checking
|
||||
if(self.status() == 'Checking') {
|
||||
if(self.status() === 'Checking') {
|
||||
return '#58A9FA'
|
||||
}
|
||||
// Check for missing data, the value is arbitrary! (2%)
|
||||
@@ -396,7 +498,7 @@ function QueueModel(parent, data) {
|
||||
return '#F8A34E'
|
||||
}
|
||||
// Set to grey, only when not Force download
|
||||
if((self.parent.parent.downloadsPaused() && self.priority() != 2) || self.pausedStatus()) {
|
||||
if((self.parent.parent.downloadsPaused() && self.priority() !== 2) || self.pausedStatus()) {
|
||||
return '#B7B7B7'
|
||||
}
|
||||
// Nothing
|
||||
@@ -424,15 +526,15 @@ function QueueModel(parent, data) {
|
||||
})
|
||||
self.statusText = ko.computed(function() {
|
||||
// Checking
|
||||
if(self.status() == 'Checking') {
|
||||
if(self.status() === 'Checking') {
|
||||
return glitterTranslate.checking
|
||||
}
|
||||
// Grabbing
|
||||
if(self.status() == 'Grabbing') {
|
||||
if(self.status() === 'Grabbing') {
|
||||
return glitterTranslate.fetch
|
||||
}
|
||||
// Pausing status
|
||||
if((self.parent.parent.downloadsPaused() && self.priority() != 2) || self.pausedStatus()) {
|
||||
if((self.parent.parent.downloadsPaused() && self.priority() !== 2) || self.pausedStatus()) {
|
||||
return glitterTranslate.paused;
|
||||
}
|
||||
// Just the time
|
||||
@@ -442,7 +544,7 @@ function QueueModel(parent, data) {
|
||||
// Icon to better show force-priority
|
||||
self.queueIcon = ko.computed(function() {
|
||||
// Force comes first
|
||||
if(self.priority() == 2) {
|
||||
if(self.priority() === 2) {
|
||||
return 'glyphicon-forward'
|
||||
}
|
||||
if(self.pausedStatus()) {
|
||||
@@ -456,17 +558,17 @@ function QueueModel(parent, data) {
|
||||
switch(param) {
|
||||
case 'category':
|
||||
// Exception for *
|
||||
if(self.category() == "*")
|
||||
if(self.category() === "*")
|
||||
return glitterTranslate.defaultText
|
||||
return self.category();
|
||||
case 'priority':
|
||||
// Onload-exception
|
||||
if(self.priority() == undefined) return;
|
||||
return ko.utils.arrayFirst(self.parent.priorityOptions(), function(item) { return item.value == self.priority()}).name;
|
||||
if(self.priority() === undefined) return;
|
||||
return ko.utils.arrayFirst(self.parent.priorityOptions(), function(item) { return item.value === self.priority()}).name;
|
||||
case 'processing':
|
||||
// Onload-exception
|
||||
if(self.unpackopts() == undefined) return;
|
||||
return ko.utils.arrayFirst(self.parent.processingOptions(), function(item) { return item.value == self.unpackopts()}).name;
|
||||
if(self.unpackopts() === undefined) return;
|
||||
return ko.utils.arrayFirst(self.parent.processingOptions(), function(item) { return item.value === self.unpackopts()}).name;
|
||||
case 'scripts':
|
||||
return self.script();
|
||||
case 'age':
|
||||
@@ -482,7 +584,7 @@ function QueueModel(parent, data) {
|
||||
self.password(data.password);
|
||||
self.index(data.index);
|
||||
self.status(data.status)
|
||||
self.isGrabbing(data.status == 'Grabbing' || data.avg_age == '-')
|
||||
self.isGrabbing(data.status === 'Grabbing' || data.avg_age === '-')
|
||||
self.totalMB(parseFloat(data.mb));
|
||||
self.remainingMB(parseFloat(data.mbleft));
|
||||
self.missingMB(parseFloat(data.mbmissing))
|
||||
@@ -493,12 +595,12 @@ function QueueModel(parent, data) {
|
||||
self.priority(parent.priorityName[data.priority]);
|
||||
self.script(data.script);
|
||||
self.unpackopts(parseInt(data.unpackopts)) // UnpackOpts fails if not parseInt'd!
|
||||
self.pausedStatus(data.status == 'Paused');
|
||||
self.pausedStatus(data.status === 'Paused');
|
||||
self.timeLeft(data.timeleft);
|
||||
|
||||
// Did the label-list change?
|
||||
// Otherwise KO will send updates to all texts during refresh()
|
||||
if(self.rawLabels != data.labels.toString()) {
|
||||
if(self.rawLabels !== data.labels.toString()) {
|
||||
// Update
|
||||
self.labels(data.labels);
|
||||
self.rawLabels = data.labels.toString();
|
||||
@@ -535,7 +637,7 @@ function QueueModel(parent, data) {
|
||||
// Do on change
|
||||
self.nameForEdit.subscribe(function(newName) {
|
||||
// Anything change or empty?
|
||||
if(!newName || self.name() == newName) return;
|
||||
if(!newName || self.name() === newName) return;
|
||||
|
||||
// Rename would abort Direct Unpack, so ask if user is sure
|
||||
if(self.direct_unpack() && !confirm(glitterTranslate.renameAbort)) return;
|
||||
@@ -625,7 +727,7 @@ function QueueModel(parent, data) {
|
||||
// Make sure no flickering (if there are more items left) and then remove
|
||||
self.parent.isLoading(self.parent.totalItems() > 1)
|
||||
parent.queueItems.remove(itemToDelete);
|
||||
parent.multiEditItems.remove(function(inList) { return inList.id == itemToDelete.id; })
|
||||
parent.multiEditItems.remove(function(inList) { return inList.id === itemToDelete.id; })
|
||||
self.parent.parent.refresh();
|
||||
// Hide notifcation
|
||||
hideNotification()
|
||||
|
||||
@@ -56,12 +56,12 @@ $(document).ready(function() {
|
||||
$('#ssl').click(function() {
|
||||
if(this.checked) {
|
||||
// Enabled SSL change port when not already a custom port
|
||||
if($('#port').val() == '119') {
|
||||
if($('#port').val() === '119') {
|
||||
$('#port').val('563')
|
||||
}
|
||||
} else {
|
||||
// Remove SSL port
|
||||
if($('#port').val() == '563') {
|
||||
if($('#port').val() === '563') {
|
||||
$('#port').val('119')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.2.0" date="2023-11-26" type="stable"/>
|
||||
<release version="4.1.0" date="2023-09-26" type="stable"/>
|
||||
<release version="4.0.3" date="2023-06-16" type="stable"/>
|
||||
<release version="4.0.2" date="2023-06-09" type="stable"/>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: team@sabnzbd.org\n"
|
||||
"Language-Team: SABnzbd <team@sabnzbd.org>\n"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Language-Team: Czech (https://app.transifex.com/sabnzbd/teams/111101/cs/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Danish (https://app.transifex.com/sabnzbd/teams/111101/da/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: German (https://app.transifex.com/sabnzbd/teams/111101/de/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Spanish (https://app.transifex.com/sabnzbd/teams/111101/es/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Finnish (https://app.transifex.com/sabnzbd/teams/111101/fi/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: French (https://app.transifex.com/sabnzbd/teams/111101/fr/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: ION, 2020\n"
|
||||
"Language-Team: Hebrew (https://app.transifex.com/sabnzbd/teams/111101/he/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Norwegian Bokmål (https://app.transifex.com/sabnzbd/teams/111101/nb/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Dutch (https://app.transifex.com/sabnzbd/teams/111101/nl/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Polish (https://app.transifex.com/sabnzbd/teams/111101/pl/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Portuguese (Brazil) (https://app.transifex.com/sabnzbd/teams/111101/pt_BR/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Romanian (https://app.transifex.com/sabnzbd/teams/111101/ro/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Russian (https://app.transifex.com/sabnzbd/teams/111101/ru/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Serbian (https://app.transifex.com/sabnzbd/teams/111101/sr/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Swedish (https://app.transifex.com/sabnzbd/teams/111101/sv/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/sabnzbd/teams/111101/zh_CN/)\n"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: team@sabnzbd.org\n"
|
||||
"Language-Team: SABnzbd <team@sabnzbd.org>\n"
|
||||
@@ -149,7 +149,7 @@ msgid "Test Notification"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgid "Resolving address"
|
||||
msgstr ""
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
@@ -233,10 +233,6 @@ msgstr ""
|
||||
msgid "Incorrect parameter"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr ""
|
||||
@@ -245,7 +241,7 @@ msgstr ""
|
||||
msgid "Server address required"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr ""
|
||||
|
||||
@@ -263,7 +259,15 @@ msgid "Permissions setting of %s might deny SABnzbd access to the files and fold
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "The Completed Download Folder cannot be the same or a subfolder of the Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
@@ -316,7 +320,7 @@ msgstr ""
|
||||
msgid "Failed to close database, see log"
|
||||
msgstr ""
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr ""
|
||||
@@ -380,7 +384,7 @@ msgid "Paused"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr ""
|
||||
|
||||
@@ -613,10 +617,6 @@ msgstr ""
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "The Completed Download Folder cannot be the same or a subfolder of the Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr ""
|
||||
@@ -744,7 +744,6 @@ msgstr ""
|
||||
msgid "Running script"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr ""
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
# Copyright 2007-2023 The SABnzbd-Team
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2023
|
||||
# Pavel C <quoing_transifex@mess.cz>, 2023
|
||||
# Safihre <safihre@sabnzbd.org>, 2023
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Pavel C <quoing_transifex@mess.cz>, 2023\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
|
||||
"Language-Team: Czech (https://app.transifex.com/sabnzbd/teams/111101/cs/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -166,7 +166,7 @@ msgid "Test Notification"
|
||||
msgstr "Otestovat notifikace"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgid "Resolving address"
|
||||
msgstr "Překládám adresu"
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
@@ -258,10 +258,6 @@ msgstr "Kvóta přesažena, pozastavuji stahování"
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Nesprávný parametr"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "UNC cesta \"%s\" zde není povolena"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr "%s není validní emailová adresa"
|
||||
@@ -270,7 +266,7 @@ msgstr "%s není validní emailová adresa"
|
||||
msgid "Server address required"
|
||||
msgstr "Adresa serveru je vyžadována"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr ""
|
||||
|
||||
@@ -290,8 +286,18 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgstr "Chyba: Fronta nené prázdná, nelze změnit složku."
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "UNC cesta \"%s\" zde není povolena"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/cfg.py
|
||||
@@ -345,7 +351,7 @@ msgstr "Spustění SQL příkazu selhalo, zkontrolujte log"
|
||||
msgid "Failed to close database, see log"
|
||||
msgstr "Nezdařilo se uzavření databáze, zkontrolujte log"
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr ""
|
||||
@@ -414,7 +420,7 @@ msgid "Paused"
|
||||
msgstr "Pozastaveno"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr ""
|
||||
"Musíte nastavit maximální rychlost linky předtím než začnete nastavovat "
|
||||
@@ -661,12 +667,6 @@ msgstr "Přihlášené selhalo, zkontrolujte jméno a heslo."
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr "Nezdařený pokus o přihlášení od %s"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr ""
|
||||
@@ -799,7 +799,6 @@ msgstr ""
|
||||
msgid "Running script"
|
||||
msgstr "Běžící skript"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr ""
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
|
||||
"Language-Team: Danish (https://app.transifex.com/sabnzbd/teams/111101/da/)\n"
|
||||
@@ -166,8 +166,8 @@ msgid "Test Notification"
|
||||
msgstr "Afprøv notifikation"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgstr " Server løsning"
|
||||
msgid "Resolving address"
|
||||
msgstr "Server løsning"
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
#: sabnzbd/api.py, sabnzbd/skintext.py
|
||||
@@ -258,10 +258,6 @@ msgstr "Kvote brugt, pause downloading"
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Fejl parameter"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "UNC søgning \"%s\" er ikke tilladt her"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr "%s er ikke en godkendt e-mail adresse"
|
||||
@@ -270,7 +266,7 @@ msgstr "%s er ikke en godkendt e-mail adresse"
|
||||
msgid "Server address required"
|
||||
msgstr "Kræver serveradresse"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr "Ugyldig server adresse."
|
||||
|
||||
@@ -290,8 +286,18 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgstr "Fejl: Køen er ikke tom, kan ikke skifte mappe."
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "UNC søgning \"%s\" er ikke tilladt her"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/cfg.py
|
||||
@@ -345,7 +351,7 @@ msgstr "SQL Kommando mislykkedes, se log"
|
||||
msgid "Failed to close database, see log"
|
||||
msgstr "Det lykkedes ikke at lukke databasen, se log"
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr "Forkert logning i historiken av %s"
|
||||
@@ -412,7 +418,7 @@ msgid "Paused"
|
||||
msgstr "Sat på pause"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr ""
|
||||
"Du skal angive den maksimale båndbredde, før du kan angive en båndbredde "
|
||||
@@ -669,12 +675,6 @@ msgstr "Godkendelse mislykkedes, kontrollere brugernavn/adgangskode."
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr "Mislykkede login forsøg fra %s"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr ""
|
||||
@@ -805,7 +805,6 @@ msgstr "Film sortering"
|
||||
msgid "Running script"
|
||||
msgstr "Køre script"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr "Udpakning af nesting for dybt [%s]"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
|
||||
"Language-Team: German (https://app.transifex.com/sabnzbd/teams/111101/de/)\n"
|
||||
@@ -182,8 +182,8 @@ msgid "Test Notification"
|
||||
msgstr "Benachrichtigungen testen"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgstr " Adresse wird aufgelöst …"
|
||||
msgid "Resolving address"
|
||||
msgstr "Adresse wird aufgelöst …"
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
#: sabnzbd/api.py, sabnzbd/skintext.py
|
||||
@@ -277,10 +277,6 @@ msgstr "Kontingent aufgebraucht, Downloads werden angehalten"
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Fehlerhafter Parameter"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "UNC-Pfad \"%s\" ist hier nicht erlaubt"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr "%s ist keine gültige E-Mail-Adresse"
|
||||
@@ -289,7 +285,7 @@ msgstr "%s ist keine gültige E-Mail-Adresse"
|
||||
msgid "Server address required"
|
||||
msgstr "Server-Adresse wird benötigt"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr "Ungültige Server-Adresse."
|
||||
|
||||
@@ -311,10 +307,20 @@ msgstr ""
|
||||
"erstellten Dateien und Ordner von SABnzbd verweigern."
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "UNC-Pfad \"%s\" ist hier nicht erlaubt"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr ""
|
||||
"Fehler: Ordner kann nicht geändert werden, da die Warteschlange nicht leer "
|
||||
"ist."
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
"Der \"Abgeschlossene Downloads\"-Ordner darf kein Unterordner des "
|
||||
"\"Temporäre Downloads\"-Ordners sein."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/cfg.py
|
||||
@@ -371,7 +377,7 @@ msgid "Failed to close database, see log"
|
||||
msgstr ""
|
||||
"Fehler beim Schliessen der Datenbank. Beachten Sie das Nachrichtenprotokoll."
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr "Ungültiges Stufen-Protokoll im Verlauf für %s"
|
||||
@@ -442,7 +448,7 @@ msgid "Paused"
|
||||
msgstr "Angehalten"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr ""
|
||||
"Bevor ein Bandbreitenlimit gesetzt werden kann, muss die maximale Bandbreite"
|
||||
@@ -706,14 +712,6 @@ msgstr ""
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr "Fehlerhafter Login Versuch von %s"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
"Der \"Abgeschlossene Downloads\"-Ordner darf kein Unterordner des "
|
||||
"\"Temporäre Downloads\"-Ordners sein."
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr "Invalides Backup Archiv"
|
||||
@@ -849,7 +847,6 @@ msgstr "Film Sortierung"
|
||||
msgid "Running script"
|
||||
msgstr "Ausführen des Skripts"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr "Entpacken zu tief verschachtelt [%s]"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
|
||||
"Language-Team: Spanish (https://app.transifex.com/sabnzbd/teams/111101/es/)\n"
|
||||
@@ -175,8 +175,8 @@ msgid "Test Notification"
|
||||
msgstr "Notificación de prueba"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgstr " Resolviendo sitio"
|
||||
msgid "Resolving address"
|
||||
msgstr "Resolviendo sitio"
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
#: sabnzbd/api.py, sabnzbd/skintext.py
|
||||
@@ -273,10 +273,6 @@ msgstr "Quota gastado, pausando cola"
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Parámetro incorrecto"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "Ruta de acceso UNC \"%s\" no permitido aqui"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr "%s no es una dirección de correo electrónico válida."
|
||||
@@ -285,7 +281,7 @@ msgstr "%s no es una dirección de correo electrónico válida."
|
||||
msgid "Server address required"
|
||||
msgstr "Se necesita la dirección del servidor"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr "Dirección del servidor no válida."
|
||||
|
||||
@@ -305,8 +301,18 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgstr "Error: Cola no esta vacía, no se puede cambiar el directorio"
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "Ruta de acceso UNC \"%s\" no permitido aqui"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/cfg.py
|
||||
@@ -363,7 +369,7 @@ msgstr "Comando SQL ha fallado, vea el registro"
|
||||
msgid "Failed to close database, see log"
|
||||
msgstr "No se pudo cerrar el base de datos, vea el registro"
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr "Registro de etapa invalido para transferencia terminada %s"
|
||||
@@ -433,7 +439,7 @@ msgid "Paused"
|
||||
msgstr "En pausa"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr ""
|
||||
"Debe establecer un ancho de banda máximo antes de poder establecer un límite"
|
||||
@@ -691,12 +697,6 @@ msgstr "Autenticación fallida, compruebe el usuario o la contraseña."
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr "Intento fallido de inicio de sesión desde %s"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr ""
|
||||
@@ -834,7 +834,6 @@ msgstr "Clasificación de películas"
|
||||
msgid "Running script"
|
||||
msgstr "Ejecutando script"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr ""
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
|
||||
"Language-Team: Finnish (https://app.transifex.com/sabnzbd/teams/111101/fi/)\n"
|
||||
@@ -168,8 +168,8 @@ msgid "Test Notification"
|
||||
msgstr "Testaa ilmoitusta"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgstr " Selvitetään osoitetta"
|
||||
msgid "Resolving address"
|
||||
msgstr "Selvitetään osoitetta"
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
#: sabnzbd/api.py, sabnzbd/skintext.py
|
||||
@@ -256,10 +256,6 @@ msgstr "Latausrajoitus saavutettu, keskeytetään lataukset"
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Virheellinen parametri"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "TUNT polku \"%s\" ei ole sallittu"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr "%s ei ole kelvollinen sähköpostiosoite"
|
||||
@@ -268,7 +264,7 @@ msgstr "%s ei ole kelvollinen sähköpostiosoite"
|
||||
msgid "Server address required"
|
||||
msgstr "Palvelimen osoite vaaditaan"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr "Virheellinen palvelimen osoite."
|
||||
|
||||
@@ -288,8 +284,18 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgstr "Virhe: Jono ei ole tyhjä, kansiota ei voida vaihtaa."
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "TUNT polku \"%s\" ei ole sallittu"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/cfg.py
|
||||
@@ -343,7 +349,7 @@ msgstr "SQL komento epäonnistui, katso loki"
|
||||
msgid "Failed to close database, see log"
|
||||
msgstr "Tietokannan sulkeminen epäonnistui, katso loki"
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr "Virheellinen tila lokihistoriassa kohteelle %s"
|
||||
@@ -410,7 +416,7 @@ msgid "Paused"
|
||||
msgstr "Keskeytetty"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr ""
|
||||
"Sinun täytyy määrittää enimmäiskaista ennen kaistarajoituksen käyttöönottoa."
|
||||
@@ -666,12 +672,6 @@ msgstr "Varmennus epäonnistui, tarkista käyttäjänimi/salasana."
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr ""
|
||||
@@ -802,7 +802,6 @@ msgstr ""
|
||||
msgid "Running script"
|
||||
msgstr "Ajetaan skripti"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr "Purkaessa havaittiin liikaa pakkauskerroksia [%s]"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Fred L <88com88@gmail.com>, 2023\n"
|
||||
"Language-Team: French (https://app.transifex.com/sabnzbd/teams/111101/fr/)\n"
|
||||
@@ -177,8 +177,8 @@ msgid "Test Notification"
|
||||
msgstr "Test de Notification"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgstr " Résolution de l'adresse"
|
||||
msgid "Resolving address"
|
||||
msgstr "Résolution de l'adresse"
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
#: sabnzbd/api.py, sabnzbd/skintext.py
|
||||
@@ -275,10 +275,6 @@ msgstr "Quota atteint, téléchargement mis en pause"
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Paramètre incorrect"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "Le chemin UNC \"%s\" n'est pas autorisé ici"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr "%s n'est pas une adresse email valide"
|
||||
@@ -287,7 +283,7 @@ msgstr "%s n'est pas une adresse email valide"
|
||||
msgid "Server address required"
|
||||
msgstr "Adresse du serveur requise"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr "Adresse du serveur erronée"
|
||||
|
||||
@@ -309,9 +305,20 @@ msgstr ""
|
||||
"fichiers et dossiers qu'il crée."
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "Le chemin UNC \"%s\" n'est pas autorisé ici"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr "La file d'attente n'est pas vide, impossible de changer de dossier."
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
"Erreur : La file d'attente n'est pas vide, impossible de changer le dossier."
|
||||
"Le dossier des téléchargements terminés ne peut pas être le même dossier que"
|
||||
" les téléchargements temporaires, ni être l'un de ses sous-dossiers"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/cfg.py
|
||||
@@ -369,7 +376,7 @@ msgstr "Echec de la commande SQL, voir le journal"
|
||||
msgid "Failed to close database, see log"
|
||||
msgstr "Impossible de fermer la base de données, voir le journal"
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr "Étape de journalisation invalide dans l'historique pour %s"
|
||||
@@ -439,7 +446,7 @@ msgid "Paused"
|
||||
msgstr "En pause"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr ""
|
||||
"Vous devez définir une bande passante maximale avant de pouvoir définir une "
|
||||
@@ -706,14 +713,6 @@ msgstr "Echec d'authentification, vérifiez les identifiant/mot de passe."
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr "Echec de la tentative de connexion de %s"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
"Le dossier des téléchargements terminés ne peut pas être le même dossier que"
|
||||
" les téléchargements temporaires, ni être l'un de ses sous-dossiers"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr "Archives de sauvegarde non valides"
|
||||
@@ -851,7 +850,6 @@ msgstr "Tri des films"
|
||||
msgid "Running script"
|
||||
msgstr "Exécution du script"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr "Arborescence trop profonde dans le fichier compressé [%s]"
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
# Copyright 2007-2023 The SABnzbd-Team
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2023
|
||||
# ION, 2023
|
||||
# Safihre <safihre@sabnzbd.org>, 2023
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: ION, 2023\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\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"
|
||||
@@ -165,8 +165,8 @@ msgid "Test Notification"
|
||||
msgstr "בחן התראה"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgstr " פותר כתובת"
|
||||
msgid "Resolving address"
|
||||
msgstr "פותר כתובת"
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
#: sabnzbd/api.py, sabnzbd/skintext.py
|
||||
@@ -254,10 +254,6 @@ msgstr "מכסה נוצלה, משהה הורדה"
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "פרמטר שגוי"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "נתיב UNC \"%s\" אינו מותר כאן"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr "%s אינה כתובת דוא״ל תקפה"
|
||||
@@ -266,7 +262,7 @@ msgstr "%s אינה כתובת דוא״ל תקפה"
|
||||
msgid "Server address required"
|
||||
msgstr "כתובת שרת דרושה"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr "כתובת שרת בלתי תקפה."
|
||||
|
||||
@@ -287,8 +283,20 @@ msgstr ""
|
||||
"הגדרת הרשאות של %s עשויה לדחות גישה מן SABnzbd אל הקבצים והתיקיות שהוא יוצר."
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgstr "שגיאה: התור אינו ריק, לא ניתן לשנות תיקייה."
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "נתיב UNC \"%s\" אינו מותר כאן"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
"תיקיית ההורדות השלמות אינה יכולה להיות אותה תיקייה או תת־תיקייה של תיקיית "
|
||||
"ההורדות הזמניות"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/cfg.py
|
||||
@@ -342,7 +350,7 @@ msgstr "פקודת SQL נכשלה, ראה יומן"
|
||||
msgid "Failed to close database, see log"
|
||||
msgstr "נכשל בסגירת מסד נתונים, ראה יומן"
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr "רישום של אירוע בלתי תקף בהיסטוריה עבור %s"
|
||||
@@ -411,7 +419,7 @@ msgid "Paused"
|
||||
msgstr "מושהה"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr "אתה חייב לקבוע רוחב פס מרבי לפני שאתה קובע מגבלת רוחב פס"
|
||||
|
||||
@@ -664,14 +672,6 @@ msgstr "אימות נכשל, בדוק שם משתמש/סיסמה."
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr "ניסיון כניסה בלתי מוצלח מן %s"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
"תיקיית ההורדות השלמות אינה יכולה להיות אותה תיקייה או תת־תיקייה של תיקיית "
|
||||
"ההורדות הזמניות"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr "ארכיון בלתי תקף של גיבוי"
|
||||
@@ -804,7 +804,6 @@ msgstr "מיון סרטים"
|
||||
msgid "Running script"
|
||||
msgstr "מריץ תסריט"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr "פריקת קינון ארוכה מדי [%s]"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
|
||||
"Language-Team: Norwegian Bokmål (https://app.transifex.com/sabnzbd/teams/111101/nb/)\n"
|
||||
@@ -164,8 +164,8 @@ msgid "Test Notification"
|
||||
msgstr "Test varslingen"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgstr " Løs adresse"
|
||||
msgid "Resolving address"
|
||||
msgstr "Løs adresse"
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
#: sabnzbd/api.py, sabnzbd/skintext.py
|
||||
@@ -252,10 +252,6 @@ msgstr "Kvote oppbrukt, setter nedlasting på pause"
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Feil parameter"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "UNC-sti \"%s\" er ikke tillatt her"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr "%s er ikke en godkjent e-post-adresse"
|
||||
@@ -264,7 +260,7 @@ msgstr "%s er ikke en godkjent e-post-adresse"
|
||||
msgid "Server address required"
|
||||
msgstr "Krever server-adresse"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr "Ugyldig server-adresse."
|
||||
|
||||
@@ -284,8 +280,18 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgstr "Feil: Køen er ikke tom, kan ikke bytte mappe."
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "UNC-sti \"%s\" er ikke tillatt her"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/cfg.py
|
||||
@@ -339,7 +345,7 @@ msgstr "SQL-kommando mislyktes, se logg"
|
||||
msgid "Failed to close database, see log"
|
||||
msgstr "Kunne ikke stenge databasen, se logg"
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr "Ugyldig scenen logging i historien for %s"
|
||||
@@ -406,7 +412,7 @@ msgid "Paused"
|
||||
msgstr "Pauset"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr "Du må sette maks båndbredde før du kan sette en båndbreddebegrensning"
|
||||
|
||||
@@ -661,12 +667,6 @@ msgstr "Godkjenning mislyktes, kontroller brukernavn og passord."
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr "Mislykket påloggingsforsøk fra %s"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr ""
|
||||
@@ -797,7 +797,6 @@ msgstr ""
|
||||
msgid "Running script"
|
||||
msgstr "Kjører skript"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr "Utpakking nestet for dypt [%s]"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
|
||||
"Language-Team: Dutch (https://app.transifex.com/sabnzbd/teams/111101/nl/)\n"
|
||||
@@ -172,8 +172,8 @@ msgid "Test Notification"
|
||||
msgstr "Test melding"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgstr " Adres opzoeken"
|
||||
msgid "Resolving address"
|
||||
msgstr "Adres opzoeken"
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
#: sabnzbd/api.py, sabnzbd/skintext.py
|
||||
@@ -268,10 +268,6 @@ msgstr "Quotum verbruikt, download is gestopt"
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Incorrecte parameter"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "UNC-pad '%s' hier niet toegestaan."
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr "%s is geen geldig e-mailadres"
|
||||
@@ -280,7 +276,7 @@ msgstr "%s is geen geldig e-mailadres"
|
||||
msgid "Server address required"
|
||||
msgstr "Serveradres verplicht"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr "Ongeldige servernaam"
|
||||
|
||||
@@ -302,8 +298,20 @@ msgstr ""
|
||||
"tot de aangemaakte bestanden en mappen."
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgstr "Fout: Wachtrij is niet leeg, andere map kiezen niet mogelijk."
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "UNC-pad '%s' hier niet toegestaan."
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
"De Map voor verwerkte downloads mag niet een map in de Tijdelijke download "
|
||||
"map zijn."
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/cfg.py
|
||||
@@ -360,7 +368,7 @@ msgstr "SQL-commando mislukt, zie logbestand"
|
||||
msgid "Failed to close database, see log"
|
||||
msgstr "Het lukt niet om de database te sluiten, zie log"
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr "Ongeldig loggen van fase in geschiedenis voor %s"
|
||||
@@ -432,7 +440,7 @@ msgid "Paused"
|
||||
msgstr "Gepauzeerd"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr ""
|
||||
"Je moet eerst een maximumbandbreedte instellen voordat je een limiet kunt "
|
||||
@@ -695,14 +703,6 @@ msgstr "Inloggen mislukt, controleer gebruikersnaam en wachtwoord."
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr "Mislukte login poging van %s"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
"De Map voor verwerkte downloads mag niet een map in de Tijdelijke download "
|
||||
"map zijn."
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr "Ongeldig backup bestand"
|
||||
@@ -838,7 +838,6 @@ msgstr "Film sorteren"
|
||||
msgid "Running script"
|
||||
msgstr "Script uitvoeren"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr "Teveel niveaus om uit te pakken [%s]"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
|
||||
"Language-Team: Polish (https://app.transifex.com/sabnzbd/teams/111101/pl/)\n"
|
||||
@@ -160,8 +160,8 @@ msgid "Test Notification"
|
||||
msgstr "Powiadomienie testowe"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgstr " Rozwiązywanie adresu"
|
||||
msgid "Resolving address"
|
||||
msgstr "Rozwiązywanie adresu"
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
#: sabnzbd/api.py, sabnzbd/skintext.py
|
||||
@@ -248,10 +248,6 @@ msgstr "Przekroczono limit, wstrzymywanie pobierania"
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Błędny parametr"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "Ścieżka UNC \"%s\" niedozwolona"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr "%s nie jest prawidłowym adresem email"
|
||||
@@ -260,7 +256,7 @@ msgstr "%s nie jest prawidłowym adresem email"
|
||||
msgid "Server address required"
|
||||
msgstr "Wymagane jest podanie adresu serwera"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr "Nieprawidłowy adres serwera."
|
||||
|
||||
@@ -280,8 +276,18 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgstr "Błąd: Kolejka nie jest pusta, nie można zmienić katalogu."
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "Ścieżka UNC \"%s\" niedozwolona"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/cfg.py
|
||||
@@ -335,7 +341,7 @@ msgstr "Błąd polecenia SQL, sprawdź logi"
|
||||
msgid "Failed to close database, see log"
|
||||
msgstr "Błąd zamykania bazy danych, sprawdź logi"
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr "Nieprawidłowy log etapu w historii dla %s"
|
||||
@@ -402,7 +408,7 @@ msgid "Paused"
|
||||
msgstr "Wstrzymano"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr ""
|
||||
"Przed ustawieniem limitu przepustowości należy ustawić maksymalną "
|
||||
@@ -661,12 +667,6 @@ msgstr "Błąd połączenia, sprawdź nazwę użytkownika i hasło."
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr ""
|
||||
@@ -797,7 +797,6 @@ msgstr ""
|
||||
msgid "Running script"
|
||||
msgstr "Uruchamianie skryptu"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr "Zbyt głęboki poziom zagnieżdżenia podczas rozpakowywania [%s]"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
|
||||
"Language-Team: Portuguese (Brazil) (https://app.transifex.com/sabnzbd/teams/111101/pt_BR/)\n"
|
||||
@@ -164,8 +164,8 @@ msgid "Test Notification"
|
||||
msgstr "Notificação de teste"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgstr " Resolvendo endereço"
|
||||
msgid "Resolving address"
|
||||
msgstr "Resolvendo endereço"
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
#: sabnzbd/api.py, sabnzbd/skintext.py
|
||||
@@ -252,10 +252,6 @@ msgstr "Quota esgotada, pausando o download"
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Parâmetro incorreto"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "O caminho UNC \"%s\" não é permitido aqui"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr "%s não é um endereço de e-mail válido"
|
||||
@@ -264,7 +260,7 @@ msgstr "%s não é um endereço de e-mail válido"
|
||||
msgid "Server address required"
|
||||
msgstr "Endereço do servidor necessário"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr "Endereço do servidor inválido."
|
||||
|
||||
@@ -284,8 +280,18 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgstr "Erro: A fila não está vazia. Não será possível mudar de pasta."
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "O caminho UNC \"%s\" não é permitido aqui"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/cfg.py
|
||||
@@ -341,7 +347,7 @@ msgstr "O comando SQL falhou. Consulte o log"
|
||||
msgid "Failed to close database, see log"
|
||||
msgstr "Falha ao fechar o banco de dados. Consulte o log"
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr "Registro inválido de etapa no histórico para %s"
|
||||
@@ -408,7 +414,7 @@ msgid "Paused"
|
||||
msgstr "Pausado"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr ""
|
||||
"Você deve definir a largura de banda máxima antes de definir um limite de "
|
||||
@@ -665,12 +671,6 @@ msgstr "Falha de autenticação, verifique usuário / senha."
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr ""
|
||||
@@ -801,7 +801,6 @@ msgstr ""
|
||||
msgid "Running script"
|
||||
msgstr "Executando script"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr "Aninhamento de descompactação com muitos níveis [%s]"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
|
||||
"Language-Team: Romanian (https://app.transifex.com/sabnzbd/teams/111101/ro/)\n"
|
||||
@@ -169,8 +169,8 @@ msgid "Test Notification"
|
||||
msgstr "Notificări Test"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgstr " Reolvare adresă"
|
||||
msgid "Resolving address"
|
||||
msgstr "Reolvare adresă"
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
#: sabnzbd/api.py, sabnzbd/skintext.py
|
||||
@@ -263,10 +263,6 @@ msgstr "Cotă epuizată, întrerupem descărcarea"
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Parametru Incorect"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "cale UNC \"%s\" nu este premisă aici"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr "%s nu este o adresă email validă"
|
||||
@@ -275,7 +271,7 @@ msgstr "%s nu este o adresă email validă"
|
||||
msgid "Server address required"
|
||||
msgstr "Adresă server necesară"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr "Adresă server invalidă"
|
||||
|
||||
@@ -295,8 +291,20 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgstr "Eroare: Coada nu este goală, nu pot schimba dosar."
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "cale UNC \"%s\" nu este premisă aici"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
"Directorul de descărcări finalizate nu poate fi același, sau un subdirector "
|
||||
"al directorului de descărcări temporare"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/cfg.py
|
||||
@@ -351,7 +359,7 @@ msgstr "Comandă SQL Nereuşită, vedeţi jurnal"
|
||||
msgid "Failed to close database, see log"
|
||||
msgstr "Închidere bază de date nereuşită, vedeţi jurnal"
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr "Jurnal istoric stagii invalid pentru %s"
|
||||
@@ -421,7 +429,7 @@ msgid "Paused"
|
||||
msgstr "Întrerupt"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr ""
|
||||
"Trebuie să seta-ţi lățimea de bandă maximă înainte de a seta o limită de "
|
||||
@@ -678,14 +686,6 @@ msgstr "Autentificare nereuşită, verifică nume utilizator/parolă."
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr "Încercare de conectare nereușită de la %s"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
"Directorul de descărcări finalizate nu poate fi același, sau un subdirector "
|
||||
"al directorului de descărcări temporare"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr ""
|
||||
@@ -820,7 +820,6 @@ msgstr ""
|
||||
msgid "Running script"
|
||||
msgstr "Rulare script"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr "Numărul de arhive încorporate este prea mare [%s]"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
|
||||
"Language-Team: Russian (https://app.transifex.com/sabnzbd/teams/111101/ru/)\n"
|
||||
@@ -164,8 +164,8 @@ msgid "Test Notification"
|
||||
msgstr "Тестовое уведомление"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgstr " Разрешение адреса"
|
||||
msgid "Resolving address"
|
||||
msgstr "Разрешение адреса"
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
#: sabnzbd/api.py, sabnzbd/skintext.py
|
||||
@@ -252,10 +252,6 @@ msgstr "Квота исчерпана. Загрузка приостановле
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Неправильный параметр"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "UNC-путь «%s» здесь не допускается"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr "%s не является допустимым адресом электронной почты"
|
||||
@@ -264,7 +260,7 @@ msgstr "%s не является допустимым адресом элект
|
||||
msgid "Server address required"
|
||||
msgstr "Требуется адрес сервера"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr "Недопустимый адрес сервера."
|
||||
|
||||
@@ -284,8 +280,18 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgstr "Ошибка: очередь не пустая, папку нельзя изменить."
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "UNC-путь «%s» здесь не допускается"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/cfg.py
|
||||
@@ -339,7 +345,7 @@ msgstr "Ошибка команды SQL (см. журнал)"
|
||||
msgid "Failed to close database, see log"
|
||||
msgstr "Не удалось закрыть базу данных (см. журнал)"
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr "Недопустимый этап ведения журнала для %s"
|
||||
@@ -406,7 +412,7 @@ msgid "Paused"
|
||||
msgstr "Приостановлено"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr ""
|
||||
|
||||
@@ -661,12 +667,6 @@ msgstr "Ошибка проверки подлинности. Проверьте
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr ""
|
||||
@@ -797,7 +797,6 @@ msgstr ""
|
||||
msgid "Running script"
|
||||
msgstr "Запуск сценария"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr ""
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
|
||||
"Language-Team: Serbian (https://app.transifex.com/sabnzbd/teams/111101/sr/)\n"
|
||||
@@ -162,8 +162,8 @@ msgid "Test Notification"
|
||||
msgstr "Probno obaveštenje"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgstr " Решавање адресе"
|
||||
msgid "Resolving address"
|
||||
msgstr "Решавање адресе"
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
#: sabnzbd/api.py, sabnzbd/skintext.py
|
||||
@@ -250,10 +250,6 @@ msgstr "Kvota utrošena, pauziram preuzimanja"
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Погрешан параметар"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "UNC путања \"%s\" није дозвољена"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr "%s nije ispravna email adresa"
|
||||
@@ -262,7 +258,7 @@ msgstr "%s nije ispravna email adresa"
|
||||
msgid "Server address required"
|
||||
msgstr "Потребна је адреса сервера"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr "Погрешна адреса сервера."
|
||||
|
||||
@@ -282,8 +278,18 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgstr "Грешка: ред није празан, фасцикла се не може променити."
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "UNC путања \"%s\" није дозвољена"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/cfg.py
|
||||
@@ -337,7 +343,7 @@ msgstr "Neuspešna SQL komanda, videti izveštaj"
|
||||
msgid "Failed to close database, see log"
|
||||
msgstr "Неуспешно затварање базе, видети извештај"
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr "Погрешне етапе извештаја можете наћи у хронологији за %s"
|
||||
@@ -404,7 +410,7 @@ msgid "Paused"
|
||||
msgstr "Паузирано"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr ""
|
||||
"Требате да поставите максимални проток пре него што поставите ограничење"
|
||||
@@ -658,12 +664,6 @@ msgstr "Аутентификација погрешна, проверити им
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr ""
|
||||
@@ -794,7 +794,6 @@ msgstr ""
|
||||
msgid "Running script"
|
||||
msgstr "Покретање скрипта"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr "Previše ugnježdenih nivoa pri raspakivanju [%s]"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
|
||||
"Language-Team: Swedish (https://app.transifex.com/sabnzbd/teams/111101/sv/)\n"
|
||||
@@ -162,8 +162,8 @@ msgid "Test Notification"
|
||||
msgstr "Testa notifikation"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgstr " Lösa adress"
|
||||
msgid "Resolving address"
|
||||
msgstr "Lösa adress"
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
#: sabnzbd/api.py, sabnzbd/skintext.py
|
||||
@@ -250,10 +250,6 @@ msgstr "Din kvot är uppnådd, pausar nerladdning"
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "Fel parameter"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "UNC sökväg \"%s\" är inte tillåten här"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr "%s är inte en godkänd e-mail adress"
|
||||
@@ -262,7 +258,7 @@ msgstr "%s är inte en godkänd e-mail adress"
|
||||
msgid "Server address required"
|
||||
msgstr "Kräver serveradress"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr "Ogiltig serveradress"
|
||||
|
||||
@@ -282,8 +278,18 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgstr "Fel: Kön är inte tom, kan inte byta mapp."
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "UNC sökväg \"%s\" är inte tillåten här"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/cfg.py
|
||||
@@ -338,7 +344,7 @@ msgstr "SQL Kommando misslyckades, se logg"
|
||||
msgid "Failed to close database, see log"
|
||||
msgstr "Det gick inte att stänga databasen, se logg"
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr "Felaktig loggning i historiken av %s"
|
||||
@@ -405,7 +411,7 @@ msgid "Paused"
|
||||
msgstr "Pausad"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr "Du måste ange maximal bandbredd innan du kan ange bandbreddsgräns"
|
||||
|
||||
@@ -660,12 +666,6 @@ msgstr "Autentisering misslyckades, kontrollera användarnamn och lösenord."
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr ""
|
||||
@@ -796,7 +796,6 @@ msgstr ""
|
||||
msgid "Running script"
|
||||
msgstr "Kör skript"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr "Nästling för djup [%s]"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/sabnzbd/teams/111101/zh_CN/)\n"
|
||||
@@ -160,8 +160,8 @@ msgid "Test Notification"
|
||||
msgstr "测试通知"
|
||||
|
||||
#: sabnzbd/api.py
|
||||
msgid " Resolving address"
|
||||
msgstr " 正在解析地址"
|
||||
msgid "Resolving address"
|
||||
msgstr "正在解析地址"
|
||||
|
||||
#. No value, used in dropdown menus
|
||||
#: sabnzbd/api.py, sabnzbd/skintext.py
|
||||
@@ -248,10 +248,6 @@ msgstr "配额已耗尽,暂停下载"
|
||||
msgid "Incorrect parameter"
|
||||
msgstr "参数不正确"
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "此处不允许使用 UNC 路径 \"%s\""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "%s is not a valid email address"
|
||||
msgstr "%s 不是有效的电子邮箱地址"
|
||||
@@ -260,7 +256,7 @@ msgstr "%s 不是有效的电子邮箱地址"
|
||||
msgid "Server address required"
|
||||
msgstr "服务器地址必填"
|
||||
|
||||
#: sabnzbd/cfg.py, sabnzbd/utils/servertests.py
|
||||
#: sabnzbd/cfg.py, sabnzbd/newswrapper.py, sabnzbd/utils/servertests.py
|
||||
msgid "Invalid server address."
|
||||
msgstr "服务器地址无效。"
|
||||
|
||||
@@ -280,8 +276,18 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Error: Queue not empty, cannot change folder."
|
||||
msgstr "错误: 队列非空,无法变更文件夹。"
|
||||
msgid "UNC path \"%s\" not allowed here"
|
||||
msgstr "此处不允许使用 UNC 路径 \"%s\""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid "Queue not empty, cannot change folder."
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/cfg.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/cfg.py
|
||||
@@ -335,7 +341,7 @@ msgstr "SQL 命令执行失败,参见日志"
|
||||
msgid "Failed to close database, see log"
|
||||
msgstr "无法关闭数据库,参见日志"
|
||||
|
||||
#. Error message
|
||||
#. Warning message
|
||||
#: sabnzbd/database.py
|
||||
msgid "Invalid stage logging in history for %s"
|
||||
msgstr "%s 历史信息中 stage 日志无效"
|
||||
@@ -402,7 +408,7 @@ msgid "Paused"
|
||||
msgstr "已暂停"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/downloader.py, sabnzbd/interface.py, sabnzbd/skintext.py
|
||||
#: sabnzbd/downloader.py, sabnzbd/skintext.py
|
||||
msgid "You must set a maximum bandwidth before you can set a bandwidth limit"
|
||||
msgstr "设置带宽限制前,您必须设置最大带宽值"
|
||||
|
||||
@@ -653,12 +659,6 @@ msgstr "身份认证失败,请检查用户名/密码。"
|
||||
msgid "Unsuccessful login attempt from %s"
|
||||
msgstr "%s 中有失败的登陆请求"
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid ""
|
||||
"The Completed Download Folder cannot be the same or a subfolder of the "
|
||||
"Temporary Download Folder"
|
||||
msgstr ""
|
||||
|
||||
#: sabnzbd/interface.py
|
||||
msgid "Invalid backup archive"
|
||||
msgstr ""
|
||||
@@ -789,7 +789,6 @@ msgstr "电影排序"
|
||||
msgid "Running script"
|
||||
msgstr "正在执行脚本"
|
||||
|
||||
#. Warning message
|
||||
#: sabnzbd/newsunpack.py
|
||||
msgid "Unpack nesting too deep [%s]"
|
||||
msgstr "解压嵌套层级过深 [%s]"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: team@sabnzbd.org\n"
|
||||
"Language-Team: SABnzbd <team@sabnzbd.org>\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Pavel C <quoing_transifex@mess.cz>, 2022\n"
|
||||
"Language-Team: Czech (https://app.transifex.com/sabnzbd/teams/111101/cs/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Danish (https://app.transifex.com/sabnzbd/teams/111101/da/)\n"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: reloxx13 <reloxx@interia.pl>, 2022\n"
|
||||
"Language-Team: German (https://app.transifex.com/sabnzbd/teams/111101/de/)\n"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Ester Molla Aragones <moarages@gmail.com>, 2020\n"
|
||||
"Language-Team: Spanish (https://app.transifex.com/sabnzbd/teams/111101/es/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Finnish (https://app.transifex.com/sabnzbd/teams/111101/fi/)\n"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Fred L <88com88@gmail.com>, 2021\n"
|
||||
"Language-Team: French (https://app.transifex.com/sabnzbd/teams/111101/fr/)\n"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: ION, 2021\n"
|
||||
"Language-Team: Hebrew (https://app.transifex.com/sabnzbd/teams/111101/he/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Norwegian Bokmål (https://app.transifex.com/sabnzbd/teams/111101/nb/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0RC2\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2021\n"
|
||||
"Language-Team: Dutch (https://app.transifex.com/sabnzbd/teams/111101/nl/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Polish (https://app.transifex.com/sabnzbd/teams/111101/pl/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Portuguese (Brazil) (https://app.transifex.com/sabnzbd/teams/111101/pt_BR/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Romanian (https://app.transifex.com/sabnzbd/teams/111101/ro/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Russian (https://app.transifex.com/sabnzbd/teams/111101/ru/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Serbian (https://app.transifex.com/sabnzbd/teams/111101/sr/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Swedish (https://app.transifex.com/sabnzbd/teams/111101/sv/)\n"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-4.1.0Beta1\n"
|
||||
"Project-Id-Version: SABnzbd-4.2.0Alpha1\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/sabnzbd/teams/111101/zh_CN/)\n"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Main requirements
|
||||
# Note that not all sub-dependencies are listed, but only ones we know could cause trouble
|
||||
sabctools==7.1.2
|
||||
sabctools==8.0.0
|
||||
cheetah3==3.2.6.post1
|
||||
cffi==1.15.1
|
||||
cffi==1.16.0
|
||||
pycparser==2.21
|
||||
feedparser==6.0.10
|
||||
configobj==5.0.8
|
||||
@@ -30,7 +30,7 @@ rebulk==3.2.0
|
||||
|
||||
# Recent cryptography versions require Rust. If you run into issues compiling this
|
||||
# SABnzbd will also work with older pre-Rust versions such as cryptography==3.3.2
|
||||
cryptography==41.0.4
|
||||
cryptography==41.0.5
|
||||
|
||||
# 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
|
||||
@@ -40,7 +40,8 @@ ujson==5.8.0
|
||||
pywin32==306; sys_platform == 'win32'
|
||||
|
||||
# macOS system calls
|
||||
pyobjc==9.2; sys_platform == 'darwin'
|
||||
pyobjc-core==10.0; sys_platform == 'darwin'
|
||||
pyobjc-framework-Cocoa==10.0; sys_platform == 'darwin'
|
||||
|
||||
# Linux notifications
|
||||
notify2==0.3.1; sys_platform != 'win32' and sys_platform != 'darwin'
|
||||
|
||||
@@ -109,11 +109,6 @@ import sabnzbd.bpsmeter
|
||||
import sabnzbd.scheduler as scheduler
|
||||
import sabnzbd.notifier as notifier
|
||||
from sabnzbd.decorators import synchronized
|
||||
from sabnzbd.constants import (
|
||||
DEFAULT_PRIORITY,
|
||||
VALID_ARCHIVES,
|
||||
REPAIR_REQUEST,
|
||||
)
|
||||
import sabnzbd.utils.ssdp
|
||||
|
||||
# Storage for the threads, variables are filled during initialization
|
||||
@@ -242,11 +237,11 @@ def initialize(pause_downloader=False, clean_up=False, repair=0):
|
||||
if cfg.wait_for_dfolder():
|
||||
filesystem.wait_for_download_folder()
|
||||
|
||||
# Set the folders to be created, then the check_incomplete_vs_complete
|
||||
# check will create them by calling get_path on them
|
||||
# Create the folders, now that we waited for them to be available
|
||||
cfg.download_dir.set_create(True)
|
||||
cfg.download_dir.create_path()
|
||||
cfg.complete_dir.set_create(True)
|
||||
filesystem.check_incomplete_vs_complete()
|
||||
cfg.complete_dir.create_path()
|
||||
|
||||
# Set call backs for Config items
|
||||
cfg.cache_limit.callback(cfg.new_limit)
|
||||
|
||||
143
sabnzbd/api.py
143
sabnzbd/api.py
@@ -51,6 +51,8 @@ from sabnzbd.constants import (
|
||||
MEBI,
|
||||
GIGI,
|
||||
AddNzbFileResult,
|
||||
PP_LOOKUP,
|
||||
STAGES,
|
||||
)
|
||||
import sabnzbd.config as config
|
||||
import sabnzbd.cfg as cfg
|
||||
@@ -72,8 +74,9 @@ from sabnzbd.filesystem import diskspace, get_ext, clip_path, remove_all, list_s
|
||||
from sabnzbd.encoding import xml_name, utob
|
||||
from sabnzbd.utils.servertests import test_nntp_server_dict
|
||||
from sabnzbd.getipaddress import localipv4, publicipv4, ipv6, dnslookup, active_socks5_proxy
|
||||
from sabnzbd.database import build_history_info, unpack_history_info, HistoryDB
|
||||
from sabnzbd.database import HistoryDB
|
||||
from sabnzbd.lang import is_rtl
|
||||
from sabnzbd.nzbstuff import NzbObject
|
||||
import sabnzbd.emailer
|
||||
import sabnzbd.sorting
|
||||
|
||||
@@ -513,12 +516,12 @@ def _api_history(name, kwargs):
|
||||
elif value:
|
||||
jobs = value.split(",")
|
||||
for job in jobs:
|
||||
path = sabnzbd.PostProcessor.get_path(job)
|
||||
if path:
|
||||
if sabnzbd.PostProcessor.get_path(job):
|
||||
sabnzbd.PostProcessor.delete(job, del_files=del_files)
|
||||
else:
|
||||
history_db = sabnzbd.get_db_connection()
|
||||
remove_all(history_db.get_path(job), recursive=True)
|
||||
if del_files:
|
||||
remove_all(history_db.get_incomplete_path(job), recursive=True)
|
||||
history_db.remove_history(job)
|
||||
sabnzbd.misc.history_updated()
|
||||
return report()
|
||||
@@ -1289,11 +1292,11 @@ def build_status(calculate_performance: bool = False, skip_dashboard: bool = Fal
|
||||
info["servers"] = []
|
||||
# Servers-list could be modified during iteration, so we need a copy
|
||||
for server in sabnzbd.Downloader.servers[:]:
|
||||
connected = sum(nw.connected for nw in server.idle_threads[:])
|
||||
activeconn = sum(nw.connected for nw in server.idle_threads.copy())
|
||||
serverconnections = []
|
||||
for nw in server.busy_threads[:]:
|
||||
for nw in server.busy_threads.copy():
|
||||
if nw.connected:
|
||||
connected += 1
|
||||
activeconn += 1
|
||||
if nw.article:
|
||||
serverconnections.append(
|
||||
{
|
||||
@@ -1304,25 +1307,31 @@ def build_status(calculate_performance: bool = False, skip_dashboard: bool = Fal
|
||||
}
|
||||
)
|
||||
|
||||
if server.warning and not (connected or server.errormsg):
|
||||
connected = server.warning
|
||||
|
||||
if server.request and not server.info:
|
||||
connected = T(" Resolving address").replace(" ", "")
|
||||
|
||||
server_info = {
|
||||
"servername": server.displayname,
|
||||
"serveractiveconn": connected,
|
||||
"serveractive": server.active,
|
||||
"serveractiveconn": activeconn,
|
||||
"servertotalconn": server.threads,
|
||||
"serverconnections": serverconnections,
|
||||
"serverssl": server.ssl,
|
||||
"serversslinfo": server.ssl_info,
|
||||
"serveractive": server.active,
|
||||
"serveripaddress": None,
|
||||
"servercanonname": None,
|
||||
"serverwarning": server.warning,
|
||||
"servererror": server.errormsg,
|
||||
"serverpriority": server.priority,
|
||||
"serveroptional": server.optional,
|
||||
"serverbps": to_units(sabnzbd.BPSMeter.server_bps.get(server.id, 0)),
|
||||
}
|
||||
|
||||
# Only add this information if we are connected
|
||||
if activeconn and server.addrinfo:
|
||||
server_info["serveripaddress"] = server.addrinfo.ipaddress
|
||||
server_info["servercanonname"] = server.addrinfo.canonname
|
||||
|
||||
if server.request and not server.addrinfo:
|
||||
server_info["serverwarning"] = T("Resolving address")
|
||||
|
||||
info["servers"].append(server_info)
|
||||
|
||||
return info
|
||||
@@ -1506,7 +1515,7 @@ def retry_job(job, new_nzb=None, password=None):
|
||||
if futuretype:
|
||||
nzo_id = sabnzbd.urlgrabber.add_url(url, pp, script, cat)
|
||||
else:
|
||||
path = history_db.get_path(job)
|
||||
path = history_db.get_incomplete_path(job)
|
||||
nzo_id = sabnzbd.NzbQueue.repair_job(path, new_nzb, password)
|
||||
if nzo_id:
|
||||
# Only remove from history if we repaired something
|
||||
@@ -1627,7 +1636,7 @@ def build_history(
|
||||
failed_only: int = 0,
|
||||
categories: Optional[List[str]] = None,
|
||||
nzo_ids: Optional[List[str]] = None,
|
||||
) -> Tuple[Dict[str, Any], int, int]:
|
||||
) -> Tuple[List[Dict[str, Any]], int, int]:
|
||||
"""Combine the jobs still in post-processing and the database history"""
|
||||
if not limit:
|
||||
limit = 1000000
|
||||
@@ -1691,28 +1700,12 @@ def build_history(
|
||||
database_history_start, database_history_limit, search, failed_only, categories, nzo_ids
|
||||
)
|
||||
|
||||
# Add the postproc items to the top of the history
|
||||
# Reverse the queue to add items to the top (faster than insert)
|
||||
items.reverse()
|
||||
|
||||
# Add the postproc items to the top of the history
|
||||
items = get_active_history(postproc_queue, items)
|
||||
|
||||
# Un-reverse the queue
|
||||
items.reverse()
|
||||
|
||||
for item in items:
|
||||
item["size"] = to_units(item["bytes"], "B")
|
||||
|
||||
if "loaded" not in item:
|
||||
item["loaded"] = False
|
||||
|
||||
path = item.get("path", "")
|
||||
item["retry"] = int_conv(item.get("status") == Status.FAILED and path and os.path.exists(path))
|
||||
# Retry of failed URL-fetch
|
||||
if item["report"] == "future":
|
||||
item["retry"] = True
|
||||
|
||||
add_active_history(postproc_queue, items)
|
||||
total_items += postproc_queue_size
|
||||
items.reverse()
|
||||
|
||||
if close_db:
|
||||
history_db.close()
|
||||
@@ -1720,48 +1713,48 @@ def build_history(
|
||||
return items, postproc_queue_size, total_items
|
||||
|
||||
|
||||
def get_active_history(queue, items):
|
||||
"""Get the jobs currently in progress and active history queue."""
|
||||
for nzo in queue:
|
||||
item = {}
|
||||
(
|
||||
item["completed"],
|
||||
item["name"],
|
||||
item["nzb_name"],
|
||||
item["category"],
|
||||
item["pp"],
|
||||
item["script"],
|
||||
item["report"],
|
||||
item["url"],
|
||||
item["status"],
|
||||
item["nzo_id"],
|
||||
item["storage"],
|
||||
item["path"],
|
||||
item["script_log"],
|
||||
item["script_line"],
|
||||
item["download_time"],
|
||||
item["postproc_time"],
|
||||
item["stage_log"],
|
||||
item["downloaded"],
|
||||
item["fail_message"],
|
||||
item["url_info"],
|
||||
item["bytes"],
|
||||
_,
|
||||
_,
|
||||
item["password"],
|
||||
) = build_history_info(nzo)
|
||||
item["action_line"] = nzo.action_line
|
||||
item = unpack_history_info(item)
|
||||
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"""
|
||||
for nzo in postproc_queue:
|
||||
# This output has to be the same as fetch_history!
|
||||
item = {
|
||||
"completed": int(time.time()),
|
||||
"name": nzo.final_name,
|
||||
"nzb_name": nzo.filename,
|
||||
"category": nzo.cat,
|
||||
"pp": PP_LOOKUP.get(opts_to_pp(nzo.repair, nzo.unpack, nzo.delete), "X"),
|
||||
"script": nzo.script,
|
||||
"report": "",
|
||||
"url": nzo.url,
|
||||
"status": nzo.status,
|
||||
"nzo_id": nzo.nzo_id,
|
||||
"storage": "",
|
||||
"path": clip_path(nzo.download_path),
|
||||
"script_line": "",
|
||||
"download_time": nzo.nzo_info.get("download_time", 0),
|
||||
"postproc_time": 0,
|
||||
"stage_log": [],
|
||||
"downloaded": nzo.bytes_downloaded,
|
||||
"completeness": None,
|
||||
"fail_message": nzo.fail_msg,
|
||||
"url_info": nzo.nzo_info.get("details", "") or nzo.nzo_info.get("more_info", ""),
|
||||
"bytes": nzo.bytes_downloaded,
|
||||
"size": to_units(nzo.bytes_downloaded, "B"),
|
||||
"meta": None,
|
||||
"series": "",
|
||||
"md5sum": "",
|
||||
"password": nzo.correct_password,
|
||||
"action_line": nzo.action_line,
|
||||
"loaded": nzo.pp_active,
|
||||
"retry": False,
|
||||
}
|
||||
# Add stage information, in the correct order
|
||||
for stage in STAGES:
|
||||
if stage in nzo.unpack_info:
|
||||
item["stage_log"].append({"name": stage, "actions": nzo.unpack_info[stage]})
|
||||
|
||||
item["loaded"] = nzo.pp_active
|
||||
if item["bytes"]:
|
||||
item["size"] = to_units(item["bytes"], "B")
|
||||
else:
|
||||
item["size"] = ""
|
||||
items.append(item)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def calc_timeleft(bytesleft, bps):
|
||||
"""Based on bytesleft and bps calculate the time left in the format HH:MM:SS"""
|
||||
|
||||
@@ -22,7 +22,7 @@ sabnzbd.articlecache - Article cache handling
|
||||
import logging
|
||||
import threading
|
||||
import struct
|
||||
from typing import Dict, List
|
||||
from typing import Dict, Collection
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.decorators import synchronized
|
||||
@@ -91,7 +91,7 @@ class ArticleCache:
|
||||
return
|
||||
|
||||
# Register article for bookkeeping in case the job is deleted
|
||||
nzo.add_saved_article(article)
|
||||
nzo.saved_articles.add(article)
|
||||
|
||||
if article.lowest_partnum and not (article.nzf.import_finished or article.nzf.filename_checked):
|
||||
# Write the first-fetched articles to temporary file unless downloading
|
||||
@@ -133,7 +133,7 @@ class ArticleCache:
|
||||
data = sabnzbd.filesystem.load_data(
|
||||
article.art_id, nzo.admin_path, remove=True, do_pickle=False, silent=True
|
||||
)
|
||||
nzo.remove_saved_article(article)
|
||||
nzo.saved_articles.discard(article)
|
||||
return data
|
||||
|
||||
def flush_articles(self):
|
||||
@@ -147,7 +147,7 @@ class ArticleCache:
|
||||
# Could fail if already deleted by purge_articles or load_data
|
||||
logging.debug("Failed to flush item from cache, probably already deleted or written to disk")
|
||||
|
||||
def purge_articles(self, articles: List[Article]):
|
||||
def purge_articles(self, articles: Collection[Article]):
|
||||
"""Remove all saved articles, from memory and disk"""
|
||||
logging.debug("Purging %s articles from the cache/disk", len(articles))
|
||||
for article in articles:
|
||||
|
||||
101
sabnzbd/cfg.py
101
sabnzbd/cfg.py
@@ -25,7 +25,7 @@ import re
|
||||
import argparse
|
||||
import socket
|
||||
import ipaddress
|
||||
from typing import List, Tuple
|
||||
from typing import List, Tuple, Union
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.config import (
|
||||
@@ -51,7 +51,11 @@ from sabnzbd.constants import (
|
||||
DEF_HTTPS_CERT_FILE,
|
||||
DEF_HTTPS_KEY_FILE,
|
||||
)
|
||||
from sabnzbd.filesystem import long_path
|
||||
from sabnzbd.filesystem import same_directory, real_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]]
|
||||
|
||||
|
||||
##############################################################################
|
||||
@@ -64,7 +68,7 @@ class ErrorCatchingArgumentParser(argparse.ArgumentParser):
|
||||
raise ValueError
|
||||
|
||||
|
||||
def clean_nice_ionice_parameters(value):
|
||||
def clean_nice_ionice_parameters(value: str) -> ValidateResult:
|
||||
"""Verify that the passed parameters are not exploits"""
|
||||
if value:
|
||||
parser = ErrorCatchingArgumentParser()
|
||||
@@ -87,30 +91,20 @@ def clean_nice_ionice_parameters(value):
|
||||
return None, value
|
||||
|
||||
|
||||
def all_lowercase(value):
|
||||
"""Lowercase everything!"""
|
||||
def all_lowercase(value: Union[str, List]) -> Tuple[None, Union[str, List]]:
|
||||
"""Lowercase and strip everything!"""
|
||||
if isinstance(value, list):
|
||||
# If list, for each item
|
||||
return None, [item.lower() for item in value]
|
||||
return None, value.lower()
|
||||
return None, [item.lower().strip() for item in value]
|
||||
return None, value.lower().strip()
|
||||
|
||||
|
||||
def lower_case_ext(value):
|
||||
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_no_unc(root, value, default):
|
||||
"""Check if path isn't a UNC path"""
|
||||
# Only need to check the 'value' part
|
||||
if value and not value.startswith(r"\\"):
|
||||
return validate_notempty(root, value, default)
|
||||
else:
|
||||
return T('UNC path "%s" not allowed here') % value, None
|
||||
|
||||
|
||||
def validate_single_tag(value: List[str]) -> Tuple[None, List[str]]:
|
||||
"""Don't split single indexer tags like "TV > HD"
|
||||
into ['TV', '>', 'HD']
|
||||
@@ -121,7 +115,7 @@ def validate_single_tag(value: List[str]) -> Tuple[None, List[str]]:
|
||||
return None, value
|
||||
|
||||
|
||||
def validate_strip_right_slash(value):
|
||||
def validate_strip_right_slash(value: str) -> Tuple[None, str]:
|
||||
"""Strips the right slash"""
|
||||
if value:
|
||||
return None, value.rstrip("/")
|
||||
@@ -131,8 +125,7 @@ def validate_strip_right_slash(value):
|
||||
RE_VAL = re.compile(r"[^@ ]+@[^.@ ]+\.[^.@ ]")
|
||||
|
||||
|
||||
def validate_email(value):
|
||||
global email_endjob, email_full, email_rss
|
||||
def validate_email(value: Union[List, str]) -> ValidateResult:
|
||||
if email_endjob() or email_full() or email_rss():
|
||||
if isinstance(value, list):
|
||||
values = value
|
||||
@@ -144,18 +137,16 @@ def validate_email(value):
|
||||
return None, value
|
||||
|
||||
|
||||
def validate_server(value):
|
||||
def validate_server(value: str) -> ValidateResult:
|
||||
"""Check if server non-empty"""
|
||||
global email_endjob, email_full, email_rss
|
||||
if value == "" and (email_endjob() or email_full() or email_rss()):
|
||||
return T("Server address required"), None
|
||||
else:
|
||||
return None, value
|
||||
|
||||
|
||||
def validate_host(value):
|
||||
def validate_host(value: str) -> ValidateResult:
|
||||
"""Check if host is valid: an IP address, or a name/FQDN that resolves to an IP address"""
|
||||
|
||||
# easy: value is a plain IPv4 or IPv6 address:
|
||||
try:
|
||||
ipaddress.ip_address(value)
|
||||
@@ -195,7 +186,7 @@ def validate_host(value):
|
||||
return T("Invalid server address."), None
|
||||
|
||||
|
||||
def validate_script(value):
|
||||
def validate_script(value: str) -> ValidateResult:
|
||||
"""Check if value is a valid script"""
|
||||
if not sabnzbd.__INITIALIZED__ or (value and sabnzbd.filesystem.is_valid_script(value)):
|
||||
return None, value
|
||||
@@ -204,7 +195,7 @@ def validate_script(value):
|
||||
return T("%s is not a valid script") % value, None
|
||||
|
||||
|
||||
def validate_permissions(value: str):
|
||||
def validate_permissions(value: str) -> ValidateResult:
|
||||
"""Check the permissions for correct input"""
|
||||
# Octal verification
|
||||
if not value:
|
||||
@@ -225,18 +216,46 @@ def validate_permissions(value: str):
|
||||
return None, value
|
||||
|
||||
|
||||
def validate_safedir(root, value, default):
|
||||
def validate_safedir(root: str, value: str, default: str) -> ValidateResult:
|
||||
"""Allow only when queues are empty and no UNC"""
|
||||
if not sabnzbd.__INITIALIZED__ or (sabnzbd.PostProcessor.empty() and sabnzbd.NzbQueue.is_empty()):
|
||||
return validate_no_unc(root, value, default)
|
||||
if value.startswith(r"\\"):
|
||||
return T('UNC path "%s" not allowed here') % value, None
|
||||
else:
|
||||
return validate_default_if_empty(root, value, default)
|
||||
else:
|
||||
return T("Error: Queue not empty, cannot change folder."), None
|
||||
return T("Queue not empty, cannot change folder."), None
|
||||
|
||||
|
||||
def validate_scriptdir_not_appdir(root, value, default):
|
||||
def validate_download_vs_complete_dir(root: str, value: str, default: str):
|
||||
"""Make sure download_dir and complete_dir are not identical
|
||||
or that download_dir is not a subfolder of complete_dir"""
|
||||
# Check what new value we are trying to set
|
||||
if default == DEF_COMPLETE_DIR:
|
||||
check_download_dir = download_dir.get_path()
|
||||
check_complete_dir = real_path(root, value)
|
||||
elif default == DEF_DOWNLOAD_DIR:
|
||||
check_download_dir = real_path(root, value)
|
||||
check_complete_dir = complete_dir.get_path()
|
||||
else:
|
||||
raise ValueError("Validator can only be used for download_dir/complete_dir")
|
||||
|
||||
if same_directory(check_download_dir, check_complete_dir):
|
||||
return (
|
||||
T("The Completed Download Folder cannot be the same or a subfolder of the Temporary Download Folder"),
|
||||
None,
|
||||
)
|
||||
elif default == DEF_COMPLETE_DIR:
|
||||
# The complete_dir allows UNC
|
||||
return validate_default_if_empty(root, value, default)
|
||||
else:
|
||||
return validate_safedir(root, value, default)
|
||||
|
||||
|
||||
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 seperator so /mnt/sabnzbd and /mnt/sabnzbd-data are not detected as equal
|
||||
if value and long_path(os.path.join(root, value)).startswith(long_path(sabnzbd.DIR_PROG) + os.pathsep):
|
||||
if value and same_directory(sabnzbd.DIR_PROG, os.path.join(root, value)):
|
||||
# Warn, but do not block
|
||||
sabnzbd.misc.helpful_warning(
|
||||
T(
|
||||
@@ -246,7 +265,7 @@ def validate_scriptdir_not_appdir(root, value, default):
|
||||
return None, value
|
||||
|
||||
|
||||
def validate_notempty(root, value, default):
|
||||
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
|
||||
@@ -311,11 +330,21 @@ socks5_proxy_url = OptionStr("misc", "socks5_proxy_url")
|
||||
##############################################################################
|
||||
permissions = OptionStr("misc", "permissions", validation=validate_permissions)
|
||||
download_dir = OptionDir(
|
||||
"misc", "download_dir", DEF_DOWNLOAD_DIR, create=False, apply_permissions=True, validation=validate_safedir
|
||||
"misc",
|
||||
"download_dir",
|
||||
DEF_DOWNLOAD_DIR,
|
||||
create=False, # Flag is modified and directory is created during initialize!
|
||||
apply_permissions=True,
|
||||
validation=validate_download_vs_complete_dir,
|
||||
)
|
||||
download_free = OptionStr("misc", "download_free")
|
||||
complete_dir = OptionDir(
|
||||
"misc", "complete_dir", DEF_COMPLETE_DIR, create=False, apply_permissions=True, validation=validate_notempty
|
||||
"misc",
|
||||
"complete_dir",
|
||||
DEF_COMPLETE_DIR,
|
||||
create=False, # Flag is modified and directory is created during initialize!
|
||||
apply_permissions=True,
|
||||
validation=validate_download_vs_complete_dir,
|
||||
)
|
||||
complete_free = OptionStr("misc", "complete_free")
|
||||
fulldisk_autoresume = OptionBool("misc", "fulldisk_autoresume", False)
|
||||
@@ -326,7 +355,7 @@ backup_dir = OptionDir("misc", "backup_dir")
|
||||
dirscan_dir = OptionDir("misc", "dirscan_dir", writable=False)
|
||||
dirscan_speed = OptionNumber("misc", "dirscan_speed", DEF_SCANRATE, minval=0, maxval=3600)
|
||||
password_file = OptionDir("misc", "password_file", "", create=False)
|
||||
log_dir = OptionDir("misc", "log_dir", "logs", validation=validate_notempty)
|
||||
log_dir = OptionDir("misc", "log_dir", "logs", validation=validate_default_if_empty)
|
||||
|
||||
|
||||
##############################################################################
|
||||
|
||||
@@ -46,8 +46,7 @@ from sabnzbd.constants import (
|
||||
from sabnzbd.decorators import synchronized
|
||||
from sabnzbd.filesystem import clip_path, real_path, create_real_path, renamer, remove_file, is_writable
|
||||
|
||||
CONFIG_LOCK = threading.Lock()
|
||||
SAVE_CONFIG_LOCK = threading.Lock()
|
||||
CONFIG_LOCK = threading.RLock()
|
||||
|
||||
|
||||
CFG_OBJ: configobj.ConfigObj # Holds INI structure
|
||||
@@ -57,7 +56,7 @@ CFG_OBJ: configobj.ConfigObj # Holds INI structure
|
||||
CFG_MODIFIED = False # Signals a change in option dictionary
|
||||
# Should be reset after saving to settings file
|
||||
|
||||
RE_PARAMFINDER = re.compile(r"""(?:'.*?')|(?:".*?")|(?:[^'",\s][^,]*)""")
|
||||
RE_PARAMFINDER = re.compile(r"""'.*?'|".*?"|[^'",\s][^,]*""")
|
||||
|
||||
|
||||
class Option:
|
||||
@@ -236,6 +235,11 @@ class OptionDir(Option):
|
||||
self.__writable: bool = writable
|
||||
super().__init__(section, keyword, default_val, add=add, public=public, protect=protect)
|
||||
|
||||
def create_path(self, path: Optional[str] = None):
|
||||
if not path:
|
||||
path = self.get()
|
||||
return create_real_path(self.keyword, self.__root, path, self.__apply_permissions, self.__writable)
|
||||
|
||||
def get(self) -> str:
|
||||
"""Return value, corrected for platform"""
|
||||
p = super().get()
|
||||
@@ -245,15 +249,12 @@ class OptionDir(Option):
|
||||
return p.replace("\\", "/") if "\\" in p else p
|
||||
|
||||
def get_path(self) -> str:
|
||||
"""Return full absolute path"""
|
||||
value = self.get()
|
||||
"""Return full absolute path, create it if necessary"""
|
||||
path = ""
|
||||
if value:
|
||||
if value := self.get():
|
||||
path = real_path(self.__root, value)
|
||||
if self.__create and not os.path.exists(path):
|
||||
_, path, _ = create_real_path(
|
||||
self.keyword, self.__root, value, self.__apply_permissions, self.__writable
|
||||
)
|
||||
_, path, _ = self.create_path(value)
|
||||
return path
|
||||
|
||||
def get_clipped_path(self) -> str:
|
||||
@@ -262,8 +263,7 @@ class OptionDir(Option):
|
||||
|
||||
def test_path(self) -> bool:
|
||||
"""Return True if path exists"""
|
||||
value = self.get()
|
||||
if value:
|
||||
if value := self.get():
|
||||
return os.path.exists(real_path(self.__root, value))
|
||||
else:
|
||||
return False
|
||||
@@ -285,9 +285,7 @@ class OptionDir(Option):
|
||||
error, value = self.__validation(self.__root, value, super().default)
|
||||
if not error:
|
||||
if value and (self.__create or create):
|
||||
res, path, error = create_real_path(
|
||||
self.keyword, self.__root, value, self.__apply_permissions, self.__writable
|
||||
)
|
||||
_, path, error = self.create_path(value)
|
||||
if not error:
|
||||
super().set(value)
|
||||
return error
|
||||
@@ -783,6 +781,7 @@ def delete_from_database(section, keyword):
|
||||
CFG_MODIFIED = True
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def get_dconfig(section, keyword, nested=False):
|
||||
"""Return a config values dictionary,
|
||||
Single item or slices based on 'section', 'keyword'
|
||||
@@ -829,6 +828,7 @@ def get_dconfig(section, keyword, nested=False):
|
||||
return True, data
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def get_config(section: str, keyword: str) -> Optional[AllConfigTypes]:
|
||||
"""Return a config object, based on 'section', 'keyword'"""
|
||||
try:
|
||||
@@ -838,6 +838,7 @@ def get_config(section: str, keyword: str) -> Optional[AllConfigTypes]:
|
||||
return None
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def set_config(kwargs):
|
||||
"""Set a config item, using values in dictionary"""
|
||||
try:
|
||||
@@ -848,6 +849,7 @@ def set_config(kwargs):
|
||||
return True
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def delete(section: str, keyword: str):
|
||||
"""Delete specific config item"""
|
||||
try:
|
||||
@@ -862,7 +864,7 @@ def delete(section: str, keyword: str):
|
||||
# This does input and output of configuration to an INI file.
|
||||
# It translates this data structure to the config database.
|
||||
##############################################################################
|
||||
@synchronized(SAVE_CONFIG_LOCK)
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def read_config(path):
|
||||
"""Read the complete INI file and check its version number
|
||||
if OK, pass values to config-database
|
||||
@@ -947,7 +949,7 @@ def _read_config(path, try_backup=False):
|
||||
return True, ""
|
||||
|
||||
|
||||
@synchronized(SAVE_CONFIG_LOCK)
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def save_config(force=False):
|
||||
"""Update Setup file with current option values"""
|
||||
global CFG_OBJ, CFG_DATABASE, CFG_MODIFIED
|
||||
@@ -1102,6 +1104,7 @@ def restore_config_backup(config_backup_data: bytes):
|
||||
logging.info("Traceback: ", exc_info=True)
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def get_servers() -> Dict[str, ConfigServer]:
|
||||
global CFG_DATABASE
|
||||
try:
|
||||
@@ -1110,6 +1113,7 @@ def get_servers() -> Dict[str, ConfigServer]:
|
||||
return {}
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def get_sorters() -> Dict[str, ConfigSorter]:
|
||||
global CFG_DATABASE
|
||||
try:
|
||||
@@ -1128,6 +1132,7 @@ def get_ordered_sorters() -> List[Dict]:
|
||||
return sorters
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def get_categories() -> Dict[str, ConfigCat]:
|
||||
"""Return link to categories section.
|
||||
This section will always contain special category '*'
|
||||
@@ -1179,6 +1184,7 @@ def get_ordered_categories() -> List[Dict]:
|
||||
return categories
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def get_rss() -> Dict[str, ConfigRSS]:
|
||||
global CFG_DATABASE
|
||||
try:
|
||||
|
||||
@@ -49,7 +49,7 @@ RENAMES_FILE = "__renames__"
|
||||
ATTRIB_FILE = "SABnzbd_attrib"
|
||||
REPAIR_REQUEST = "repair-all.sab"
|
||||
|
||||
SABCTOOLS_VERSION_REQUIRED = "7.1.2"
|
||||
SABCTOOLS_VERSION_REQUIRED = "8.0.0"
|
||||
|
||||
DB_HISTORY_VERSION = 1
|
||||
DB_HISTORY_NAME = "history%s.db" % DB_HISTORY_VERSION
|
||||
@@ -110,6 +110,8 @@ PAUSED_PRIORITY = -2
|
||||
DUP_PRIORITY = -3
|
||||
STOP_PRIORITY = -4
|
||||
|
||||
PP_LOOKUP = {0: "", 1: "R", 2: "U", 3: "D"}
|
||||
|
||||
INTERFACE_PRIORITIES = {
|
||||
FORCE_PRIORITY: "Force",
|
||||
REPAIR_PRIORITY: "Repair",
|
||||
|
||||
@@ -27,41 +27,20 @@ import sys
|
||||
import threading
|
||||
import sqlite3
|
||||
from sqlite3 import Connection, Cursor
|
||||
from typing import Union, Dict, Optional, List, Sequence
|
||||
from typing import Optional, List, Sequence, Dict, Any, Tuple
|
||||
|
||||
import sabnzbd
|
||||
import sabnzbd.cfg
|
||||
from sabnzbd.constants import DB_HISTORY_NAME, STAGES, Status
|
||||
from sabnzbd.constants import DB_HISTORY_NAME, STAGES, Status, PP_LOOKUP
|
||||
from sabnzbd.bpsmeter import this_week, this_month
|
||||
from sabnzbd.decorators import synchronized
|
||||
from sabnzbd.encoding import ubtou, utob
|
||||
from sabnzbd.misc import int_conv, caller_name, opts_to_pp
|
||||
from sabnzbd.misc import int_conv, caller_name, opts_to_pp, to_units
|
||||
from sabnzbd.filesystem import remove_file, clip_path
|
||||
|
||||
DB_LOCK = threading.RLock()
|
||||
|
||||
|
||||
def convert_search(search):
|
||||
"""Convert classic wildcard to SQL wildcard"""
|
||||
if not search:
|
||||
# Default value
|
||||
search = ""
|
||||
else:
|
||||
# Allow * for wildcard matching and space
|
||||
search = search.replace("*", "%").replace(" ", "%")
|
||||
|
||||
# Allow ^ for start of string and $ for end of string
|
||||
if search and search.startswith("^"):
|
||||
search = search.replace("^", "")
|
||||
search += "%"
|
||||
elif search and search.endswith("$"):
|
||||
search = search.replace("$", "")
|
||||
search = "%" + search
|
||||
else:
|
||||
search = "%" + search + "%"
|
||||
return search
|
||||
|
||||
|
||||
class HistoryDB:
|
||||
"""Class to access the History database
|
||||
Each class-instance will create an access channel that
|
||||
@@ -277,9 +256,9 @@ class HistoryDB:
|
||||
save=True,
|
||||
)
|
||||
|
||||
def add_history_db(self, nzo, storage="", postproc_time=0, script_output="", script_line=""):
|
||||
def add_history_db(self, nzo, storage: str, postproc_time: int, script_output: str, script_line: str):
|
||||
"""Add a new job entry to the database"""
|
||||
t = build_history_info(nzo, storage, postproc_time, script_output, script_line, series_info=True)
|
||||
t = build_history_info(nzo, storage, postproc_time, script_output, script_line)
|
||||
|
||||
self.execute(
|
||||
"""INSERT INTO history (completed, name, nzb_name, category, pp, script, report,
|
||||
@@ -299,7 +278,7 @@ class HistoryDB:
|
||||
failed_only: int = 0,
|
||||
categories: 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)]
|
||||
|
||||
@@ -363,7 +342,7 @@ class HistoryDB:
|
||||
total = self.cursor.fetchone()["COUNT(*)"]
|
||||
return total > 0
|
||||
|
||||
def get_history_size(self):
|
||||
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
|
||||
"""
|
||||
@@ -388,34 +367,32 @@ class HistoryDB:
|
||||
|
||||
return total, month, week
|
||||
|
||||
def get_script_log(self, nzo_id):
|
||||
def get_script_log(self, nzo_id: str) -> str:
|
||||
"""Return decompressed log file"""
|
||||
data = ""
|
||||
t = (nzo_id,)
|
||||
if self.execute("""SELECT script_log FROM history WHERE nzo_id = ?""", t):
|
||||
if self.execute("""SELECT script_log FROM history WHERE nzo_id = ?""", (nzo_id,)):
|
||||
try:
|
||||
data = ubtou(zlib.decompress(self.cursor.fetchone()["script_log"]))
|
||||
except:
|
||||
pass
|
||||
return data
|
||||
|
||||
def get_name(self, nzo_id):
|
||||
def get_name(self, nzo_id: str) -> str:
|
||||
"""Return name of the job `nzo_id`"""
|
||||
t = (nzo_id,)
|
||||
name = ""
|
||||
if self.execute("""SELECT name FROM history WHERE nzo_id = ?""", t):
|
||||
if self.execute("""SELECT name FROM history WHERE nzo_id = ?""", (nzo_id,)):
|
||||
try:
|
||||
name = self.cursor.fetchone()["name"]
|
||||
return self.cursor.fetchone()["name"]
|
||||
except TypeError:
|
||||
# No records found
|
||||
pass
|
||||
return name
|
||||
|
||||
def get_path(self, nzo_id: str):
|
||||
"""Return the `incomplete` path of the job `nzo_id` if it is still there"""
|
||||
t = (nzo_id,)
|
||||
def get_incomplete_path(self, nzo_id: str) -> str:
|
||||
"""Return the `incomplete` path of the job `nzo_id` if
|
||||
the job failed and if the path is still there"""
|
||||
path = ""
|
||||
if self.execute("""SELECT path FROM history WHERE nzo_id = ?""", t):
|
||||
if self.execute("""SELECT path FROM history WHERE nzo_id = ? AND status = ?""", (nzo_id, Status.FAILED)):
|
||||
try:
|
||||
path = self.cursor.fetchone()["path"]
|
||||
except TypeError:
|
||||
@@ -423,12 +400,11 @@ class HistoryDB:
|
||||
pass
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
return None
|
||||
return path
|
||||
|
||||
def get_other(self, nzo_id):
|
||||
def get_other(self, nzo_id: str) -> Tuple[str, str, str, str, str]:
|
||||
"""Return additional data for job `nzo_id`"""
|
||||
t = (nzo_id,)
|
||||
if self.execute("""SELECT * FROM history WHERE nzo_id = ?""", t):
|
||||
if self.execute("""SELECT * FROM history WHERE nzo_id = ?""", (nzo_id,)):
|
||||
try:
|
||||
item = self.cursor.fetchone()
|
||||
return item["report"], item["url"], item["pp"], item["script"], item["category"]
|
||||
@@ -446,13 +422,32 @@ class HistoryDB:
|
||||
self.close()
|
||||
|
||||
|
||||
_PP_LOOKUP = {0: "", 1: "R", 2: "U", 3: "D"}
|
||||
def convert_search(search: str) -> str:
|
||||
"""Convert classic wildcard to SQL wildcard"""
|
||||
if not search:
|
||||
# Default value
|
||||
search = ""
|
||||
else:
|
||||
# Allow * for wildcard matching and space
|
||||
search = search.replace("*", "%").replace(" ", "%")
|
||||
|
||||
# Allow ^ for start of string and $ for end of string
|
||||
if search and search.startswith("^"):
|
||||
search = search.replace("^", "")
|
||||
search += "%"
|
||||
elif search and search.endswith("$"):
|
||||
search = search.replace("$", "")
|
||||
search = "%" + search
|
||||
else:
|
||||
search = "%" + search + "%"
|
||||
return search
|
||||
|
||||
|
||||
def build_history_info(nzo, workdir_complete="", postproc_time=0, script_output="", script_line="", series_info=False):
|
||||
def build_history_info(nzo, workdir_complete: str, postproc_time: int, script_output: str, script_line: str):
|
||||
"""Collects all the information needed for the database"""
|
||||
nzo: sabnzbd.nzbstuff.NzbObject
|
||||
completed = int(time.time())
|
||||
pp = _PP_LOOKUP.get(opts_to_pp(nzo.repair, nzo.unpack, nzo.delete), "X")
|
||||
pp = PP_LOOKUP.get(opts_to_pp(nzo.repair, nzo.unpack, nzo.delete), "X")
|
||||
|
||||
if script_output:
|
||||
# Compress the output of the script
|
||||
@@ -474,7 +469,8 @@ def build_history_info(nzo, workdir_complete="", postproc_time=0, script_output=
|
||||
|
||||
# Analyze series info only when job is finished
|
||||
series = ""
|
||||
if series_info and (show_analysis := sabnzbd.newsunpack.analyse_show(nzo.final_name))["job_type"] == "tv":
|
||||
show_analysis = sabnzbd.newsunpack.analyse_show(nzo.final_name)
|
||||
if show_analysis["job_type"] == "tv":
|
||||
seriesname, season, episode = (show_analysis[key] for key in ("title", "season", "episode"))
|
||||
if seriesname and season and episode:
|
||||
series = "%s/%s/%s" % (seriesname.lower(), season, episode)
|
||||
@@ -507,13 +503,12 @@ def build_history_info(nzo, workdir_complete="", postproc_time=0, script_output=
|
||||
)
|
||||
|
||||
|
||||
def unpack_history_info(item: Union[Dict, sqlite3.Row]):
|
||||
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
|
||||
"""
|
||||
# Convert result to dictionary
|
||||
if isinstance(item, sqlite3.Row):
|
||||
item = dict(item)
|
||||
item = dict(item)
|
||||
|
||||
# Stage Name is separated by ::: stage lines by ; and stages by \r\n
|
||||
lst = item["stage_log"]
|
||||
@@ -522,7 +517,7 @@ def unpack_history_info(item: Union[Dict, sqlite3.Row]):
|
||||
try:
|
||||
all_stages_lines = lst.split("\r\n")
|
||||
except:
|
||||
logging.error(T("Invalid stage logging in history for %s"), item["name"])
|
||||
logging.warning(T("Invalid stage logging in history for %s"), item["name"])
|
||||
logging.debug("Lines: %s", lst)
|
||||
all_stages_lines = []
|
||||
|
||||
@@ -536,7 +531,7 @@ def unpack_history_info(item: Union[Dict, sqlite3.Row]):
|
||||
try:
|
||||
stage["actions"] = logs.split(";")
|
||||
except:
|
||||
logging.error(T("Invalid stage logging in history for %s"), item["name"])
|
||||
logging.warning(T("Invalid stage logging in history for %s"), item["name"])
|
||||
logging.debug("Logs: %s", logs)
|
||||
parsed_stage_log.append(stage)
|
||||
|
||||
@@ -546,11 +541,24 @@ def unpack_history_info(item: Union[Dict, sqlite3.Row]):
|
||||
else:
|
||||
item["stage_log"] = []
|
||||
|
||||
if item["script_log"]:
|
||||
item["script_log"] = ""
|
||||
# The action line is only available for items in the postproc queue
|
||||
if "action_line" not in item:
|
||||
item["action_line"] = ""
|
||||
# Remove database id
|
||||
item.pop("id")
|
||||
|
||||
# Human-readable size
|
||||
item["size"] = to_units(item["bytes"], "B")
|
||||
|
||||
# We do not want the raw script output here
|
||||
item.pop("script_log")
|
||||
|
||||
# The action line and loaded is only available for items in the postproc queue
|
||||
item["action_line"] = ""
|
||||
item["loaded"] = False
|
||||
|
||||
# Retry and retry for failed URL-fetch
|
||||
item["retry"] = int_conv(item["status"] == Status.FAILED and item["path"] and os.path.exists(item["path"]))
|
||||
if item["report"] == "future":
|
||||
item["retry"] = True
|
||||
|
||||
return item
|
||||
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class BadUu(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def decode(article: Article, raw_data: bytearray):
|
||||
def decode(article: Article, data_view: memoryview):
|
||||
decoded_data = None
|
||||
nzo = article.nzf.nzo
|
||||
art_id = article.article
|
||||
@@ -79,9 +79,9 @@ def decode(article: Article, raw_data: bytearray):
|
||||
logging.debug("Decoding %s", art_id)
|
||||
|
||||
if article.nzf.type == "uu":
|
||||
decoded_data = decode_uu(article, raw_data)
|
||||
decoded_data = decode_uu(article, bytes(data_view))
|
||||
else:
|
||||
decoded_data = decode_yenc(article, raw_data)
|
||||
decoded_data = decode_yenc(article, data_view)
|
||||
|
||||
article_success = True
|
||||
|
||||
@@ -112,23 +112,23 @@ def decode(article: Article, raw_data: bytearray):
|
||||
|
||||
except (BadYenc, ValueError):
|
||||
# Handles precheck and badly formed articles
|
||||
if nzo.precheck and raw_data and raw_data.startswith(b"223 "):
|
||||
if nzo.precheck and data_view and data_view[:4] == b"223 ":
|
||||
# STAT was used, so we only get a status code
|
||||
article_success = True
|
||||
else:
|
||||
# Try uu-decoding
|
||||
if (not nzo.precheck) and article.nzf.type != "yenc":
|
||||
if not nzo.precheck and article.nzf.type != "yenc":
|
||||
try:
|
||||
decoded_data = decode_uu(article, raw_data)
|
||||
decoded_data = decode_uu(article, bytes(data_view))
|
||||
logging.debug("Found uu-encoded article %s in job %s", art_id, nzo.final_name)
|
||||
article_success = True
|
||||
except Exception:
|
||||
except:
|
||||
pass
|
||||
# Only bother with further checks if uu-decoding didn't work out
|
||||
if not article_success:
|
||||
# Convert the first 2000 bytes of raw socket data to article lines,
|
||||
# and examine the headers (for precheck) or body (for download).
|
||||
for line in raw_data[:2000].split(b"\r\n"):
|
||||
for line in bytes(data_view[:2000]).split(b"\r\n"):
|
||||
lline = line.lower()
|
||||
if lline.startswith(b"message-id:"):
|
||||
article_success = True
|
||||
@@ -170,9 +170,16 @@ def decode(article: Article, raw_data: bytearray):
|
||||
sabnzbd.NzbQueue.register_article(article, article_success)
|
||||
|
||||
|
||||
def decode_yenc(article: Article, data: bytearray) -> bytearray:
|
||||
def decode_yenc(article: Article, data_view: memoryview) -> bytearray:
|
||||
# Let SABCTools do all the heavy lifting
|
||||
yenc_filename, article.file_size, article.data_begin, article.data_size, crc_correct = sabctools.yenc_decode(data)
|
||||
(
|
||||
decoded_data,
|
||||
yenc_filename,
|
||||
article.file_size,
|
||||
article.data_begin,
|
||||
article.data_size,
|
||||
crc_correct,
|
||||
) = sabctools.yenc_decode(data_view)
|
||||
|
||||
nzf = article.nzf
|
||||
# Assume it is yenc
|
||||
@@ -182,7 +189,7 @@ def decode_yenc(article: Article, data: bytearray) -> bytearray:
|
||||
if not nzf.filename_checked and yenc_filename:
|
||||
# Set the md5-of-16k if this is the first article
|
||||
if article.lowest_partnum:
|
||||
nzf.md5of16k = hashlib.md5(data[:16384]).digest()
|
||||
nzf.md5of16k = hashlib.md5(decoded_data[:16384]).digest()
|
||||
|
||||
# Try the rename, even if it's not the first article
|
||||
# For example when the first article was missing
|
||||
@@ -191,14 +198,14 @@ def decode_yenc(article: Article, data: bytearray) -> bytearray:
|
||||
# CRC check
|
||||
if crc_correct is None:
|
||||
logging.info("CRC Error in %s", article.article)
|
||||
raise BadData(data)
|
||||
raise BadData(decoded_data)
|
||||
|
||||
article.crc32 = crc_correct
|
||||
|
||||
return data
|
||||
return decoded_data
|
||||
|
||||
|
||||
def decode_uu(article: Article, raw_data: bytearray) -> bytes:
|
||||
def decode_uu(article: Article, raw_data: bytes) -> bytes:
|
||||
"""Try to uu-decode an article. The raw_data may or may not contain headers.
|
||||
If there are headers, they will be separated from the body by at least one
|
||||
empty line. In case of no headers, the first line seems to always be the nntp
|
||||
@@ -222,7 +229,7 @@ def decode_uu(article: Article, raw_data: bytearray) -> bytes:
|
||||
# Try to find an empty line separating the body from headers or response
|
||||
# code and set the expected payload start to the next line.
|
||||
try:
|
||||
uu_start = raw_data[:limit].index(bytearray(b"")) + 1
|
||||
uu_start = raw_data[:limit].index(b"") + 1
|
||||
except ValueError:
|
||||
# No empty line, look for a response code instead
|
||||
if raw_data[0].startswith(b"222 "):
|
||||
|
||||
@@ -29,6 +29,9 @@ from threading import Lock, RLock, Condition
|
||||
NZBQUEUE_LOCK = RLock()
|
||||
DOWNLOADER_CV = Condition(NZBQUEUE_LOCK)
|
||||
|
||||
# All operations that modify downloader state need to be locked
|
||||
DOWNLOADER_LOCK = RLock()
|
||||
|
||||
|
||||
def synchronized(lock: Union[Lock, RLock]):
|
||||
def wrap(func: Callable):
|
||||
|
||||
@@ -180,24 +180,31 @@ class DirectUnpacker(threading.Thread):
|
||||
with START_STOP_LOCK:
|
||||
if not self.active_instance or not self.active_instance.stdout:
|
||||
break
|
||||
char = self.active_instance.stdout.read(1)
|
||||
|
||||
while 1:
|
||||
# Keep reading until reaching space or end of line
|
||||
# to prevent continuous locking and unlocking
|
||||
char = self.active_instance.stdout.read(1)
|
||||
linebuf += char
|
||||
|
||||
if char in (b" ", b"\n", b""):
|
||||
break
|
||||
|
||||
# End of program
|
||||
if not char:
|
||||
# End of program
|
||||
break
|
||||
linebuf += char
|
||||
|
||||
# Continue if it's not a space or end of line
|
||||
if char not in (b" ", b"\n"):
|
||||
continue
|
||||
|
||||
# Handle whole lines
|
||||
if char == b"\n":
|
||||
# When reaching end-of-line, we can safely convert and add to the log
|
||||
linebuf_encoded = platform_btou(linebuf.strip())
|
||||
unrar_log.append(linebuf_encoded)
|
||||
linebuf = b""
|
||||
|
||||
# Skip empty lines
|
||||
if not linebuf_encoded:
|
||||
continue
|
||||
unrar_log.append(linebuf_encoded)
|
||||
|
||||
# Error? Let PP-handle this job
|
||||
if any(
|
||||
error_text in linebuf_encoded
|
||||
|
||||
@@ -23,21 +23,19 @@ import time
|
||||
import select
|
||||
import logging
|
||||
from math import ceil
|
||||
from threading import Thread, RLock
|
||||
from threading import Thread, RLock, current_thread
|
||||
import socket
|
||||
import sys
|
||||
import ssl
|
||||
from typing import List, Dict, Optional, Union, Set
|
||||
import concurrent
|
||||
from concurrent.futures import ThreadPoolExecutor, Future
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.decorators import synchronized, NzbQueueLocker, DOWNLOADER_CV
|
||||
from sabnzbd.decorators import synchronized, NzbQueueLocker, DOWNLOADER_CV, DOWNLOADER_LOCK
|
||||
from sabnzbd.newswrapper import NewsWrapper, NNTPPermanentError
|
||||
import sabnzbd.config as config
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.misc import from_units, get_server_addrinfo, helpful_warning, int_conv
|
||||
from sabnzbd.utils.happyeyeballs import happyeyeballs
|
||||
from sabnzbd.misc import from_units, helpful_warning, int_conv, MultiAddQueue
|
||||
from sabnzbd.happyeyeballs import happyeyeballs, AddrInfo
|
||||
from sabnzbd.constants import SOFT_QUEUE_LIMIT
|
||||
|
||||
|
||||
@@ -61,7 +59,6 @@ _ARTICLE_PREFETCH = 20
|
||||
_DEFAULT_CHUNK_SIZE = 32768
|
||||
|
||||
TIMER_LOCK = RLock()
|
||||
DOWNLOADER_LOCK = RLock()
|
||||
|
||||
|
||||
class Server:
|
||||
@@ -94,7 +91,7 @@ class Server:
|
||||
"bad_cons",
|
||||
"errormsg",
|
||||
"warning",
|
||||
"info",
|
||||
"addrinfo",
|
||||
"ssl_info",
|
||||
"request",
|
||||
"have_body",
|
||||
@@ -139,18 +136,25 @@ class Server:
|
||||
self.retention: int = retention
|
||||
self.send_group: bool = send_group
|
||||
|
||||
# TODO: Remove for final release
|
||||
if send_group:
|
||||
helpful_warning(
|
||||
"You have 'Send Group' enabled for %s. Could you let us know why? https://github.com/sabnzbd/sabnzbd/discussions/2715",
|
||||
self.displayname,
|
||||
)
|
||||
|
||||
self.username: Optional[str] = username
|
||||
self.password: Optional[str] = password
|
||||
|
||||
self.busy_threads: List[NewsWrapper] = []
|
||||
self.busy_threads: Set[NewsWrapper] = set()
|
||||
self.next_busy_threads_check: float = 0
|
||||
self.idle_threads: List[NewsWrapper] = []
|
||||
self.idle_threads: Set[NewsWrapper] = set()
|
||||
self.next_article_search: float = 0
|
||||
self.active: bool = True
|
||||
self.bad_cons: int = 0
|
||||
self.errormsg: str = ""
|
||||
self.warning: str = ""
|
||||
self.info: Optional[List] = None # Will hold getaddrinfo() list
|
||||
self.addrinfo: Union[AddrInfo, None, bool] = None # Will hold fasted address information
|
||||
self.ssl_info: str = "" # Will hold the type and cipher of SSL connection
|
||||
self.request: bool = False # True if a getaddrinfo() request is pending
|
||||
self.have_body: bool = True # Assume server has "BODY", until proven otherwise
|
||||
@@ -161,38 +165,11 @@ class Server:
|
||||
if threads:
|
||||
# Initialize threads
|
||||
for i in range(threads):
|
||||
self.idle_threads.append(NewsWrapper(self, i + 1))
|
||||
self.idle_threads.add(NewsWrapper(self, i + 1))
|
||||
|
||||
# Tell the BPSMeter about this server
|
||||
sabnzbd.BPSMeter.init_server_stats(self.id)
|
||||
|
||||
@property
|
||||
def hostip(self) -> str:
|
||||
"""In case a server still has active connections, we use the same IP again.
|
||||
If new connection then use happyeyeballs if there are multiple options.
|
||||
In case of problems: return the host name itself
|
||||
"""
|
||||
# Check if already a successful ongoing connection
|
||||
if self.busy_threads and self.busy_threads[0].nntp:
|
||||
# Re-use that IP
|
||||
logging.debug("%s: Re-using address %s", self.host, self.busy_threads[0].nntp.host)
|
||||
return self.busy_threads[0].nntp.host
|
||||
|
||||
# Determine IP
|
||||
ip = None
|
||||
if self.info:
|
||||
# RFC6555 / Happy Eyeballs in case of multiple options
|
||||
if len(self.info) > 1:
|
||||
ip = happyeyeballs(self.host, port=self.port)
|
||||
|
||||
# Just 1 IP found, or problem with happyeyeballs, return first one
|
||||
if not ip:
|
||||
ip = self.info[0][4][0]
|
||||
else:
|
||||
ip = self.host
|
||||
logging.debug("%s: Connecting to address %s", self.host, ip)
|
||||
return ip
|
||||
|
||||
def deactivate(self):
|
||||
"""Deactivate server and reset queued articles"""
|
||||
self.active = False
|
||||
@@ -202,8 +179,8 @@ class Server:
|
||||
"""Remove all connections from server"""
|
||||
for nw in self.idle_threads:
|
||||
sabnzbd.Downloader.remove_socket(nw)
|
||||
nw.hard_reset(send_quit=True)
|
||||
self.idle_threads = []
|
||||
nw.hard_reset()
|
||||
self.idle_threads = set()
|
||||
|
||||
@synchronized(DOWNLOADER_LOCK)
|
||||
def get_article(self):
|
||||
@@ -235,22 +212,22 @@ class Server:
|
||||
sabnzbd.NzbQueue.reset_try_lists(article, remove_fetcher_from_trylist=False)
|
||||
self.article_queue = []
|
||||
|
||||
def request_info(self):
|
||||
"""Launch async request to resolve server address.
|
||||
getaddrinfo() can be very slow. In some situations this can lead
|
||||
to delayed starts and timeouts on connections.
|
||||
def request_addrinfo(self):
|
||||
"""Launch async request to resolve server address and perform Happy Eyeballs.
|
||||
In some situations this can be slow and result in delayed starts and timeouts on connections.
|
||||
Because of this, the results will be cached in the server object."""
|
||||
if not self.request:
|
||||
self.request = True
|
||||
Thread(target=self._request_info_internal).start()
|
||||
Thread(target=self.request_addrinfo_blocking).start()
|
||||
|
||||
def _request_info_internal(self):
|
||||
"""Async attempt to run getaddrinfo() for specified server"""
|
||||
def request_addrinfo_blocking(self):
|
||||
"""Blocking attempt to run getaddrinfo() and Happy Eyeballs for specified server"""
|
||||
logging.debug("Retrieving server address information for %s", self.host)
|
||||
self.info = get_server_addrinfo(self.host, self.port)
|
||||
if not self.info:
|
||||
self.addrinfo = happyeyeballs(self.host, self.port)
|
||||
if not self.addrinfo:
|
||||
self.bad_cons += self.threads
|
||||
self.info = False
|
||||
# Notify next call to maybe_block_server
|
||||
self.addrinfo = False
|
||||
else:
|
||||
self.bad_cons = 0
|
||||
self.request = False
|
||||
@@ -270,8 +247,6 @@ class Downloader(Thread):
|
||||
"bandwidth_limit",
|
||||
"bandwidth_perc",
|
||||
"sleep_time",
|
||||
"recv_pool",
|
||||
"recv_threads",
|
||||
"paused_for_postproc",
|
||||
"shutdown",
|
||||
"server_restarts",
|
||||
@@ -281,7 +256,6 @@ class Downloader(Thread):
|
||||
"timers",
|
||||
"last_max_chunk_size",
|
||||
"max_chunk_size",
|
||||
"process_tasks",
|
||||
)
|
||||
|
||||
def __init__(self, paused=False):
|
||||
@@ -308,12 +282,6 @@ class Downloader(Thread):
|
||||
self.last_max_chunk_size: int = 0
|
||||
self.max_chunk_size: int = _DEFAULT_CHUNK_SIZE
|
||||
|
||||
self.recv_threads: int = cfg.receive_threads()
|
||||
self.recv_pool: Optional[ThreadPoolExecutor] = ThreadPoolExecutor(self.recv_threads)
|
||||
logging.debug("Receive threads: %s", self.recv_threads)
|
||||
|
||||
self.process_tasks: Set[Future[NewsWrapper, int]] = set()
|
||||
|
||||
self.paused_for_postproc: bool = False
|
||||
self.shutdown: bool = False
|
||||
|
||||
@@ -507,7 +475,7 @@ class Downloader(Thread):
|
||||
|
||||
def maybe_block_server(self, server: Server):
|
||||
# Was it resolving problem?
|
||||
if server.info is False:
|
||||
if server.addrinfo is False:
|
||||
# Warn about resolving issues
|
||||
errormsg = T("Cannot connect to server %s [%s]") % (server.host, T("Server name does not resolve"))
|
||||
if server.errormsg != errormsg:
|
||||
@@ -534,27 +502,27 @@ class Downloader(Thread):
|
||||
self.plan_server(server, _PENALTY_TIMEOUT)
|
||||
|
||||
# Remove all connections to server
|
||||
for nw in server.idle_threads + server.busy_threads:
|
||||
self.__reset_nw(nw, "forcing disconnect", warn=False, wait=False, retry_article=False, send_quit=False)
|
||||
for nw in server.idle_threads | server.busy_threads:
|
||||
self.__reset_nw(nw, "forcing disconnect", warn=False, wait=False, retry_article=False)
|
||||
|
||||
# Make sure server address resolution is refreshed
|
||||
server.info = None
|
||||
server.addrinfo = None
|
||||
|
||||
@staticmethod
|
||||
def decode(article, raw_data: Optional[bytearray] = None):
|
||||
def decode(article, data_view: Optional[memoryview] = None):
|
||||
"""Decode article"""
|
||||
# Article was requested and fetched, update article stats for the server
|
||||
sabnzbd.BPSMeter.register_server_article_tried(article.fetcher.id)
|
||||
|
||||
# Handle broken articles directly
|
||||
if not raw_data:
|
||||
if not data_view:
|
||||
if not article.search_new_server():
|
||||
sabnzbd.NzbQueue.register_article(article, success=False)
|
||||
article.nzf.nzo.increase_bad_articles_counter("missing_articles")
|
||||
return
|
||||
|
||||
# Decode and send to article cache
|
||||
sabnzbd.decoder.decode(article, raw_data)
|
||||
sabnzbd.decoder.decode(article, data_view)
|
||||
|
||||
def run(self):
|
||||
# Verify SSL certificate checking
|
||||
@@ -573,6 +541,13 @@ class Downloader(Thread):
|
||||
# Check server expiration dates
|
||||
check_server_expiration()
|
||||
|
||||
# Initialize queue and threads
|
||||
process_nw_queue = MultiAddQueue()
|
||||
for _ in range(cfg.receive_threads()):
|
||||
# Started as daemon, so we don't need any shutdown logic in the worker
|
||||
# The Downloader code will make sure shutdown is handled gracefully
|
||||
Thread(target=self.process_nw_worker, args=(self.read_fds, process_nw_queue), daemon=True).start()
|
||||
|
||||
# Catch all errors, just in case
|
||||
try:
|
||||
while 1:
|
||||
@@ -584,12 +559,12 @@ class Downloader(Thread):
|
||||
|
||||
for server in self.servers:
|
||||
# Skip this server if there's no point searching for new stuff to do
|
||||
if not server.busy_threads and server.next_article_search > now:
|
||||
if server.addrinfo and not server.busy_threads and server.next_article_search > now:
|
||||
continue
|
||||
|
||||
if server.next_busy_threads_check < now:
|
||||
server.next_busy_threads_check = now + _SERVER_CHECK_DELAY
|
||||
for nw in server.busy_threads[:]:
|
||||
for nw in server.busy_threads.copy():
|
||||
if (nw.nntp and nw.nntp.error_msg) or (nw.timeout and now > nw.timeout):
|
||||
if nw.nntp and nw.nntp.error_msg:
|
||||
# Already showed error
|
||||
@@ -621,18 +596,18 @@ class Downloader(Thread):
|
||||
):
|
||||
continue
|
||||
|
||||
for nw in server.idle_threads[:]:
|
||||
for nw in server.idle_threads.copy():
|
||||
if nw.timeout:
|
||||
if now < nw.timeout:
|
||||
continue
|
||||
else:
|
||||
nw.timeout = None
|
||||
|
||||
if not server.info:
|
||||
if not server.addrinfo:
|
||||
# Only request info if there's stuff in the queue
|
||||
if not sabnzbd.NzbQueue.is_empty():
|
||||
self.maybe_block_server(server)
|
||||
server.request_info()
|
||||
server.request_addrinfo()
|
||||
break
|
||||
|
||||
nw.article = server.get_article()
|
||||
@@ -640,7 +615,7 @@ class Downloader(Thread):
|
||||
break
|
||||
|
||||
server.idle_threads.remove(nw)
|
||||
server.busy_threads.append(nw)
|
||||
server.busy_threads.add(nw)
|
||||
|
||||
if nw.connected:
|
||||
self.__request_article(nw)
|
||||
@@ -659,14 +634,12 @@ class Downloader(Thread):
|
||||
|
||||
if self.force_disconnect or self.shutdown:
|
||||
for server in self.servers:
|
||||
for nw in server.idle_threads + server.busy_threads:
|
||||
for nw in server.idle_threads | server.busy_threads:
|
||||
# Send goodbye if we have open socket
|
||||
if nw.nntp:
|
||||
self.__reset_nw(
|
||||
nw, "forcing disconnect", wait=False, count_article_try=False, send_quit=True
|
||||
)
|
||||
self.__reset_nw(nw, "forcing disconnect", wait=False, count_article_try=False)
|
||||
# Make sure server address resolution is refreshed
|
||||
server.info = None
|
||||
server.addrinfo = None
|
||||
server.reset_article_queue()
|
||||
self.force_disconnect = False
|
||||
|
||||
@@ -705,53 +678,38 @@ class Downloader(Thread):
|
||||
DOWNLOADER_CV.wait()
|
||||
|
||||
if now > next_bpsmeter_update:
|
||||
# Do not update statistics and check levels every loop
|
||||
BPSMeter.update()
|
||||
next_bpsmeter_update = now + _BPSMETER_UPDATE_DELAY
|
||||
self.check_assembler_levels()
|
||||
|
||||
if not read:
|
||||
continue
|
||||
|
||||
if self.recv_threads > 1:
|
||||
# Submit a process_nw task to the pool for every NewWrapper which is readable
|
||||
for selected in read:
|
||||
self.process_tasks.add(self.recv_pool.submit(self.process_nw, self.read_fds[selected]))
|
||||
# Submit all readable sockets to be processed and wait for completion
|
||||
process_nw_queue.put_multiple(read)
|
||||
process_nw_queue.join()
|
||||
|
||||
# Wait for all the tasks to complete
|
||||
concurrent.futures.wait(self.process_tasks)
|
||||
|
||||
# Clear task list
|
||||
self.process_tasks.clear()
|
||||
else:
|
||||
for selected in read:
|
||||
self.process_nw(self.read_fds[selected])
|
||||
|
||||
# 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))
|
||||
sabnzbd.BPSMeter.delayed_assembler += 1
|
||||
logged_counter = 0
|
||||
|
||||
while not self.shutdown and sabnzbd.Assembler.queue_level() >= 1:
|
||||
# Only log/update once every second, to not waste any CPU-cycles
|
||||
if not logged_counter % 10:
|
||||
# Make sure the BPS-meter is updated
|
||||
sabnzbd.BPSMeter.update()
|
||||
|
||||
# Update who is delaying us
|
||||
logging.debug(
|
||||
"Delayed - %d seconds - Assembler queue: %d",
|
||||
logged_counter / 10,
|
||||
sabnzbd.Assembler.queue.qsize(),
|
||||
)
|
||||
|
||||
# Wait and update the queue sizes
|
||||
time.sleep(0.1)
|
||||
logged_counter += 1
|
||||
except:
|
||||
logging.error(T("Fatal error in Downloader"), exc_info=True)
|
||||
|
||||
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."""
|
||||
try:
|
||||
logging.debug("Starting Downloader receive thread: %s", current_thread().name)
|
||||
while True:
|
||||
# The read_fds is passed by reference, so we can access its items!
|
||||
self.process_nw(read_fds[nw_queue.get()])
|
||||
nw_queue.task_done()
|
||||
except:
|
||||
# We cannot break out of the Downloader from here, so just pause
|
||||
logging.error(T("Fatal error in Downloader"), exc_info=True)
|
||||
self.pause()
|
||||
|
||||
def process_nw(self, nw: NewsWrapper):
|
||||
"""Receive data from NewsWrapper and handle response"""
|
||||
"""Receive data from a NewsWrapper and handle the response"""
|
||||
try:
|
||||
bytes_received, done = nw.recv_chunk()
|
||||
except ssl.SSLWantReadError:
|
||||
@@ -828,7 +786,7 @@ class Downloader(Thread):
|
||||
server.errormsg = server.warning = ""
|
||||
|
||||
# Decode
|
||||
self.decode(article, nw.get_data_buffer())
|
||||
self.decode(article, nw.data_view[: nw.data_position])
|
||||
|
||||
if sabnzbd.LOG_ALL:
|
||||
logging.debug("Thread %s@%s: %s done", nw.thrdnum, server.host, article.article)
|
||||
@@ -848,10 +806,34 @@ class Downloader(Thread):
|
||||
self.__request_article(nw)
|
||||
return
|
||||
|
||||
with DOWNLOADER_LOCK:
|
||||
server.busy_threads.remove(nw)
|
||||
server.idle_threads.append(nw)
|
||||
self.remove_socket(nw)
|
||||
# Make socket available again
|
||||
server.busy_threads.discard(nw)
|
||||
server.idle_threads.add(nw)
|
||||
self.remove_socket(nw)
|
||||
|
||||
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))
|
||||
sabnzbd.BPSMeter.delayed_assembler += 1
|
||||
logged_counter = 0
|
||||
|
||||
while not self.shutdown and sabnzbd.Assembler.queue_level() >= 1:
|
||||
# Only log/update once every second, to not waste any CPU-cycles
|
||||
if not logged_counter % 10:
|
||||
# Make sure the BPS-meter is updated
|
||||
sabnzbd.BPSMeter.update()
|
||||
|
||||
# Update who is delaying us
|
||||
logging.debug(
|
||||
"Delayed - %d seconds - Assembler queue: %d",
|
||||
logged_counter / 10,
|
||||
sabnzbd.Assembler.queue.qsize(),
|
||||
)
|
||||
|
||||
# Wait and update the queue sizes
|
||||
time.sleep(0.1)
|
||||
logged_counter += 1
|
||||
|
||||
@synchronized(DOWNLOADER_LOCK)
|
||||
def __finish_connect_nw(self, nw: NewsWrapper) -> bool:
|
||||
@@ -873,7 +855,7 @@ class Downloader(Thread):
|
||||
errormsg = T("Too many connections to server %s [%s]") % (server.host, error.msg)
|
||||
if server.active:
|
||||
# Don't count this for the tries (max_art_tries) on this server
|
||||
self.__reset_nw(nw, send_quit=True)
|
||||
self.__reset_nw(nw)
|
||||
self.plan_server(server, _PENALTY_TOOMANY)
|
||||
server.threads -= 1
|
||||
elif error.code in (502, 481, 482) and clues_too_many_ip(error.msg):
|
||||
@@ -917,7 +899,7 @@ class Downloader(Thread):
|
||||
if penalty and (block or server.optional):
|
||||
self.plan_server(server, penalty)
|
||||
# Note that the article is discard for this server if the server is not required
|
||||
self.__reset_nw(nw, retry_article=retry_article, send_quit=True)
|
||||
self.__reset_nw(nw, retry_article=retry_article)
|
||||
|
||||
# Set error for server and warn user if it was first time thrown
|
||||
if errormsg and server.active and server.errormsg != errormsg:
|
||||
@@ -944,7 +926,6 @@ class Downloader(Thread):
|
||||
wait: bool = True,
|
||||
count_article_try: bool = True,
|
||||
retry_article: bool = True,
|
||||
send_quit: bool = False,
|
||||
):
|
||||
# Some warnings are errors, and not added as server.warning
|
||||
if warn and reset_msg:
|
||||
@@ -954,10 +935,8 @@ class Downloader(Thread):
|
||||
logging.debug("Thread %s@%s: %s", nw.thrdnum, nw.server.host, reset_msg)
|
||||
|
||||
# Make sure this NewsWrapper is in the idle threads
|
||||
if nw in nw.server.busy_threads:
|
||||
nw.server.busy_threads.remove(nw)
|
||||
if nw not in nw.server.idle_threads:
|
||||
nw.server.idle_threads.append(nw)
|
||||
nw.server.busy_threads.discard(nw)
|
||||
nw.server.idle_threads.add(nw)
|
||||
|
||||
# Make sure it is not in the readable sockets
|
||||
self.remove_socket(nw)
|
||||
@@ -983,7 +962,7 @@ class Downloader(Thread):
|
||||
nw.article.fetcher.article_queue.append(nw.article)
|
||||
|
||||
# Reset connection object
|
||||
nw.hard_reset(wait, send_quit=send_quit)
|
||||
nw.hard_reset(wait)
|
||||
|
||||
# Empty SSL info, it might change on next connect
|
||||
nw.server.ssl_info = ""
|
||||
@@ -1004,11 +983,11 @@ class Downloader(Thread):
|
||||
self.add_socket(nw.nntp.fileno, nw)
|
||||
except socket.error as err:
|
||||
logging.info("Looks like server closed connection: %s", err)
|
||||
self.__reset_nw(nw, "server broke off connection", warn=True, send_quit=False)
|
||||
self.__reset_nw(nw, "server broke off connection", warn=True)
|
||||
except:
|
||||
logging.error(T("Suspect error in downloader"))
|
||||
logging.info("Traceback: ", exc_info=True)
|
||||
self.__reset_nw(nw, "server broke off connection", warn=True, send_quit=False)
|
||||
self.__reset_nw(nw, "server broke off connection", warn=True)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Timed restart of servers admin.
|
||||
|
||||
@@ -234,21 +234,25 @@ def sanitize_filename(name: str) -> str:
|
||||
# preserving the extension (max ext length 20)
|
||||
# Note: some filesystem can handle up to 255 UTF chars (which is more than 255 bytes) in the filename,
|
||||
# but we stay on the safe side: max DEF_FILE_MAX bytes
|
||||
if len(utob(name)) + len(utob(ext)) > DEF_FILE_MAX:
|
||||
logging.debug("Filename %s is too long, so truncating", name + ext)
|
||||
# Too long filenames are often caused by incorrect non-ascii chars,
|
||||
# so brute-force remove those non-ascii chars
|
||||
name = ubtou(name.encode("ascii", "ignore"))
|
||||
# Now it's plain ASCII, so no need for len(str.encode()) anymore; plain len() is enough
|
||||
if len(name) + len(ext) > DEF_FILE_MAX:
|
||||
# still too long, limit the extension
|
||||
maxextlength = 20 # max length of an extension
|
||||
if len(ext) > maxextlength:
|
||||
# allow first <maxextlength> chars, including the starting dot
|
||||
ext = ext[:maxextlength]
|
||||
try:
|
||||
if len(utob(name)) + len(utob(ext)) > DEF_FILE_MAX:
|
||||
logging.debug("Filename %s is too long, so truncating", name + ext)
|
||||
# Too long filenames are often caused by incorrect non-ascii chars,
|
||||
# so brute-force remove those non-ascii chars
|
||||
name = ubtou(name.encode("ascii", "ignore"))
|
||||
# Now it's plain ASCII, so no need for len(str.encode()) anymore; plain len() is enough
|
||||
if len(name) + len(ext) > DEF_FILE_MAX:
|
||||
# Still too long, limit the basename
|
||||
name = name[: DEF_FILE_MAX - len(ext)]
|
||||
# still too long, limit the extension
|
||||
maxextlength = 20 # max length of an extension
|
||||
if len(ext) > maxextlength:
|
||||
# allow first <maxextlength> chars, including the starting dot
|
||||
ext = ext[:maxextlength]
|
||||
if len(name) + len(ext) > DEF_FILE_MAX:
|
||||
# Still too long, limit the basename
|
||||
name = name[: DEF_FILE_MAX - len(ext)]
|
||||
except UnicodeError:
|
||||
# Just in case of strange encoding problems, like #2714
|
||||
pass
|
||||
|
||||
lowext = ext.lower()
|
||||
if lowext == ".par2" and lowext != ext:
|
||||
@@ -407,10 +411,10 @@ def create_real_path(
|
||||
return False, path, None
|
||||
|
||||
|
||||
def same_file(a: str, b: str) -> int:
|
||||
def same_directory(a: str, b: str) -> int:
|
||||
"""Return 0 if A and B have nothing in common
|
||||
return 1 if A and B are actually the same path
|
||||
return 2 if B is a subfolder of A
|
||||
return 2 if B is a sub-folder of A
|
||||
"""
|
||||
if sabnzbd.WIN32 or sabnzbd.MACOS:
|
||||
a = clip_path(a.lower())
|
||||
@@ -419,6 +423,13 @@ def same_file(a: str, b: str) -> int:
|
||||
a = os.path.normpath(os.path.abspath(a))
|
||||
b = os.path.normpath(os.path.abspath(b))
|
||||
|
||||
# Need to add seperator so /mnt/sabnzbd and /mnt/sabnzbd-data are not detected as equal
|
||||
# But only if it doesn't already end in a slash, for example C:\
|
||||
if not a.endswith(os.sep):
|
||||
a = a + os.sep
|
||||
if not b.endswith(os.sep):
|
||||
b = b + os.sep
|
||||
|
||||
# If it's the same file, it's also a sub-folder
|
||||
is_subfolder = 0
|
||||
if b.startswith(a):
|
||||
@@ -865,11 +876,11 @@ def renamer(old: str, new: str, create_local_directories: bool = False) -> str:
|
||||
oldpath, _ = os.path.split(old)
|
||||
# Check not outside directory
|
||||
# In case of "same_file() == 1": same directory, so nothing to do
|
||||
if same_file(oldpath, path) == 0:
|
||||
if same_directory(oldpath, path) == 0:
|
||||
# Outside current directory, this is most likely malicious
|
||||
logging.error(T("Blocked attempt to create directory %s"), path)
|
||||
raise OSError("Refusing to go outside directory")
|
||||
elif same_file(oldpath, path) == 2:
|
||||
elif same_directory(oldpath, path) == 2:
|
||||
# Sub-directory, so create if does not yet exist:
|
||||
create_all_dirs(path)
|
||||
|
||||
@@ -1193,20 +1204,6 @@ def load_admin(data_id: str, remove=False, silent=False) -> Any:
|
||||
return load_data(data_id, sabnzbd.cfg.admin_dir.get_path(), remove=remove, silent=silent)
|
||||
|
||||
|
||||
def check_incomplete_vs_complete():
|
||||
"""Make sure download_dir and complete_dir are not identical
|
||||
or that download_dir is not a subfolder of complete_dir"""
|
||||
complete = sabnzbd.cfg.complete_dir.get_path()
|
||||
if same_file(sabnzbd.cfg.download_dir.get_path(), complete):
|
||||
if real_path("X", sabnzbd.cfg.download_dir()) == long_path(sabnzbd.cfg.download_dir()):
|
||||
# Abs path, so set download_dir as an abs path inside the complete_dir
|
||||
sabnzbd.cfg.download_dir.set(os.path.join(complete, "incomplete"))
|
||||
else:
|
||||
sabnzbd.cfg.download_dir.set("incomplete")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def wait_for_download_folder():
|
||||
"""Wait for download folder to become available"""
|
||||
while not sabnzbd.cfg.download_dir.test_path():
|
||||
|
||||
@@ -20,7 +20,6 @@ sabnzbd.getipaddress
|
||||
"""
|
||||
|
||||
import socket
|
||||
import multiprocessing.pool
|
||||
import functools
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
126
sabnzbd/happyeyeballs.py
Normal file
126
sabnzbd/happyeyeballs.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/python3 -OO
|
||||
# Copyright 2007-2023 The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
"""
|
||||
sabnzbd.happyeyeballs - Python implementation of RFC 6555 / Happy Eyeballs: find the quickest IPv4/IPv6 connection
|
||||
"""
|
||||
|
||||
# Python implementation of RFC 6555 / Happy Eyeballs: find the quickest IPv4/IPv6 connection
|
||||
# See https://tools.ietf.org/html/rfc6555
|
||||
# Method: Start parallel sessions using threads, and only wait for the quickest successful socket connect
|
||||
# See https://tools.ietf.org/html/rfc6555#section-4.1
|
||||
# We do not implement caching, as the lookup result is stored in the Server object
|
||||
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
import queue
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple, Union, Optional
|
||||
|
||||
from sabnzbd import cfg as cfg
|
||||
|
||||
# We always prefer IPv6 connections
|
||||
IP4_DELAY = 0.1
|
||||
|
||||
|
||||
# For typing and convenience!
|
||||
@dataclass
|
||||
class AddrInfo:
|
||||
family: socket.AddressFamily
|
||||
type: socket.SocketKind
|
||||
proto: int
|
||||
canonname: str
|
||||
sockaddr: Union[Tuple[str, int], Tuple[str, int, int, int]]
|
||||
ipaddress: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
# For easy access
|
||||
self.ipaddress = self.sockaddr[0]
|
||||
|
||||
|
||||
# Called by each thread
|
||||
def do_socket_connect(result_queue: queue.Queue, addrinfo: AddrInfo, ipv4_delay: int):
|
||||
"""Connect to the ip, and put the result into the queue"""
|
||||
try:
|
||||
s = socket.socket(addrinfo.family, addrinfo.type)
|
||||
s.settimeout(3)
|
||||
|
||||
# Delay IPv4 connects in case we need it
|
||||
if ipv4_delay and addrinfo.family == socket.AddressFamily.AF_INET:
|
||||
time.sleep(ipv4_delay)
|
||||
|
||||
try:
|
||||
s.connect(addrinfo.sockaddr)
|
||||
finally:
|
||||
s.close()
|
||||
result_queue.put((addrinfo, True))
|
||||
except:
|
||||
# We got an exception, so no successful connect on IP & port:
|
||||
result_queue.put((addrinfo, False))
|
||||
|
||||
|
||||
def happyeyeballs(host: str, port: int) -> Optional[AddrInfo]:
|
||||
"""Return the fastest result of getaddrinfo() based on RFC 6555 / Happy Eyeballs,
|
||||
including IPv6 addresses if desired. Returns None in case no addresses were returned
|
||||
or if no connection could be made to any of the addresses"""
|
||||
try:
|
||||
# Time how long it took us
|
||||
start = time.time()
|
||||
|
||||
# Get address information, by default both IPV4 and IPV6
|
||||
family = socket.AF_UNSPEC
|
||||
if not cfg.ipv6_servers():
|
||||
family = socket.AF_INET
|
||||
|
||||
all_addrinfo = []
|
||||
ipv4_delay = 0
|
||||
last_canonname = ""
|
||||
for addrinfo in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM, flags=socket.AI_CANONNAME):
|
||||
# Convert to AddrInfo
|
||||
all_addrinfo.append(addrinfo := AddrInfo(*addrinfo))
|
||||
# We only want delay for IPv4 in case we got any IPv6
|
||||
if addrinfo.family == socket.AddressFamily.AF_INET6:
|
||||
ipv4_delay = IP4_DELAY
|
||||
# The canonname is only reported once per alias
|
||||
if addrinfo.canonname:
|
||||
last_canonname = addrinfo.canonname
|
||||
elif last_canonname:
|
||||
addrinfo.canonname = last_canonname
|
||||
logging.debug("Available addresses for %s (port=%d): %d", host, port, len(all_addrinfo))
|
||||
|
||||
# Fill queue used for threads that will return the results
|
||||
# Even if there is just 1 result, we still check if we can connect
|
||||
result_queue: queue.Queue[Tuple[AddrInfo, bool]] = queue.Queue()
|
||||
for addrinfo in all_addrinfo:
|
||||
threading.Thread(target=do_socket_connect, args=(result_queue, addrinfo, ipv4_delay), daemon=True).start()
|
||||
|
||||
# start reading from the Queue for message from the threads:
|
||||
result = None
|
||||
for _ in range(len(all_addrinfo)):
|
||||
connect_result = result_queue.get()
|
||||
if connect_result[1]:
|
||||
result = connect_result[0]
|
||||
break
|
||||
|
||||
logging.info("Quickest IP address for %s (port=%d): %s (%s)", host, port, result.ipaddress, result.canonname)
|
||||
logging.debug("Happy Eyeballs lookup and port connect took: %d ms", int(1000 * (time.time() - start)))
|
||||
return result
|
||||
except Exception as e:
|
||||
logging.debug("Failed Happy Eyeballs lookup: %s", e)
|
||||
return None
|
||||
@@ -47,20 +47,19 @@ from sabnzbd.misc import (
|
||||
get_base_url,
|
||||
is_ipv4_addr,
|
||||
is_ipv6_addr,
|
||||
get_server_addrinfo,
|
||||
is_lan_addr,
|
||||
is_local_addr,
|
||||
is_loopback_addr,
|
||||
ip_in_subnet,
|
||||
helpful_warning,
|
||||
recursive_html_escape,
|
||||
)
|
||||
from sabnzbd.happyeyeballs import happyeyeballs
|
||||
from sabnzbd.filesystem import (
|
||||
real_path,
|
||||
globber,
|
||||
globber_full,
|
||||
clip_path,
|
||||
same_file,
|
||||
same_directory,
|
||||
setname_from_path,
|
||||
)
|
||||
from sabnzbd.encoding import xml_name, utob
|
||||
@@ -180,8 +179,7 @@ def secured_expose(
|
||||
|
||||
# Some pages need correct API key
|
||||
if check_api_key:
|
||||
msg = check_apikey(kwargs)
|
||||
if msg:
|
||||
if msg := check_apikey(kwargs):
|
||||
cherrypy.response.status = 403
|
||||
if cfg.api_warnings():
|
||||
return msg
|
||||
@@ -470,8 +468,7 @@ class MainPage:
|
||||
def scriptlog(self, **kwargs):
|
||||
"""Needed for all skins, URL is fixed due to postproc"""
|
||||
# No session key check, due to fixed URLs
|
||||
name = kwargs.get("name")
|
||||
if name:
|
||||
if name := kwargs.get("name"):
|
||||
history_db = sabnzbd.get_db_connection()
|
||||
return ShowString(history_db.get_name(name), history_db.get_script_log(name))
|
||||
else:
|
||||
@@ -739,21 +736,9 @@ class ConfigFolders:
|
||||
@secured_expose(check_api_key=True, check_configlock=True)
|
||||
def saveDirectories(self, **kwargs):
|
||||
for kw in LIST_DIRPAGE + LIST_BOOL_DIRPAGE:
|
||||
value = kwargs.get(kw)
|
||||
if value is not None or kw in LIST_BOOL_DIRPAGE:
|
||||
if kw in ("complete_dir", "dirscan_dir", "backup_dir"):
|
||||
msg = config.get_config("misc", kw).set(value, create=True)
|
||||
else:
|
||||
msg = config.get_config("misc", kw).set(value)
|
||||
if msg:
|
||||
# return sabnzbd.api.report('json', error=msg)
|
||||
return badParameterResponse(msg, kwargs.get("ajax"))
|
||||
if msg := config.get_config("misc", kw).set(kwargs.get(kw)):
|
||||
return badParameterResponse(msg, kwargs.get("ajax"))
|
||||
|
||||
if not sabnzbd.filesystem.check_incomplete_vs_complete():
|
||||
return badParameterResponse(
|
||||
T("The Completed Download Folder cannot be the same or a subfolder of the Temporary Download Folder"),
|
||||
kwargs.get("ajax"),
|
||||
)
|
||||
config.save_config()
|
||||
if kwargs.get("ajax"):
|
||||
return sabnzbd.api.report()
|
||||
@@ -833,8 +818,7 @@ class ConfigSwitches:
|
||||
@secured_expose(check_api_key=True, check_configlock=True)
|
||||
def saveSwitches(self, **kwargs):
|
||||
for kw in SWITCH_LIST:
|
||||
msg = config.get_config("misc", kw).set(kwargs.get(kw))
|
||||
if msg:
|
||||
if msg := config.get_config("misc", kw).set(kwargs.get(kw)):
|
||||
return badParameterResponse(msg, kwargs.get("ajax"))
|
||||
|
||||
config.save_config()
|
||||
@@ -934,10 +918,7 @@ class ConfigSpecial:
|
||||
@secured_expose(check_api_key=True, check_configlock=True)
|
||||
def saveSpecial(self, **kwargs):
|
||||
for kw in SPECIAL_BOOL_LIST + SPECIAL_VALUE_LIST + SPECIAL_LIST_LIST:
|
||||
item = config.get_config("misc", kw)
|
||||
value = kwargs.get(kw)
|
||||
msg = item.set(value)
|
||||
if msg:
|
||||
if msg := config.get_config("misc", kw).set(kwargs.get(kw)):
|
||||
return badParameterResponse(msg)
|
||||
|
||||
config.save_config()
|
||||
@@ -961,6 +942,8 @@ GENERAL_LIST = (
|
||||
"socks5_proxy_url",
|
||||
"auto_browser",
|
||||
"check_new_rel",
|
||||
"bandwidth_max",
|
||||
"bandwidth_perc",
|
||||
)
|
||||
|
||||
|
||||
@@ -995,8 +978,6 @@ class ConfigGeneral:
|
||||
for kw in GENERAL_LIST:
|
||||
conf[kw] = config.get_config("misc", kw)()
|
||||
|
||||
conf["bandwidth_max"] = cfg.bandwidth_max()
|
||||
conf["bandwidth_perc"] = cfg.bandwidth_perc()
|
||||
conf["nzb_key"] = cfg.nzb_key()
|
||||
conf["caller_url"] = cherrypy.request.base + cfg.url_base()
|
||||
|
||||
@@ -1009,10 +990,7 @@ class ConfigGeneral:
|
||||
def saveGeneral(self, **kwargs):
|
||||
# Handle general options
|
||||
for kw in GENERAL_LIST:
|
||||
item = config.get_config("misc", kw)
|
||||
value = kwargs.get(kw)
|
||||
msg = item.set(value)
|
||||
if msg:
|
||||
if msg := config.get_config("misc", kw).set(kwargs.get(kw)):
|
||||
return badParameterResponse(msg, ajax=kwargs.get("ajax"))
|
||||
|
||||
# Handle special options
|
||||
@@ -1021,16 +999,6 @@ class ConfigGeneral:
|
||||
web_dir = kwargs.get("web_dir")
|
||||
change_web_dir(web_dir)
|
||||
|
||||
bandwidth_max = kwargs.get("bandwidth_max")
|
||||
if bandwidth_max is not None:
|
||||
cfg.bandwidth_max.set(bandwidth_max)
|
||||
bandwidth_perc = kwargs.get("bandwidth_perc")
|
||||
if bandwidth_perc is not None:
|
||||
cfg.bandwidth_perc.set(bandwidth_perc)
|
||||
bandwidth_perc = cfg.bandwidth_perc()
|
||||
if bandwidth_perc and not bandwidth_max:
|
||||
helpful_warning(T("You must set a maximum bandwidth before you can set a bandwidth limit"))
|
||||
|
||||
config.save_config()
|
||||
|
||||
# Update CherryPy authentication
|
||||
@@ -1182,7 +1150,7 @@ def handle_server(kwargs, root=None, new_svr=False):
|
||||
kwargs["connections"] = "1"
|
||||
|
||||
if kwargs.get("enable") == "1":
|
||||
if not get_server_addrinfo(host, int_conv(port)):
|
||||
if not happyeyeballs(host, int_conv(port)):
|
||||
return badParameterResponse(T('Server address "%s:%s" is not valid.') % (host, port), ajax)
|
||||
|
||||
# Default server name is just the host name
|
||||
@@ -1803,7 +1771,7 @@ class ConfigCats:
|
||||
|
||||
if newname:
|
||||
# Check if this cat-dir is not sub-folder of incomplete
|
||||
if same_file(cfg.download_dir.get_path(), real_path(cfg.complete_dir.get_path(), kwargs["dir"])):
|
||||
if same_directory(cfg.download_dir.get_path(), real_path(cfg.complete_dir.get_path(), kwargs["dir"])):
|
||||
return T("Category folder cannot be a subfolder of the Temporary Download Folder.")
|
||||
|
||||
# Delete current one and replace with new one
|
||||
@@ -2169,7 +2137,8 @@ class ConfigNotify:
|
||||
def saveNotify(self, **kwargs):
|
||||
for section in NOTIFY_OPTIONS:
|
||||
for option in NOTIFY_OPTIONS[section]:
|
||||
config.get_config(section, option).set(kwargs.get(option))
|
||||
if msg := config.get_config(section, option).set(kwargs.get(option)):
|
||||
return badParameterResponse(msg, kwargs.get("ajax"))
|
||||
config.save_config()
|
||||
if kwargs.get("ajax"):
|
||||
return sabnzbd.api.report()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user