mirror of
https://github.com/sabnzbd/sabnzbd.git
synced 2026-01-03 21:20:24 -05:00
Compare commits
261 Commits
3.0.2RC2
...
bugfix/han
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ca80e481c | ||
|
|
f5f8aa985e | ||
|
|
1a848cf5fe | ||
|
|
b748b05fbd | ||
|
|
9f2a9c32c0 | ||
|
|
92d0b0163a | ||
|
|
c50e2a4026 | ||
|
|
69ffa159c7 | ||
|
|
81089fc20a | ||
|
|
3d09f72c90 | ||
|
|
ef7d84b24d | ||
|
|
9b71f8ca4b | ||
|
|
04c3fc77cb | ||
|
|
c6cc6f4537 | ||
|
|
f31a4440f1 | ||
|
|
84b1e60803 | ||
|
|
a434a5f25d | ||
|
|
09e844a63f | ||
|
|
c55e114131 | ||
|
|
575fbc06aa | ||
|
|
19376805de | ||
|
|
5ea6a31bc2 | ||
|
|
2714ffe04d | ||
|
|
c38eac0e46 | ||
|
|
fccc57fd52 | ||
|
|
fea309da11 | ||
|
|
d867881162 | ||
|
|
af9a7d2fb3 | ||
|
|
259584b24f | ||
|
|
38f61f64c7 | ||
|
|
3e9bfba4d6 | ||
|
|
be26c7f080 | ||
|
|
6b8befdc67 | ||
|
|
423e4e429b | ||
|
|
53aba47915 | ||
|
|
87f90b004f | ||
|
|
0b96afb055 | ||
|
|
8e99ebe5ef | ||
|
|
6e06d954fe | ||
|
|
497abb83da | ||
|
|
7ffebd97b9 | ||
|
|
55a5855720 | ||
|
|
adc828dc8a | ||
|
|
6c5c9e0147 | ||
|
|
baa9ffb948 | ||
|
|
92541fec23 | ||
|
|
b1f6448ae0 | ||
|
|
fc72cf0451 | ||
|
|
c76d931b01 | ||
|
|
02ef37d381 | ||
|
|
329b420c0d | ||
|
|
10049d0c1f | ||
|
|
1e602d86bd | ||
|
|
f22ab0068e | ||
|
|
3700e45e7f | ||
|
|
36196a176e | ||
|
|
72907de5ef | ||
|
|
9a7385789e | ||
|
|
d13893d1c7 | ||
|
|
1a8031c75d | ||
|
|
9d10261a9f | ||
|
|
d0a7ff00fc | ||
|
|
b80d0ee458 | ||
|
|
53069492b1 | ||
|
|
3e2dad4a7e | ||
|
|
fca1e5355e | ||
|
|
47c0fd706f | ||
|
|
4c4ffb2f54 | ||
|
|
ade477c6e5 | ||
|
|
719b966709 | ||
|
|
2085c04717 | ||
|
|
12a4e34075 | ||
|
|
13dd81ebbd | ||
|
|
a9492eb25f | ||
|
|
4dabbb7590 | ||
|
|
64b78bddd6 | ||
|
|
5a02554380 | ||
|
|
c312f3917f | ||
|
|
30654af261 | ||
|
|
29aa329038 | ||
|
|
cfbb0d3bf6 | ||
|
|
388f77ea52 | ||
|
|
139c2f3c14 | ||
|
|
dab544bc93 | ||
|
|
0070fce88d | ||
|
|
c27ecfe339 | ||
|
|
746de90700 | ||
|
|
c580f1aff7 | ||
|
|
93b429af8b | ||
|
|
f0e2e783a8 | ||
|
|
9c2af4281a | ||
|
|
c12e25217b | ||
|
|
d5d0903591 | ||
|
|
72bde214a3 | ||
|
|
3ae2cbcd2c | ||
|
|
82b3f210f6 | ||
|
|
b8e67c558d | ||
|
|
371bcfbf5b | ||
|
|
d75f1ed966 | ||
|
|
5e4c3e0fa4 | ||
|
|
2c2642a92a | ||
|
|
afa0a206bc | ||
|
|
57a8661988 | ||
|
|
a57b58b675 | ||
|
|
8b051462a8 | ||
|
|
3bde8373a3 | ||
|
|
73df161cd0 | ||
|
|
9c83fd14bc | ||
|
|
ab020a0654 | ||
|
|
14e77f3f9b | ||
|
|
730d717936 | ||
|
|
91a7a83cd5 | ||
|
|
6fb586e30f | ||
|
|
05b069ab8e | ||
|
|
33a9eca696 | ||
|
|
2b969c987c | ||
|
|
f6c15490cc | ||
|
|
da5e95595d | ||
|
|
56343b9d19 | ||
|
|
d2a4f5cbe5 | ||
|
|
bf5f071e9d | ||
|
|
5d14aac430 | ||
|
|
f69f895418 | ||
|
|
e572c34743 | ||
|
|
822f3a760f | ||
|
|
274c236860 | ||
|
|
29d074732d | ||
|
|
097cec5283 | ||
|
|
f0ee73f03b | ||
|
|
691110af2c | ||
|
|
1c7d3cc66d | ||
|
|
58df97961b | ||
|
|
61cefb3308 | ||
|
|
694b0178e6 | ||
|
|
48ae414941 | ||
|
|
b143767f8d | ||
|
|
11de24ad4f | ||
|
|
a9c5f2e184 | ||
|
|
ed3ad27560 | ||
|
|
a6632b6e3e | ||
|
|
2d49e7b4ce | ||
|
|
c097ad828d | ||
|
|
7125ee469f | ||
|
|
f91646f956 | ||
|
|
5bd86b6fb7 | ||
|
|
e12ed3e6f1 | ||
|
|
33a5d34bbf | ||
|
|
94662f5831 | ||
|
|
a37ffe5b4d | ||
|
|
fa1b421dad | ||
|
|
93727c52ae | ||
|
|
0108730004 | ||
|
|
10b97e708a | ||
|
|
cfa23ca27e | ||
|
|
5290eaefc7 | ||
|
|
2626b715ab | ||
|
|
99bc350f5f | ||
|
|
ee38441779 | ||
|
|
f0d31e0dc2 | ||
|
|
4a08b47c07 | ||
|
|
2d588a6498 | ||
|
|
510ec977b8 | ||
|
|
420a3d385d | ||
|
|
30185d1dbe | ||
|
|
642f949ae9 | ||
|
|
872804e1f4 | ||
|
|
b5a1575d5a | ||
|
|
95197f94be | ||
|
|
fedab57f29 | ||
|
|
3054c568ac | ||
|
|
c0fcc34f52 | ||
|
|
521e2bd7aa | ||
|
|
db4db08550 | ||
|
|
977f0204a7 | ||
|
|
78d12ddb03 | ||
|
|
433dcab02b | ||
|
|
c57563d5ca | ||
|
|
fcc4a44695 | ||
|
|
7d9f9b4d1f | ||
|
|
f790a9601f | ||
|
|
13d44d1ed9 | ||
|
|
d57ecd4eaa | ||
|
|
f14e5ba400 | ||
|
|
5638f435ba | ||
|
|
6b7b8a8203 | ||
|
|
942f95364e | ||
|
|
e997fb6679 | ||
|
|
3b8a96de23 | ||
|
|
75d6d10649 | ||
|
|
26736657fd | ||
|
|
27b6194d53 | ||
|
|
5c158db350 | ||
|
|
f54c173479 | ||
|
|
d51b337045 | ||
|
|
3d693a7b8d | ||
|
|
99f34ab71d | ||
|
|
cd2f95ac90 | ||
|
|
bacea59c0c | ||
|
|
f7e84a8f11 | ||
|
|
1452ddd5e4 | ||
|
|
abdbdd63f4 | ||
|
|
a92d2b585e | ||
|
|
3dae1bd104 | ||
|
|
e07c0c0981 | ||
|
|
b7dcd051b1 | ||
|
|
25223c8b85 | ||
|
|
d7b1a73777 | ||
|
|
df19d4d323 | ||
|
|
10bc4ed611 | ||
|
|
b063055e78 | ||
|
|
4f1f422701 | ||
|
|
7e44a3759f | ||
|
|
0a5a4ec0da | ||
|
|
c9a5280c7a | ||
|
|
c953498a9d | ||
|
|
c0ec8fcea2 | ||
|
|
7562444763 | ||
|
|
747add419e | ||
|
|
f242053d6c | ||
|
|
235df91a37 | ||
|
|
97ffa0bac2 | ||
|
|
c6bc7d93f4 | ||
|
|
bcc7573756 | ||
|
|
1f554816b6 | ||
|
|
411463bc57 | ||
|
|
1c26685c8c | ||
|
|
1dd4afa5e2 | ||
|
|
f9d4477cb1 | ||
|
|
6cbee09950 | ||
|
|
bcf6a5bd09 | ||
|
|
a00092f5cc | ||
|
|
b36b345ef3 | ||
|
|
029d97e21c | ||
|
|
13331e0709 | ||
|
|
68ad931728 | ||
|
|
475aa60bcd | ||
|
|
2937d8a022 | ||
|
|
6627510a59 | ||
|
|
ac9448cacc | ||
|
|
2d7b6717a9 | ||
|
|
c4aad2a4bd | ||
|
|
4566f23984 | ||
|
|
b6621fc333 | ||
|
|
084b2b357f | ||
|
|
aa5c63f467 | ||
|
|
c365065cdb | ||
|
|
e4a42de095 | ||
|
|
245935b7ac | ||
|
|
2540a8174f | ||
|
|
38c0a75759 | ||
|
|
eac5f20937 | ||
|
|
a71a2a7a4b | ||
|
|
856fdd3493 | ||
|
|
cbd54bdfe8 | ||
|
|
f294f8c740 | ||
|
|
3079976165 | ||
|
|
020005e89b | ||
|
|
b73a6d2a7f | ||
|
|
af5acd16f7 | ||
|
|
9d98dbb2a6 | ||
|
|
01406ca2e7 |
4
.github/workflows/black.yml
vendored
4
.github/workflows/black.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Black Code Formatter
|
||||
uses: lgeiger/black-action@v1.0.1
|
||||
uses: lgeiger/black-action@master
|
||||
with:
|
||||
args: >
|
||||
SABnzbd.py
|
||||
@@ -16,5 +16,5 @@ jobs:
|
||||
tools
|
||||
tests
|
||||
--line-length=120
|
||||
--target-version=py35
|
||||
--target-version=py36
|
||||
--check
|
||||
|
||||
37
.github/workflows/integration_testing.yml
vendored
Normal file
37
.github/workflows/integration_testing.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: CI Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test ${{ matrix.os }} - Python ${{ matrix.python-version }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
os: [ubuntu-20.04]
|
||||
include:
|
||||
- os: macos-latest
|
||||
python-version: 3.9
|
||||
- os: windows-latest
|
||||
python-version: 3.9
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install system dependencies
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get install unrar p7zip-full par2 chromium-chromedriver
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python --version
|
||||
pip install --upgrade pip
|
||||
pip install --upgrade -r requirements.txt
|
||||
pip install --upgrade -r tests/requirements.txt
|
||||
- name: Test SABnzbd
|
||||
run: pytest -s
|
||||
|
||||
|
||||
33
.github/workflows/translations.yml
vendored
Normal file
33
.github/workflows/translations.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Update translatable texts
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
translations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Generate translatable texts
|
||||
run: |
|
||||
python3 tools/extract_pot.py
|
||||
- name: Install Transifex client
|
||||
# Sudo is needed to link the "tx"-command
|
||||
run: |
|
||||
sudo -H python3 -m pip install setuptools wheel
|
||||
sudo -H python3 -m pip install transifex-client
|
||||
- name: Push/pull Transifex translations
|
||||
run: |
|
||||
tx push --source --parallel
|
||||
tx pull --all --force --parallel
|
||||
env:
|
||||
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
||||
- name: Push translatable and translated texts back to repo
|
||||
uses: stefanzweifel/git-auto-commit-action@v4.5.1
|
||||
with:
|
||||
commit_message: Update translatable texts
|
||||
commit_user_name: SABnzbd Automation
|
||||
commit_user_email: bugs@sabnzbd.org
|
||||
commit_author: SABnzbd Automation <bugs@sabnzbd.org>
|
||||
@@ -1,7 +0,0 @@
|
||||
path_classifiers:
|
||||
oldinterfaces:
|
||||
- interfaces/smpl
|
||||
- interfaces/Plush
|
||||
library:
|
||||
- "*knockout*"
|
||||
- "**/*min*"
|
||||
46
.travis.yml
46
.travis.yml
@@ -1,46 +0,0 @@
|
||||
matrix:
|
||||
include:
|
||||
# On Linux we test all supported Python versions
|
||||
# On macOS we only test the semi-recent version that is included
|
||||
- os: linux
|
||||
language: python
|
||||
python: "3.5"
|
||||
- os: linux
|
||||
language: python
|
||||
python: "3.6"
|
||||
- os: linux
|
||||
language: python
|
||||
python: "3.7"
|
||||
- os: linux
|
||||
language: python
|
||||
python: "3.8"
|
||||
- os: osx
|
||||
addons:
|
||||
chrome: stable
|
||||
env:
|
||||
- HOMEBREW_NO_AUTO_UPDATE=1
|
||||
|
||||
install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
|
||||
LATEST_CHROMEDRIVER=$(curl -s https://chromedriver.storage.googleapis.com/LATEST_RELEASE) &&
|
||||
wget --no-verbose -O /tmp/chromedriver.zip https://chromedriver.storage.googleapis.com/$LATEST_CHROMEDRIVER/chromedriver_mac64.zip &&
|
||||
sudo unzip /tmp/chromedriver.zip chromedriver -d /usr/local/bin/;
|
||||
else
|
||||
sudo add-apt-repository ppa:jcfp -y;
|
||||
sudo apt-get update -q;
|
||||
sudo apt-get install unrar p7zip-full par2 chromium-chromedriver -y;
|
||||
ln -s /usr/lib/chromium-browser/chromedriver ~/bin/chromedriver;
|
||||
fi;
|
||||
- python3 --version
|
||||
- python3 -m pip install --upgrade pip
|
||||
- python3 -m pip install --upgrade wheel
|
||||
- python3 -m pip install --upgrade -r requirements.txt
|
||||
- python3 -m pip install --upgrade -r tests/requirements.txt
|
||||
|
||||
script:
|
||||
- python3 -m pytest -s
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
on_failure: change
|
||||
24
.tx/config
Normal file
24
.tx/config
Normal file
@@ -0,0 +1,24 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[sabnzbd-translations.po-main-sabnzbd-pot--develop]
|
||||
file_filter = po/main/<lang>.po
|
||||
minimum_perc = 0
|
||||
source_file = po/main/SABnzbd.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[sabnzbd-translations.po-email-sabemail-pot--develop]
|
||||
file_filter = po/email/<lang>.po
|
||||
minimum_perc = 0
|
||||
source_file = po/email/SABemail.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[sabnzbd-translations.po-nsis-sabnsis-pot--develop]
|
||||
file_filter = po/nsis/<lang>.po
|
||||
minimum_perc = 0
|
||||
source_file = po/nsis/SABnsis.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
16
ABOUT.txt
16
ABOUT.txt
@@ -1,16 +0,0 @@
|
||||
*******************************************
|
||||
*** This is SABnzbd 3.0.0 ***
|
||||
*******************************************
|
||||
|
||||
SABnzbd is an open-source cross-platform binary newsreader.
|
||||
It simplifies the process of downloading from Usenet dramatically,
|
||||
thanks to its friendly web-based user interface and advanced
|
||||
built-in post-processing options that automatically verify, repair,
|
||||
extract and clean up posts downloaded from Usenet.
|
||||
SABnzbd also has a fully customizable user interface,
|
||||
and offers a complete API for third-party applications to hook into.
|
||||
|
||||
There is an extensive Wiki on the use of SABnzbd.
|
||||
https://sabnzbd.org/wiki/
|
||||
|
||||
Please also read the file "ISSUES.txt"
|
||||
@@ -1,4 +1,4 @@
|
||||
SABnzbd 3.0.0
|
||||
SABnzbd 3.2.0
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
0) LICENSE
|
||||
@@ -52,7 +52,7 @@ Specific guides to install from source are available for Windows and macOS:
|
||||
https://sabnzbd.org/wiki/installation/install-macos
|
||||
https://sabnzbd.org/wiki/installation/install-from-source-windows
|
||||
|
||||
Only Python 3.5 and above is supported.
|
||||
Only Python 3.6 and above is supported.
|
||||
|
||||
On Linux systems you need to install:
|
||||
par2 unrar unzip python3-setuptools python3-pip
|
||||
|
||||
21
ISSUES.txt
21
ISSUES.txt
@@ -14,15 +14,15 @@
|
||||
For these the server blocking method is not very favourable.
|
||||
There is an INI-only option that will limit blocks to 1 minute.
|
||||
no_penalties = 1
|
||||
See: https://sabnzbd.org/wiki/configuration/3.0/special
|
||||
See: https://sabnzbd.org/wiki/configuration/3.1/special
|
||||
|
||||
- Some third-party utilties try to probe SABnzbd API in such a way that you will
|
||||
often see warnings about unauthenticated access.
|
||||
If you are sure these probes are harmless, you can suppress the warnings by
|
||||
setting the option "api_warnings" to 0.
|
||||
See: https://sabnzbd.org/wiki/configuration/3.0/special
|
||||
See: https://sabnzbd.org/wiki/configuration/3.1/special
|
||||
|
||||
- On OSX you may encounter downloaded files with foreign characters.
|
||||
- On macOS you may encounter downloaded files with foreign characters.
|
||||
The par2 repair may fail when the files were created on a Windows system.
|
||||
The problem is caused by the PAR2 utility and we cannot fix this now.
|
||||
This does not apply to files inside RAR files.
|
||||
@@ -33,25 +33,14 @@
|
||||
We cannot solve this problem, because the Operating System (read Windows)
|
||||
prevents the removal.
|
||||
|
||||
- Memory usage can sometimes have high peaks. This makes using SABnzbd on very low
|
||||
memory systems (e.g. a NAS device or a router) a challenge.
|
||||
In particular on Synology (SynoCommunity) the device may report that SABnzbd is using
|
||||
a lot of memory even when idle. In this case the memory is usually not actually used by
|
||||
SABnzbd and will be available if required by other apps or the system. More information
|
||||
can be found in the discussion here: https://github.com/SynoCommunity/spksrc/issues/2856
|
||||
|
||||
- SABnzbd is not compatible with some software firewall versions.
|
||||
The Microsoft Windows Firewall works fine, but remember to tell this
|
||||
firewall that SABnzbd is allowed to talk to other computers.
|
||||
|
||||
- When SABnzbd cannot send notification emails, check your virus scanner,
|
||||
firewall or security suite. It may be blocking outgoing email.
|
||||
|
||||
- When you are using external drives or network shares on OSX or Linux
|
||||
- When you are using external drives or network shares on macOS or Linux
|
||||
make sure that the drives are mounted.
|
||||
The operating system will simply redirect your files to alternative locations.
|
||||
You may have trouble finding the files when mounting the drive later.
|
||||
On OSX, SABnzbd will not create new folders in /Volumes.
|
||||
On macOS, SABnzbd will not create new folders in /Volumes.
|
||||
The result will be a failed job that can be retried once the volume has been mounted.
|
||||
|
||||
- If you use a mounted drive as "temporary download folder", it must be present when SABnzbd
|
||||
|
||||
4
PKG-INFO
4
PKG-INFO
@@ -1,7 +1,7 @@
|
||||
Metadata-Version: 1.0
|
||||
Name: SABnzbd
|
||||
Version: 3.0.0RC1
|
||||
Summary: SABnzbd-3.0.0RC1
|
||||
Version: 3.2.0-develop
|
||||
Summary: SABnzbd-3.2.0-develop
|
||||
Home-page: https://sabnzbd.org
|
||||
Author: The SABnzbd Team
|
||||
Author-email: team@sabnzbd.org
|
||||
|
||||
@@ -18,7 +18,7 @@ If you want to know more you can head over to our website: https://sabnzbd.org.
|
||||
|
||||
SABnzbd has a few dependencies you'll need before you can get running. If you've previously run SABnzbd from one of the various Linux packages, then you likely already have all the needed dependencies. If not, here's what you're looking for:
|
||||
|
||||
- `python` (Python 3.5 and higher, often called `python3`)
|
||||
- `python` (Python 3.6 and higher, often called `python3`)
|
||||
- Python modules listed in `requirements.txt`
|
||||
- `par2` (Multi-threaded par2 installation guide can be found [here](https://sabnzbd.org/wiki/installation/multicore-par2))
|
||||
- `unrar` (make sure you get the "official" non-free version of unrar)
|
||||
|
||||
80
README.mkd
80
README.mkd
@@ -1,56 +1,38 @@
|
||||
Release Notes - SABnzbd 3.0.0 RC 1
|
||||
Release Notes - SABnzbd 3.1.0 Release Candidate 1
|
||||
=========================================================
|
||||
|
||||
## About this new version
|
||||
We have been working for months to upgrade the SABnzbd code from Python 2 to Python 3.
|
||||
Although it might not sound like a big change, we had to rewrite almost every part of
|
||||
the code. We also included a number of new features, listed below.
|
||||
## Changes and bugfixes since 3.1.0 Beta 2
|
||||
- Deobfuscate final filenames can now be used when job folders are disabled.
|
||||
- Deobfuscate final filenames will ignore blu-ray disc files.
|
||||
- Clear error if Complete Folder is set as a subfolder of the Temporary Folder.
|
||||
- Filtering of history by category would not filter jobs in post-processing.
|
||||
|
||||
## Big changes in 3.0.0
|
||||
- Python 3.5 and above are the only supported versions of Python.
|
||||
- Cache handling is greatly improved, resulting in more stable speeds on some systems.
|
||||
- Articles failing with CRC errors are now retried on other servers.
|
||||
- SFV files, even obfuscated, will be used for renaming when there are no par2 files.
|
||||
- Fully obfuscated RAR-sets with no verification files are detected and extracted.
|
||||
- Built-in internet bandwidth test.
|
||||
- Windows Service support was changed. The service will need to be reinstalled!
|
||||
Documentation: https://sabnzbd.org/wiki/advanced/sabnzbd-as-a-windows-service
|
||||
- The Windows installer is 64-bit only, for 32-bit please use the standalone package.
|
||||
## Changes since 3.0.2
|
||||
- Added option to automatically deobfuscate final filenames: after unpacking,
|
||||
detect and rename obfuscated or meaningless filenames to the job name,
|
||||
similar to the Deobfuscate.py post-processing script.
|
||||
- Switched to Transifex as our translations platform:
|
||||
Help us translate SABnzbd in your language! Add untranslated texts or
|
||||
improved existing translations here: https://sabnzbd.org/wiki/translate
|
||||
- Redesigned job availability-check to be more efficient and reliable.
|
||||
- Skip repair on Retry if all sets were previously successfully verified.
|
||||
- Passwords included in the filename no longer have to be at the end.
|
||||
- Restore limit on length of foldernames (`max_foldername_length`).
|
||||
- Added password input box on the Add NZB screen.
|
||||
- Show warning that Pyton 3.5 support will be dropped after 3.1.0.
|
||||
- Windows/macOS: update UnRar to 5.91 and MultiPar to 1.3.1.0.
|
||||
- Windows: retry `Access Denied` when renaming files on Windows.
|
||||
|
||||
## Other changes since 2.3.9
|
||||
- Files inside an NZB that are fully identical are now skipped automatically.
|
||||
- Folders of jobs that failed post-processing are renamed to `_FAILED_`.
|
||||
- Blocking of unwanted extensions that are directly inside an NZB.
|
||||
- In Python 3 OpenSSL 1.1.1 is used for Windows and macOS, as a result
|
||||
newsservers manually set to `RC4-MD5` cipher can no longer connect.
|
||||
Documentation: https://sabnzbd.org/wiki/advanced/ssl-ciphers
|
||||
- TLS1.3 support for newsserver connections.
|
||||
- SABYenc, par2 and unrar are now required to start downloading.
|
||||
- Growl-support was removed.
|
||||
- The `smpl` skin was removed.
|
||||
- Using the API with `output=text` to add NZB's will report the `nzo_ids` instead of `ok`.
|
||||
- Queue-item labels are no longer part of the name but separated in API-property `labels`.
|
||||
- API-calls `tapi` and `qstatus` were removed.
|
||||
- On Windows only Multipar is available for repair.
|
||||
- Linux tray icon support was improved.
|
||||
- On Linux special permission bits are removed from files after download.
|
||||
- macOS features such as the menu and notifications now use native code.
|
||||
|
||||
## Bugfixes since 2.3.9
|
||||
- Resolved potential security issue in FAT-filesystem check.
|
||||
- Sample removal did not work if only 1 sample file was present.
|
||||
- Crash on badly formatted RSS-feeds or readout during editing.
|
||||
- Automatic aborting of jobs that can't be completed would sometimes not trigger.
|
||||
- Windows systems could enter standby state during downloading.
|
||||
- Some errors thrown by unrar were not caught.
|
||||
- Files and sockets were not always closed correctly.
|
||||
- Unwanted extension check was overly aggressively deleting folders
|
||||
|
||||
## Upgrade notices
|
||||
- When upgrading from 2.x.x or older the queue will be converted. Job order,
|
||||
settings and data will be preserved, but if you decide to go back to 2.x.x
|
||||
your queue cannot be downgraded again. But you can restore the jobs by going
|
||||
to the Status page and running Queue Repair.
|
||||
## Bugfixes since 3.0.2
|
||||
- Assembler crashes could occur due to race condition in `ArticleCache`.
|
||||
- On HTTP-redirects the scheme/hostname/port were ignored when behind a proxy.
|
||||
- Strip slash of the end of `url_base` as it could break other code.
|
||||
- Unpacking with a relative folder set for a category could fail.
|
||||
- Paused priority of pre-queue script was ignored.
|
||||
- Duplicate Detection did not check filenames in History.
|
||||
- Downloaded bytes could show as exceeding the total bytes of a job.
|
||||
- Windows: non-Latin languages were displayed incorrectly in the installer.
|
||||
- Windows: could fail to create folders on some network shares.
|
||||
|
||||
## Known problems and solutions
|
||||
- Read the file "ISSUES.txt"
|
||||
|
||||
235
SABnzbd.py
235
SABnzbd.py
@@ -17,13 +17,14 @@
|
||||
|
||||
import sys
|
||||
|
||||
if sys.hexversion < 0x03050000:
|
||||
print("Sorry, requires Python 3.5 or above")
|
||||
if sys.hexversion < 0x03060000:
|
||||
print("Sorry, requires Python 3.6 or above")
|
||||
print("You can read more at: https://sabnzbd.org/python3")
|
||||
sys.exit(1)
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
import importlib.util
|
||||
import traceback
|
||||
import getopt
|
||||
import signal
|
||||
@@ -33,21 +34,16 @@ import subprocess
|
||||
import ssl
|
||||
import time
|
||||
import re
|
||||
from typing import List, Dict, Any
|
||||
|
||||
try:
|
||||
import Cheetah
|
||||
|
||||
if Cheetah.Version[0] != "3":
|
||||
raise ValueError
|
||||
import feedparser
|
||||
import configobj
|
||||
import cherrypy
|
||||
import portend
|
||||
import cryptography
|
||||
import chardet
|
||||
except ValueError:
|
||||
print("Sorry, requires Python module Cheetah 3 or higher.")
|
||||
sys.exit(1)
|
||||
except ImportError as e:
|
||||
print("Not all required Python modules are available, please check requirements.txt")
|
||||
print("Missing module:", e.name)
|
||||
@@ -71,15 +67,17 @@ from sabnzbd.misc import (
|
||||
get_serv_parms,
|
||||
get_from_url,
|
||||
upload_file_to_sabnzbd,
|
||||
probablyipv4,
|
||||
)
|
||||
from sabnzbd.filesystem import get_ext, real_path, long_path, globber_full, remove_file
|
||||
from sabnzbd.panic import panic_tmpl, panic_port, panic_host, panic, launch_a_browser
|
||||
import sabnzbd.scheduler as scheduler
|
||||
import sabnzbd.config as config
|
||||
import sabnzbd.cfg
|
||||
import sabnzbd.downloader
|
||||
import sabnzbd.notifier as notifier
|
||||
import sabnzbd.zconfig
|
||||
from sabnzbd.getipaddress import localipv4, publicipv4, ipv6
|
||||
import sabnzbd.utils.ssdp as ssdp
|
||||
|
||||
try:
|
||||
import win32api
|
||||
@@ -107,36 +105,49 @@ def guard_loglevel():
|
||||
LOG_FLAG = True
|
||||
|
||||
|
||||
def warning_helpful(*args, **kwargs):
|
||||
""" Wrapper to ignore helpfull warnings if desired """
|
||||
if sabnzbd.cfg.helpfull_warnings():
|
||||
return logging.warning(*args, **kwargs)
|
||||
return logging.info(*args, **kwargs)
|
||||
|
||||
|
||||
logging.warning_helpful = warning_helpful
|
||||
|
||||
|
||||
class GUIHandler(logging.Handler):
|
||||
""" Logging handler collects the last warnings/errors/exceptions
|
||||
to be displayed in the web-gui
|
||||
"""Logging handler collects the last warnings/errors/exceptions
|
||||
to be displayed in the web-gui
|
||||
"""
|
||||
|
||||
def __init__(self, size):
|
||||
""" Initializes the handler """
|
||||
logging.Handler.__init__(self)
|
||||
self.size = size
|
||||
self.store = []
|
||||
self._size: int = size
|
||||
self.store: List[Dict[str, Any]] = []
|
||||
|
||||
def emit(self, record):
|
||||
def emit(self, record: logging.LogRecord):
|
||||
""" Emit a record by adding it to our private queue """
|
||||
if record.levelname == "WARNING":
|
||||
sabnzbd.LAST_WARNING = record.msg % record.args
|
||||
else:
|
||||
sabnzbd.LAST_ERROR = record.msg % record.args
|
||||
|
||||
if len(self.store) >= self.size:
|
||||
# Loose the oldest record
|
||||
self.store.pop(0)
|
||||
# If % is part of the msg, this could fail
|
||||
try:
|
||||
# Append traceback, if available
|
||||
warning = {"type": record.levelname, "text": record.msg % record.args, "time": int(time.time())}
|
||||
if record.exc_info:
|
||||
warning["text"] = "%s\n%s" % (warning["text"], traceback.format_exc())
|
||||
self.store.append(warning)
|
||||
except UnicodeDecodeError:
|
||||
# Catch elusive Unicode conversion problems
|
||||
pass
|
||||
parsed_msg = record.msg % record.args
|
||||
except TypeError:
|
||||
parsed_msg = record.msg + str(record.args)
|
||||
|
||||
if record.levelno == logging.WARNING:
|
||||
sabnzbd.notifier.send_notification(T("Warning"), parsed_msg, "warning")
|
||||
else:
|
||||
sabnzbd.notifier.send_notification(T("Error"), parsed_msg, "error")
|
||||
|
||||
# Append traceback, if available
|
||||
warning = {"type": record.levelname, "text": parsed_msg, "time": int(time.time())}
|
||||
if record.exc_info:
|
||||
warning["text"] = "%s\n%s" % (warning["text"], traceback.format_exc())
|
||||
|
||||
# Loose the oldest record
|
||||
if len(self.store) >= self._size:
|
||||
self.store.pop(0)
|
||||
self.store.append(warning)
|
||||
|
||||
def clear(self):
|
||||
self.store = []
|
||||
@@ -238,7 +249,7 @@ def daemonize():
|
||||
|
||||
# Get log file path and remove the log file if it got too large
|
||||
log_path = os.path.join(sabnzbd.cfg.log_dir.get_path(), DEF_LOG_ERRFILE)
|
||||
if os.path.exists(log_path) and os.path.getsize(log_path) > sabnzbd.cfg.log_size.get_int():
|
||||
if os.path.exists(log_path) and os.path.getsize(log_path) > sabnzbd.cfg.log_size():
|
||||
remove_file(log_path)
|
||||
|
||||
# Replace file descriptors for stdin, stdout, and stderr
|
||||
@@ -281,7 +292,7 @@ def identify_web_template(key, defweb, wdir):
|
||||
full_main = real_path(full_dir, DEF_MAIN_TMPL)
|
||||
|
||||
if not os.path.exists(full_main):
|
||||
logging.warning(T("Cannot find web template: %s, trying standard template"), full_main)
|
||||
logging.warning_helpful(T("Cannot find web template: %s, trying standard template"), full_main)
|
||||
full_dir = real_path(sabnzbd.DIR_INTERFACES, DEF_STDINTF)
|
||||
full_main = real_path(full_dir, DEF_MAIN_TMPL)
|
||||
if not os.path.exists(full_main):
|
||||
@@ -325,7 +336,6 @@ def get_user_profile_paths(vista_plus):
|
||||
if sabnzbd.DAEMON:
|
||||
# In daemon mode, do not try to access the user profile
|
||||
# just assume that everything defaults to the program dir
|
||||
sabnzbd.DIR_APPDATA = sabnzbd.DIR_PROG
|
||||
sabnzbd.DIR_LCLDATA = sabnzbd.DIR_PROG
|
||||
sabnzbd.DIR_HOME = sabnzbd.DIR_PROG
|
||||
if sabnzbd.WIN32:
|
||||
@@ -339,8 +349,6 @@ def get_user_profile_paths(vista_plus):
|
||||
try:
|
||||
from win32com.shell import shell, shellcon
|
||||
|
||||
path = shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, None, 0)
|
||||
sabnzbd.DIR_APPDATA = os.path.join(path, DEF_WORKDIR)
|
||||
path = shell.SHGetFolderPath(0, shellcon.CSIDL_LOCAL_APPDATA, None, 0)
|
||||
sabnzbd.DIR_LCLDATA = os.path.join(path, DEF_WORKDIR)
|
||||
sabnzbd.DIR_HOME = os.environ["USERPROFILE"]
|
||||
@@ -349,18 +357,16 @@ def get_user_profile_paths(vista_plus):
|
||||
if vista_plus:
|
||||
root = os.environ["AppData"]
|
||||
user = os.environ["USERPROFILE"]
|
||||
sabnzbd.DIR_APPDATA = "%s\\%s" % (root.replace("\\Roaming", "\\Local"), DEF_WORKDIR)
|
||||
sabnzbd.DIR_LCLDATA = "%s\\%s" % (root.replace("\\Roaming", "\\Local"), DEF_WORKDIR)
|
||||
sabnzbd.DIR_HOME = user
|
||||
else:
|
||||
root = os.environ["USERPROFILE"]
|
||||
sabnzbd.DIR_APPDATA = "%s\\%s" % (root, DEF_WORKDIR)
|
||||
sabnzbd.DIR_LCLDATA = "%s\\%s" % (root, DEF_WORKDIR)
|
||||
sabnzbd.DIR_HOME = root
|
||||
sabnzbd.DIR_LCLDATA = sabnzbd.DIR_APPDATA
|
||||
except:
|
||||
pass
|
||||
|
||||
# Long-path everything
|
||||
sabnzbd.DIR_APPDATA = long_path(sabnzbd.DIR_APPDATA)
|
||||
sabnzbd.DIR_LCLDATA = long_path(sabnzbd.DIR_LCLDATA)
|
||||
sabnzbd.DIR_HOME = long_path(sabnzbd.DIR_HOME)
|
||||
return
|
||||
@@ -368,16 +374,14 @@ def get_user_profile_paths(vista_plus):
|
||||
elif sabnzbd.DARWIN:
|
||||
home = os.environ.get("HOME")
|
||||
if home:
|
||||
sabnzbd.DIR_APPDATA = "%s/Library/Application Support/SABnzbd" % home
|
||||
sabnzbd.DIR_LCLDATA = sabnzbd.DIR_APPDATA
|
||||
sabnzbd.DIR_LCLDATA = "%s/Library/Application Support/SABnzbd" % home
|
||||
sabnzbd.DIR_HOME = home
|
||||
return
|
||||
else:
|
||||
# Unix/Linux
|
||||
home = os.environ.get("HOME")
|
||||
if home:
|
||||
sabnzbd.DIR_APPDATA = "%s/.%s" % (home, DEF_WORKDIR)
|
||||
sabnzbd.DIR_LCLDATA = sabnzbd.DIR_APPDATA
|
||||
sabnzbd.DIR_LCLDATA = "%s/.%s" % (home, DEF_WORKDIR)
|
||||
sabnzbd.DIR_HOME = home
|
||||
return
|
||||
|
||||
@@ -424,10 +428,12 @@ def print_modules():
|
||||
logging.info("UNRAR binary... found (%s)", sabnzbd.newsunpack.RAR_COMMAND)
|
||||
|
||||
# Report problematic unrar
|
||||
if sabnzbd.newsunpack.RAR_PROBLEM and not sabnzbd.cfg.ignore_wrong_unrar():
|
||||
if sabnzbd.newsunpack.RAR_PROBLEM:
|
||||
have_str = "%.2f" % (float(sabnzbd.newsunpack.RAR_VERSION) / 100)
|
||||
want_str = "%.2f" % (float(sabnzbd.constants.REC_RAR_VERSION) / 100)
|
||||
logging.warning(T("Your UNRAR version is %s, we recommend version %s or higher.<br />"), have_str, want_str)
|
||||
logging.warning_helpful(
|
||||
T("Your UNRAR version is %s, we recommend version %s or higher.<br />"), have_str, want_str
|
||||
)
|
||||
elif not (sabnzbd.WIN32 or sabnzbd.DARWIN):
|
||||
logging.info("UNRAR binary version %.2f", (float(sabnzbd.newsunpack.RAR_VERSION) / 100))
|
||||
else:
|
||||
@@ -500,8 +506,8 @@ def check_resolve(host):
|
||||
|
||||
|
||||
def get_webhost(cherryhost, cherryport, https_port):
|
||||
""" Determine the webhost address and port,
|
||||
return (host, port, browserhost)
|
||||
"""Determine the webhost address and port,
|
||||
return (host, port, browserhost)
|
||||
"""
|
||||
if cherryhost == "0.0.0.0" and not check_resolve("127.0.0.1"):
|
||||
cherryhost = ""
|
||||
@@ -525,7 +531,7 @@ def get_webhost(cherryhost, cherryport, https_port):
|
||||
# Valid user defined name?
|
||||
info = socket.getaddrinfo(cherryhost, None)
|
||||
except socket.error:
|
||||
if cherryhost not in ("localhost", "127.0.0.1", "::1"):
|
||||
if cherryhost not in LOCALHOSTS:
|
||||
cherryhost = "0.0.0.0"
|
||||
try:
|
||||
info = socket.getaddrinfo(localhost, None)
|
||||
@@ -592,12 +598,12 @@ def get_webhost(cherryhost, cherryport, https_port):
|
||||
except socket.error:
|
||||
cherryhost = cherryhost.strip("[]")
|
||||
|
||||
if ipv6 and ipv4 and (browserhost not in ("localhost", "127.0.0.1", "[::1]", "::1")):
|
||||
if ipv6 and ipv4 and browserhost not in LOCALHOSTS:
|
||||
sabnzbd.AMBI_LOCALHOST = True
|
||||
logging.info("IPV6 has priority on this system, potential Firefox issue")
|
||||
|
||||
if ipv6 and ipv4 and cherryhost == "" and sabnzbd.WIN32:
|
||||
logging.warning(T("Please be aware the 0.0.0.0 hostname will need an IPv6 address for external access"))
|
||||
logging.warning_helpful(T("Please be aware the 0.0.0.0 hostname will need an IPv6 address for external access"))
|
||||
|
||||
if cherryhost == "localhost" and not sabnzbd.WIN32 and not sabnzbd.DARWIN:
|
||||
# On the Ubuntu family, localhost leads to problems for CherryPy
|
||||
@@ -607,7 +613,7 @@ def get_webhost(cherryhost, cherryport, https_port):
|
||||
if ips[0] != "127.0.0.1":
|
||||
browserhost = "127.0.0.1"
|
||||
|
||||
# This is to please Chrome on OSX
|
||||
# This is to please Chrome on macOS
|
||||
if cherryhost == "localhost" and sabnzbd.DARWIN:
|
||||
cherryhost = "127.0.0.1"
|
||||
browserhost = "localhost"
|
||||
@@ -672,8 +678,8 @@ def find_free_port(host, currentport):
|
||||
|
||||
|
||||
def check_for_sabnzbd(url, upload_nzbs, allow_browser=True):
|
||||
""" Check for a running instance of sabnzbd on this port
|
||||
allow_browser==True|None will launch the browser, False will not.
|
||||
"""Check for a running instance of sabnzbd on this port
|
||||
allow_browser==True|None will launch the browser, False will not.
|
||||
"""
|
||||
if allow_browser is None:
|
||||
allow_browser = True
|
||||
@@ -695,10 +701,10 @@ def check_for_sabnzbd(url, upload_nzbs, allow_browser=True):
|
||||
|
||||
|
||||
def evaluate_inipath(path):
|
||||
""" Derive INI file path from a partial path.
|
||||
Full file path: if file does not exist the name must contain a dot
|
||||
but not a leading dot.
|
||||
foldername is enough, the standard name will be appended.
|
||||
"""Derive INI file path from a partial path.
|
||||
Full file path: if file does not exist the name must contain a dot
|
||||
but not a leading dot.
|
||||
foldername is enough, the standard name will be appended.
|
||||
"""
|
||||
path = os.path.normpath(os.path.abspath(path))
|
||||
inipath = os.path.join(path, DEF_INI_FILE)
|
||||
@@ -715,16 +721,16 @@ def evaluate_inipath(path):
|
||||
|
||||
|
||||
def commandline_handler():
|
||||
""" Split win32-service commands are true parameters
|
||||
Returns:
|
||||
service, sab_opts, serv_opts, upload_nzbs
|
||||
"""Split win32-service commands are true parameters
|
||||
Returns:
|
||||
service, sab_opts, serv_opts, upload_nzbs
|
||||
"""
|
||||
service = ""
|
||||
sab_opts = []
|
||||
serv_opts = [os.path.normpath(os.path.abspath(sys.argv[0]))]
|
||||
upload_nzbs = []
|
||||
|
||||
# OSX binary: get rid of the weird -psn_0_123456 parameter
|
||||
# macOS binary: get rid of the weird -psn_0_123456 parameter
|
||||
for arg in sys.argv:
|
||||
if arg.startswith("-psn_"):
|
||||
sys.argv.remove(arg)
|
||||
@@ -848,7 +854,6 @@ def main():
|
||||
pid_path = None
|
||||
pid_file = None
|
||||
new_instance = False
|
||||
osx_console = False
|
||||
ipv6_hosting = None
|
||||
|
||||
_service, sab_opts, _serv_opts, upload_nzbs = commandline_handler()
|
||||
@@ -1111,7 +1116,7 @@ def main():
|
||||
try:
|
||||
if not no_file_log:
|
||||
rollover_log = logging.handlers.RotatingFileHandler(
|
||||
sabnzbd.LOGFILE, "a+", sabnzbd.cfg.log_size.get_int(), sabnzbd.cfg.log_backups()
|
||||
sabnzbd.LOGFILE, "a+", sabnzbd.cfg.log_size(), sabnzbd.cfg.log_backups()
|
||||
)
|
||||
rollover_log.setFormatter(logging.Formatter(logformat))
|
||||
logger.addHandler(rollover_log)
|
||||
@@ -1133,8 +1138,19 @@ def main():
|
||||
if no_file_log:
|
||||
logging.info("Console logging only")
|
||||
|
||||
# Start SABnzbd
|
||||
logging.info("--------------------------------")
|
||||
logging.info("%s-%s (rev=%s)", sabnzbd.MY_NAME, sabnzbd.__version__, sabnzbd.__baseline__)
|
||||
logging.info("%s-%s", sabnzbd.MY_NAME, sabnzbd.__version__)
|
||||
|
||||
# See if we can get version from git when running an unknown revision
|
||||
if sabnzbd.__baseline__ == "unknown":
|
||||
try:
|
||||
sabnzbd.__baseline__ = sabnzbd.misc.run_command(
|
||||
["git", "rev-parse", "--short", "HEAD"], cwd=sabnzbd.DIR_PROG
|
||||
).strip()
|
||||
except:
|
||||
pass
|
||||
logging.info("Commit: %s", sabnzbd.__baseline__)
|
||||
logging.info("Full executable path = %s", sabnzbd.MY_FULLNAME)
|
||||
if sabnzbd.WIN32:
|
||||
suffix = ""
|
||||
@@ -1158,7 +1174,7 @@ def main():
|
||||
|
||||
# On Linux/FreeBSD/Unix "UTF-8" is strongly, strongly adviced:
|
||||
if not sabnzbd.WIN32 and not sabnzbd.DARWIN and not ("utf-8" in sabnzbd.encoding.CODEPAGE.lower()):
|
||||
logging.warning(
|
||||
logging.warning_helpful(
|
||||
T(
|
||||
"SABnzbd was started with encoding %s, this should be UTF-8. Expect problems with Unicoded file and directory names in downloads."
|
||||
),
|
||||
@@ -1168,12 +1184,19 @@ def main():
|
||||
# SSL Information
|
||||
logging.info("SSL version = %s", ssl.OPENSSL_VERSION)
|
||||
|
||||
# Load (extra) certificates in the binary distributions
|
||||
if hasattr(sys, "frozen") and (sabnzbd.WIN32 or sabnzbd.DARWIN):
|
||||
# The certifi package brings the latest certificates on build
|
||||
# This will cause the create_default_context to load it automatically
|
||||
os.environ["SSL_CERT_FILE"] = os.path.join(sabnzbd.DIR_PROG, "cacert.pem")
|
||||
logging.info("Loaded additional certificates from %s", os.environ["SSL_CERT_FILE"])
|
||||
# Load (extra) certificates if supplied by certifi
|
||||
# This is optional and provided in the binaries
|
||||
if importlib.util.find_spec("certifi") is not None:
|
||||
import certifi
|
||||
|
||||
try:
|
||||
os.environ["SSL_CERT_FILE"] = certifi.where()
|
||||
logging.info("Certifi version: %s", certifi.__version__)
|
||||
logging.info("Loaded additional certificates from: %s", os.environ["SSL_CERT_FILE"])
|
||||
except:
|
||||
# Sometimes the certificate file is blocked
|
||||
logging.warning(T("Could not load additional certificates from certifi package"))
|
||||
logging.info("Traceback: ", exc_info=True)
|
||||
|
||||
# Extra startup info
|
||||
if sabnzbd.cfg.log_level() > 1:
|
||||
@@ -1181,9 +1204,6 @@ def main():
|
||||
ctx = ssl.create_default_context()
|
||||
logging.debug("Available certificates: %s", repr(ctx.cert_store_stats()))
|
||||
|
||||
# Show IPv4/IPv6 address
|
||||
from sabnzbd.getipaddress import localipv4, publicipv4, ipv6
|
||||
|
||||
mylocalipv4 = localipv4()
|
||||
if mylocalipv4:
|
||||
logging.debug("My local IPv4 address = %s", mylocalipv4)
|
||||
@@ -1219,7 +1239,7 @@ def main():
|
||||
if autobrowser is not None:
|
||||
sabnzbd.cfg.autobrowser.set(autobrowser)
|
||||
|
||||
sabnzbd.initialize(pause, clean_up, evalSched=True, repair=repair)
|
||||
sabnzbd.initialize(pause, clean_up, repair=repair)
|
||||
|
||||
os.chdir(sabnzbd.DIR_PROG)
|
||||
|
||||
@@ -1395,6 +1415,7 @@ def main():
|
||||
|
||||
# Make available from both URLs
|
||||
main_page = sabnzbd.interface.MainPage()
|
||||
cherrypy.Application.relative_urls = "server"
|
||||
cherrypy.tree.mount(main_page, "/", config=appconfig)
|
||||
cherrypy.tree.mount(main_page, sabnzbd.cfg.url_base(), config=appconfig)
|
||||
|
||||
@@ -1463,25 +1484,37 @@ def main():
|
||||
check_latest_version()
|
||||
autorestarted = False
|
||||
|
||||
# ZeroConfig/Bonjour needs a ip. Lets try to find it.
|
||||
try:
|
||||
z_host = socket.gethostbyname(socket.gethostname())
|
||||
except socket.gaierror:
|
||||
z_host = cherryhost
|
||||
sabnzbd.zconfig.set_bonjour(z_host, cherryport)
|
||||
# bonjour/zeroconf needs an ip. Lets try to find it.
|
||||
external_host = localipv4() # IPv4 address of the LAN interface. This is the normal use case
|
||||
if not external_host:
|
||||
# None, so no network / default route, so let's set to ...
|
||||
external_host = "127.0.0.1"
|
||||
elif probablyipv4(cherryhost) and cherryhost not in LOCALHOSTS + ("0.0.0.0", "::"):
|
||||
# a hard-configured cherryhost other than the usual, so let's take that (good or wrong)
|
||||
external_host = cherryhost
|
||||
logging.debug("bonjour/zeroconf/SSDP using host: %s", external_host)
|
||||
sabnzbd.zconfig.set_bonjour(external_host, cherryport)
|
||||
|
||||
# Start SSDP if SABnzbd is running exposed
|
||||
if cherryhost not in LOCALHOSTS:
|
||||
# Set URL for browser for external hosts
|
||||
if enable_https:
|
||||
ssdp_url = "https://%s:%s%s" % (external_host, cherryport, sabnzbd.cfg.url_base())
|
||||
else:
|
||||
ssdp_url = "http://%s:%s%s" % (external_host, cherryport, sabnzbd.cfg.url_base())
|
||||
ssdp.start_ssdp(
|
||||
external_host,
|
||||
"SABnzbd",
|
||||
ssdp_url,
|
||||
"SABnzbd %s" % sabnzbd.__version__,
|
||||
"SABnzbd Team",
|
||||
"https://sabnzbd.org/",
|
||||
"SABnzbd %s" % sabnzbd.__version__,
|
||||
)
|
||||
|
||||
# Have to keep this running, otherwise logging will terminate
|
||||
timer = 0
|
||||
while not sabnzbd.SABSTOP:
|
||||
if sabnzbd.LAST_WARNING:
|
||||
msg = sabnzbd.LAST_WARNING
|
||||
sabnzbd.LAST_WARNING = None
|
||||
sabnzbd.notifier.send_notification(T("Warning"), msg, "warning")
|
||||
if sabnzbd.LAST_ERROR:
|
||||
msg = sabnzbd.LAST_ERROR
|
||||
sabnzbd.LAST_ERROR = None
|
||||
sabnzbd.notifier.send_notification(T("Error"), msg, "error")
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
# Check for loglevel changes
|
||||
@@ -1498,7 +1531,7 @@ def main():
|
||||
# Keep OS awake (if needed)
|
||||
sabnzbd.keep_awake()
|
||||
# Restart scheduler (if needed)
|
||||
scheduler.restart()
|
||||
sabnzbd.Scheduler.restart(plan_restart=False)
|
||||
# Save config (if needed)
|
||||
config.save_config()
|
||||
# Check the threads
|
||||
@@ -1512,17 +1545,18 @@ def main():
|
||||
# Check for auto-restart request
|
||||
# Or special restart cases like Mac and WindowsService
|
||||
if sabnzbd.TRIGGER_RESTART:
|
||||
logging.info("Performing triggered restart")
|
||||
# Shutdown
|
||||
sabnzbd.shutdown_program()
|
||||
|
||||
if sabnzbd.downloader.Downloader.do.paused:
|
||||
if sabnzbd.Downloader.paused:
|
||||
sabnzbd.RESTART_ARGS.append("-p")
|
||||
if autorestarted:
|
||||
sabnzbd.RESTART_ARGS.append("--autorestarted")
|
||||
sys.argv = sabnzbd.RESTART_ARGS
|
||||
|
||||
os.chdir(org_dir)
|
||||
# If OSX frozen restart of app instead of embedded python
|
||||
# If macOS frozen restart of app instead of embedded python
|
||||
if hasattr(sys, "frozen") and sabnzbd.DARWIN:
|
||||
# [[NSProcessInfo processInfo] processIdentifier]]
|
||||
# logging.info("%s" % (NSProcessInfo.processInfo().processIdentifier()))
|
||||
@@ -1530,7 +1564,7 @@ def main():
|
||||
my_name = sabnzbd.MY_FULLNAME.replace("/Contents/MacOS/SABnzbd", "")
|
||||
my_args = " ".join(sys.argv[1:])
|
||||
cmd = 'kill -9 %s && open "%s" --args %s' % (my_pid, my_name, my_args)
|
||||
logging.info("Launching: ", cmd)
|
||||
logging.info("Launching: %s", cmd)
|
||||
os.system(cmd)
|
||||
elif sabnzbd.WIN_SERVICE:
|
||||
# Use external service handler to do the restart
|
||||
@@ -1635,13 +1669,14 @@ https://sabnzbd.org/wiki/advanced/sabnzbd-as-a-windows-service
|
||||
|
||||
|
||||
def handle_windows_service():
|
||||
""" Handle everything for Windows Service
|
||||
Returns True when any service commands were detected or
|
||||
when we have started as a service.
|
||||
"""Handle everything for Windows Service
|
||||
Returns True when any service commands were detected or
|
||||
when we have started as a service.
|
||||
"""
|
||||
# Detect if running as Windows Service (only Vista and above!)
|
||||
# Adapted from https://stackoverflow.com/a/55248281/5235502
|
||||
if win32ts.ProcessIdToSessionId(win32api.GetCurrentProcessId()) == 0:
|
||||
# Only works when run from the exe-files
|
||||
if hasattr(sys, "frozen") and win32ts.ProcessIdToSessionId(win32api.GetCurrentProcessId()) == 0:
|
||||
servicemanager.Initialize()
|
||||
servicemanager.PrepareToHostSingle(SABnzbd)
|
||||
servicemanager.StartServiceCtrlDispatcher()
|
||||
@@ -1690,7 +1725,7 @@ if __name__ == "__main__":
|
||||
|
||||
elif sabnzbd.DARWIN and sabnzbd.FOUNDATION:
|
||||
|
||||
# OSX binary runner
|
||||
# macOS binary runner
|
||||
from threading import Thread
|
||||
from PyObjCTools import AppHelper
|
||||
from AppKit import NSApplication
|
||||
@@ -1701,9 +1736,7 @@ if __name__ == "__main__":
|
||||
# This code is made with trial-and-error, please improve!
|
||||
class startApp(Thread):
|
||||
def run(self):
|
||||
logging.info("[osx] sabApp Starting - starting main thread")
|
||||
main()
|
||||
logging.info("[osx] sabApp Stopping - main thread quit ")
|
||||
AppHelper.stopEventLoop()
|
||||
|
||||
sabApp = startApp()
|
||||
|
||||
15
appveyor.yml
15
appveyor.yml
@@ -1,15 +0,0 @@
|
||||
environment:
|
||||
# We only test the latest Python version
|
||||
matrix:
|
||||
- PYTHON: "C:\\Python38-x64"
|
||||
|
||||
install:
|
||||
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
|
||||
- python --version
|
||||
- python -m pip install --upgrade pip
|
||||
- python -m pip install --upgrade wheel
|
||||
- python -m pip install --upgrade -r requirements.txt
|
||||
- python -m pip install --upgrade -r tests/requirements.txt
|
||||
|
||||
build_script:
|
||||
- python -m pytest -s
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--#set global $pane="Config"#-->
|
||||
<!--#set global $help_uri="configuration/3.0/configure"#-->
|
||||
<!--#set global $help_uri="configuration/3.1/configure"#-->
|
||||
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
|
||||
|
||||
<!--#from sabnzbd.encoding import CODEPAGE#-->
|
||||
@@ -9,7 +9,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">$T('version'): </th>
|
||||
<td>$version [$build]</td>
|
||||
<td>$version [<a href="https://github.com/sabnzbd/sabnzbd/commit/$build" target="_blank">$build</a>]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">$T('uptime'): </th>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--#set global $pane="Categories"#-->
|
||||
<!--#set global $help_uri="configuration/3.0/categories"#-->
|
||||
<!--#set global $help_uri="configuration/3.1/categories"#-->
|
||||
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
|
||||
<div class="colmask">
|
||||
<div class="section">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--#set global $pane="Folders"#-->
|
||||
<!--#set global $help_uri="configuration/3.0/folders"#-->
|
||||
<!--#set global $help_uri="configuration/3.1/folders"#-->
|
||||
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
|
||||
|
||||
<div class="colmask">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--#set global $pane="General"#-->
|
||||
<!--#set global $help_uri="configuration/3.0/general"#-->
|
||||
<!--#set global $help_uri="configuration/3.1/general"#-->
|
||||
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
|
||||
|
||||
<div class="colmask">
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<!--#set global $pane="Email"#-->
|
||||
<!--#set global $help_uri="configuration/3.0/notifications"#-->
|
||||
<!--#set global $help_uri="configuration/3.1/notifications"#-->
|
||||
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
|
||||
|
||||
<!--#def show_notify_checkboxes($section_label)#-->
|
||||
<!--#for $type in $notify_keys#-->
|
||||
<!--#for $type in $notify_types#-->
|
||||
<div class="field-pair">
|
||||
<label class="config wide" for="${section_label}_prio_$type">
|
||||
$T($notify_texts[$type]).replace('/', ' / ') <!--#if $type == 'download'#--> / $T('link-pause') / $T('link-resume')<!--#end if#-->
|
||||
$T($notify_types[$type]).replace('/', ' / ')
|
||||
</label>
|
||||
<input type="checkbox" name="${section_label}_prio_$type" id="${section_label}_prio_$type" value="1" <!--#if int($getVar($section_label + '_prio_' + $type)) > 0 then 'checked="checked"' else ""#--> />
|
||||
</div>
|
||||
@@ -232,10 +232,10 @@
|
||||
<span class="desc">$T('explain-prowl_apikey')</span>
|
||||
</div>
|
||||
<!--#set $section_label = 'prowl'#-->
|
||||
<!--#for $type in $notify_keys#-->
|
||||
<!--#for $type in $notify_types#-->
|
||||
<div class="field-pair">
|
||||
<label class="config" for="${section_label}_prio_$type">
|
||||
$T($notify_texts[$type]).replace('/', ' / ') <!--#if $type == 'download'#--> / $T('link-pause') / $T('link-resume')<!--#end if#-->
|
||||
$T($notify_types[$type]).replace('/', ' / ')
|
||||
</label>
|
||||
<select name="${section_label}_prio_$type" id="${section_label}_prio_$type">
|
||||
<option value="-3" <!--#if $getVar($section_label + '_prio_' + $type) == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option>
|
||||
@@ -298,10 +298,10 @@
|
||||
<span class="desc">$T('explain-pushover_emergency_expire')</span>
|
||||
</div>
|
||||
<!--#set $section_label = 'pushover'#-->
|
||||
<!--#for $type in $notify_keys#-->
|
||||
<!--#for $type in $notify_types#-->
|
||||
<div class="field-pair">
|
||||
<label class="config" for="${section_label}_prio_$type">
|
||||
$T($notify_texts[$type]).replace('/', ' / ') <!--#if $type == 'download'#--> / $T('link-pause') / $T('link-resume')<!--#end if#-->
|
||||
$T($notify_types[$type]).replace('/', ' / ')
|
||||
</label>
|
||||
<select name="${section_label}_prio_$type" id="${section_label}_prio_$type">
|
||||
<option value="-3" <!--#if $getVar($section_label + '_prio_' + $type) == -3 then 'selected="selected"' else ""#--> >$T('pushover-off')</option>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--#set global $pane="RSS"#-->
|
||||
<!--#set global $help_uri="configuration/3.0/rss"#-->
|
||||
<!--#set global $help_uri="configuration/3.1/rss"#-->
|
||||
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
|
||||
<div class="colmask">
|
||||
<!--#if not $active_feed#-->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--#set global $pane="Scheduling"#-->
|
||||
<!--#set global $help_uri="configuration/3.0/scheduling"#-->
|
||||
<!--#set global $help_uri="configuration/3.1/scheduling"#-->
|
||||
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
|
||||
|
||||
<%
|
||||
@@ -50,7 +50,7 @@ else:
|
||||
<select name="action" id="action">
|
||||
<optgroup label="$T('sch-action')">
|
||||
<!--#for $action in $actions#-->
|
||||
<option value="$action" data-action="" data-noarg="<!--#if $action is 'speedlimit' then 0 else 1#-->">$actions_lng[$action]</option>
|
||||
<option value="$action" data-action="" data-noarg="<!--#if $action == 'speedlimit' then 0 else 1#-->">$actions_lng[$action]</option>
|
||||
<!--#end for#-->
|
||||
</optgroup>
|
||||
<optgroup label="$T('cmenu-servers')">
|
||||
|
||||
@@ -1,45 +1,9 @@
|
||||
<!--#set global $pane="Servers"#-->
|
||||
<!--#set global $help_uri="configuration/3.0/servers"#-->
|
||||
<!--#set global $help_uri="configuration/3.1/servers"#-->
|
||||
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
|
||||
|
||||
<!--
|
||||
We need to find how many months we have recorded so far, so we
|
||||
loop over all the dates to find the lowest value and then use
|
||||
this to calculate the date-selector and maximum value per month.
|
||||
-->
|
||||
<!--#import json#-->
|
||||
<!--#import datetime#-->
|
||||
<!--#import sabnzbd.misc#-->
|
||||
|
||||
<!--#set month_names = [$T('January'), $T('February'), $T('March'), $T('April'), $T('May'), $T('June'), $T('July'), $T('August'), $T('September'), $T('October'), $T('November'), $T('December')] #-->
|
||||
<!--#set min_date = datetime.date.today()#-->
|
||||
<!--#set max_data_all = {}#-->
|
||||
|
||||
<!--#for $server in $servers #-->
|
||||
<!--#if 'amounts' in $server#-->
|
||||
<!--#set max_data_server = {}#-->
|
||||
<!--#for date in $server['amounts'][4]#-->
|
||||
<!--#set split_date = $date.split('-')#-->
|
||||
<!--#set min_date = min(min_date, datetime.date(int(split_date[0]), int(split_date[1]), 1))#-->
|
||||
|
||||
<!--#set month_date = $date[:7]#-->
|
||||
<!--#if $month_date not in $max_data_server#-->
|
||||
<!--#set max_data_server[$month_date] = 0#-->
|
||||
<!--#end if#-->
|
||||
<!--#set max_data_server[$month_date] = max(max_data_server[$month_date], $server['amounts'][4][$date])#-->
|
||||
<!--#end for#-->
|
||||
|
||||
<!--#for month_date in max_data_server#-->
|
||||
<!--#if $month_date not in $max_data_all#-->
|
||||
<!--#set max_data_all[$month_date] = 0#-->
|
||||
<!--#end if#-->
|
||||
<!--#set max_data_all[$month_date] = max(max_data_all[$month_date], max_data_server[$month_date])#-->
|
||||
<!--#end for#-->
|
||||
<!--#end if#-->
|
||||
<!--#end for#-->
|
||||
|
||||
<!--#set months_recorded = list(sabnzbd.misc.monthrange(min_date, datetime.date.today()))#-->
|
||||
<!--#$months_recorded.reverse()#-->
|
||||
|
||||
<script type="text/javascript">
|
||||
// Define variable needed for the server-plots
|
||||
@@ -53,21 +17,13 @@
|
||||
<input type="checkbox" id="advanced-settings-button" name="advanced-settings-button"> $T('button-advanced')
|
||||
</label>
|
||||
|
||||
<!--#if $months_recorded#-->
|
||||
<div class="advanced-buttonSeperator"></div>
|
||||
<div class="chart-selector-container" title="$T('srv-bandwidth')">
|
||||
<span class="glyphicon glyphicon-signal"></span>
|
||||
<select name="chart-selector" id="chart-selector">
|
||||
<!--#for $cur_date in months_recorded#-->
|
||||
<!--#set month_date = '%d-%02d' % ($cur_date.year, $cur_date.month)#-->
|
||||
<!--#if $month_date not in $max_data_all#-->
|
||||
<!--#set max_data_all[$month_date] = 0#-->
|
||||
<!--#end if#-->
|
||||
<option value="$month_date" data-max="$max_data_all[$month_date]">$month_names[$cur_date.month-1] $cur_date.year</option>
|
||||
<!--#end for#-->
|
||||
</select>
|
||||
<!--#set today = datetime.date.today()#-->
|
||||
<input type="date" name="chart-start" id="chart-start" value="<!--#echo (today-datetime.timedelta(days=30)).strftime('%Y-%m-%d')#-->"> -
|
||||
<input type="date" name="chart-end" id="chart-end" value="<!--#echo today.strftime('%Y-%m-%d')#-->">
|
||||
</div>
|
||||
<!--#end if#-->
|
||||
</div>
|
||||
<div class="section" id="addServerContent" style="display: none;">
|
||||
<div class="col2">
|
||||
@@ -287,7 +243,7 @@
|
||||
$T('today'): $(server['amounts'][3])B<br/>
|
||||
$T('thisWeek'): $(server['amounts'][2])B<br/>
|
||||
$T('thisMonth'): $(server['amounts'][1])B<br/>
|
||||
<span id="server-data-label-${cur}"></span>: <span id="server-data-value-${cur}"></span>
|
||||
$T('custom'): <span id="server-data-value-${cur}"></span>
|
||||
</div>
|
||||
<div class="server-chart" data-serverid="${cur}"s>
|
||||
<div id="server-chart-${cur}" class="ct-chart"></div>
|
||||
@@ -331,58 +287,72 @@
|
||||
}
|
||||
|
||||
function showCharts() {
|
||||
// This month
|
||||
var theMonth = \$('#chart-selector').val()
|
||||
var thisDay = new Date()
|
||||
// Get the constants
|
||||
const startDate = new Date(\$('#chart-start').val())
|
||||
const endDate = new Date(\$('#chart-end').val())
|
||||
const oneDay = 24 * 60 * 60 * 1000
|
||||
const nrDays = Math.round((endDate-startDate)/oneDay)
|
||||
|
||||
// What month are we doing?
|
||||
var inputDate = new Date(theMonth+'-01')
|
||||
var baseDate = new Date(inputDate.getUTCFullYear(), inputDate.getUTCMonth(), 1)
|
||||
var maxDaysInMonth = new Date(baseDate.getFullYear(), baseDate.getMonth()+1, 0).getDate()
|
||||
// Show only maximum 10 labels to avoid cluttering
|
||||
const labelStep = Math.round(nrDays/10)
|
||||
|
||||
// Set the new maximum
|
||||
chartOptions.axisY.high = \$('#chart-selector :selected').data('max');
|
||||
chartOptions.axisY.low = 0
|
||||
// Save largest value
|
||||
var maxVal = 0
|
||||
|
||||
// For each chart
|
||||
\$('.server-chart').each(function(i, elemn) {
|
||||
var server_id = \$(elemn).data('serverid')
|
||||
\$('.server-chart').each(function(j, elemn) {
|
||||
const server_id = \$(elemn).data('serverid')
|
||||
var totalThisRange = 0
|
||||
|
||||
// Fill the data array
|
||||
var data = {
|
||||
labels: [],
|
||||
series: [[]]
|
||||
};
|
||||
var totalThisMonth = 0
|
||||
for(var i = 1; i < maxDaysInMonth+1; i++) {
|
||||
|
||||
for(var i = 0; i < nrDays+1; i++) {
|
||||
// Update the date
|
||||
const checkDate = new Date(startDate)
|
||||
checkDate.setDate(checkDate.getDate() + i);
|
||||
|
||||
// Add X-label
|
||||
if(i % 3 == 1) {
|
||||
data['labels'].push(i)
|
||||
if(i % labelStep === 0) {
|
||||
data['labels'].push(checkDate.getDate())
|
||||
} else {
|
||||
data['labels'].push(NaN)
|
||||
}
|
||||
|
||||
// Get formatted date
|
||||
baseDate.setDate(i)
|
||||
var dateCheck = toFormattedDate(baseDate)
|
||||
// Date we can check in the array
|
||||
const dateCheck = toFormattedDate(checkDate)
|
||||
|
||||
// Add data if we have it
|
||||
if(dateCheck in serverData[server_id]) {
|
||||
data['series'][0].push(serverData[server_id][dateCheck])
|
||||
totalThisMonth += serverData[server_id][dateCheck]
|
||||
} else if(thisDay.getYear() == baseDate.getYear() && thisDay.getMonth() == baseDate.getMonth() && thisDay.getDate() < i) {
|
||||
data['series'][0].push(NaN)
|
||||
} else {
|
||||
totalThisRange += serverData[server_id][dateCheck]
|
||||
maxVal = Math.max(maxVal, serverData[server_id][dateCheck])
|
||||
} else {
|
||||
data['series'][0].push(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the text value
|
||||
\$('#server-data-label-' + server_id).text(\$('#chart-selector :selected').text())
|
||||
\$('#server-data-value-' + server_id).text(filesize(totalThisMonth, {round: 1}))
|
||||
\$('#server-data-value-' + server_id).text(filesize(totalThisRange, {round: 1}))
|
||||
|
||||
// Save data in a very ugly way, but we need to do this
|
||||
// so we can calculate the maximum Y-axis for all graphs
|
||||
\$(elemn).data("chart-data", data)
|
||||
})
|
||||
|
||||
// Set the maximum
|
||||
chartOptions.axisY.high = maxVal;
|
||||
chartOptions.axisY.low = 0
|
||||
|
||||
// Update all the axis with the largest value and draw the graph
|
||||
\$('.server-chart').each(function(j, elemn) {
|
||||
const server_id = \$(elemn).data('serverid')
|
||||
|
||||
// Show the chart
|
||||
chart = new Chartist.Line('#server-chart-'+server_id, data, chartOptions);
|
||||
chart = new Chartist.Line('#server-chart-'+server_id, \$(elemn).data("chart-data"), chartOptions)
|
||||
chart.on('created', function(context) {
|
||||
// Make sure to add this as the first child so it's at the bottom
|
||||
context.svg.elem('rect', {
|
||||
@@ -391,7 +361,7 @@
|
||||
width: context.chartRect.width(),
|
||||
height: context.chartRect.height()+2,
|
||||
fill: 'none',
|
||||
stroke: '#B9B9B9',
|
||||
stroke: '#b9b9b9',
|
||||
'stroke-width': '1px'
|
||||
}, '', context.svg, true)
|
||||
\$('#server-chart-'+server_id+' .ct-label.ct-vertical').each(function(index, elmn) {
|
||||
@@ -399,6 +369,10 @@
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
// Limit input to sensible values
|
||||
\$('#chart-start').attr("max", \$('#chart-end').val())
|
||||
\$('#chart-end').attr("min", \$('#chart-start').val())
|
||||
}
|
||||
|
||||
// Need to mitigate timezone effects!
|
||||
@@ -425,7 +399,7 @@
|
||||
/**
|
||||
Update charts when changed
|
||||
**/
|
||||
\$('#chart-selector').on('change', function(elemn) {
|
||||
\$('#chart-start, #chart-end').on('change', function(elemn) {
|
||||
showCharts()
|
||||
|
||||
// Lets us leave (needs to be called after the change event)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--#set global $pane="Sorting"#-->
|
||||
<!--#set global $help_uri="configuration/3.0/sorting"#-->
|
||||
<!--#set global $help_uri="configuration/3.1/sorting"#-->
|
||||
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
|
||||
|
||||
<div class="colmask">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--#set global $pane="Special"#-->
|
||||
<!--#set global $help_uri="configuration/3.0/special"#-->
|
||||
<!--#set global $help_uri="configuration/3.1/special"#-->
|
||||
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
|
||||
|
||||
<div class="colmask">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--#set global $pane="Switches"#-->
|
||||
<!--#set global $help_uri="configuration/3.0/switches"#-->
|
||||
<!--#set global $help_uri="configuration/3.1/switches"#-->
|
||||
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
|
||||
|
||||
<div class="colmask">
|
||||
@@ -135,7 +135,7 @@
|
||||
<div class="field-pair advanced-settings">
|
||||
<label class="config" for="auto_sort">$T('opt-auto_sort')</label>
|
||||
<select name="auto_sort" id="auto_sort">
|
||||
<option value=""></option>
|
||||
<option value="">$T('default')</option>
|
||||
<option value="avg_age asc" <!--#if $auto_sort == "avg_age asc" then 'selected="selected"' else ""#--> >$T('Glitter-sortAgeAsc')</option>
|
||||
<option value="avg_age desc" <!--#if $auto_sort == "avg_age desc" then 'selected="selected"' else ""#--> >$T('Glitter-sortAgeDesc')</option>
|
||||
<option value="name asc" <!--#if $auto_sort == "name asc" then 'selected="selected"' else ""#--> >$T('Glitter-sortNameAsc')</option>
|
||||
@@ -237,6 +237,11 @@
|
||||
<input type="checkbox" name="ignore_samples" id="ignore_samples" value="1" <!--#if int($ignore_samples) > 0 then 'checked="checked"' else ""#--> />
|
||||
<span class="desc">$T('explain-ignore_samples') $T('igsam-del').</span>
|
||||
</div>
|
||||
<div class="field-pair">
|
||||
<label class="config" for="deobfuscate_final_filenames">$T('opt-deobfuscate_final_filenames')</label>
|
||||
<input type="checkbox" name="deobfuscate_final_filenames" id="deobfuscate_final_filenames" value="1" <!--#if int($deobfuscate_final_filenames) > 0 then 'checked="checked"' else ""#--> />
|
||||
<span class="desc">$T('explain-deobfuscate_final_filenames')</span>
|
||||
</div>
|
||||
<div class="field-pair advanced-settings">
|
||||
<label class="config" for="enable_meta">$T('opt-enable_meta')</label>
|
||||
<input type="checkbox" name="enable_meta" id="enable_meta" value="1" <!--#if int($enable_meta) > 0 then 'checked="checked"' else ""#--> />
|
||||
|
||||
@@ -795,6 +795,7 @@ input[type="submit"]:hover {
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="url"],
|
||||
input[type="date"],
|
||||
input[type="number"],
|
||||
input[type="password"],
|
||||
textarea,
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<p><strong>If you encounter an error, please include the log file (click on <span class="glyphicon glyphicon-wrench"></span> ) when contacting us.</strong></p>
|
||||
<span class="glyphicon glyphicon-home"></span> <a href="https://forums.sabnzbd.org/viewforum.php?f=11" target="_blank">SABnzbd Forum</a><br />
|
||||
<span class="glyphicon glyphicon-plane"></span> <a href="https://github.com/sabnzbd/sabnzbd/" target="_blank">SABnzbd on Github</a><br />
|
||||
<span class="glyphicon glyphicon-globe"></span> <a href="https://translations.launchpad.net/sabnzbd" target="_blank">Translations of SABnzbd</a><br />
|
||||
<span class="glyphicon glyphicon-globe"></span> <a href="https://sabnzbd.org/wiki/translate" target="_blank">Translations of SABnzbd</a><br />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -439,7 +439,7 @@
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4 class="modal-title">$T('Glitter-addNZB')</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body form-horizontal">
|
||||
<div class="row">
|
||||
<form data-bind="submit: addNZBFromURL" class="col-sm-6">
|
||||
<fieldset>
|
||||
@@ -470,29 +470,45 @@
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<hr />
|
||||
<div class="row form-horizontal">
|
||||
<label class="col-sm-6 control-label">$T('Glitter-addnzbFilename')</label>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">$T('name')</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="nzbname" id="nzbname" placeholder="$T('name')" class="form-control" />
|
||||
<input type="text" name="nzbname" id="nzbname" placeholder="$T('Glitter-addnzbFilename')" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="clearfix"></div>
|
||||
<div class="add-nzb-inputbox" title="$T('category')" data-tooltip="true" data-placement="left">
|
||||
<span class="glyphicon glyphicon-tag"></span>
|
||||
<select name="Category" class="form-control" data-bind="options: queue.categoriesList, optionsValue: 'catValue', optionsText: 'catText',"></select>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">$T('srv-password')</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="password" id="password" placeholder="$T('srv-optional')" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-nzb-inputbox" title="$T('priority')" data-tooltip="true" data-placement="left">
|
||||
<span class="glyphicon glyphicon-sort-by-attributes-alt"></span>
|
||||
<select name="Priority" class="form-control" data-bind="options: queue.priorityOptions, optionsValue: 'value', optionsText: 'name', optionsCaption: '$T('default')'"></select>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">$T('category')</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="Category" class="form-control" data-bind="options: queue.categoriesList, optionsValue: 'catValue', optionsText: 'catText',"></select>
|
||||
<span class="glyphicon glyphicon-tag"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-nzb-inputbox" title="$T('swtag-pp')" data-tooltip="true" data-placement="left">
|
||||
<span class="glyphicon glyphicon-check"></span>
|
||||
<select name="Processing" class="form-control" data-bind="options: queue.processingOptions, optionsValue: 'value', optionsText: 'name', optionsCaption: '$T('default')'"></select>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">$T('priority')</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="Priority" class="form-control" data-bind="options: queue.priorityOptions, optionsValue: 'value', optionsText: 'name', optionsCaption: '$T('default')'"></select>
|
||||
<span class="glyphicon glyphicon-sort-by-attributes-alt"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-nzb-inputbox" title="$T('eoq-scripts')" data-tooltip="true" data-placement="left">
|
||||
<span class="glyphicon glyphicon-flash"></span>
|
||||
<select name="Post-processing" class="form-control" data-bind="options: queue.scriptsList, optionsCaption: '$T('default')'"></select>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">$T('swtag-pp')</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="Processing" class="form-control" data-bind="options: queue.processingOptions, optionsValue: 'value', optionsText: 'name', optionsCaption: '$T('default')'"></select>
|
||||
<span class="glyphicon glyphicon-check"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">$T('eoq-scripts')</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="Post-processing" class="form-control" data-bind="options: queue.scriptsList, optionsCaption: '$T('default')', enable: (queue.scriptsList().length > 1)"></select>
|
||||
<span class="glyphicon glyphicon-flash"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
@@ -668,6 +668,7 @@ function ViewModel() {
|
||||
mode: "addurl",
|
||||
name: $(form.nzbURL).val(),
|
||||
nzbname: $('#nzbname').val(),
|
||||
password: $('#password').val(),
|
||||
script: $('#modal-add-nzb select[name="Post-processing"]').val(),
|
||||
priority: $('#modal-add-nzb select[name="Priority"]').val(),
|
||||
pp: $('#modal-add-nzb select[name="Processing"]').val()
|
||||
@@ -707,6 +708,7 @@ function ViewModel() {
|
||||
data.append("name", file);
|
||||
data.append("mode", "addfile");
|
||||
data.append("nzbname", $('#nzbname').val());
|
||||
data.append("password", $('#password').val());
|
||||
data.append("script", $('#modal-add-nzb select[name="Post-processing"]').val())
|
||||
data.append("priority", $('#modal-add-nzb select[name="Priority"]').val())
|
||||
data.append("apikey", apiKey);
|
||||
|
||||
@@ -228,11 +228,11 @@ function QueueListModel(parent) {
|
||||
switch($(event.currentTarget).data('action')) {
|
||||
case 'sortAgeAsc':
|
||||
sort = 'avg_age';
|
||||
dir = 'asc';
|
||||
dir = 'desc';
|
||||
break;
|
||||
case 'sortAgeDesc':
|
||||
sort = 'avg_age';
|
||||
dir = 'desc';
|
||||
dir = 'asc';
|
||||
break;
|
||||
case 'sortNameAsc':
|
||||
sort = 'name';
|
||||
@@ -751,4 +751,4 @@ function QueueModel(parent, data) {
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,18 +852,24 @@ tr.queue-item>td:first-child>a {
|
||||
|
||||
.multioperations-selector .add-nzb-inputbox {
|
||||
width: 20%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.multioperations-selector .add-nzb-inputbox select {
|
||||
display: inline-block;
|
||||
width: calc(100% - 30px);
|
||||
margin: 5px 0px 5px 2px;
|
||||
}
|
||||
|
||||
.multioperations-selector .add-nzb-inputbox-small {
|
||||
width: 80px;
|
||||
float: right;
|
||||
padding-left: 0;
|
||||
padding-top: 2px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.multioperations-selector .add-nzb-inputbox-small .label {
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 2px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.multioperations-selector .add-nzb-inputbox-small span {
|
||||
@@ -884,7 +890,7 @@ tr.queue-item>td:first-child>a {
|
||||
|
||||
.multioperations-selector .add-nzb-inputbox-options {
|
||||
width: auto;
|
||||
padding-right: 6px;
|
||||
padding-right: 7px;
|
||||
}
|
||||
|
||||
.multioperations-selector .add-nzb-inputbox-small label[for="multiedit-pause"],
|
||||
@@ -1209,7 +1215,7 @@ tr.queue-item>td:first-child>a {
|
||||
right: 0;
|
||||
padding: 8px 10px 3px 0px;
|
||||
opacity: 0.7;
|
||||
z-index: 999;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.search-box a span {
|
||||
@@ -1276,7 +1282,8 @@ tr.queue-item>td:first-child>a {
|
||||
min-height: 270px;
|
||||
}
|
||||
|
||||
#modal-options .form-group {
|
||||
#modal-options .form-group,
|
||||
#modal-add-nzb .form-group {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
@@ -1388,7 +1395,8 @@ tr.queue-item>td:first-child>a {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#modal-options .col-sm-6 {
|
||||
#modal-options .col-sm-6,
|
||||
#modal-add-nzb .col-sm-6 {
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
@@ -1520,25 +1528,14 @@ tr.queue-item>td:first-child>a {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#modal-add-nzb .add-nzb-inputbox:nth-child(even) select {
|
||||
width: 100%;
|
||||
#modal-add-nzb .col-sm-6:first-of-type label {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.add-nzb-inputbox {
|
||||
float: left;
|
||||
width: 50%;
|
||||
margin: 5px 0px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.add-nzb-inputbox select {
|
||||
#modal-add-nzb select {
|
||||
width: calc(100% - 35px);
|
||||
display: inline-block;
|
||||
width: calc(100% - 10px);
|
||||
}
|
||||
|
||||
.add-nzb-inputbox span {
|
||||
display: inline-block;
|
||||
margin: 8px 2px 0px -20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.btn-file {
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
<span id="warning_box"><b><a href="${path}status/#tabs-warnings" id="last_warning"><span id="have_warnings">$have_warnings</span> $T('warnings')</a></b></span>
|
||||
#if $pane=="Main"#
|
||||
#if $new_release#⋅ <a href="$new_rel_url" id="new_release" target="_blank">$T('Plush-updateAvailable').replace(' ',' ')</a>#end if#
|
||||
This skin is no longer actively maintained! <a href="${path}config/general/#web_dir"><strong>We recommend using the Glitter skin.</strong></a>
|
||||
#end if#
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -306,8 +306,8 @@ jQuery(function($){
|
||||
$('#queue_sort_list .queue_sort').click(function(event) {
|
||||
var sort, dir;
|
||||
switch ($(this).attr('id')) {
|
||||
case 'sortAgeAsc': sort='avg_age'; dir='asc'; break;
|
||||
case 'sortAgeDesc': sort='avg_age'; dir='desc'; break;
|
||||
case 'sortAgeAsc': sort='avg_age'; dir='desc'; break;
|
||||
case 'sortAgeDesc': sort='avg_age'; dir='asc'; break;
|
||||
case 'sortNameAsc': sort='name'; dir='asc'; break;
|
||||
case 'sortNameDesc': sort='name'; dir='desc'; break;
|
||||
case 'sortSizeAsc': sort='size'; dir='asc'; break;
|
||||
|
||||
BIN
osx/unrar/unrar
BIN
osx/unrar/unrar
Binary file not shown.
@@ -5,13 +5,13 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\n"
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: shypike@sabnzbd.org\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Last-Translator: team@sabnzbd.org\n"
|
||||
"Language-Team: SABnzbd <team@sabnzbd.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=ASCII\n"
|
||||
"Content-Transfer-Encoding: 7bit\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
"##\n"
|
||||
|
||||
114
po/email/cs.po
Normal file
114
po/email/cs.po
Normal file
@@ -0,0 +1,114 @@
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Language-Team: Czech (https://www.transifex.com/sabnzbd/teams/111101/cs/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: cs\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
"##\n"
|
||||
"## Default Email template for SABnzbd\n"
|
||||
"## This a Cheetah template\n"
|
||||
"## Documentation: http://sabnzbd.wikidot.com/email-templates\n"
|
||||
"##\n"
|
||||
"## Newlines and whitespace are significant!\n"
|
||||
"##\n"
|
||||
"## These are the email headers\n"
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
"\n"
|
||||
"Results of the job:\n"
|
||||
"<!--#for $stage in $stages #-->\n"
|
||||
"Stage $stage <!--#slurp#-->\n"
|
||||
"<!--#for $result in $stages[$stage]#-->\n"
|
||||
" $result <!--#slurp#-->\n"
|
||||
"<!--#end for#-->\n"
|
||||
"<!--#end for#-->\n"
|
||||
"<!--#if $script!=\"\" #-->\n"
|
||||
"Output from user script \"$script\" (Exit code = $script_ret):\n"
|
||||
"$script_output\n"
|
||||
"<!--#end if#-->\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"Enjoy!\n"
|
||||
"<!--#else#-->\n"
|
||||
"Sorry!\n"
|
||||
"<!--#end if#-->\n"
|
||||
msgstr ""
|
||||
|
||||
#: email/rss.tmpl:1
|
||||
msgid ""
|
||||
"##\n"
|
||||
"## RSS Email template for SABnzbd\n"
|
||||
"## This a Cheetah template\n"
|
||||
"## Documentation: http://sabnzbd.wikidot.com/email-templates\n"
|
||||
"##\n"
|
||||
"## Newlines and whitespace are significant!\n"
|
||||
"##\n"
|
||||
"## These are the email headers\n"
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has added $amount jobs to the queue\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"\n"
|
||||
"SABnzbd has added $amount job(s) to the queue.\n"
|
||||
"They are from RSS feed \"$feed\".\n"
|
||||
"<!--#for $job in $jobs#-->\n"
|
||||
" $job <!--#slurp#-->\n"
|
||||
"<!--#end for#-->\n"
|
||||
"\n"
|
||||
"Bye\n"
|
||||
msgstr ""
|
||||
|
||||
#: email/badfetch.tmpl:1
|
||||
msgid ""
|
||||
"##\n"
|
||||
"## Bad URL Fetch Email template for SABnzbd\n"
|
||||
"## This a Cheetah template\n"
|
||||
"## Documentation: http://sabnzbd.wikidot.com/email-templates\n"
|
||||
"##\n"
|
||||
"## Newlines and whitespace are significant!\n"
|
||||
"##\n"
|
||||
"## These are the email headers\n"
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd failed to fetch an NZB\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"\n"
|
||||
"SABnzbd has failed to retrieve the NZB from $url.\n"
|
||||
"The error message was: $msg\n"
|
||||
"\n"
|
||||
"Bye\n"
|
||||
msgstr ""
|
||||
@@ -1,21 +1,21 @@
|
||||
# Danish translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-04-28 21:31+0000\n"
|
||||
"PO-Revision-Date: 2018-11-27 23:39+0000\n"
|
||||
"Last-Translator: scootergrisen <scootergrisen@gmail.com>\n"
|
||||
"Language-Team: Danish <da@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Danish (https://www.transifex.com/sabnzbd/teams/111101/da/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-04-29 06:36+0000\n"
|
||||
"X-Generator: Launchpad (build d1105341713c5be348effe2a5142c4a210ce4cde)\n"
|
||||
"Language: da\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
@@ -30,19 +30,16 @@ msgid ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
@@ -75,19 +72,16 @@ msgstr ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd har <!--#if $status then \"hentet\" else \"fejlet\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd har <!--#if $status then \"hentet\" else \"fejlet\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## Herefter kommer kroppen, den tomme linje skal være der!\n"
|
||||
"\n"
|
||||
"Hej,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd har hentet \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin "
|
||||
"#\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd har hentet \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd kunne ikke hente \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd kunne ikke hente \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Færdig kl. $end_time\n"
|
||||
"Hentet $size\n"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# German translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-04-28 21:31+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Thomas Lucke (Lucky) <Unknown>\n"
|
||||
"Language-Team: German <de@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: German (https://www.transifex.com/sabnzbd/teams/111101/de/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-04-29 06:36+0000\n"
|
||||
"X-Generator: Launchpad (build d1105341713c5be348effe2a5142c4a210ce4cde)\n"
|
||||
"Language: de\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
@@ -30,19 +30,16 @@ msgid ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
@@ -77,20 +74,16 @@ msgstr ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd <!--#if $status then \"hat\" else \"konnte\" #--> Auftrag "
|
||||
"$name <!--#if $status then \"erfolgreich ausgeführt\" else \"nicht "
|
||||
"ausführen\" #-->\n"
|
||||
"Subject: SABnzbd <!--#if $status then \"hat\" else \"konnte\" #--> Auftrag $name <!--#if $status then \"erfolgreich ausgeführt\" else \"nicht ausführen\" #-->\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd hat \"$name\" <!--#if $msgid==\"\" then \"\" else \"(Newzbin #\" + "
|
||||
"$msgid + \")\"#--> heruntergeladen\n"
|
||||
"SABnzbd hat \"$name\" <!--#if $msgid==\"\" then \"\" else \"(Newzbin #\" + $msgid + \")\"#--> heruntergeladen\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd konnte \"$name\" <!--#if $msgid==\"\" then \"\" else \"(Newzbin #\" "
|
||||
"+ $msgid + \")\"#--> nicht herunterladen\n"
|
||||
"SABnzbd konnte \"$name\" <!--#if $msgid==\"\" then \"\" else \"(Newzbin #\" + $msgid + \")\"#--> nicht herunterladen\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Fertiggestellt: $end_time\n"
|
||||
"Heruntergeladen: $size\n"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Spanish translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-04-28 21:31+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: shypike <Unknown>\n"
|
||||
"Language-Team: Spanish <es@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Spanish (https://www.transifex.com/sabnzbd/teams/111101/es/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-04-29 06:36+0000\n"
|
||||
"X-Generator: Launchpad (build d1105341713c5be348effe2a5142c4a210ce4cde)\n"
|
||||
"Language: es\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
@@ -30,19 +30,16 @@ msgid ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
@@ -75,20 +72,16 @@ msgstr ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd <!--#if $status then \"he bajado\" else \"fallo en bajar\" "
|
||||
"#--> job $name\n"
|
||||
"Subject: SABnzbd <!--#if $status then \"he bajado\" else \"fallo en bajar\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## !Después de esto viene el cuerpo del mensaje, la línea en blanco es "
|
||||
"necesaria!\n"
|
||||
"## !Después de esto viene el cuerpo del mensaje, la línea en blanco es necesaria!\n"
|
||||
"\n"
|
||||
"Hola,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd he bajado \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin "
|
||||
"#\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd he bajado \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd fallo en bajar \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd fallo en bajar \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Terminado a las $end_time\n"
|
||||
"$size bajado\n"
|
||||
@@ -101,8 +94,7 @@ msgstr ""
|
||||
"<!--#end for#-->\n"
|
||||
"<!--#end for#-->\n"
|
||||
"<!--#if $script!=\"\" #-->\n"
|
||||
"Producción desde el script de usuario \"$script\" (Exit code = "
|
||||
"$script_ret):\n"
|
||||
"Producción desde el script de usuario \"$script\" (Exit code = $script_ret):\n"
|
||||
"$script_output\n"
|
||||
"<!--#end if#-->\n"
|
||||
"<!--#if $status #-->\n"
|
||||
@@ -153,8 +145,7 @@ msgstr ""
|
||||
"Subject: SABnzbd he añadido $amount transferencia(s) a la cola\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## !Después de esto viene el cuerpo del mensaje, la línea en blanco es "
|
||||
"necesaria!\n"
|
||||
"## !Después de esto viene el cuerpo del mensaje, la línea en blanco es necesaria!\n"
|
||||
"\n"
|
||||
"Hola,\n"
|
||||
"\n"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Finnish translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-04-28 21:31+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Matti Ylönen <Unknown>\n"
|
||||
"Language-Team: Finnish <fi@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Finnish (https://www.transifex.com/sabnzbd/teams/111101/fi/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-04-29 06:36+0000\n"
|
||||
"X-Generator: Launchpad (build d1105341713c5be348effe2a5142c4a210ce4cde)\n"
|
||||
"Language: fi\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
@@ -30,19 +30,16 @@ msgid ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
@@ -75,20 +72,16 @@ msgstr ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd on <!--#if $status then \"valmistunut\" else "
|
||||
"\"epäonnistunut\" #--> työssä $name\n"
|
||||
"Subject: SABnzbd on <!--#if $status then \"valmistunut\" else \"epäonnistunut\" #--> työssä $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## Tämän jälkeen tulee viestin runko, ensimmäinen rivinvaihto on "
|
||||
"pakollinen!\n"
|
||||
"## Tämän jälkeen tulee viestin runko, ensimmäinen rivinvaihto on pakollinen!\n"
|
||||
"\n"
|
||||
"Hei,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd on ladannut \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin "
|
||||
"#\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd on ladannut \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd on epäonnistunut \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#--> latauksessa\n"
|
||||
"SABnzbd on epäonnistunut \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#--> latauksessa\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Valmistui $end_time\n"
|
||||
"Ladattu $size\n"
|
||||
@@ -152,8 +145,7 @@ msgstr ""
|
||||
"Subject: SABnzbd on lisännyt $amount työtä jonoon\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## Tämän jälkeen tulee viestin runko, ensimmäinen rivinvaihto on "
|
||||
"pakollinen!\n"
|
||||
"## Tämän jälkeen tulee viestin runko, ensimmäinen rivinvaihto on pakollinen!\n"
|
||||
"\n"
|
||||
"Hei,\n"
|
||||
"\n"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# French translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-04-28 21:31+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Fox Ace <Unknown>\n"
|
||||
"Language-Team: French <fr@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: French (https://www.transifex.com/sabnzbd/teams/111101/fr/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-04-29 06:36+0000\n"
|
||||
"X-Generator: Launchpad (build d1105341713c5be348effe2a5142c4a210ce4cde)\n"
|
||||
"Language: fr\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
@@ -30,19 +30,16 @@ msgid ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
@@ -76,19 +73,16 @@ msgstr ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd <!--#if $status#-->Succès<!--#else#-->Echec<!--#end if#--> "
|
||||
"du téléchargement $name\n"
|
||||
"Subject: SABnzbd <!--#if $status#-->Succès<!--#else#-->Echec<!--#end if#--> du téléchargement $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## Après cela vient le contenu, la ligne vide est nécessaire! \n"
|
||||
"\n"
|
||||
"Bonjour,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd a téléchargé avec succès \"$name\" <!--#if $msgid==\"\" then \"\" "
|
||||
"else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd a téléchargé avec succès \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd a téléchargé sans succès \"$name\" <!--#if $msgid==\"\" then \"\" "
|
||||
"else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd a téléchargé sans succès \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Terminé à $end_time\n"
|
||||
"Téléchargé $size\n"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Hebrew translation for sabnzbd
|
||||
# Copyright (c) 2017 Rosetta Contributors and Canonical Ltd 2017
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
|
||||
#
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# ION, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-04-28 21:31+0000\n"
|
||||
"PO-Revision-Date: 2019-01-21 15:26+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>\n"
|
||||
"Language-Team: Hebrew <he@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: ION, 2020\n"
|
||||
"Language-Team: Hebrew (https://www.transifex.com/sabnzbd/teams/111101/he/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-04-29 06:36+0000\n"
|
||||
"X-Generator: Launchpad (build d1105341713c5be348effe2a5142c4a210ce4cde)\n"
|
||||
"Language: he\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
@@ -30,19 +30,16 @@ msgid ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
@@ -65,27 +62,24 @@ msgid ""
|
||||
"<!--#end if#-->\n"
|
||||
msgstr ""
|
||||
"##\n"
|
||||
"## SABnzbd תבנית דוא\"ל ברירת מחדל עבור\n"
|
||||
"## SABnzbd תבנית דוא״ל ברירת מחדל עבור\n"
|
||||
"## זאת תבנית ברדלס\n"
|
||||
"## http://sabnzbd.wikidot.com/email-templates :תיעוד\n"
|
||||
"##\n"
|
||||
"## !שורות חדשות ורווחים לבנים הם משמעותיים\n"
|
||||
"##\n"
|
||||
"## אלו כותרות הדוא\"ל\n"
|
||||
"## אלו כותרות הדוא״ל\n"
|
||||
"$to :אל\n"
|
||||
"$from :מאת\n"
|
||||
"תאריך: $date\n"
|
||||
"<!--#if $status then \"completed\" else \"failed\" #--> $name יש עבודה אשר "
|
||||
"SABnzbd-נושא: ל\n"
|
||||
"<!--#if $status then \"completed\" else \"failed\" #--> $name יש עבודה אשר SABnzbd-נושא: ל\n"
|
||||
"## !אחרי זה בא הגוף, השורה הריקה דרושה\n"
|
||||
"\n"
|
||||
",היי\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd הוריד את \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin "
|
||||
"#\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd הוריד את \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd נכשל להוריד את \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd נכשל להוריד את \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"הסתיים ב-$end_time\n"
|
||||
"הורדו $size\n"
|
||||
@@ -136,13 +130,13 @@ msgid ""
|
||||
"Bye\n"
|
||||
msgstr ""
|
||||
"##\n"
|
||||
"## SABnzbd עבור RSS תבנית דוא\"ל\n"
|
||||
"## SABnzbd עבור RSS תבנית דוא״ל\n"
|
||||
"## זאת תבנית ברדלס\n"
|
||||
"## http://sabnzbd.wikidot.com/email-templates :תיעוד\n"
|
||||
"##\n"
|
||||
"## !שורות חדשות ורווחים לבנים הם משמעותיים\n"
|
||||
"##\n"
|
||||
"## אלו כותרות הדוא\"ל\n"
|
||||
"## אלו כותרות הדוא״ל\n"
|
||||
"$to :אל\n"
|
||||
"$from :מאת\n"
|
||||
"תאריך: $date\n"
|
||||
@@ -185,13 +179,13 @@ msgid ""
|
||||
"Bye\n"
|
||||
msgstr ""
|
||||
"##\n"
|
||||
"## SABnzbd רעה עבור URL תבנית דוא\"ל של משיכת\n"
|
||||
"## SABnzbd רעה עבור URL תבנית דוא״ל של משיכת\n"
|
||||
"## זאת תבנית ברדלס\n"
|
||||
"## http://sabnzbd.wikidot.com/email-templates :תיעוד\n"
|
||||
"##\n"
|
||||
"## !שורות חדשות ורווחים לבנים הם משמעותיים\n"
|
||||
"##\n"
|
||||
"## אלו כותרות הדוא\"ל\n"
|
||||
"## אלו כותרות הדוא״ל\n"
|
||||
"$to :אל\n"
|
||||
"$from :מאת\n"
|
||||
"תאריך: $date\n"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Norwegian Bokmal translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-04-28 21:31+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: Norwegian Bokmal <nb@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\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://www.transifex.com/sabnzbd/teams/111101/nb/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-04-29 06:36+0000\n"
|
||||
"X-Generator: Launchpad (build d1105341713c5be348effe2a5142c4a210ce4cde)\n"
|
||||
"Language: nb\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
@@ -30,19 +30,16 @@ msgid ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
@@ -77,19 +74,16 @@ msgstr ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd har <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"jobb $name\n"
|
||||
"Subject: SABnzbd har <!--#if $status then \"completed\" else \"failed\" #--> jobb $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hei,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd har lastet ned \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd har lastet ned \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd mislyktes med å laste ned \"$name\" <!--#if $msgid==\"\" then \"\" "
|
||||
"else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd mislyktes med å laste ned \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Ferdig $end_time\n"
|
||||
"Nedlastet $size\n"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Dutch translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-04-28 21:31+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: shypike <Unknown>\n"
|
||||
"Language-Team: Dutch <nl@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Dutch (https://www.transifex.com/sabnzbd/teams/111101/nl/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-04-29 06:36+0000\n"
|
||||
"X-Generator: Launchpad (build d1105341713c5be348effe2a5142c4a210ce4cde)\n"
|
||||
"Language: nl\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
@@ -30,19 +30,16 @@ msgid ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
@@ -75,19 +72,16 @@ msgstr ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd: opdracht $name is <!--#if $status then \"klaar\" else "
|
||||
"\"mislukt\" #-->\n"
|
||||
"Subject: SABnzbd: opdracht $name is <!--#if $status then \"klaar\" else \"mislukt\" #-->\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## Hier onder volgt de hoofdtekst, de lege regel is noodzakelijk!\n"
|
||||
"\n"
|
||||
"Hallo,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd heeft \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + "
|
||||
"$msgid + \")\"#--> gedownload\n"
|
||||
"SABnzbd heeft \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#--> gedownload\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd is niet geslaagd in het downloaden van \"$name\" <!--#if "
|
||||
"$msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd is niet geslaagd in het downloaden van \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Klaar om $end_time\n"
|
||||
"Hoeveelheid gedownload $size\n"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Polish translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-04-28 21:31+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Tomasz 'Zen' Napierala <tomasz@napierala.org>\n"
|
||||
"Language-Team: Polish <pl@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Polish (https://www.transifex.com/sabnzbd/teams/111101/pl/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-04-29 06:36+0000\n"
|
||||
"X-Generator: Launchpad (build d1105341713c5be348effe2a5142c4a210ce4cde)\n"
|
||||
"Language: pl\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
@@ -30,19 +30,16 @@ msgid ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
@@ -75,19 +72,16 @@ msgstr ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd <!--#if $status then \"zakończył\" else \"zakończył z "
|
||||
"błędem\" #--> zadanie $name\n"
|
||||
"Subject: SABnzbd <!--#if $status then \"zakończył\" else \"zakończył z błędem\" #--> zadanie $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## Następnie treść maila, wymagana jest pusta linia!\n"
|
||||
"\n"
|
||||
"Cześć,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd pobrał \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" "
|
||||
"+ $msgid + \")\"#-->\n"
|
||||
"SABnzbd pobrał \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd nie pobrał \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin "
|
||||
"#\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd nie pobrał \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Zakończono o $end_time\n"
|
||||
"Pobrano $size\n"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Brazilian Portuguese translation for sabnzbd
|
||||
# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
|
||||
#
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-04-28 21:31+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: lrrosa <Unknown>\n"
|
||||
"Language-Team: Brazilian Portuguese <pt_BR@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Portuguese (Brazil) (https://www.transifex.com/sabnzbd/teams/111101/pt_BR/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-04-29 06:36+0000\n"
|
||||
"X-Generator: Launchpad (build d1105341713c5be348effe2a5142c4a210ce4cde)\n"
|
||||
"Language: pt_BR\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
@@ -30,19 +30,16 @@ msgid ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
@@ -75,19 +72,16 @@ msgstr ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd <!--#if $status then \"completou \" else \"falhou n\" #-->a "
|
||||
"tarefa $name\n"
|
||||
"Subject: SABnzbd <!--#if $status then \"completou \" else \"falhou n\" #-->a tarefa $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## Depois daqui vem o corpo. A linha vazia é necessária!\n"
|
||||
"\n"
|
||||
"Olá,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd baixou \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" "
|
||||
"+ $msgid + \")\"#-->\n"
|
||||
"SABnzbd baixou \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd falhou no download de \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd falhou no download de \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Completado em $end_time\n"
|
||||
"Baixados $size\n"
|
||||
@@ -148,16 +142,14 @@ msgstr ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd adicionou $amount <!--#if $amount == \"1\" then \"tarefa\" "
|
||||
"else \"tarefas\" #--> à fila\n"
|
||||
"Subject: SABnzbd adicionou $amount <!--#if $amount == \"1\" then \"tarefa\" else \"tarefas\" #--> à fila\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## Depois daqui vem o corpo. A linha vazia é necessária!\n"
|
||||
"\n"
|
||||
"Olá,\n"
|
||||
"\n"
|
||||
"SABnzbd adicionou $amount <!--#if $amount == \"1\" then \"tarefa\" else "
|
||||
"\"tarefas\" #--> à fila.\n"
|
||||
"SABnzbd adicionou $amount <!--#if $amount == \"1\" then \"tarefa\" else \"tarefas\" #--> à fila.\n"
|
||||
"Elas são do feed RSS \"$feed\".\n"
|
||||
"<!--#for $job in $jobs#-->\n"
|
||||
" $job <!--#slurp#-->\n"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Romanian translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-04-28 21:31+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: nicusor <Unknown>\n"
|
||||
"Language-Team: Romanian <ro@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Romanian (https://www.transifex.com/sabnzbd/teams/111101/ro/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-04-29 06:36+0000\n"
|
||||
"X-Generator: Launchpad (build d1105341713c5be348effe2a5142c4a210ce4cde)\n"
|
||||
"Language: ro\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
@@ -30,19 +30,16 @@ msgid ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
@@ -75,19 +72,16 @@ msgstr ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd <!--#if $status then \"a terminat\" else \"nu a reuşit\" #--"
|
||||
"> sarcina $name\n"
|
||||
"Subject: SABnzbd <!--#if $status then \"a terminat\" else \"nu a reuşit\" #--> sarcina $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## După acesta urmează conţinutul, este necesar o linie goală!\n"
|
||||
"\n"
|
||||
"Salut,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd a descărcat \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin "
|
||||
"#\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd a descărcat \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd nu a reuşit să descarce \"$name\" <!--#if $msgid==\"\" then \"\" "
|
||||
"else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd nu a reuşit să descarce \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Terminat la $end_time\n"
|
||||
"Mărime $size\n"
|
||||
@@ -100,8 +94,7 @@ msgstr ""
|
||||
"<!--#end for#-->\n"
|
||||
"<!--#end for#-->\n"
|
||||
"<!--#if $script!=\"\" #-->\n"
|
||||
"Rezultatul script-ului utilizatorului \"$script\" (Exit code = "
|
||||
"$script_ret):\n"
|
||||
"Rezultatul script-ului utilizatorului \"$script\" (Exit code = $script_ret):\n"
|
||||
"$script_output\n"
|
||||
"<!--#end if#-->\n"
|
||||
"<!--#if $status #-->\n"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Russian translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-04-28 21:31+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Pavel Maryanov <Unknown>\n"
|
||||
"Language-Team: Russian <gnu@mx.ru>\n"
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Russian (https://www.transifex.com/sabnzbd/teams/111101/ru/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-04-29 06:36+0000\n"
|
||||
"X-Generator: Launchpad (build d1105341713c5be348effe2a5142c4a210ce4cde)\n"
|
||||
"Language: ru\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
@@ -30,19 +30,16 @@ msgid ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
@@ -75,19 +72,16 @@ msgstr ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd: задание $name <!--#if $status then \"успешно выполнено\" "
|
||||
"else \"не удалось выполнить\" #-->\n"
|
||||
"Subject: SABnzbd: задание $name <!--#if $status then \"успешно выполнено\" else \"не удалось выполнить\" #-->\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## Теперь следует тело сообщения. Пустая строка является обязательной!\n"
|
||||
"\n"
|
||||
"Привет.\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"Системой SABnzbd загружено задание «$name» <!--#if $msgid==\"\" then \"\" "
|
||||
"else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"Системой SABnzbd загружено задание «$name» <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"Системе SABnzbd не удалось загрузить «$name» <!--#if $msgid==\"\" then \"\" "
|
||||
"else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"Системе SABnzbd не удалось загрузить «$name» <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Время окончания загрузки: $end_time\n"
|
||||
"Загруженный размер: $size\n"
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
# Serbian translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
# Мирослав Николић <miroslavnikolic@rocketmail.com>, 2011.
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-04-28 21:31+0000\n"
|
||||
"PO-Revision-Date: 2017-06-24 19:51+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>\n"
|
||||
"Language-Team: Launchpad Serbian Translators\n"
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Serbian (https://www.transifex.com/sabnzbd/teams/111101/sr/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-04-29 06:36+0000\n"
|
||||
"X-Generator: Launchpad (build d1105341713c5be348effe2a5142c4a210ce4cde)\n"
|
||||
"Language: sr\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
@@ -31,19 +30,16 @@ msgid ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
@@ -76,19 +72,16 @@ msgstr ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: САБнзбд је <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"посао „$name“\n"
|
||||
"Subject: САБнзбд је <!--#if $status then \"completed\" else \"failed\" #--> посао „$name“\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## После тога долази разрада, празни редови су потребни!\n"
|
||||
"\n"
|
||||
"Здраво,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"САБнзбд је преузео „$name“ <!--#if $msgid==\"\" then \"\" else \"(newzbin "
|
||||
"#\" + $msgid + \")\"#-->\n"
|
||||
"САБнзбд је преузео „$name“ <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"САБнзбд није успео да преузме „$name“ <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"САБнзбд није успео да преузме „$name“ <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Завршено је у $end_time\n"
|
||||
"Преузето је $size\n"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Swedish translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-04-28 21:31+0000\n"
|
||||
"PO-Revision-Date: 2017-06-24 19:50+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>\n"
|
||||
"Language-Team: Swedish <sv@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Swedish (https://www.transifex.com/sabnzbd/teams/111101/sv/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-04-29 06:36+0000\n"
|
||||
"X-Generator: Launchpad (build d1105341713c5be348effe2a5142c4a210ce4cde)\n"
|
||||
"Language: sv\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
@@ -30,19 +30,16 @@ msgid ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
@@ -75,19 +72,16 @@ msgstr ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hej,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd har laddat ned \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd har laddat ned \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd misslyckades med att ladda ned \"$name\" <!--#if $msgid==\"\" then "
|
||||
"\"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd misslyckades med att ladda ned \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Färdig $end_time\n"
|
||||
"Nedladdat $size\n"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Chinese (Simplified) translation for sabnzbd
|
||||
# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
|
||||
#
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-04-28 21:31+0000\n"
|
||||
"PO-Revision-Date: 2015-10-24 11:05+0000\n"
|
||||
"Last-Translator: shypike <Unknown>\n"
|
||||
"Language-Team: Chinese (Simplified) <zh_CN@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.2.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Chinese (China) (https://www.transifex.com/sabnzbd/teams/111101/zh_CN/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-04-29 06:36+0000\n"
|
||||
"X-Generator: Launchpad (build d1105341713c5be348effe2a5142c4a210ce4cde)\n"
|
||||
"Language: zh_CN\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
@@ -30,19 +30,16 @@ msgid ""
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
@@ -82,11 +79,9 @@ msgstr ""
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd 已完成 \"$name\" 的下载 <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" "
|
||||
"+ $msgid + \")\"#-->\n"
|
||||
"SABnzbd 已完成 \"$name\" 的下载 <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd 下载 \"$name\" 失败 <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + "
|
||||
"$msgid + \")\"#-->\n"
|
||||
"SABnzbd 下载 \"$name\" 失败 <!--#if $msgid==\"\" then \"\" else \"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"完成于 $end_time\n"
|
||||
"已下载 $size\n"
|
||||
|
||||
1439
po/main/SABnzbd.pot
1439
po/main/SABnzbd.pot
File diff suppressed because it is too large
Load Diff
5087
po/main/cs.po
Normal file
5087
po/main/cs.po
Normal file
File diff suppressed because it is too large
Load Diff
2448
po/main/da.po
2448
po/main/da.po
File diff suppressed because it is too large
Load Diff
2560
po/main/de.po
2560
po/main/de.po
File diff suppressed because it is too large
Load Diff
2739
po/main/es.po
2739
po/main/es.po
File diff suppressed because it is too large
Load Diff
2478
po/main/fi.po
2478
po/main/fi.po
File diff suppressed because it is too large
Load Diff
2579
po/main/fr.po
2579
po/main/fr.po
File diff suppressed because it is too large
Load Diff
2064
po/main/he.po
2064
po/main/he.po
File diff suppressed because it is too large
Load Diff
2432
po/main/nb.po
2432
po/main/nb.po
File diff suppressed because it is too large
Load Diff
2490
po/main/nl.po
2490
po/main/nl.po
File diff suppressed because it is too large
Load Diff
2454
po/main/pl.po
2454
po/main/pl.po
File diff suppressed because it is too large
Load Diff
2472
po/main/pt_BR.po
2472
po/main/pt_BR.po
File diff suppressed because it is too large
Load Diff
2486
po/main/ro.po
2486
po/main/ro.po
File diff suppressed because it is too large
Load Diff
2398
po/main/ru.po
2398
po/main/ru.po
File diff suppressed because it is too large
Load Diff
2417
po/main/sr.po
2417
po/main/sr.po
File diff suppressed because it is too large
Load Diff
2411
po/main/sv.po
2411
po/main/sv.po
File diff suppressed because it is too large
Load Diff
2382
po/main/zh_CN.po
2382
po/main/zh_CN.po
File diff suppressed because it is too large
Load Diff
@@ -7,11 +7,11 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: shypike@sabnzbd.org\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Last-Translator: team@sabnzbd.org\n"
|
||||
"Language-Team: SABnzbd <team@sabnzbd.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=ASCII\n"
|
||||
"Content-Transfer-Encoding: 7bit\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
|
||||
73
po/nsis/cs.po
Normal file
73
po/nsis/cs.po
Normal file
@@ -0,0 +1,73 @@
|
||||
# SABnzbd Translation Template file NSIS
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Language-Team: Czech (https://www.transifex.com/sabnzbd/teams/111101/cs/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: cs\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The SABnzbd Windows Service changed in SABnzbd 3.0.0. \\nYou will need to "
|
||||
"reinstall the SABnzbd service. \\n\\nClick `OK` to remove the existing "
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installer only supports 64-bit Windows, use the standalone version to "
|
||||
"run on 32-bit Windows."
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr ""
|
||||
@@ -1,21 +1,21 @@
|
||||
# Danish translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file NSIS
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-05-07 19:13+0000\n"
|
||||
"PO-Revision-Date: 2018-11-27 23:30+0000\n"
|
||||
"Last-Translator: scootergrisen <scootergrisen@gmail.com>\n"
|
||||
"Language-Team: Danish <da@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Danish (https://www.transifex.com/sabnzbd/teams/111101/da/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-05-08 04:51+0000\n"
|
||||
"X-Generator: Launchpad (build fbdff7602bd10fb883bf7e2ddcc7fd5a16f60398)\n"
|
||||
"Language: da\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
@@ -72,35 +72,9 @@ msgid ""
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"Du kan ikke overskrive en eksisterende installation. \\n\\nKlik `OK` for at "
|
||||
"fjerne den tidligere version eller `Annuller` for at annullere opgraderingen."
|
||||
"fjerne den tidligere version eller `Annuller` for at annullere "
|
||||
"opgraderingen."
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Dine indstillinger og data vil blive bevaret."
|
||||
|
||||
#~ msgid "Go to the SABnzbd Wiki"
|
||||
#~ msgstr "Gå til SABnzbd Wiki"
|
||||
|
||||
#~ msgid ""
|
||||
#~ " >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
|
||||
#~ "release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
#~ msgstr ""
|
||||
#~ " >>>> ADVARSEL <<<<\\r\\n\\r\\nVenligst, se først "
|
||||
#~ "produktbemærkningerne eller gå til http://wiki.sabnzbd.org/introducing-0-7-0 "
|
||||
#~ "!"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "This system requires the Microsoft runtime library VC90 to be installed "
|
||||
#~ "first. Do you want to do that now?"
|
||||
#~ msgstr ""
|
||||
#~ "Systemet kræver at Microsoft runtime-biblioteket VC90 skal installeres "
|
||||
#~ "først. Vil du gøre det nu?"
|
||||
|
||||
#~ msgid "Cannot install without runtime library, retry?"
|
||||
#~ msgstr "Kan ikke installere uden runtime-bibliotek, prøv igen?"
|
||||
|
||||
#~ msgid "Download error, retry?"
|
||||
#~ msgstr "Fejl ved download, prøv igen?"
|
||||
|
||||
#~ msgid "Downloading Microsoft runtime installer..."
|
||||
#~ msgstr "Downloader Microsoft runtime-installationsfil..."
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# German translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file NSIS
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-05-07 19:13+0000\n"
|
||||
"PO-Revision-Date: 2020-06-22 22:10+0000\n"
|
||||
"Last-Translator: reloxx <Unknown>\n"
|
||||
"Language-Team: German <de@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: German (https://www.transifex.com/sabnzbd/teams/111101/de/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-06-23 06:02+0000\n"
|
||||
"X-Generator: Launchpad (build 1cbd0aa39df153c901321817f9b57cf3f232b507)\n"
|
||||
"Language: de\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
@@ -35,18 +35,16 @@ msgid ""
|
||||
"reinstall the SABnzbd service. \\n\\nClick `OK` to remove the existing "
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"Aufgrund von Änderungen am SABnzbd Windows Service ab Version 3.0.0 ist es "
|
||||
"nötig,\\nden Windows Service neu zu installieren.\\n\\n\r\n"
|
||||
"Drücke `OK` um den existierenden Service zu löschen oder `Abbrechen` um "
|
||||
"dieses Upgrade abzubrechen."
|
||||
"Aufgrund von Änderungen am SABnzbd Windows Service ab Version 3.0.0 ist es nötig,\\nden Windows Service neu zu installieren.\\n\\n\r\n"
|
||||
"Drücke `OK` um den existierenden Service zu löschen oder `Abbrechen` um dieses Upgrade abzubrechen."
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installer only supports 64-bit Windows, use the standalone version to "
|
||||
"run on 32-bit Windows."
|
||||
msgstr ""
|
||||
"Der Installer unterstützt nur Windows 64-bit. Benutze die Standalone Version "
|
||||
"für Windows 32-bit."
|
||||
"Der Installer unterstützt nur Windows 64-bit. Benutze die Standalone Version"
|
||||
" für Windows 32-bit."
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
@@ -84,33 +82,3 @@ msgstr ""
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Ihre Einstellungen und Daten bleiben erhalten."
|
||||
|
||||
#~ msgid "Download error, retry?"
|
||||
#~ msgstr "Download-Fehler. Erneut versuchen?"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "This system requires the Microsoft runtime library VC90 to be installed "
|
||||
#~ "first. Do you want to do that now?"
|
||||
#~ msgstr ""
|
||||
#~ "Dieses System erfordert die Installation der Laufzeitbibliothek VC90 von "
|
||||
#~ "Microsoft. Möchten Sie die Installation jetzt durchführen?"
|
||||
|
||||
#~ msgid "Cannot install without runtime library, retry?"
|
||||
#~ msgstr ""
|
||||
#~ "Installation ohne Laufzeitbibliothek nicht möglich. Erneut versuchen?"
|
||||
|
||||
#~ msgid "Downloading Microsoft runtime installer..."
|
||||
#~ msgstr ""
|
||||
#~ "Installationsprogramm für Microsoft-Laufzeitbibliothek wird "
|
||||
#~ "heruntergeladen..."
|
||||
|
||||
#~ msgid ""
|
||||
#~ " >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
|
||||
#~ "release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
#~ msgstr ""
|
||||
#~ " >>>> WARNING <<<<\\r\\n\\r\\nBitte lesen Sie zuerst die "
|
||||
#~ "Versionshinweise oder gehen Sie zu http://wiki.sabnzbd.org/introducing-0-7-0 "
|
||||
#~ "!"
|
||||
|
||||
#~ msgid "Go to the SABnzbd Wiki"
|
||||
#~ msgstr "Gehen Sie auf die SABnzbd Wiki-Seite"
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
# Spanish translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file NSIS
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
# Ester Molla Aragones <moarages@gmail.com>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-05-07 19:13+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Victor Herrero <victorhera@gmail.com>\n"
|
||||
"Language-Team: Spanish <es@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\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://www.transifex.com/sabnzbd/teams/111101/es/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-05-08 04:51+0000\n"
|
||||
"X-Generator: Launchpad (build fbdff7602bd10fb883bf7e2ddcc7fd5a16f60398)\n"
|
||||
"Language: es\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
@@ -35,12 +36,18 @@ msgid ""
|
||||
"reinstall the SABnzbd service. \\n\\nClick `OK` to remove the existing "
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"El servicio de Windows para SABnzbd ha cambiado en la versión SABnzbd "
|
||||
"3.0.0.\\nNecesitará volver a instalar el servicio SABnzbd. \\n\\nHaga clic "
|
||||
"en \"OK\" para eliminar los servicios existentes o \"Cancelar\" para "
|
||||
"cancelar la actualización."
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installer only supports 64-bit Windows, use the standalone version to "
|
||||
"run on 32-bit Windows."
|
||||
msgstr ""
|
||||
"El instalador solo admite Windows 64-bit, utilice la versión independiente "
|
||||
"para ejecutar Windows 32-bit."
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
@@ -77,30 +84,3 @@ msgstr ""
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Tus ajustes y datos se mantendrán intactos."
|
||||
|
||||
#~ msgid "Download error, retry?"
|
||||
#~ msgstr "Error en la descarga, ¿probamos de nuevo?"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "This system requires the Microsoft runtime library VC90 to be installed "
|
||||
#~ "first. Do you want to do that now?"
|
||||
#~ msgstr ""
|
||||
#~ "Este sistema requiere la ejecución de la biblioteca Microsoft runtime VC90 "
|
||||
#~ "que debe ser instalada. ¿Quieres hacerlo ahora?"
|
||||
|
||||
#~ msgid "Cannot install without runtime library, retry?"
|
||||
#~ msgstr ""
|
||||
#~ "No se puede instalar sin la biblioteca runtime, ¿Lo volvemos a intentar?"
|
||||
|
||||
#~ msgid "Downloading Microsoft runtime installer..."
|
||||
#~ msgstr "Descargando el instalador de Microsoft runtime..."
|
||||
|
||||
#~ msgid "Go to the SABnzbd Wiki"
|
||||
#~ msgstr "Ir al wiki de SABnzbd"
|
||||
|
||||
#~ msgid ""
|
||||
#~ " >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
|
||||
#~ "release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
#~ msgstr ""
|
||||
#~ " >>>> ATENCION <<<<\\r\\n\\r\\nPor favor, compruebe las "
|
||||
#~ "notas de version o visite http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Finnish translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file NSIS
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-05-07 19:13+0000\n"
|
||||
"PO-Revision-Date: 2017-04-02 07:38+0000\n"
|
||||
"Last-Translator: Paavo Rissanen <paavo.rissanen@outlook.com>\n"
|
||||
"Language-Team: Finnish <fi@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Finnish (https://www.transifex.com/sabnzbd/teams/111101/fi/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-05-08 04:51+0000\n"
|
||||
"X-Generator: Launchpad (build fbdff7602bd10fb883bf7e2ddcc7fd5a16f60398)\n"
|
||||
"Language: fi\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
@@ -77,30 +77,3 @@ msgstr ""
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Asetuksiasi ja tietojasi ei poisteta."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "This system requires the Microsoft runtime library VC90 to be installed "
|
||||
#~ "first. Do you want to do that now?"
|
||||
#~ msgstr ""
|
||||
#~ "Tämä järjestelmä vaatii, että Microsoft runtime kirjasto VC90 täytyy asentaa "
|
||||
#~ "ensin. Haluatko asentaa sen nyt?"
|
||||
|
||||
#~ msgid "Cannot install without runtime library, retry?"
|
||||
#~ msgstr "Ei voida asentaa ilman runtime kirjastoa, yritä uudelleen?"
|
||||
|
||||
#~ msgid "Download error, retry?"
|
||||
#~ msgstr "Latausvirhe, yritä uudelleen?"
|
||||
|
||||
#~ msgid "Downloading Microsoft runtime installer..."
|
||||
#~ msgstr "Ladataan Microsoft runtime asennusta..."
|
||||
|
||||
#~ msgid "Go to the SABnzbd Wiki"
|
||||
#~ msgstr "Siirry SABnzbd wikiin"
|
||||
|
||||
#~ msgid ""
|
||||
#~ " >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
|
||||
#~ "release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
#~ msgstr ""
|
||||
#~ " >>>> VAROITUS <<<<\\r\\n\\r\\nOle hyvä ja tarkista "
|
||||
#~ "julkaisutiedot tai käy osoitteessa http://wiki.sabnzbd.org/introducing-0-7-0 "
|
||||
#~ "!"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# French translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file NSIS
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-05-07 19:13+0000\n"
|
||||
"PO-Revision-Date: 2020-05-07 20:22+0000\n"
|
||||
"Last-Translator: Fred <88com88@gmail.com>\n"
|
||||
"Language-Team: French <fr@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: French (https://www.transifex.com/sabnzbd/teams/111101/fr/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-05-08 04:51+0000\n"
|
||||
"X-Generator: Launchpad (build fbdff7602bd10fb883bf7e2ddcc7fd5a16f60398)\n"
|
||||
"Language: fr\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
@@ -35,8 +35,8 @@ msgid ""
|
||||
"reinstall the SABnzbd service. \\n\\nClick `OK` to remove the existing "
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"Le service Windows SABnzbd a changé dans SABnzbd 3.0.0. \\nVous allez devoir "
|
||||
"réinstaller le service SABnzbd. \\n\\nCliquez sur 'OK' pour supprimer les "
|
||||
"Le service Windows SABnzbd a changé dans SABnzbd 3.0.0. \\nVous allez devoir"
|
||||
" réinstaller le service SABnzbd. \\n\\nCliquez sur 'OK' pour supprimer les "
|
||||
"services existants ou sur 'Annuler' pour annuler cette mise à niveau."
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
@@ -44,8 +44,8 @@ msgid ""
|
||||
"The installer only supports 64-bit Windows, use the standalone version to "
|
||||
"run on 32-bit Windows."
|
||||
msgstr ""
|
||||
"Le programme d'installation ne prend en charge que Windows 64 bits, utilisez "
|
||||
"la version standalone pour l'exécuter sur Windows 32 bits."
|
||||
"Le programme d'installation ne prend en charge que Windows 64 bits, utilisez"
|
||||
" la version standalone pour l'exécuter sur Windows 32 bits."
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
@@ -83,30 +83,3 @@ msgstr ""
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Vos paramètres et données seront conservés."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "This system requires the Microsoft runtime library VC90 to be installed "
|
||||
#~ "first. Do you want to do that now?"
|
||||
#~ msgstr ""
|
||||
#~ "Ce système nécessite que la bibliothèque d'exécution Microsoft vc90 soit "
|
||||
#~ "installée en premier. Voulez-vous le faire maintenant?"
|
||||
|
||||
#~ msgid ""
|
||||
#~ " >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
|
||||
#~ "release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
#~ msgstr ""
|
||||
#~ " >>>> AVERTISSEMENT <<<<\\r\\n\\r\\nS'il vous plaît, "
|
||||
#~ "vérifiez d'abord les notes de version ou consultez "
|
||||
#~ "http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
|
||||
#~ msgid "Go to the SABnzbd Wiki"
|
||||
#~ msgstr "Aller sur le Wiki de SABnzbd"
|
||||
|
||||
#~ msgid "Download error, retry?"
|
||||
#~ msgstr "Erreur de téléchargement, réessayer ?"
|
||||
|
||||
#~ msgid "Cannot install without runtime library, retry?"
|
||||
#~ msgstr "Impossible d'installer sans moteur d'exécution, réessayer?"
|
||||
|
||||
#~ msgid "Downloading Microsoft runtime installer..."
|
||||
#~ msgstr "Téléchargement de Microsoft runtime installer..."
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Hebrew translation for sabnzbd
|
||||
# Copyright (c) 2017 Rosetta Contributors and Canonical Ltd 2017
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
|
||||
#
|
||||
# SABnzbd Translation Template file NSIS
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-05-07 19:13+0000\n"
|
||||
"PO-Revision-Date: 2020-05-10 13:54+0000\n"
|
||||
"Last-Translator: ION IL <Unknown>\n"
|
||||
"Language-Team: Hebrew <he@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Hebrew (https://www.transifex.com/sabnzbd/teams/111101/he/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-05-11 06:01+0000\n"
|
||||
"X-Generator: Launchpad (build fbdff7602bd10fb883bf7e2ddcc7fd5a16f60398)\n"
|
||||
"Language: he\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
@@ -82,30 +82,3 @@ msgstr ""
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "ההגדרות והנתונים שלך יישמרו."
|
||||
|
||||
#~ msgid "Download error, retry?"
|
||||
#~ msgstr "שגיאת הורדה, לנסות שוב?"
|
||||
|
||||
#~ msgid "Cannot install without runtime library, retry?"
|
||||
#~ msgstr "לא ניתן להתקין ללא ספרית זמן-אמת, לנסות שוב?"
|
||||
|
||||
#~ msgid "Downloading Microsoft runtime installer..."
|
||||
#~ msgstr "מוריד מתקין זמן-אמת של Microsoft..."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "This system requires the Microsoft runtime library VC90 to be installed "
|
||||
#~ "first. Do you want to do that now?"
|
||||
#~ msgstr ""
|
||||
#~ "מערכת זו דורשת את ספרית זמן-אמת VC90 של Microsoft שתהיה מותקנת תחילה. האם "
|
||||
#~ "ברצונך להתקין אותה כעת?"
|
||||
|
||||
#~ msgid ""
|
||||
#~ " >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
|
||||
#~ "release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
#~ msgstr ""
|
||||
#~ " >>>> אזהרה <<<<\\r\\n\\r\\n! "
|
||||
#~ "http://wiki.sabnzbd.org/introducing-0-7-0 אנא, בדוק תחילה את הערות השחרור או "
|
||||
#~ "לך אל"
|
||||
|
||||
#~ msgid "Go to the SABnzbd Wiki"
|
||||
#~ msgstr "SABnzbd לך אל וויקי"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Norwegian Bokmal translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file NSIS
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-05-07 19:13+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: Norwegian Bokmal <nb@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\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://www.transifex.com/sabnzbd/teams/111101/nb/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-05-08 04:51+0000\n"
|
||||
"X-Generator: Launchpad (build fbdff7602bd10fb883bf7e2ddcc7fd5a16f60398)\n"
|
||||
"Language: nb\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
@@ -71,37 +71,10 @@ msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"Du kan ikke overskrive en eksisterende installasjon. \\n\\nTrykk 'OK' for å "
|
||||
"fjerne tidligere installasjon, eller 'Avbryt' for å avbryte denne "
|
||||
"Du kan ikke overskrive en eksisterende installasjon. \\n\\nTrykk 'OK' for å"
|
||||
" fjerne tidligere installasjon, eller 'Avbryt' for å avbryte denne "
|
||||
"oppgraderingen."
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Dine innstillinger og data vil bli tatt vare på."
|
||||
|
||||
#~ msgid ""
|
||||
#~ " >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
|
||||
#~ "release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
#~ msgstr ""
|
||||
#~ " >>>> ADVARSEL <<<<\\r\\n\\r\\nVennligst sjekk "
|
||||
#~ "versjonsmerknadene først, eller gå til http://wiki.sabnzbd.org/introducing-0-"
|
||||
#~ "7-0 !"
|
||||
|
||||
#~ msgid "Go to the SABnzbd Wiki"
|
||||
#~ msgstr "Gå til SABnzbd Wiki"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "This system requires the Microsoft runtime library VC90 to be installed "
|
||||
#~ "first. Do you want to do that now?"
|
||||
#~ msgstr ""
|
||||
#~ "Dette sytemet krever at Microsoft runtime library VC90 er installert først. "
|
||||
#~ "Ønsker du å gjøre dette nå?"
|
||||
|
||||
#~ msgid "Cannot install without runtime library, retry?"
|
||||
#~ msgstr "Kan ikke installere uten runtime library, prøve på nytt?"
|
||||
|
||||
#~ msgid "Download error, retry?"
|
||||
#~ msgstr "Nedlasting feilet, prøve på nytt?"
|
||||
|
||||
#~ msgid "Downloading Microsoft runtime installer..."
|
||||
#~ msgstr "Laster ned Microsoft runtime installer..."
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Dutch translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file NSIS
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-05-07 19:13+0000\n"
|
||||
"PO-Revision-Date: 2020-05-03 15:01+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>\n"
|
||||
"Language-Team: Dutch <nl@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Dutch (https://www.transifex.com/sabnzbd/teams/111101/nl/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-05-08 04:51+0000\n"
|
||||
"X-Generator: Launchpad (build fbdff7602bd10fb883bf7e2ddcc7fd5a16f60398)\n"
|
||||
"Language: nl\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
@@ -35,6 +35,8 @@ msgid ""
|
||||
"reinstall the SABnzbd service. \\n\\nClick `OK` to remove the existing "
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"De SABnzbd Windows Service is aangepast in SABnzbd 3.0.0. Hierdoor zal je de service opnieuw moeten installeren.\\n\\n\n"
|
||||
"Klik `Ok` om de bestaande services te verwijderen of `Annuleren` om te stoppen."
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
@@ -79,29 +81,3 @@ msgstr ""
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Je instellingen en bestanden blijven behouden."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "This system requires the Microsoft runtime library VC90 to be installed "
|
||||
#~ "first. Do you want to do that now?"
|
||||
#~ msgstr ""
|
||||
#~ "Op dit systeem moeten eerst de Microsoft runtime bibliotheek VC90 "
|
||||
#~ "geïnstalleerd worden. Wilt u dat nu doen?"
|
||||
|
||||
#~ msgid "Downloading Microsoft runtime installer..."
|
||||
#~ msgstr "Downloaden van de Microsoft bibliotheek"
|
||||
|
||||
#~ msgid ""
|
||||
#~ " >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
|
||||
#~ "release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
#~ msgstr ""
|
||||
#~ " >>>> WAARSCHUWING <<<<\\\\r\\\\n\\\\r\\\\nLees eerst het "
|
||||
#~ "vrijgave bericht of ga naar http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
|
||||
#~ msgid "Cannot install without runtime library, retry?"
|
||||
#~ msgstr "Installeren heeft geen zin zonder de bibliotheek, opnieuw proberen?"
|
||||
|
||||
#~ msgid "Download error, retry?"
|
||||
#~ msgstr "Download mislukt, opnieuw proberen?"
|
||||
|
||||
#~ msgid "Go to the SABnzbd Wiki"
|
||||
#~ msgstr "Ga naar de SABnzbd-Wiki"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Polish translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file NSIS
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-05-07 19:13+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Tomasz 'Zen' Napierala <tomasz@napierala.org>\n"
|
||||
"Language-Team: Polish <pl@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Polish (https://www.transifex.com/sabnzbd/teams/111101/pl/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-05-08 04:51+0000\n"
|
||||
"X-Generator: Launchpad (build fbdff7602bd10fb883bf7e2ddcc7fd5a16f60398)\n"
|
||||
"Language: pl\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
@@ -77,29 +77,3 @@ msgstr ""
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Twoje ustawienia i dane zostaną zachowane."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "This system requires the Microsoft runtime library VC90 to be installed "
|
||||
#~ "first. Do you want to do that now?"
|
||||
#~ msgstr ""
|
||||
#~ "Ten system wymaga najpierw zainstalowania bibliotek Microsoft VC90. Czy "
|
||||
#~ "chcesz wykonać teraz instalację?"
|
||||
|
||||
#~ msgid "Cannot install without runtime library, retry?"
|
||||
#~ msgstr "Nie można wykonać instalacji bez bibliotek, spróbować ponownie?"
|
||||
|
||||
#~ msgid "Download error, retry?"
|
||||
#~ msgstr "Problem z pobieraniem, spróbować ponownie?"
|
||||
|
||||
#~ msgid "Downloading Microsoft runtime installer..."
|
||||
#~ msgstr "Pobieranie instalatora bibliotek Microsoft..."
|
||||
|
||||
#~ msgid ""
|
||||
#~ " >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
|
||||
#~ "release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
#~ msgstr ""
|
||||
#~ " >>>> UWAGA <<<<\\r\\n\\r\\nNajpierw przeczytaj informacje "
|
||||
#~ "o wydaniu lub odwiedź http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
|
||||
#~ msgid "Go to the SABnzbd Wiki"
|
||||
#~ msgstr "Idź do wiki SABnzbd"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Brazilian Portuguese translation for sabnzbd
|
||||
# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
|
||||
#
|
||||
# SABnzbd Translation Template file NSIS
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-05-07 19:13+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: lrrosa <Unknown>\n"
|
||||
"Language-Team: Brazilian Portuguese <pt_BR@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Portuguese (Brazil) (https://www.transifex.com/sabnzbd/teams/111101/pt_BR/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-05-08 04:51+0000\n"
|
||||
"X-Generator: Launchpad (build fbdff7602bd10fb883bf7e2ddcc7fd5a16f60398)\n"
|
||||
"Language: pt_BR\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
@@ -77,29 +77,3 @@ msgstr ""
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Suas configurações e os dados serão preservados."
|
||||
|
||||
#~ msgid "Download error, retry?"
|
||||
#~ msgstr "Houve um erro de download. Quer tentar novamente?"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "This system requires the Microsoft runtime library VC90 to be installed "
|
||||
#~ "first. Do you want to do that now?"
|
||||
#~ msgstr ""
|
||||
#~ "Este sistema precisa que a biblioteca runtime Microsoft VC90 seja instalada "
|
||||
#~ "antes. Você quer fazer isso agora?"
|
||||
|
||||
#~ msgid "Cannot install without runtime library, retry?"
|
||||
#~ msgstr "Não é possível instalar sem a biblioteca runtime. Quer repetir?"
|
||||
|
||||
#~ msgid "Downloading Microsoft runtime installer..."
|
||||
#~ msgstr "Baixando o instalador runtime da Microsoft ..."
|
||||
|
||||
#~ msgid ""
|
||||
#~ " >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
|
||||
#~ "release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
#~ msgstr ""
|
||||
#~ " >>>> ATENÇÃO <<<<\\r\\n\\r\\nPor favor, verifique primeiro "
|
||||
#~ "as notas de lançamento ou vá até http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
|
||||
#~ msgid "Go to the SABnzbd Wiki"
|
||||
#~ msgstr "Vá para a Wiki do SABnzbd"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Romanian translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file NSIS
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-05-07 19:13+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: nicusor <Unknown>\n"
|
||||
"Language-Team: Romanian <ro@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Romanian (https://www.transifex.com/sabnzbd/teams/111101/ro/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-05-08 04:51+0000\n"
|
||||
"X-Generator: Launchpad (build fbdff7602bd10fb883bf7e2ddcc7fd5a16f60398)\n"
|
||||
"Language: ro\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
@@ -71,36 +71,9 @@ msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"Nu puteți suprascrie instalarea existentă. \\n\\nClick `OK` pentru a elimina "
|
||||
"versiunea anterioară sau `Anulare` pentru a anula actualizarea."
|
||||
"Nu puteți suprascrie instalarea existentă. \\n\\nClick `OK` pentru a elimina"
|
||||
" versiunea anterioară sau `Anulare` pentru a anula actualizarea."
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Setările şi informaţiile vor fi salvate."
|
||||
|
||||
#~ msgid "Download error, retry?"
|
||||
#~ msgstr "Eroare descărcare, încerc din nou?"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "This system requires the Microsoft runtime library VC90 to be installed "
|
||||
#~ "first. Do you want to do that now?"
|
||||
#~ msgstr ""
|
||||
#~ "Acest sistem necesită librăria Microsoft VC90 instalată. Dortiți să faceți "
|
||||
#~ "asta acum ?"
|
||||
|
||||
#~ msgid "Cannot install without runtime library, retry?"
|
||||
#~ msgstr "Nu pot instala fără rutină librărie, încerc din nou?"
|
||||
|
||||
#~ msgid "Downloading Microsoft runtime installer..."
|
||||
#~ msgstr "Descărcare rutină instalare Microsoft..."
|
||||
|
||||
#~ msgid ""
|
||||
#~ " >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
|
||||
#~ "release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
#~ msgstr ""
|
||||
#~ " >>>> ATENŢIE <<<<\\r\\n\\r\\nVă rugăm, să verificaţi mai "
|
||||
#~ "întâi notele de publicare sau să vizitaţi "
|
||||
#~ "http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
|
||||
#~ msgid "Go to the SABnzbd Wiki"
|
||||
#~ msgstr "Dute la Wiki SABnzbd"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Russian translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file NSIS
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-05-07 19:13+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Pavel Maryanov <Unknown>\n"
|
||||
"Language-Team: Russian <gnu@mx.ru>\n"
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Russian (https://www.transifex.com/sabnzbd/teams/111101/ru/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-05-08 04:51+0000\n"
|
||||
"X-Generator: Launchpad (build fbdff7602bd10fb883bf7e2ddcc7fd5a16f60398)\n"
|
||||
"Language: ru\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
@@ -78,21 +78,3 @@ msgstr ""
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Ваши параметры и данные будут сохранены."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "This system requires the Microsoft runtime library VC90 to be installed "
|
||||
#~ "first. Do you want to do that now?"
|
||||
#~ msgstr ""
|
||||
#~ "Для этой системы сначала необходимо установить библиотеку времени выполнения "
|
||||
#~ "Microsoft VC90. Сделать это сейчас?"
|
||||
|
||||
#~ msgid "Downloading Microsoft runtime installer..."
|
||||
#~ msgstr "Загрузка программы установки Microsoft..."
|
||||
|
||||
#~ msgid "Download error, retry?"
|
||||
#~ msgstr "Ошибка загрузки. Повторить попытку?"
|
||||
|
||||
#~ msgid "Cannot install without runtime library, retry?"
|
||||
#~ msgstr ""
|
||||
#~ "Не удаётся выполнить установку без библиотеки времени выполнения. Повторить "
|
||||
#~ "попытку?"
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
# Serbian translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
# Мирослав Николић <miroslavnikolic@rocketmail.com>, 2011.
|
||||
# SABnzbd Translation Template file NSIS
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-05-07 19:13+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Ozzii <Unknown>\n"
|
||||
"Language-Team: Launchpad Serbian Translators\n"
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Serbian (https://www.transifex.com/sabnzbd/teams/111101/sr/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-05-08 04:51+0000\n"
|
||||
"X-Generator: Launchpad (build fbdff7602bd10fb883bf7e2ddcc7fd5a16f60398)\n"
|
||||
"Language: sr\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
@@ -78,29 +77,3 @@ msgstr ""
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Ваша подешавања и подаци биће сачувани."
|
||||
|
||||
#~ msgid "Download error, retry?"
|
||||
#~ msgstr "Грешка у преузимању, да поновим?"
|
||||
|
||||
#~ msgid "Go to the SABnzbd Wiki"
|
||||
#~ msgstr "Иди на вики САБнзбд-а"
|
||||
|
||||
#~ msgid ""
|
||||
#~ " >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
|
||||
#~ "release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
#~ msgstr ""
|
||||
#~ " >>>> ПАЖЊА <<<<\\r\\n\\r\\nПрво проверите белешке о издању "
|
||||
#~ "или идите на „http://wiki.sabnzbd.org/introducing-0-7-0“ !"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "This system requires the Microsoft runtime library VC90 to be installed "
|
||||
#~ "first. Do you want to do that now?"
|
||||
#~ msgstr ""
|
||||
#~ "Овај систем захтева да буде прво инсталирана Мајкрософтова извршна "
|
||||
#~ "библиотека „VC90“. Да ли желите то да урадите?"
|
||||
|
||||
#~ msgid "Downloading Microsoft runtime installer..."
|
||||
#~ msgstr "Преузимам Мајкрософтов извршни програм за инсталацију..."
|
||||
|
||||
#~ msgid "Cannot install without runtime library, retry?"
|
||||
#~ msgstr "Не могу да инсталирам без извршне библиотеке, да поновим?"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Swedish translation for sabnzbd
|
||||
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
|
||||
#
|
||||
# SABnzbd Translation Template file NSIS
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-05-07 19:13+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Andreas Lindberg <andypandyswe@gmail.com>\n"
|
||||
"Language-Team: Swedish <sv@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Swedish (https://www.transifex.com/sabnzbd/teams/111101/sv/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-05-08 04:51+0000\n"
|
||||
"X-Generator: Launchpad (build fbdff7602bd10fb883bf7e2ddcc7fd5a16f60398)\n"
|
||||
"Language: sv\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
@@ -78,29 +78,3 @@ msgstr ""
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Dina inställningar och ditt data kommer att bevaras."
|
||||
|
||||
#~ msgid "Go to the SABnzbd Wiki"
|
||||
#~ msgstr "Gå till SABnzbd Wiki"
|
||||
|
||||
#~ msgid ""
|
||||
#~ " >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
|
||||
#~ "release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
#~ msgstr ""
|
||||
#~ " >>>> VARNING <<<<\\r\\n\\r\\nVänligen läs först "
|
||||
#~ "releasenoteringarna eller gå till http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "This system requires the Microsoft runtime library VC90 to be installed "
|
||||
#~ "first. Do you want to do that now?"
|
||||
#~ msgstr ""
|
||||
#~ "Detta system kräver att Microsofts runtimebibliotek VC90 är installerat. "
|
||||
#~ "Vill du göra detta nu?"
|
||||
|
||||
#~ msgid "Download error, retry?"
|
||||
#~ msgstr "Misslyckat nedladdningsförsök, försök igen?"
|
||||
|
||||
#~ msgid "Cannot install without runtime library, retry?"
|
||||
#~ msgstr "Kan inte installera utan runtimebibliotek, försök igen?"
|
||||
|
||||
#~ msgid "Downloading Microsoft runtime installer..."
|
||||
#~ msgstr "Laddar ned Microsofts runtimeinstaller..."
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Chinese (Simplified) translation for sabnzbd
|
||||
# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
|
||||
#
|
||||
# SABnzbd Translation Template file NSIS
|
||||
# Copyright 2011-2020 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
# Translators:
|
||||
# Safihre <safihre@sabnzbd.org>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2020-05-07 19:13+0000\n"
|
||||
"PO-Revision-Date: 2017-05-28 17:17+0000\n"
|
||||
"Last-Translator: ninjai <ninjai.us@gmail.com>\n"
|
||||
"Language-Team: Chinese (Simplified) <zh_CN@li.org>\n"
|
||||
"Project-Id-Version: SABnzbd-3.0.0-develop\n"
|
||||
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
|
||||
"Language-Team: Chinese (China) (https://www.transifex.com/sabnzbd/teams/111101/zh_CN/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2020-05-08 04:51+0000\n"
|
||||
"X-Generator: Launchpad (build fbdff7602bd10fb883bf7e2ddcc7fd5a16f60398)\n"
|
||||
"Language: zh_CN\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
@@ -75,27 +75,3 @@ msgstr "不可以覆盖安装。\\n\\n点击“确定”可移除旧版,或点
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "您的设置及数据将会保留。"
|
||||
|
||||
#~ msgid ""
|
||||
#~ " >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
|
||||
#~ "release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
#~ msgstr ""
|
||||
#~ " >>>> *警告* <<<<\\r\\n\\r\\n请首先查看版本说明或访问 "
|
||||
#~ "http://wiki.sabnzbd.org/introducing-0-7-0 !"
|
||||
|
||||
#~ msgid "Go to the SABnzbd Wiki"
|
||||
#~ msgstr "访问 SABnzbd Wiki"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "This system requires the Microsoft runtime library VC90 to be installed "
|
||||
#~ "first. Do you want to do that now?"
|
||||
#~ msgstr "该系统需要先安装 Microsoft 运行时库 VC90。是否希望立即安装?"
|
||||
|
||||
#~ msgid "Cannot install without runtime library, retry?"
|
||||
#~ msgstr "没有运行时库无法安装,重试?"
|
||||
|
||||
#~ msgid "Download error, retry?"
|
||||
#~ msgstr "下载出错,重试?"
|
||||
|
||||
#~ msgid "Downloading Microsoft runtime installer..."
|
||||
#~ msgstr "正在下载 Microsoft 运行时安装程序..."
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
sabyenc3
|
||||
cheetah3
|
||||
sabyenc3>=4.0.0
|
||||
cheetah3>=3.0.0
|
||||
cryptography
|
||||
feedparser
|
||||
feedparser>=6.0.0
|
||||
configobj
|
||||
cheroot<8.4.3
|
||||
cherrypy
|
||||
portend
|
||||
chardet
|
||||
|
||||
@@ -15,23 +15,19 @@
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# Imported to be referenced from other files directly
|
||||
from sabnzbd.version import __version__, __baseline__
|
||||
|
||||
import os
|
||||
import logging
|
||||
import datetime
|
||||
import tempfile
|
||||
import pickle
|
||||
import gzip
|
||||
import subprocess
|
||||
import time
|
||||
import socket
|
||||
import cherrypy
|
||||
import sys
|
||||
import re
|
||||
import ssl
|
||||
from threading import Lock, Thread
|
||||
from typing import Any, AnyStr
|
||||
|
||||
##############################################################################
|
||||
# Determine platform flags
|
||||
@@ -74,56 +70,72 @@ elif os.name == "posix":
|
||||
except:
|
||||
pass
|
||||
|
||||
# Imported to be referenced from other files directly
|
||||
from sabnzbd.version import __version__, __baseline__
|
||||
|
||||
# Now we can import safely
|
||||
from sabnzbd.nzbqueue import NzbQueue
|
||||
from sabnzbd.postproc import PostProcessor
|
||||
from sabnzbd.downloader import Downloader
|
||||
from sabnzbd.decoder import Decoder
|
||||
from sabnzbd.assembler import Assembler
|
||||
from sabnzbd.rating import Rating
|
||||
import sabnzbd.misc as misc
|
||||
import sabnzbd.filesystem as filesystem
|
||||
import sabnzbd.powersup as powersup
|
||||
from sabnzbd.dirscanner import DirScanner
|
||||
from sabnzbd.urlgrabber import URLGrabber
|
||||
import sabnzbd.scheduler as scheduler
|
||||
import sabnzbd.rss as rss
|
||||
import sabnzbd.emailer as emailer
|
||||
from sabnzbd.articlecache import ArticleCache
|
||||
import sabnzbd.newsunpack
|
||||
import sabnzbd.encoding as encoding
|
||||
import sabnzbd.config as config
|
||||
from sabnzbd.bpsmeter import BPSMeter
|
||||
import sabnzbd.cfg as cfg
|
||||
import sabnzbd.database
|
||||
import sabnzbd.lang as lang
|
||||
import sabnzbd.par2file as par2file
|
||||
import sabnzbd.nzbparser as nzbparser
|
||||
import sabnzbd.nzbstuff
|
||||
import sabnzbd.getipaddress
|
||||
import sabnzbd.newsunpack
|
||||
import sabnzbd.par2file
|
||||
import sabnzbd.api
|
||||
import sabnzbd.interface
|
||||
import sabnzbd.nzbstuff as nzbstuff
|
||||
import sabnzbd.zconfig
|
||||
import sabnzbd.directunpacker as directunpacker
|
||||
import sabnzbd.dirscanner
|
||||
import sabnzbd.urlgrabber
|
||||
import sabnzbd.nzbqueue
|
||||
import sabnzbd.postproc
|
||||
import sabnzbd.downloader
|
||||
import sabnzbd.decoder
|
||||
import sabnzbd.assembler
|
||||
import sabnzbd.rating
|
||||
import sabnzbd.articlecache
|
||||
import sabnzbd.bpsmeter
|
||||
import sabnzbd.scheduler as scheduler
|
||||
from sabnzbd.decorators import synchronized
|
||||
from sabnzbd.constants import (
|
||||
NORMAL_PRIORITY,
|
||||
DEFAULT_PRIORITY,
|
||||
VALID_ARCHIVES,
|
||||
REPAIR_REQUEST,
|
||||
QUEUE_FILE_NAME,
|
||||
QUEUE_VERSION,
|
||||
QUEUE_FILE_TMPL,
|
||||
)
|
||||
import sabnzbd.getipaddress as getipaddress
|
||||
import sabnzbd.utils.ssdp
|
||||
|
||||
LINUX_POWER = powersup.HAVE_DBUS
|
||||
# Storage for the threads, variables are filled during initialization
|
||||
ArticleCache: sabnzbd.articlecache.ArticleCache
|
||||
Rating: sabnzbd.rating.Rating
|
||||
Assembler: sabnzbd.assembler.Assembler
|
||||
Decoder: sabnzbd.decoder.Decoder
|
||||
Downloader: sabnzbd.downloader.Downloader
|
||||
PostProcessor: sabnzbd.postproc.PostProcessor
|
||||
NzbQueue: sabnzbd.nzbqueue.NzbQueue
|
||||
URLGrabber: sabnzbd.urlgrabber.URLGrabber
|
||||
DirScanner: sabnzbd.dirscanner.DirScanner
|
||||
BPSMeter: sabnzbd.bpsmeter.BPSMeter
|
||||
RSSReader: sabnzbd.rss.RSSReader
|
||||
Scheduler: sabnzbd.scheduler.Scheduler
|
||||
|
||||
# Regular constants
|
||||
START = datetime.datetime.now()
|
||||
|
||||
MY_NAME = None
|
||||
MY_FULLNAME = None
|
||||
RESTART_ARGS = []
|
||||
NEW_VERSION = (None, None)
|
||||
DIR_HOME = None
|
||||
DIR_APPDATA = None
|
||||
DIR_LCLDATA = None
|
||||
DIR_PROG = None
|
||||
DIR_INTERFACES = None
|
||||
@@ -135,6 +147,7 @@ QUEUECOMPLETEACTION = None # stores the name of the function to be called
|
||||
QUEUECOMPLETEARG = None # stores an extra arguments that need to be passed
|
||||
|
||||
DAEMON = None
|
||||
LINUX_POWER = powersup.HAVE_DBUS
|
||||
|
||||
LOGFILE = None
|
||||
WEBLOGFILE = None
|
||||
@@ -158,8 +171,6 @@ PAUSED_ALL = False
|
||||
TRIGGER_RESTART = False # To trigger restart for Scheduler, WinService and Mac
|
||||
WINTRAY = None # Thread for the Windows SysTray icon
|
||||
WEBUI_READY = False
|
||||
LAST_WARNING = None
|
||||
LAST_ERROR = None
|
||||
EXTERNAL_IPV6 = False
|
||||
LAST_HISTORY_UPDATE = 1
|
||||
|
||||
@@ -169,6 +180,7 @@ DOWNLOAD_DIR_SPEED = 0
|
||||
COMPLETE_DIR_SPEED = 0
|
||||
INTERNET_BANDWIDTH = 0
|
||||
|
||||
|
||||
# Rendering of original command line arguments in Config
|
||||
CMDLINE = " ".join(['"%s"' % arg for arg in sys.argv])
|
||||
|
||||
@@ -180,7 +192,6 @@ __SHUTTING_DOWN__ = False
|
||||
# Signal Handler
|
||||
##############################################################################
|
||||
def sig_handler(signum=None, frame=None):
|
||||
global SABSTOP, WINTRAY
|
||||
if sabnzbd.WIN32 and signum is not None and DAEMON and signum == 5:
|
||||
# Ignore the "logoff" event when running as a Win32 daemon
|
||||
return True
|
||||
@@ -197,7 +208,7 @@ def sig_handler(signum=None, frame=None):
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
pid_file()
|
||||
SABSTOP = True
|
||||
sabnzbd.SABSTOP = True
|
||||
os._exit(0)
|
||||
|
||||
|
||||
@@ -215,13 +226,11 @@ def get_db_connection(thread_index=0):
|
||||
|
||||
|
||||
@synchronized(INIT_LOCK)
|
||||
def initialize(pause_downloader=False, clean_up=False, evalSched=False, repair=0):
|
||||
global __INITIALIZED__, __SHUTTING_DOWN__, LOGFILE, WEBLOGFILE, LOGHANDLER, GUIHANDLER, AMBI_LOCALHOST, WAITEXIT, DAEMON, MY_NAME, MY_FULLNAME, NEW_VERSION, DIR_HOME, DIR_APPDATA, DIR_LCLDATA, DIR_PROG, DIR_INTERFACES, DARWIN, RESTART_REQ
|
||||
|
||||
if __INITIALIZED__:
|
||||
def initialize(pause_downloader=False, clean_up=False, repair=0):
|
||||
if sabnzbd.__INITIALIZED__:
|
||||
return False
|
||||
|
||||
__SHUTTING_DOWN__ = False
|
||||
sabnzbd.__SHUTTING_DOWN__ = False
|
||||
|
||||
# Set global database connection for Web-UI threads
|
||||
cherrypy.engine.subscribe("start_thread", get_db_connection)
|
||||
@@ -272,11 +281,6 @@ def initialize(pause_downloader=False, clean_up=False, evalSched=False, repair=0
|
||||
cfg.enable_https_verification.callback(guard_https_ver)
|
||||
guard_https_ver()
|
||||
|
||||
# Set cache limit
|
||||
if not cfg.cache_limit() or (cfg.cache_limit() in ("200M", "450M") and (sabnzbd.WIN32 or sabnzbd.DARWIN)):
|
||||
cfg.cache_limit.set(misc.get_cache_limit())
|
||||
ArticleCache.do.new_limit(cfg.cache_limit.get_int())
|
||||
|
||||
check_incomplete_vs_complete()
|
||||
|
||||
# Set language files
|
||||
@@ -284,26 +288,9 @@ def initialize(pause_downloader=False, clean_up=False, evalSched=False, repair=0
|
||||
lang.set_language(cfg.language())
|
||||
sabnzbd.api.clear_trans_cache()
|
||||
|
||||
# Set end-of-queue action
|
||||
sabnzbd.change_queue_complete_action(cfg.queue_complete(), new=False)
|
||||
|
||||
# One time conversion "speedlimit" in schedules.
|
||||
if not cfg.sched_converted():
|
||||
schedules = cfg.schedules()
|
||||
newsched = []
|
||||
for sched in schedules:
|
||||
if "speedlimit" in sched:
|
||||
newsched.append(re.sub(r"(speedlimit \d+)$", r"\1K", sched))
|
||||
else:
|
||||
newsched.append(sched)
|
||||
cfg.schedules.set(newsched)
|
||||
cfg.sched_converted.set(1)
|
||||
|
||||
# Second time schedule conversion
|
||||
if cfg.sched_converted() != 2:
|
||||
cfg.schedules.set(["%s %s" % (1, schedule) for schedule in cfg.schedules()])
|
||||
cfg.sched_converted.set(2)
|
||||
config.save_config()
|
||||
|
||||
# Convert auto-sort
|
||||
if cfg.auto_sort() == "0":
|
||||
cfg.auto_sort.set("")
|
||||
@@ -320,103 +307,97 @@ def initialize(pause_downloader=False, clean_up=False, evalSched=False, repair=0
|
||||
pause_downloader = True
|
||||
|
||||
# Initialize threads
|
||||
rss.init()
|
||||
sabnzbd.ArticleCache = sabnzbd.articlecache.ArticleCache()
|
||||
sabnzbd.BPSMeter = sabnzbd.bpsmeter.BPSMeter()
|
||||
sabnzbd.NzbQueue = sabnzbd.nzbqueue.NzbQueue()
|
||||
sabnzbd.Downloader = sabnzbd.downloader.Downloader(pause_downloader or sabnzbd.BPSMeter.read())
|
||||
sabnzbd.Decoder = sabnzbd.decoder.Decoder()
|
||||
sabnzbd.Assembler = sabnzbd.assembler.Assembler()
|
||||
sabnzbd.PostProcessor = sabnzbd.postproc.PostProcessor()
|
||||
sabnzbd.DirScanner = sabnzbd.dirscanner.DirScanner()
|
||||
sabnzbd.Rating = sabnzbd.rating.Rating()
|
||||
sabnzbd.URLGrabber = sabnzbd.urlgrabber.URLGrabber()
|
||||
sabnzbd.RSSReader = sabnzbd.rss.RSSReader()
|
||||
sabnzbd.Scheduler = sabnzbd.scheduler.Scheduler()
|
||||
|
||||
paused = BPSMeter.do.read()
|
||||
# Run startup tasks
|
||||
sabnzbd.NzbQueue.read_queue(repair)
|
||||
sabnzbd.Scheduler.analyse(pause_downloader)
|
||||
|
||||
NzbQueue()
|
||||
|
||||
Downloader(pause_downloader or paused)
|
||||
|
||||
Decoder()
|
||||
|
||||
Assembler()
|
||||
|
||||
PostProcessor()
|
||||
|
||||
NzbQueue.do.read_queue(repair)
|
||||
|
||||
DirScanner()
|
||||
|
||||
Rating()
|
||||
|
||||
URLGrabber()
|
||||
|
||||
scheduler.init()
|
||||
|
||||
if evalSched:
|
||||
scheduler.analyse(pause_downloader)
|
||||
# Set cache limit for new users
|
||||
if not cfg.cache_limit():
|
||||
cfg.cache_limit.set(misc.get_cache_limit())
|
||||
sabnzbd.ArticleCache.new_limit(cfg.cache_limit.get_int())
|
||||
|
||||
logging.info("All processes started")
|
||||
RESTART_REQ = False
|
||||
__INITIALIZED__ = True
|
||||
return True
|
||||
sabnzbd.RESTART_REQ = False
|
||||
sabnzbd.__INITIALIZED__ = True
|
||||
|
||||
|
||||
@synchronized(INIT_LOCK)
|
||||
def start():
|
||||
global __INITIALIZED__
|
||||
|
||||
if __INITIALIZED__:
|
||||
if sabnzbd.__INITIALIZED__:
|
||||
logging.debug("Starting postprocessor")
|
||||
PostProcessor.do.start()
|
||||
sabnzbd.PostProcessor.start()
|
||||
|
||||
logging.debug("Starting assembler")
|
||||
Assembler.do.start()
|
||||
sabnzbd.Assembler.start()
|
||||
|
||||
logging.debug("Starting downloader")
|
||||
Downloader.do.start()
|
||||
sabnzbd.Downloader.start()
|
||||
|
||||
logging.debug("Starting decoders")
|
||||
Decoder.do.start()
|
||||
sabnzbd.Decoder.start()
|
||||
|
||||
scheduler.start()
|
||||
logging.debug("Starting scheduler")
|
||||
sabnzbd.Scheduler.start()
|
||||
|
||||
logging.debug("Starting dirscanner")
|
||||
DirScanner.do.start()
|
||||
sabnzbd.DirScanner.start()
|
||||
|
||||
Rating.do.start()
|
||||
logging.debug("Starting rating")
|
||||
sabnzbd.Rating.start()
|
||||
|
||||
logging.debug("Starting urlgrabber")
|
||||
URLGrabber.do.start()
|
||||
sabnzbd.URLGrabber.start()
|
||||
|
||||
|
||||
@synchronized(INIT_LOCK)
|
||||
def halt():
|
||||
global __INITIALIZED__, __SHUTTING_DOWN__
|
||||
|
||||
if __INITIALIZED__:
|
||||
if sabnzbd.__INITIALIZED__:
|
||||
logging.info("SABnzbd shutting down...")
|
||||
__SHUTTING_DOWN__ = True
|
||||
sabnzbd.__SHUTTING_DOWN__ = True
|
||||
|
||||
# Stop the windows tray icon
|
||||
if sabnzbd.WINTRAY:
|
||||
sabnzbd.WINTRAY.terminate = True
|
||||
|
||||
sabnzbd.zconfig.remove_server()
|
||||
sabnzbd.utils.ssdp.stop_ssdp()
|
||||
|
||||
sabnzbd.directunpacker.abort_all()
|
||||
|
||||
rss.stop()
|
||||
logging.debug("Stopping RSSReader")
|
||||
sabnzbd.RSSReader.stop()
|
||||
|
||||
logging.debug("Stopping URLGrabber")
|
||||
URLGrabber.do.stop()
|
||||
sabnzbd.URLGrabber.stop()
|
||||
try:
|
||||
URLGrabber.do.join()
|
||||
sabnzbd.URLGrabber.join()
|
||||
except:
|
||||
pass
|
||||
|
||||
logging.debug("Stopping rating")
|
||||
Rating.do.stop()
|
||||
sabnzbd.Rating.stop()
|
||||
try:
|
||||
Rating.do.join()
|
||||
sabnzbd.Rating.join()
|
||||
except:
|
||||
pass
|
||||
|
||||
logging.debug("Stopping dirscanner")
|
||||
DirScanner.do.stop()
|
||||
sabnzbd.DirScanner.stop()
|
||||
try:
|
||||
DirScanner.do.join()
|
||||
sabnzbd.DirScanner.join()
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -426,20 +407,20 @@ def halt():
|
||||
|
||||
# Decoder handles join gracefully
|
||||
logging.debug("Stopping decoders")
|
||||
Decoder.do.stop()
|
||||
Decoder.do.join()
|
||||
sabnzbd.Decoder.stop()
|
||||
sabnzbd.Decoder.join()
|
||||
|
||||
logging.debug("Stopping assembler")
|
||||
Assembler.do.stop()
|
||||
sabnzbd.Assembler.stop()
|
||||
try:
|
||||
Assembler.do.join()
|
||||
sabnzbd.Assembler.join()
|
||||
except:
|
||||
pass
|
||||
|
||||
logging.debug("Stopping postprocessor")
|
||||
PostProcessor.do.stop()
|
||||
sabnzbd.PostProcessor.stop()
|
||||
try:
|
||||
PostProcessor.do.join()
|
||||
sabnzbd.PostProcessor.join()
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -453,11 +434,12 @@ def halt():
|
||||
# Since all warm-restarts have been removed, it's not longer
|
||||
# needed to stop the scheduler.
|
||||
# We must tell the scheduler to deactivate.
|
||||
scheduler.abort()
|
||||
logging.debug("Terminating scheduler")
|
||||
sabnzbd.Scheduler.abort()
|
||||
|
||||
logging.info("All processes stopped")
|
||||
|
||||
__INITIALIZED__ = False
|
||||
sabnzbd.__INITIALIZED__ = False
|
||||
|
||||
|
||||
def trigger_restart(timeout=None):
|
||||
@@ -466,15 +448,6 @@ def trigger_restart(timeout=None):
|
||||
if timeout:
|
||||
time.sleep(timeout)
|
||||
|
||||
# Add extra arguments
|
||||
if sabnzbd.downloader.Downloader.do.paused:
|
||||
sabnzbd.RESTART_ARGS.append("-p")
|
||||
sys.argv = sabnzbd.RESTART_ARGS
|
||||
|
||||
# Stop all services
|
||||
sabnzbd.halt()
|
||||
cherrypy.engine.exit()
|
||||
|
||||
if sabnzbd.WIN32:
|
||||
# Remove connection info for faster restart
|
||||
del_connection_info()
|
||||
@@ -483,6 +456,15 @@ def trigger_restart(timeout=None):
|
||||
if hasattr(sys, "frozen"):
|
||||
sabnzbd.TRIGGER_RESTART = True
|
||||
else:
|
||||
# Add extra arguments
|
||||
if sabnzbd.Downloader.paused:
|
||||
sabnzbd.RESTART_ARGS.append("-p")
|
||||
sys.argv = sabnzbd.RESTART_ARGS
|
||||
|
||||
# Stop all services
|
||||
sabnzbd.halt()
|
||||
cherrypy.engine.exit()
|
||||
|
||||
# Do the restart right now
|
||||
cherrypy.engine._do_execv()
|
||||
|
||||
@@ -492,18 +474,17 @@ def trigger_restart(timeout=None):
|
||||
##############################################################################
|
||||
def new_limit():
|
||||
""" Callback for article cache changes """
|
||||
ArticleCache.do.new_limit(cfg.cache_limit.get_int())
|
||||
sabnzbd.ArticleCache.new_limit(cfg.cache_limit.get_int())
|
||||
|
||||
|
||||
def guard_restart():
|
||||
""" Callback for config options requiring a restart """
|
||||
global RESTART_REQ
|
||||
sabnzbd.RESTART_REQ = True
|
||||
|
||||
|
||||
def guard_top_only():
|
||||
""" Callback for change of top_only option """
|
||||
NzbQueue.do.set_top_only(cfg.top_only())
|
||||
sabnzbd.NzbQueue.set_top_only(cfg.top_only())
|
||||
|
||||
|
||||
def guard_pause_on_pp():
|
||||
@@ -512,17 +493,17 @@ def guard_pause_on_pp():
|
||||
pass # Not safe to idle downloader, because we don't know
|
||||
# if post-processing is active now
|
||||
else:
|
||||
Downloader.do.resume_from_postproc()
|
||||
sabnzbd.Downloader.resume_from_postproc()
|
||||
|
||||
|
||||
def guard_quota_size():
|
||||
""" Callback for change of quota_size """
|
||||
BPSMeter.do.change_quota()
|
||||
sabnzbd.BPSMeter.change_quota()
|
||||
|
||||
|
||||
def guard_quota_dp():
|
||||
""" Callback for change of quota_day or quota_period """
|
||||
scheduler.restart(force=True)
|
||||
sabnzbd.Scheduler.restart()
|
||||
|
||||
|
||||
def guard_language():
|
||||
@@ -532,8 +513,8 @@ def guard_language():
|
||||
|
||||
|
||||
def set_https_verification(value):
|
||||
""" Set HTTPS-verification state while returning current setting
|
||||
False = disable verification
|
||||
"""Set HTTPS-verification state while returning current setting
|
||||
False = disable verification
|
||||
"""
|
||||
prev = ssl._create_default_https_context == ssl.create_default_context
|
||||
if value:
|
||||
@@ -548,7 +529,7 @@ def guard_https_ver():
|
||||
set_https_verification(cfg.enable_https_verification())
|
||||
|
||||
|
||||
def add_url(url, pp=None, script=None, cat=None, priority=None, nzbname=None):
|
||||
def add_url(url, pp=None, script=None, cat=None, priority=None, nzbname=None, password=None):
|
||||
""" Add NZB based on a URL, attributes optional """
|
||||
if "http" not in url:
|
||||
return
|
||||
@@ -566,35 +547,39 @@ def add_url(url, pp=None, script=None, cat=None, priority=None, nzbname=None):
|
||||
msg = "%s - %s" % (nzbname, msg)
|
||||
|
||||
# Generate the placeholder
|
||||
future_nzo = NzbQueue.do.generate_future(msg, pp, script, cat, url=url, priority=priority, nzbname=nzbname)
|
||||
URLGrabber.do.add(url, future_nzo)
|
||||
future_nzo = sabnzbd.NzbQueue.generate_future(msg, pp, script, cat, url=url, priority=priority, nzbname=nzbname)
|
||||
|
||||
# Set password
|
||||
if not future_nzo.password:
|
||||
future_nzo.password = password
|
||||
|
||||
# Get it!
|
||||
sabnzbd.URLGrabber.add(url, future_nzo)
|
||||
return future_nzo.nzo_id
|
||||
|
||||
|
||||
def save_state():
|
||||
""" Save all internal bookkeeping to disk """
|
||||
ArticleCache.do.flush_articles()
|
||||
NzbQueue.do.save()
|
||||
BPSMeter.do.save()
|
||||
rss.save()
|
||||
Rating.do.save()
|
||||
DirScanner.do.save()
|
||||
PostProcessor.do.save()
|
||||
sabnzbd.ArticleCache.flush_articles()
|
||||
sabnzbd.NzbQueue.save()
|
||||
sabnzbd.BPSMeter.save()
|
||||
sabnzbd.Rating.save()
|
||||
sabnzbd.DirScanner.save()
|
||||
sabnzbd.PostProcessor.save()
|
||||
sabnzbd.RSSReader.save()
|
||||
|
||||
|
||||
def pause_all():
|
||||
""" Pause all activities than cause disk access """
|
||||
global PAUSED_ALL
|
||||
PAUSED_ALL = True
|
||||
Downloader.do.pause()
|
||||
sabnzbd.PAUSED_ALL = True
|
||||
sabnzbd.Downloader.pause()
|
||||
logging.debug("PAUSED_ALL active")
|
||||
|
||||
|
||||
def unpause_all():
|
||||
""" Resume all activities """
|
||||
global PAUSED_ALL
|
||||
PAUSED_ALL = False
|
||||
Downloader.do.resume()
|
||||
sabnzbd.PAUSED_ALL = False
|
||||
sabnzbd.Downloader.resume()
|
||||
logging.debug("PAUSED_ALL inactive")
|
||||
|
||||
|
||||
@@ -603,20 +588,20 @@ def unpause_all():
|
||||
##############################################################################
|
||||
|
||||
|
||||
def backup_exists(filename):
|
||||
def backup_exists(filename: str) -> bool:
|
||||
""" Return True if backup exists and no_dupes is set """
|
||||
path = cfg.nzb_backup_dir.get_path()
|
||||
return path and os.path.exists(os.path.join(path, filename + ".gz"))
|
||||
|
||||
|
||||
def backup_nzb(filename, data):
|
||||
def backup_nzb(filename: str, data: AnyStr):
|
||||
""" Backup NZB file """
|
||||
path = cfg.nzb_backup_dir.get_path()
|
||||
if path:
|
||||
save_compressed(path, filename, data)
|
||||
|
||||
|
||||
def save_compressed(folder, filename, data):
|
||||
def save_compressed(folder: str, filename: str, data: AnyStr):
|
||||
""" Save compressed NZB file in folder """
|
||||
if filename.endswith(".nzb"):
|
||||
filename += ".gz"
|
||||
@@ -626,7 +611,7 @@ def save_compressed(folder, filename, data):
|
||||
try:
|
||||
# Have to get around the path being put inside the tgz
|
||||
with open(os.path.join(folder, filename), "wb") as tgz_file:
|
||||
f = gzip.GzipFile(filename, fileobj=tgz_file)
|
||||
f = gzip.GzipFile(filename, fileobj=tgz_file, mode="wb")
|
||||
f.write(encoding.utob(data))
|
||||
f.flush()
|
||||
f.close()
|
||||
@@ -646,7 +631,7 @@ def add_nzbfile(
|
||||
script=None,
|
||||
cat=None,
|
||||
catdir=None,
|
||||
priority=NORMAL_PRIORITY,
|
||||
priority=DEFAULT_PRIORITY,
|
||||
nzbname=None,
|
||||
nzo_info=None,
|
||||
url=None,
|
||||
@@ -655,8 +640,8 @@ def add_nzbfile(
|
||||
password=None,
|
||||
nzo_id=None,
|
||||
):
|
||||
""" Add file, either a single NZB-file or an archive.
|
||||
All other parameters are passed to the NZO-creation.
|
||||
"""Add file, either a single NZB-file or an archive.
|
||||
All other parameters are passed to the NZO-creation.
|
||||
"""
|
||||
if pp == "-1":
|
||||
pp = None
|
||||
@@ -740,7 +725,7 @@ def enable_server(server):
|
||||
logging.warning(T("Trying to set status of non-existing server %s"), server)
|
||||
return
|
||||
config.save_config()
|
||||
Downloader.do.update_server(server, server)
|
||||
sabnzbd.Downloader.update_server(server, server)
|
||||
|
||||
|
||||
def disable_server(server):
|
||||
@@ -751,7 +736,7 @@ def disable_server(server):
|
||||
logging.warning(T("Trying to set status of non-existing server %s"), server)
|
||||
return
|
||||
config.save_config()
|
||||
Downloader.do.update_server(server, server)
|
||||
sabnzbd.Downloader.update_server(server, server)
|
||||
|
||||
|
||||
def system_shutdown():
|
||||
@@ -810,19 +795,17 @@ def restart_program():
|
||||
|
||||
|
||||
def change_queue_complete_action(action, new=True):
|
||||
""" Action or script to be performed once the queue has been completed
|
||||
Scripts are prefixed with 'script_'
|
||||
When "new" is False, check whether non-script actions are acceptable
|
||||
"""Action or script to be performed once the queue has been completed
|
||||
Scripts are prefixed with 'script_'
|
||||
When "new" is False, check whether non-script actions are acceptable
|
||||
"""
|
||||
global QUEUECOMPLETE, QUEUECOMPLETEACTION, QUEUECOMPLETEARG
|
||||
|
||||
_action = None
|
||||
_argument = None
|
||||
if "script_" in action:
|
||||
if action.startswith("script_"):
|
||||
# all scripts are labeled script_xxx
|
||||
_action = run_script
|
||||
_argument = action.replace("script_", "")
|
||||
elif new or cfg.queue_complete_pers.get():
|
||||
_argument = action.replace("script_", "", 1)
|
||||
elif new or cfg.queue_complete_pers():
|
||||
if action == "shutdown_pc":
|
||||
_action = system_shutdown
|
||||
elif action == "hibernate_pc":
|
||||
@@ -841,45 +824,30 @@ def change_queue_complete_action(action, new=True):
|
||||
config.save_config()
|
||||
|
||||
# keep the name of the action for matching the current select in queue.tmpl
|
||||
QUEUECOMPLETE = action
|
||||
QUEUECOMPLETEACTION = _action
|
||||
QUEUECOMPLETEARG = _argument
|
||||
sabnzbd.QUEUECOMPLETE = action
|
||||
sabnzbd.QUEUECOMPLETEACTION = _action
|
||||
sabnzbd.QUEUECOMPLETEARG = _argument
|
||||
|
||||
|
||||
def run_script(script):
|
||||
""" Run a user script (queue complete only) """
|
||||
command = [os.path.join(cfg.script_dir.get_path(), script)]
|
||||
if os.path.exists(command[0]):
|
||||
script_path = filesystem.make_script_path(script)
|
||||
if script_path:
|
||||
try:
|
||||
stup, need_shell, command, creationflags = sabnzbd.newsunpack.build_command(command)
|
||||
logging.info("Spawning external command %s", command)
|
||||
subprocess.Popen(
|
||||
command,
|
||||
shell=need_shell,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
startupinfo=stup,
|
||||
creationflags=creationflags,
|
||||
)
|
||||
script_output = misc.run_command([script_path])
|
||||
logging.info("Output of queue-complete script %s: \n%s", script, script_output)
|
||||
except:
|
||||
logging.debug("Failed script %s, Traceback: ", script, exc_info=True)
|
||||
|
||||
|
||||
def empty_queues():
|
||||
""" Return True if queues empty or non-existent """
|
||||
global __INITIALIZED__
|
||||
return (not __INITIALIZED__) or (PostProcessor.do.empty() and NzbQueue.do.is_empty())
|
||||
logging.info("Failed queue-complete script %s, Traceback: ", script, exc_info=True)
|
||||
|
||||
|
||||
def keep_awake():
|
||||
""" If we still have work to do, keep Windows/OSX system awake """
|
||||
""" If we still have work to do, keep Windows/macOS system awake """
|
||||
if KERNEL32 or FOUNDATION:
|
||||
if sabnzbd.cfg.keep_awake():
|
||||
ES_CONTINUOUS = 0x80000000
|
||||
ES_SYSTEM_REQUIRED = 0x00000001
|
||||
if (not Downloader.do.is_paused() and not NzbQueue.do.is_empty()) or (
|
||||
not PostProcessor.do.paused and not PostProcessor.do.empty()
|
||||
if (not sabnzbd.Downloader.is_paused() and not sabnzbd.NzbQueue.is_empty()) or (
|
||||
not sabnzbd.PostProcessor.paused and not sabnzbd.PostProcessor.empty()
|
||||
):
|
||||
if KERNEL32:
|
||||
# Set ES_SYSTEM_REQUIRED until the next call
|
||||
@@ -900,8 +868,8 @@ def keep_awake():
|
||||
|
||||
|
||||
def get_new_id(prefix, folder, check_list=None):
|
||||
""" Return unique prefixed admin identifier within folder
|
||||
optionally making sure that id is not in the check_list.
|
||||
"""Return unique prefixed admin identifier within folder
|
||||
optionally making sure that id is not in the check_list.
|
||||
"""
|
||||
for n in range(100):
|
||||
try:
|
||||
@@ -979,7 +947,7 @@ def load_data(data_id, path, remove=True, do_pickle=True, silent=False):
|
||||
return data
|
||||
|
||||
|
||||
def remove_data(_id, path):
|
||||
def remove_data(_id: str, path: str):
|
||||
""" Remove admin file """
|
||||
path = os.path.join(path, _id)
|
||||
try:
|
||||
@@ -989,13 +957,13 @@ def remove_data(_id, path):
|
||||
logging.debug("Failed to remove %s", path)
|
||||
|
||||
|
||||
def save_admin(data, data_id):
|
||||
def save_admin(data: Any, data_id: str):
|
||||
""" Save data in admin folder in specified format """
|
||||
logging.debug("[%s] Saving data for %s", misc.caller_name(), data_id)
|
||||
save_data(data, data_id, cfg.admin_dir.get_path())
|
||||
|
||||
|
||||
def load_admin(data_id, remove=False, silent=False):
|
||||
def load_admin(data_id: str, remove=False, silent=False) -> Any:
|
||||
""" Read data in admin folder in specified format """
|
||||
logging.debug("[%s] Loading data for %s", misc.caller_name(), data_id)
|
||||
return load_data(data_id, cfg.admin_dir.get_path(), remove=remove, silent=silent)
|
||||
@@ -1024,85 +992,87 @@ def check_repair_request():
|
||||
|
||||
|
||||
def check_all_tasks():
|
||||
""" Check every task and restart safe ones, else restart program
|
||||
Return True when everything is under control
|
||||
"""Check every task and restart safe ones, else restart program
|
||||
Return True when everything is under control
|
||||
"""
|
||||
if __SHUTTING_DOWN__ or not __INITIALIZED__:
|
||||
return True
|
||||
|
||||
# Non-restartable threads, require program restart
|
||||
if not sabnzbd.PostProcessor.do.is_alive():
|
||||
if not sabnzbd.PostProcessor.is_alive():
|
||||
logging.info("Restarting because of crashed postprocessor")
|
||||
return False
|
||||
if not Downloader.do.is_alive():
|
||||
if not sabnzbd.Downloader.is_alive():
|
||||
logging.info("Restarting because of crashed downloader")
|
||||
return False
|
||||
if not Decoder.do.is_alive():
|
||||
if not sabnzbd.Decoder.is_alive():
|
||||
logging.info("Restarting because of crashed decoder")
|
||||
return False
|
||||
if not Assembler.do.is_alive():
|
||||
if not sabnzbd.Assembler.is_alive():
|
||||
logging.info("Restarting because of crashed assembler")
|
||||
return False
|
||||
|
||||
# Kick the downloader, in case it missed the semaphore
|
||||
Downloader.do.wakeup()
|
||||
sabnzbd.Downloader.wakeup()
|
||||
|
||||
# Make sure the right servers are active
|
||||
Downloader.do.check_timers()
|
||||
sabnzbd.Downloader.check_timers()
|
||||
|
||||
# Restartable threads
|
||||
if not DirScanner.do.is_alive():
|
||||
if not sabnzbd.DirScanner.is_alive():
|
||||
logging.info("Restarting crashed dirscanner")
|
||||
DirScanner.do.__init__()
|
||||
if not URLGrabber.do.is_alive():
|
||||
sabnzbd.DirScanner.__init__()
|
||||
if not sabnzbd.URLGrabber.is_alive():
|
||||
logging.info("Restarting crashed urlgrabber")
|
||||
URLGrabber.do.__init__()
|
||||
if not Rating.do.is_alive():
|
||||
sabnzbd.URLGrabber.__init__()
|
||||
if not sabnzbd.Rating.is_alive():
|
||||
logging.info("Restarting crashed rating")
|
||||
Rating.do.__init__()
|
||||
if not sabnzbd.scheduler.sched_check():
|
||||
sabnzbd.Rating.__init__()
|
||||
if not sabnzbd.Scheduler.is_alive():
|
||||
logging.info("Restarting crashed scheduler")
|
||||
sabnzbd.scheduler.init()
|
||||
sabnzbd.downloader.Downloader.do.unblock_all()
|
||||
sabnzbd.Scheduler.restart()
|
||||
sabnzbd.Downloader.unblock_all()
|
||||
|
||||
# Check one-shot pause
|
||||
sabnzbd.scheduler.pause_check()
|
||||
sabnzbd.Scheduler.pause_check()
|
||||
|
||||
# Check (and terminate) idle jobs
|
||||
sabnzbd.nzbqueue.NzbQueue.do.stop_idle_jobs()
|
||||
sabnzbd.NzbQueue.stop_idle_jobs()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def pid_file(pid_path=None, pid_file=None, port=0):
|
||||
""" Create or remove pid file """
|
||||
global DIR_PID
|
||||
if not sabnzbd.WIN32:
|
||||
if pid_path and pid_path.startswith("/"):
|
||||
DIR_PID = os.path.join(pid_path, "sabnzbd-%d.pid" % port)
|
||||
sabnzbd.DIR_PID = os.path.join(pid_path, "sabnzbd-%d.pid" % port)
|
||||
elif pid_file and pid_file.startswith("/"):
|
||||
DIR_PID = pid_file
|
||||
sabnzbd.DIR_PID = pid_file
|
||||
|
||||
if DIR_PID:
|
||||
if sabnzbd.DIR_PID:
|
||||
try:
|
||||
if port:
|
||||
with open(DIR_PID, "w") as f:
|
||||
with open(sabnzbd.DIR_PID, "w") as f:
|
||||
f.write("%d\n" % os.getpid())
|
||||
else:
|
||||
filesystem.remove_file(DIR_PID)
|
||||
filesystem.remove_file(sabnzbd.DIR_PID)
|
||||
except:
|
||||
logging.warning("Cannot access PID file %s", DIR_PID)
|
||||
logging.warning(T("Cannot access PID file %s"), sabnzbd.DIR_PID)
|
||||
|
||||
|
||||
def check_incomplete_vs_complete():
|
||||
""" Make sure "incomplete" and "complete" are not identical """
|
||||
"""Make sure download_dir and complete_dir are not identical
|
||||
or that download_dir is not a subfolder of complete_dir"""
|
||||
complete = cfg.complete_dir.get_path()
|
||||
if filesystem.same_file(cfg.download_dir.get_path(), complete):
|
||||
if filesystem.real_path("X", cfg.download_dir()) == cfg.download_dir():
|
||||
# Abs path, so set an abs path too
|
||||
if filesystem.real_path("X", cfg.download_dir()) == filesystem.long_path(cfg.download_dir()):
|
||||
# Abs path, so set download_dir as an abs path inside the complete_dir
|
||||
cfg.download_dir.set(os.path.join(complete, "incomplete"))
|
||||
else:
|
||||
cfg.download_dir.set("incomplete")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def wait_for_download_folder():
|
||||
@@ -1112,18 +1082,13 @@ def wait_for_download_folder():
|
||||
time.sleep(2.0)
|
||||
|
||||
|
||||
# Required wrapper because nzbstuff.py cannot import downloader.py
|
||||
def highest_server(me):
|
||||
return sabnzbd.downloader.Downloader.do.highest_server(me)
|
||||
|
||||
|
||||
def test_ipv6():
|
||||
""" Check if external IPv6 addresses are reachable """
|
||||
if not cfg.selftest_host():
|
||||
# User disabled the test, assume active IPv6
|
||||
return True
|
||||
try:
|
||||
info = getipaddress.addresslookup6(cfg.selftest_host())
|
||||
info = sabnzbd.getipaddress.addresslookup6(cfg.selftest_host())
|
||||
except:
|
||||
logging.debug(
|
||||
"Test IPv6: Disabling IPv6, because it looks like it's not available. Reason: %s", sys.exc_info()[0]
|
||||
|
||||
373
sabnzbd/api.py
373
sabnzbd/api.py
@@ -22,13 +22,14 @@ sabnzbd.api - api
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
import gc
|
||||
import datetime
|
||||
import time
|
||||
import json
|
||||
import cherrypy
|
||||
import locale
|
||||
|
||||
from threading import Thread
|
||||
from typing import List
|
||||
|
||||
try:
|
||||
import win32api
|
||||
@@ -41,20 +42,15 @@ from sabnzbd.constants import (
|
||||
VALID_ARCHIVES,
|
||||
VALID_NZB_FILES,
|
||||
Status,
|
||||
TOP_PRIORITY,
|
||||
REPAIR_PRIORITY,
|
||||
HIGH_PRIORITY,
|
||||
FORCE_PRIORITY,
|
||||
NORMAL_PRIORITY,
|
||||
LOW_PRIORITY,
|
||||
INTERFACE_PRIORITIES,
|
||||
KIBI,
|
||||
MEBI,
|
||||
GIGI,
|
||||
)
|
||||
import sabnzbd.config as config
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.downloader import Downloader
|
||||
from sabnzbd.nzbqueue import NzbQueue
|
||||
import sabnzbd.scheduler as scheduler
|
||||
from sabnzbd.skintext import SKIN_TEXT
|
||||
from sabnzbd.utils.pathbrowser import folders_at_path
|
||||
from sabnzbd.utils.getperformance import getcpu
|
||||
@@ -68,19 +64,15 @@ from sabnzbd.misc import (
|
||||
calc_age,
|
||||
opts_to_pp,
|
||||
)
|
||||
from sabnzbd.filesystem import diskspace, get_ext, globber_full, clip_path, remove_all
|
||||
from sabnzbd.filesystem import diskspace, get_ext, globber_full, clip_path, remove_all, userxbit
|
||||
from sabnzbd.encoding import xml_name
|
||||
from sabnzbd.postproc import PostProcessor
|
||||
from sabnzbd.articlecache import ArticleCache
|
||||
from sabnzbd.utils.servertests import test_nntp_server_dict
|
||||
from sabnzbd.bpsmeter import BPSMeter
|
||||
from sabnzbd.rating import Rating
|
||||
from sabnzbd.getipaddress import localipv4, publicipv4, ipv6, addresslookup
|
||||
from sabnzbd.newsunpack import userxbit
|
||||
from sabnzbd.database import build_history_info, unpack_history_info, HistoryDB
|
||||
import sabnzbd.notifier
|
||||
import sabnzbd.rss
|
||||
import sabnzbd.emailer
|
||||
import sabnzbd.sorting
|
||||
|
||||
##############################################################################
|
||||
# API error messages
|
||||
@@ -202,12 +194,12 @@ def _api_queue(name, output, kwargs):
|
||||
def _api_queue_delete(output, value, kwargs):
|
||||
""" API: accepts output, value """
|
||||
if value.lower() == "all":
|
||||
removed = NzbQueue.do.remove_all(kwargs.get("search"))
|
||||
removed = sabnzbd.NzbQueue.remove_all(kwargs.get("search"))
|
||||
return report(output, keyword="", data={"status": bool(removed), "nzo_ids": removed})
|
||||
elif value:
|
||||
items = value.split(",")
|
||||
delete_all_data = int_conv(kwargs.get("del_files"))
|
||||
removed = NzbQueue.do.remove_multiple(items, delete_all_data=delete_all_data)
|
||||
removed = sabnzbd.NzbQueue.remove_multiple(items, delete_all_data=delete_all_data)
|
||||
return report(output, keyword="", data={"status": bool(removed), "nzo_ids": removed})
|
||||
else:
|
||||
return report(output, _MSG_NO_VALUE)
|
||||
@@ -217,7 +209,7 @@ def _api_queue_delete_nzf(output, value, kwargs):
|
||||
""" API: accepts value(=nzo_id), value2(=nzf_id) """
|
||||
value2 = kwargs.get("value2")
|
||||
if value and value2:
|
||||
removed = NzbQueue.do.remove_nzf(value, value2, force_delete=True)
|
||||
removed = sabnzbd.NzbQueue.remove_nzf(value, value2, force_delete=True)
|
||||
return report(output, keyword="", data={"status": bool(removed), "nzf_ids": removed})
|
||||
else:
|
||||
return report(output, _MSG_NO_VALUE2)
|
||||
@@ -228,7 +220,7 @@ def _api_queue_rename(output, value, kwargs):
|
||||
value2 = kwargs.get("value2")
|
||||
value3 = kwargs.get("value3")
|
||||
if value and value2:
|
||||
ret = NzbQueue.do.change_name(value, value2, value3)
|
||||
ret = sabnzbd.NzbQueue.change_name(value, value2, value3)
|
||||
return report(output, keyword="", data={"status": ret})
|
||||
else:
|
||||
return report(output, _MSG_NO_VALUE2)
|
||||
@@ -242,7 +234,7 @@ def _api_queue_change_complete_action(output, value, kwargs):
|
||||
|
||||
def _api_queue_purge(output, value, kwargs):
|
||||
""" API: accepts output """
|
||||
removed = NzbQueue.do.remove_all(kwargs.get("search"))
|
||||
removed = sabnzbd.NzbQueue.remove_all(kwargs.get("search"))
|
||||
return report(output, keyword="", data={"status": bool(removed), "nzo_ids": removed})
|
||||
|
||||
|
||||
@@ -250,7 +242,7 @@ def _api_queue_pause(output, value, kwargs):
|
||||
""" API: accepts output, value(=list of nzo_id) """
|
||||
if value:
|
||||
items = value.split(",")
|
||||
handled = NzbQueue.do.pause_multiple_nzo(items)
|
||||
handled = sabnzbd.NzbQueue.pause_multiple_nzo(items)
|
||||
else:
|
||||
handled = False
|
||||
return report(output, keyword="", data={"status": bool(handled), "nzo_ids": handled})
|
||||
@@ -260,7 +252,7 @@ def _api_queue_resume(output, value, kwargs):
|
||||
""" API: accepts output, value(=list of nzo_id) """
|
||||
if value:
|
||||
items = value.split(",")
|
||||
handled = NzbQueue.do.resume_multiple_nzo(items)
|
||||
handled = sabnzbd.NzbQueue.resume_multiple_nzo(items)
|
||||
else:
|
||||
handled = False
|
||||
return report(output, keyword="", data={"status": bool(handled), "nzo_ids": handled})
|
||||
@@ -275,7 +267,7 @@ def _api_queue_priority(output, value, kwargs):
|
||||
priority = int(value2)
|
||||
except:
|
||||
return report(output, _MSG_INT_VALUE)
|
||||
pos = NzbQueue.do.set_priority(value, priority)
|
||||
pos = sabnzbd.NzbQueue.set_priority(value, priority)
|
||||
# Returns the position in the queue, -1 is incorrect job-id
|
||||
return report(output, keyword="position", data=pos)
|
||||
except:
|
||||
@@ -289,7 +281,7 @@ def _api_queue_sort(output, value, kwargs):
|
||||
sort = kwargs.get("sort")
|
||||
direction = kwargs.get("dir", "")
|
||||
if sort:
|
||||
NzbQueue.do.sort_queue(sort, direction)
|
||||
sabnzbd.NzbQueue.sort_queue(sort, direction)
|
||||
return report(output)
|
||||
else:
|
||||
return report(output, _MSG_NO_VALUE2)
|
||||
@@ -307,13 +299,13 @@ def _api_queue_default(output, value, kwargs):
|
||||
|
||||
def _api_queue_rating(output, value, kwargs):
|
||||
""" API: accepts output, value(=nzo_id), type, setting, detail """
|
||||
vote_map = {"up": Rating.VOTE_UP, "down": Rating.VOTE_DOWN}
|
||||
vote_map = {"up": sabnzbd.Rating.VOTE_UP, "down": sabnzbd.Rating.VOTE_DOWN}
|
||||
flag_map = {
|
||||
"spam": Rating.FLAG_SPAM,
|
||||
"encrypted": Rating.FLAG_ENCRYPTED,
|
||||
"expired": Rating.FLAG_EXPIRED,
|
||||
"other": Rating.FLAG_OTHER,
|
||||
"comment": Rating.FLAG_COMMENT,
|
||||
"spam": sabnzbd.Rating.FLAG_SPAM,
|
||||
"encrypted": sabnzbd.Rating.FLAG_ENCRYPTED,
|
||||
"expired": sabnzbd.Rating.FLAG_EXPIRED,
|
||||
"other": sabnzbd.Rating.FLAG_OTHER,
|
||||
"comment": sabnzbd.Rating.FLAG_COMMENT,
|
||||
}
|
||||
content_type = kwargs.get("type")
|
||||
setting = kwargs.get("setting")
|
||||
@@ -329,7 +321,7 @@ def _api_queue_rating(output, value, kwargs):
|
||||
if content_type == "flag":
|
||||
flag = flag_map[setting]
|
||||
if cfg.rating_enable():
|
||||
Rating.do.update_user_rating(value, video, audio, vote, flag, kwargs.get("detail"))
|
||||
sabnzbd.Rating.update_user_rating(value, video, audio, vote, flag, kwargs.get("detail"))
|
||||
return report(output)
|
||||
except:
|
||||
return report(output, _MSG_BAD_SERVER_PARMS)
|
||||
@@ -350,7 +342,7 @@ def _api_translate(name, output, kwargs):
|
||||
def _api_addfile(name, output, kwargs):
|
||||
""" API: accepts name, output, pp, script, cat, priority, nzbname """
|
||||
# Normal upload will send the nzb in a kw arg called name or nzbfile
|
||||
if not name:
|
||||
if not name or isinstance(name, str):
|
||||
name = kwargs.get("nzbfile", None)
|
||||
if hasattr(name, "file") and hasattr(name, "filename") and name.filename:
|
||||
cat = kwargs.get("cat")
|
||||
@@ -366,6 +358,7 @@ def _api_addfile(name, output, kwargs):
|
||||
cat=cat,
|
||||
priority=kwargs.get("priority"),
|
||||
nzbname=kwargs.get("nzbname"),
|
||||
password=kwargs.get("password"),
|
||||
)
|
||||
return report(output, keyword="", data={"status": res == 0, "nzo_ids": nzo_ids})
|
||||
else:
|
||||
@@ -391,7 +384,7 @@ def _api_retry(name, output, kwargs):
|
||||
def _api_cancel_pp(name, output, kwargs):
|
||||
""" API: accepts name, output, value(=nzo_id) """
|
||||
nzo_id = kwargs.get("value")
|
||||
if PostProcessor.do.cancel_pp(nzo_id):
|
||||
if sabnzbd.PostProcessor.cancel_pp(nzo_id):
|
||||
return report(output, keyword="", data={"status": True, "nzo_id": nzo_id})
|
||||
else:
|
||||
return report(output, _MSG_NO_ITEM)
|
||||
@@ -410,10 +403,18 @@ def _api_addlocalfile(name, output, kwargs):
|
||||
cat = cat_convert(xcat)
|
||||
priority = kwargs.get("priority")
|
||||
nzbname = kwargs.get("nzbname")
|
||||
password = kwargs.get("password")
|
||||
|
||||
if get_ext(name) in VALID_ARCHIVES + VALID_NZB_FILES:
|
||||
res, nzo_ids = sabnzbd.add_nzbfile(
|
||||
name, pp=pp, script=script, cat=cat, priority=priority, keep=True, nzbname=nzbname
|
||||
name,
|
||||
pp=pp,
|
||||
script=script,
|
||||
cat=cat,
|
||||
priority=priority,
|
||||
keep=True,
|
||||
nzbname=nzbname,
|
||||
password=password,
|
||||
)
|
||||
return report(output, keyword="", data={"status": res == 0, "nzo_ids": nzo_ids})
|
||||
else:
|
||||
@@ -432,7 +433,7 @@ def _api_switch(name, output, kwargs):
|
||||
value = kwargs.get("value")
|
||||
value2 = kwargs.get("value2")
|
||||
if value and value2:
|
||||
pos, prio = NzbQueue.do.switch(value, value2)
|
||||
pos, prio = sabnzbd.NzbQueue.switch(value, value2)
|
||||
# Returns the new position and new priority (if different)
|
||||
return report(output, keyword="result", data={"position": pos, "priority": prio})
|
||||
else:
|
||||
@@ -448,7 +449,7 @@ def _api_change_cat(name, output, kwargs):
|
||||
cat = value2
|
||||
if cat == "None":
|
||||
cat = None
|
||||
result = NzbQueue.do.change_cat(nzo_id, cat)
|
||||
result = sabnzbd.NzbQueue.change_cat(nzo_id, cat)
|
||||
return report(output, keyword="status", data=bool(result > 0))
|
||||
else:
|
||||
return report(output, _MSG_NO_VALUE)
|
||||
@@ -463,7 +464,7 @@ def _api_change_script(name, output, kwargs):
|
||||
script = value2
|
||||
if script.lower() == "none":
|
||||
script = None
|
||||
result = NzbQueue.do.change_script(nzo_id, script)
|
||||
result = sabnzbd.NzbQueue.change_script(nzo_id, script)
|
||||
return report(output, keyword="status", data=bool(result > 0))
|
||||
else:
|
||||
return report(output, _MSG_NO_VALUE)
|
||||
@@ -475,7 +476,7 @@ def _api_change_opts(name, output, kwargs):
|
||||
value2 = kwargs.get("value2")
|
||||
result = 0
|
||||
if value and value2 and value2.isdigit():
|
||||
result = NzbQueue.do.change_opts(value, int(value2))
|
||||
result = sabnzbd.NzbQueue.change_opts(value, int(value2))
|
||||
return report(output, keyword="status", data=bool(result > 0))
|
||||
|
||||
|
||||
@@ -492,7 +493,7 @@ def _api_history(name, output, kwargs):
|
||||
limit = int_conv(kwargs.get("limit"))
|
||||
last_history_update = int_conv(kwargs.get("last_history_update", 0))
|
||||
search = kwargs.get("search")
|
||||
failed_only = kwargs.get("failed_only")
|
||||
failed_only = int_conv(kwargs.get("failed_only"))
|
||||
categories = kwargs.get("category")
|
||||
|
||||
# Do we need to send anything?
|
||||
@@ -528,7 +529,7 @@ def _api_history(name, output, kwargs):
|
||||
return report(output, _MSG_NO_VALUE)
|
||||
elif not name:
|
||||
history = {}
|
||||
grand, month, week, day = BPSMeter.do.get_sums()
|
||||
grand, month, week, day = sabnzbd.BPSMeter.get_sums()
|
||||
history["total_size"], history["month_size"], history["week_size"], history["day_size"] = (
|
||||
to_units(grand),
|
||||
to_units(month),
|
||||
@@ -536,7 +537,7 @@ def _api_history(name, output, kwargs):
|
||||
to_units(day),
|
||||
)
|
||||
history["slots"], fetched_items, history["noofslots"] = build_history(
|
||||
start=start, limit=limit, search=search, failed_only=failed_only, categories=categories, output=output
|
||||
start=start, limit=limit, search=search, failed_only=failed_only, categories=categories
|
||||
)
|
||||
history["last_history_update"] = sabnzbd.LAST_HISTORY_UPDATE
|
||||
history["version"] = sabnzbd.__version__
|
||||
@@ -561,9 +562,10 @@ def _api_addurl(name, output, kwargs):
|
||||
cat = kwargs.get("cat")
|
||||
priority = kwargs.get("priority")
|
||||
nzbname = kwargs.get("nzbname", "")
|
||||
password = kwargs.get("password", "")
|
||||
|
||||
if name:
|
||||
nzo_id = sabnzbd.add_url(name, pp, script, cat, priority, nzbname)
|
||||
nzo_id = sabnzbd.add_url(name, pp, script, cat, priority, nzbname, password)
|
||||
# Reporting a list of NZO's, for compatibility with other add-methods
|
||||
return report(output, keyword="", data={"status": True, "nzo_ids": [nzo_id]})
|
||||
else:
|
||||
@@ -573,14 +575,14 @@ def _api_addurl(name, output, kwargs):
|
||||
|
||||
def _api_pause(name, output, kwargs):
|
||||
""" API: accepts output """
|
||||
scheduler.plan_resume(0)
|
||||
Downloader.do.pause()
|
||||
sabnzbd.Scheduler.plan_resume(0)
|
||||
sabnzbd.Downloader.pause()
|
||||
return report(output)
|
||||
|
||||
|
||||
def _api_resume(name, output, kwargs):
|
||||
""" API: accepts output """
|
||||
scheduler.plan_resume(0)
|
||||
sabnzbd.Scheduler.plan_resume(0)
|
||||
sabnzbd.unpause_all()
|
||||
return report(output)
|
||||
|
||||
@@ -647,13 +649,14 @@ def _api_restart_repair(name, output, kwargs):
|
||||
""" API: accepts output """
|
||||
logging.info("Queue repair requested by API")
|
||||
sabnzbd.request_repair()
|
||||
sabnzbd.trigger_restart()
|
||||
# Do the shutdown async to still send goodbye to browser
|
||||
Thread(target=sabnzbd.trigger_restart, kwargs={"timeout": 1}).start()
|
||||
return report(output)
|
||||
|
||||
|
||||
def _api_disconnect(name, output, kwargs):
|
||||
""" API: accepts output """
|
||||
Downloader.do.disconnect()
|
||||
sabnzbd.Downloader.disconnect()
|
||||
return report(output)
|
||||
|
||||
|
||||
@@ -666,7 +669,7 @@ def _api_osx_icon(name, output, kwargs):
|
||||
|
||||
def _api_rescan(name, output, kwargs):
|
||||
""" API: accepts output """
|
||||
NzbQueue.do.scan_jobs(all_jobs=False, action=True)
|
||||
sabnzbd.NzbQueue.scan_jobs(all_jobs=False, action=True)
|
||||
return report(output)
|
||||
|
||||
|
||||
@@ -685,26 +688,26 @@ def _api_eval_sort(name, output, kwargs):
|
||||
|
||||
def _api_watched_now(name, output, kwargs):
|
||||
""" API: accepts output """
|
||||
sabnzbd.dirscanner.dirscan()
|
||||
sabnzbd.DirScanner.scan()
|
||||
return report(output)
|
||||
|
||||
|
||||
def _api_resume_pp(name, output, kwargs):
|
||||
""" API: accepts output """
|
||||
PostProcessor.do.paused = False
|
||||
sabnzbd.PostProcessor.paused = False
|
||||
return report(output)
|
||||
|
||||
|
||||
def _api_pause_pp(name, output, kwargs):
|
||||
""" API: accepts output """
|
||||
PostProcessor.do.paused = True
|
||||
sabnzbd.PostProcessor.paused = True
|
||||
return report(output)
|
||||
|
||||
|
||||
def _api_rss_now(name, output, kwargs):
|
||||
""" API: accepts output """
|
||||
# Run RSS scan async, because it can take a long time
|
||||
scheduler.force_rss()
|
||||
sabnzbd.Scheduler.force_rss()
|
||||
return report(output)
|
||||
|
||||
|
||||
@@ -715,7 +718,8 @@ def _api_retry_all(name, output, kwargs):
|
||||
|
||||
def _api_reset_quota(name, output, kwargs):
|
||||
""" Reset quota left """
|
||||
BPSMeter.do.reset_quota(force=True)
|
||||
sabnzbd.BPSMeter.reset_quota(force=True)
|
||||
return report(output)
|
||||
|
||||
|
||||
def _api_test_email(name, output, kwargs):
|
||||
@@ -820,13 +824,13 @@ def _api_config_speedlimit(output, kwargs):
|
||||
value = kwargs.get("value")
|
||||
if not value:
|
||||
value = "0"
|
||||
Downloader.do.limit_speed(value)
|
||||
sabnzbd.Downloader.limit_speed(value)
|
||||
return report(output)
|
||||
|
||||
|
||||
def _api_config_get_speedlimit(output, kwargs):
|
||||
""" API: accepts output """
|
||||
return report(output, keyword="speedlimit", data=Downloader.do.get_limit())
|
||||
return report(output, keyword="speedlimit", data=sabnzbd.Downloader.get_limit())
|
||||
|
||||
|
||||
def _api_config_set_colorscheme(output, kwargs):
|
||||
@@ -842,7 +846,7 @@ def _api_config_set_colorscheme(output, kwargs):
|
||||
def _api_config_set_pause(output, kwargs):
|
||||
""" API: accepts output, value(=pause interval) """
|
||||
value = kwargs.get("value")
|
||||
scheduler.plan_resume(int_conv(value))
|
||||
sabnzbd.Scheduler.plan_resume(int_conv(value))
|
||||
return report(output)
|
||||
|
||||
|
||||
@@ -891,16 +895,24 @@ def _api_config_undefined(output, kwargs):
|
||||
|
||||
def _api_server_stats(name, output, kwargs):
|
||||
""" API: accepts output """
|
||||
sum_t, sum_m, sum_w, sum_d = BPSMeter.do.get_sums()
|
||||
sum_t, sum_m, sum_w, sum_d = sabnzbd.BPSMeter.get_sums()
|
||||
stats = {"total": sum_t, "month": sum_m, "week": sum_w, "day": sum_d, "servers": {}}
|
||||
|
||||
for svr in config.get_servers():
|
||||
t, m, w, d, daily = BPSMeter.do.amounts(svr)
|
||||
t, m, w, d, daily = sabnzbd.BPSMeter.amounts(svr)
|
||||
stats["servers"][svr] = {"total": t or 0, "month": m or 0, "week": w or 0, "day": d or 0, "daily": daily or {}}
|
||||
|
||||
return report(output, keyword="", data=stats)
|
||||
|
||||
|
||||
def _api_gc_stats(name, output, kwargs):
|
||||
"""Function only intended for internal testing of the memory handling"""
|
||||
# Collect before we check
|
||||
gc.collect()
|
||||
# We cannot create any lists/dicts, as they would create a reference
|
||||
return report(output, data=[str(obj) for obj in gc.get_objects() if isinstance(obj, sabnzbd.nzbstuff.TryList)])
|
||||
|
||||
|
||||
##############################################################################
|
||||
_api_table = {
|
||||
"server_stats": (_api_server_stats, 2),
|
||||
@@ -937,6 +949,7 @@ _api_table = {
|
||||
"restart_repair": (_api_restart_repair, 2),
|
||||
"disconnect": (_api_disconnect, 2),
|
||||
"osx_icon": (_api_osx_icon, 3),
|
||||
"gc_stats": (_api_gc_stats, 3),
|
||||
"rescan": (_api_rescan, 2),
|
||||
"eval_sort": (_api_eval_sort, 2),
|
||||
"watched_now": (_api_watched_now, 2),
|
||||
@@ -994,10 +1007,10 @@ def api_level(cmd, name):
|
||||
|
||||
|
||||
def report(output, error=None, keyword="value", data=None):
|
||||
""" Report message in json, xml or plain text
|
||||
If error is set, only an status/error report is made.
|
||||
If no error and no data, only a status report is made.
|
||||
Else, a data report is made (optional 'keyword' for outer XML section).
|
||||
"""Report message in json, xml or plain text
|
||||
If error is set, only an status/error report is made.
|
||||
If no error and no data, only a status report is made.
|
||||
Else, a data report is made (optional 'keyword' for outer XML section).
|
||||
"""
|
||||
if output == "json":
|
||||
content = "application/json;charset=UTF-8"
|
||||
@@ -1041,10 +1054,10 @@ def report(output, error=None, keyword="value", data=None):
|
||||
|
||||
|
||||
class xml_factory:
|
||||
""" Recursive xml string maker. Feed it a mixed tuple/dict/item object and will output into an xml string
|
||||
Current limitations:
|
||||
In Two tiered lists hard-coded name of "item": <cat_list><item> </item></cat_list>
|
||||
In Three tiered lists hard-coded name of "slot": <tier1><slot><tier2> </tier2></slot></tier1>
|
||||
"""Recursive xml string maker. Feed it a mixed tuple/dict/item object and will output into an xml string
|
||||
Current limitations:
|
||||
In Two tiered lists hard-coded name of "item": <cat_list><item> </item></cat_list>
|
||||
In Three tiered lists hard-coded name of "slot": <tier1><slot><tier2> </tier2></slot></tier1>
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
@@ -1112,7 +1125,7 @@ def handle_server_api(output, kwargs):
|
||||
else:
|
||||
config.ConfigServer(name, kwargs)
|
||||
old_name = None
|
||||
Downloader.do.update_server(old_name, name)
|
||||
sabnzbd.Downloader.update_server(old_name, name)
|
||||
return name
|
||||
|
||||
|
||||
@@ -1173,7 +1186,7 @@ def build_status(skip_dashboard=False, output=None):
|
||||
info["logfile"] = sabnzbd.LOGFILE
|
||||
info["weblogfile"] = sabnzbd.WEBLOGFILE
|
||||
info["loglevel"] = str(cfg.log_level())
|
||||
info["folders"] = NzbQueue.do.scan_jobs(all_jobs=False, action=False)
|
||||
info["folders"] = sabnzbd.NzbQueue.scan_jobs(all_jobs=False, action=False)
|
||||
info["configfn"] = config.get_filename()
|
||||
|
||||
# Dashboard: Speed of System
|
||||
@@ -1204,7 +1217,7 @@ def build_status(skip_dashboard=False, output=None):
|
||||
info["dnslookup"] = None
|
||||
|
||||
info["servers"] = []
|
||||
servers = sorted(Downloader.do.servers[:], key=lambda svr: "%02d%s" % (svr.priority, svr.displayname.lower()))
|
||||
servers = sorted(sabnzbd.Downloader.servers[:], key=lambda svr: "%02d%s" % (svr.priority, svr.displayname.lower()))
|
||||
for server in servers:
|
||||
serverconnections = []
|
||||
connected = 0
|
||||
@@ -1289,13 +1302,6 @@ def build_queue(start=0, limit=0, trans=False, output=None, search=None):
|
||||
)
|
||||
|
||||
datestart = datetime.datetime.now()
|
||||
priorities = {
|
||||
TOP_PRIORITY: "Force",
|
||||
REPAIR_PRIORITY: "Repair",
|
||||
HIGH_PRIORITY: "High",
|
||||
NORMAL_PRIORITY: "Normal",
|
||||
LOW_PRIORITY: "Low",
|
||||
}
|
||||
limit = int_conv(limit)
|
||||
start = int_conv(start)
|
||||
|
||||
@@ -1326,7 +1332,7 @@ def build_queue(start=0, limit=0, trans=False, output=None, search=None):
|
||||
slot["index"] = n
|
||||
slot["nzo_id"] = str(nzo_id)
|
||||
slot["unpackopts"] = str(opts_to_pp(pnfo.repair, pnfo.unpack, pnfo.delete))
|
||||
slot["priority"] = priorities[priority] if priority >= LOW_PRIORITY else priorities[NORMAL_PRIORITY]
|
||||
slot["priority"] = INTERFACE_PRIORITIES.get(priority, NORMAL_PRIORITY)
|
||||
slot["script"] = pnfo.script if pnfo.script else "None"
|
||||
slot["filename"] = pnfo.filename
|
||||
slot["labels"] = pnfo.labels
|
||||
@@ -1334,8 +1340,8 @@ def build_queue(start=0, limit=0, trans=False, output=None, search=None):
|
||||
slot["cat"] = pnfo.category if pnfo.category else "None"
|
||||
slot["mbleft"] = "%.2f" % mbleft
|
||||
slot["mb"] = "%.2f" % mb
|
||||
slot["size"] = format_bytes(bytes_total)
|
||||
slot["sizeleft"] = format_bytes(bytesleft)
|
||||
slot["size"] = to_units(bytes_total, "B")
|
||||
slot["sizeleft"] = to_units(bytesleft, "B")
|
||||
slot["percentage"] = "%s" % (int(((mb - mbleft) / mb) * 100)) if mb != mbleft else "0"
|
||||
slot["mbmissing"] = "%.2f" % (pnfo.bytes_missing / MEBI)
|
||||
slot["direct_unpack"] = pnfo.direct_unpack
|
||||
@@ -1343,7 +1349,7 @@ def build_queue(start=0, limit=0, trans=False, output=None, search=None):
|
||||
slot["mb_fmt"] = locale.format_string("%d", int(mb), True)
|
||||
slot["mbdone_fmt"] = locale.format_string("%d", int(mb - mbleft), True)
|
||||
|
||||
if not Downloader.do.paused and status not in (Status.PAUSED, Status.FETCHING, Status.GRABBING):
|
||||
if not sabnzbd.Downloader.paused and status not in (Status.PAUSED, Status.FETCHING, Status.GRABBING):
|
||||
if is_propagating:
|
||||
slot["status"] = Status.PROP
|
||||
elif status == Status.CHECKING:
|
||||
@@ -1352,16 +1358,16 @@ def build_queue(start=0, limit=0, trans=False, output=None, search=None):
|
||||
slot["status"] = Status.DOWNLOADING
|
||||
else:
|
||||
# Ensure compatibility of API status
|
||||
if status == Status.DELETED or priority == TOP_PRIORITY:
|
||||
if status == Status.DELETED or priority == FORCE_PRIORITY:
|
||||
status = Status.DOWNLOADING
|
||||
slot["status"] = "%s" % status
|
||||
|
||||
if (
|
||||
Downloader.do.paused
|
||||
or Downloader.do.postproc
|
||||
sabnzbd.Downloader.paused
|
||||
or sabnzbd.Downloader.postproc
|
||||
or is_propagating
|
||||
or status not in (Status.DOWNLOADING, Status.FETCHING, Status.QUEUED)
|
||||
) and priority != TOP_PRIORITY:
|
||||
) and priority != FORCE_PRIORITY:
|
||||
slot["timeleft"] = "0:00:00"
|
||||
slot["eta"] = "unknown"
|
||||
else:
|
||||
@@ -1381,7 +1387,7 @@ def build_queue(start=0, limit=0, trans=False, output=None, search=None):
|
||||
else:
|
||||
slot["avg_age"] = calc_age(average_date, bool(trans))
|
||||
|
||||
rating = Rating.do.get_rating_by_nzo(nzo_id)
|
||||
rating = sabnzbd.Rating.get_rating_by_nzo(nzo_id)
|
||||
slot["has_rating"] = rating is not None
|
||||
if rating:
|
||||
slot["rating_avg_video"] = rating.avg_video
|
||||
@@ -1400,18 +1406,17 @@ def build_queue(start=0, limit=0, trans=False, output=None, search=None):
|
||||
|
||||
def fast_queue():
|
||||
""" Return paused, bytes_left, bpsnow, time_left """
|
||||
bytes_left = NzbQueue.do.remaining()
|
||||
paused = Downloader.do.paused
|
||||
bpsnow = BPSMeter.do.bps
|
||||
bytes_left = sabnzbd.sabnzbd.NzbQueue.remaining()
|
||||
paused = sabnzbd.Downloader.paused
|
||||
bpsnow = sabnzbd.BPSMeter.bps
|
||||
time_left = calc_timeleft(bytes_left, bpsnow)
|
||||
return paused, bytes_left, bpsnow, time_left
|
||||
|
||||
|
||||
def build_file_list(nzo_id):
|
||||
""" Build file lists for specified job
|
||||
"""
|
||||
def build_file_list(nzo_id: str):
|
||||
"""Build file lists for specified job"""
|
||||
jobs = []
|
||||
nzo = NzbQueue.do.get_nzo(nzo_id)
|
||||
nzo = sabnzbd.sabnzbd.NzbQueue.get_nzo(nzo_id)
|
||||
if nzo:
|
||||
pnfo = nzo.gather_info(full=True)
|
||||
|
||||
@@ -1488,7 +1493,7 @@ def retry_job(job, new_nzb=None, password=None):
|
||||
nzo_id = sabnzbd.add_url(url, pp, script, cat)
|
||||
else:
|
||||
path = history_db.get_path(job)
|
||||
nzo_id = NzbQueue.do.repair_job(path, new_nzb, password)
|
||||
nzo_id = sabnzbd.NzbQueue.repair_job(path, new_nzb, password)
|
||||
if nzo_id:
|
||||
# Only remove from history if we repaired something
|
||||
history_db.remove_history(job)
|
||||
@@ -1517,9 +1522,9 @@ def del_job_files(job_paths):
|
||||
def del_hist_job(job, del_files):
|
||||
""" Remove history element """
|
||||
if job:
|
||||
path = PostProcessor.do.get_path(job)
|
||||
path = sabnzbd.PostProcessor.get_path(job)
|
||||
if path:
|
||||
PostProcessor.do.delete(job, del_files=del_files)
|
||||
sabnzbd.PostProcessor.delete(job, del_files=del_files)
|
||||
else:
|
||||
history_db = sabnzbd.get_db_connection()
|
||||
remove_all(history_db.get_path(job), recursive=True)
|
||||
@@ -1540,9 +1545,9 @@ _SKIN_CACHE = {} # Stores pre-translated acronyms
|
||||
|
||||
|
||||
def Ttemplate(txt):
|
||||
""" Translation function for Skin texts
|
||||
This special is to be used in interface.py for template processing
|
||||
to be passed for the $T function: so { ..., 'T' : Ttemplate, ...}
|
||||
"""Translation function for Skin texts
|
||||
This special is to be used in interface.py for template processing
|
||||
to be passed for the $T function: so { ..., 'T' : Ttemplate, ...}
|
||||
"""
|
||||
global _SKIN_CACHE
|
||||
if txt in _SKIN_CACHE:
|
||||
@@ -1569,10 +1574,10 @@ def build_header(webdir="", output=None, trans_functions=True):
|
||||
except:
|
||||
uptime = "-"
|
||||
|
||||
speed_limit = Downloader.do.get_limit()
|
||||
speed_limit = sabnzbd.Downloader.get_limit()
|
||||
if speed_limit <= 0:
|
||||
speed_limit = 100
|
||||
speed_limit_abs = Downloader.do.get_limit_abs()
|
||||
speed_limit_abs = sabnzbd.Downloader.get_limit_abs()
|
||||
if speed_limit_abs <= 0:
|
||||
speed_limit_abs = ""
|
||||
|
||||
@@ -1604,14 +1609,14 @@ def build_header(webdir="", output=None, trans_functions=True):
|
||||
header["darwin"] = sabnzbd.DARWIN
|
||||
|
||||
header["power_options"] = sabnzbd.WIN32 or sabnzbd.DARWIN or sabnzbd.LINUX_POWER
|
||||
header["pp_pause_event"] = sabnzbd.scheduler.pp_pause_event()
|
||||
header["pp_pause_event"] = sabnzbd.Scheduler.pp_pause_event
|
||||
|
||||
header["apikey"] = cfg.api_key()
|
||||
header["new_release"], header["new_rel_url"] = sabnzbd.NEW_VERSION
|
||||
|
||||
header["version"] = sabnzbd.__version__
|
||||
header["paused"] = Downloader.do.paused or Downloader.do.postproc
|
||||
header["pause_int"] = scheduler.pause_int()
|
||||
header["paused"] = bool(sabnzbd.Downloader.paused or sabnzbd.Downloader.postproc)
|
||||
header["pause_int"] = sabnzbd.Scheduler.pause_int()
|
||||
header["paused_all"] = sabnzbd.PAUSED_ALL
|
||||
|
||||
header["diskspace1"] = "%.2f" % diskspace_info["download_dir"][1]
|
||||
@@ -1627,13 +1632,13 @@ def build_header(webdir="", output=None, trans_functions=True):
|
||||
header["have_warnings"] = str(sabnzbd.GUIHANDLER.count())
|
||||
header["finishaction"] = sabnzbd.QUEUECOMPLETE
|
||||
|
||||
header["quota"] = to_units(BPSMeter.do.quota)
|
||||
header["have_quota"] = bool(BPSMeter.do.quota > 0.0)
|
||||
header["left_quota"] = to_units(BPSMeter.do.left)
|
||||
header["quota"] = to_units(sabnzbd.BPSMeter.quota)
|
||||
header["have_quota"] = bool(sabnzbd.BPSMeter.quota > 0.0)
|
||||
header["left_quota"] = to_units(sabnzbd.BPSMeter.left)
|
||||
|
||||
anfo = ArticleCache.do.cache_info()
|
||||
anfo = sabnzbd.ArticleCache.cache_info()
|
||||
header["cache_art"] = str(anfo.article_sum)
|
||||
header["cache_size"] = format_bytes(anfo.cache_size)
|
||||
header["cache_size"] = to_units(anfo.cache_size, "B")
|
||||
header["cache_max"] = str(anfo.cache_limit)
|
||||
|
||||
return header
|
||||
@@ -1644,8 +1649,8 @@ def build_queue_header(search=None, start=0, limit=0, output=None):
|
||||
|
||||
header = build_header(output=output)
|
||||
|
||||
bytespersec = BPSMeter.do.bps
|
||||
qnfo = NzbQueue.do.queue_info(search=search, start=start, limit=limit)
|
||||
bytespersec = sabnzbd.BPSMeter.bps
|
||||
qnfo = sabnzbd.NzbQueue.queue_info(search=search, start=start, limit=limit)
|
||||
|
||||
bytesleft = qnfo.bytes_left
|
||||
bytes_total = qnfo.bytes
|
||||
@@ -1654,11 +1659,11 @@ def build_queue_header(search=None, start=0, limit=0, output=None):
|
||||
header["speed"] = to_units(bytespersec)
|
||||
header["mbleft"] = "%.2f" % (bytesleft / MEBI)
|
||||
header["mb"] = "%.2f" % (bytes_total / MEBI)
|
||||
header["sizeleft"] = format_bytes(bytesleft)
|
||||
header["size"] = format_bytes(bytes_total)
|
||||
header["sizeleft"] = to_units(bytesleft, "B")
|
||||
header["size"] = to_units(bytes_total, "B")
|
||||
header["noofslots_total"] = qnfo.q_fullsize
|
||||
|
||||
if Downloader.do.paused or Downloader.do.postproc:
|
||||
if sabnzbd.Downloader.paused or sabnzbd.Downloader.postproc:
|
||||
status = Status.PAUSED
|
||||
elif bytespersec > 0:
|
||||
status = Status.DOWNLOADING
|
||||
@@ -1677,50 +1682,48 @@ def build_queue_header(search=None, start=0, limit=0, output=None):
|
||||
return header, qnfo.list, bytespersec, qnfo.q_fullsize, qnfo.bytes_left_previous_page
|
||||
|
||||
|
||||
def build_history(start=None, limit=None, search=None, failed_only=0, categories=None, output=None):
|
||||
limit = int_conv(limit)
|
||||
def build_history(start=0, limit=0, search=None, failed_only=0, categories=None):
|
||||
"""Combine the jobs still in post-processing and the database history"""
|
||||
if not limit:
|
||||
limit = 1000000
|
||||
start = int_conv(start)
|
||||
failed_only = int_conv(failed_only)
|
||||
|
||||
def matches_search(text, search_text):
|
||||
# Replace * with .* and ' ' with .
|
||||
search_text = search_text.strip().replace("*", ".*").replace(" ", ".*") + ".*?"
|
||||
try:
|
||||
re_search = re.compile(search_text, re.I)
|
||||
except:
|
||||
logging.error(T("Failed to compile regex for search term: %s"), search_text)
|
||||
return False
|
||||
return re_search.search(text)
|
||||
|
||||
# Grab any items that are active or queued in postproc
|
||||
queue = PostProcessor.do.get_queue()
|
||||
postproc_queue = sabnzbd.PostProcessor.get_queue()
|
||||
|
||||
# Filter out any items that don't match the search
|
||||
if search:
|
||||
queue = [nzo for nzo in queue if matches_search(nzo.final_name, search)]
|
||||
# Filter out any items that don't match the search term or category
|
||||
if postproc_queue:
|
||||
# It would be more efficient to iterate only once, but we accept the penalty for code clarity
|
||||
if isinstance(search, list):
|
||||
postproc_queue = [nzo for nzo in postproc_queue if nzo.cat in categories]
|
||||
|
||||
if isinstance(search, str):
|
||||
# Replace * with .* and ' ' with .
|
||||
search_text = search.strip().replace("*", ".*").replace(" ", ".*") + ".*?"
|
||||
try:
|
||||
re_search = re.compile(search_text, re.I)
|
||||
postproc_queue = [nzo for nzo in postproc_queue if re_search.search(nzo.final_name)]
|
||||
except:
|
||||
logging.error(T("Failed to compile regex for search term: %s"), search_text)
|
||||
|
||||
# Multi-page support for postproc items
|
||||
full_queue_size = len(queue)
|
||||
if start > full_queue_size:
|
||||
postproc_queue_size = len(postproc_queue)
|
||||
if start > postproc_queue_size:
|
||||
# On a page where we shouldn't show postproc items
|
||||
queue = []
|
||||
h_limit = limit
|
||||
postproc_queue = []
|
||||
database_history_limit = limit
|
||||
else:
|
||||
try:
|
||||
if limit:
|
||||
queue = queue[start : start + limit]
|
||||
postproc_queue = postproc_queue[start : start + limit]
|
||||
else:
|
||||
queue = queue[start:]
|
||||
postproc_queue = postproc_queue[start:]
|
||||
except:
|
||||
pass
|
||||
# Remove the amount of postproc items from the db request for history items
|
||||
h_limit = max(limit - len(queue), 0)
|
||||
database_history_limit = max(limit - len(postproc_queue), 0)
|
||||
database_history_start = max(start - postproc_queue_size, 0)
|
||||
|
||||
h_start = max(start - full_queue_size, 0)
|
||||
|
||||
# Aquire the db instance
|
||||
# Acquire the db instance
|
||||
try:
|
||||
history_db = sabnzbd.get_db_connection()
|
||||
close_db = False
|
||||
@@ -1730,50 +1733,53 @@ def build_history(start=None, limit=None, search=None, failed_only=0, categories
|
||||
close_db = True
|
||||
|
||||
# Fetch history items
|
||||
if not h_limit:
|
||||
items, fetched_items, total_items = history_db.fetch_history(h_start, 1, search, failed_only, categories)
|
||||
if not database_history_limit:
|
||||
items, fetched_items, total_items = history_db.fetch_history(
|
||||
database_history_start, 1, search, failed_only, categories
|
||||
)
|
||||
items = []
|
||||
else:
|
||||
items, fetched_items, total_items = history_db.fetch_history(h_start, h_limit, search, failed_only, categories)
|
||||
items, fetched_items, total_items = history_db.fetch_history(
|
||||
database_history_start, database_history_limit, search, failed_only, categories
|
||||
)
|
||||
|
||||
# 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(queue, items)
|
||||
items = get_active_history(postproc_queue, items)
|
||||
|
||||
# Unreverse the queue
|
||||
# Un-reverse the queue
|
||||
items.reverse()
|
||||
|
||||
# Global check if rating is enabled
|
||||
rating_enabled = cfg.rating_enable()
|
||||
|
||||
for item in items:
|
||||
item["size"] = format_bytes(item["bytes"])
|
||||
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
|
||||
|
||||
if Rating.do:
|
||||
rating = Rating.do.get_rating_by_nzo(item["nzo_id"])
|
||||
else:
|
||||
rating = None
|
||||
if rating_enabled:
|
||||
rating = sabnzbd.Rating.get_rating_by_nzo(item["nzo_id"])
|
||||
item["has_rating"] = rating is not None
|
||||
if rating:
|
||||
item["rating_avg_video"] = rating.avg_video
|
||||
item["rating_avg_audio"] = rating.avg_audio
|
||||
item["rating_avg_vote_up"] = rating.avg_vote_up
|
||||
item["rating_avg_vote_down"] = rating.avg_vote_down
|
||||
item["rating_user_video"] = rating.user_video
|
||||
item["rating_user_audio"] = rating.user_audio
|
||||
item["rating_user_vote"] = rating.user_vote
|
||||
|
||||
item["has_rating"] = rating is not None
|
||||
if rating:
|
||||
item["rating_avg_video"] = rating.avg_video
|
||||
item["rating_avg_audio"] = rating.avg_audio
|
||||
item["rating_avg_vote_up"] = rating.avg_vote_up
|
||||
item["rating_avg_vote_down"] = rating.avg_vote_down
|
||||
item["rating_user_video"] = rating.user_video
|
||||
item["rating_user_audio"] = rating.user_audio
|
||||
item["rating_user_vote"] = rating.user_vote
|
||||
|
||||
total_items += full_queue_size
|
||||
total_items += postproc_queue_size
|
||||
fetched_items = len(items)
|
||||
|
||||
if close_db:
|
||||
@@ -1782,15 +1788,9 @@ def build_history(start=None, limit=None, search=None, failed_only=0, categories
|
||||
return items, fetched_items, total_items
|
||||
|
||||
|
||||
def get_active_history(queue=None, items=None):
|
||||
def get_active_history(queue, items):
|
||||
""" Get the currently in progress and active history queue. """
|
||||
if items is None:
|
||||
items = []
|
||||
if queue is None:
|
||||
queue = PostProcessor.do.get_queue()
|
||||
|
||||
for nzo in queue:
|
||||
history = build_history_info(nzo)
|
||||
item = {}
|
||||
(
|
||||
item["completed"],
|
||||
@@ -1811,20 +1811,19 @@ def get_active_history(queue=None, items=None):
|
||||
item["postproc_time"],
|
||||
item["stage_log"],
|
||||
item["downloaded"],
|
||||
item["completeness"],
|
||||
item["fail_message"],
|
||||
item["url_info"],
|
||||
item["bytes"],
|
||||
_,
|
||||
_,
|
||||
item["password"],
|
||||
) = history
|
||||
) = build_history_info(nzo)
|
||||
item["action_line"] = nzo.action_line
|
||||
item = unpack_history_info(item)
|
||||
|
||||
item["loaded"] = nzo.pp_active
|
||||
if item["bytes"]:
|
||||
item["size"] = format_bytes(item["bytes"])
|
||||
item["size"] = to_units(item["bytes"], "B")
|
||||
else:
|
||||
item["size"] = ""
|
||||
items.append(item)
|
||||
@@ -1832,14 +1831,6 @@ def get_active_history(queue=None, items=None):
|
||||
return items
|
||||
|
||||
|
||||
def format_bytes(bytes_string):
|
||||
b = to_units(bytes_string)
|
||||
if b == "":
|
||||
return b
|
||||
else:
|
||||
return b + "B"
|
||||
|
||||
|
||||
def calc_timeleft(bytesleft, bps):
|
||||
""" Calculate the time left in the format HH:MM:SS """
|
||||
try:
|
||||
@@ -1863,7 +1854,7 @@ def calc_timeleft(bytesleft, bps):
|
||||
return "0:00:00"
|
||||
|
||||
|
||||
def list_scripts(default=False, none=True):
|
||||
def list_scripts(default: bool = False, none: bool = True) -> List[str]:
|
||||
""" Return a list of script names, optionally with 'Default' added """
|
||||
lst = []
|
||||
path = cfg.script_dir.get_path()
|
||||
@@ -1880,6 +1871,8 @@ def list_scripts(default=False, none=True):
|
||||
or (not sabnzbd.WIN32 and userxbit(script) and not os.path.basename(script).startswith("."))
|
||||
):
|
||||
lst.append(os.path.basename(script))
|
||||
# Make sure capitalization is ignored to avoid strange results
|
||||
lst = sorted(lst, key=str.casefold)
|
||||
if none:
|
||||
lst.insert(0, "None")
|
||||
if default:
|
||||
@@ -1888,8 +1881,8 @@ def list_scripts(default=False, none=True):
|
||||
|
||||
|
||||
def list_cats(default=True):
|
||||
""" Return list of (ordered) categories,
|
||||
when default==False use '*' for Default category
|
||||
"""Return list of (ordered) categories,
|
||||
when default==False use '*' for Default category
|
||||
"""
|
||||
lst = [cat["name"] for cat in config.get_ordered_categories()]
|
||||
if default:
|
||||
@@ -1928,7 +1921,7 @@ def del_from_section(kwargs):
|
||||
del item
|
||||
config.save_config()
|
||||
if section == "servers":
|
||||
Downloader.do.update_server(keyword, None)
|
||||
sabnzbd.Downloader.update_server(keyword, None)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -1937,15 +1930,13 @@ def del_from_section(kwargs):
|
||||
def history_remove_failed():
|
||||
""" Remove all failed jobs from history, including files """
|
||||
logging.info("Scheduled removal of all failed jobs")
|
||||
history_db = HistoryDB()
|
||||
del_job_files(history_db.get_failed_paths())
|
||||
history_db.remove_failed()
|
||||
history_db.close()
|
||||
with HistoryDB() as history_db:
|
||||
del_job_files(history_db.get_failed_paths())
|
||||
history_db.remove_failed()
|
||||
|
||||
|
||||
def history_remove_completed():
|
||||
""" Remove all completed jobs from history """
|
||||
logging.info("Scheduled removal of all completed jobs")
|
||||
history_db = HistoryDB()
|
||||
history_db.remove_completed()
|
||||
history_db.close()
|
||||
with HistoryDB() as history_db:
|
||||
history_db.remove_completed()
|
||||
|
||||
@@ -22,25 +22,24 @@ sabnzbd.articlecache - Article cache handling
|
||||
import logging
|
||||
import threading
|
||||
import struct
|
||||
from typing import Dict, List
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.decorators import synchronized
|
||||
from sabnzbd.constants import GIGI, ANFO, MEBI, LIMIT_DECODE_QUEUE, MIN_DECODE_QUEUE
|
||||
from sabnzbd.nzbstuff import Article
|
||||
|
||||
# Operations on lists and dicts are atomic, but for
|
||||
# the bytes counter we do need a lock
|
||||
ARTICLE_LOCK = threading.Lock()
|
||||
# Operations on the article table are handled via try/except.
|
||||
# The counters need to be made atomic to ensure consistency.
|
||||
ARTICLE_COUNTER_LOCK = threading.RLock()
|
||||
|
||||
|
||||
class ArticleCache:
|
||||
do = None
|
||||
|
||||
def __init__(self):
|
||||
self.__cache_limit_org = 0
|
||||
self.__cache_limit = 0
|
||||
self.__cache_size = 0
|
||||
self.__article_list = [] # List of buffered articles
|
||||
self.__article_table = {} # Dict of buffered articles
|
||||
self.__article_table: Dict[Article, bytes] = {} # Dict of buffered articles
|
||||
|
||||
# Limit for the decoder is based on the total available cache
|
||||
# so it can be larger on memory-rich systems
|
||||
@@ -52,12 +51,10 @@ class ArticleCache:
|
||||
if sabnzbd.DARWIN or sabnzbd.WIN64 or (struct.calcsize("P") * 8) == 64:
|
||||
self.__cache_upper_limit = 4 * GIGI
|
||||
|
||||
ArticleCache.do = self
|
||||
|
||||
def cache_info(self):
|
||||
return ANFO(len(self.__article_list), abs(self.__cache_size), self.__cache_limit_org)
|
||||
return ANFO(len(self.__article_table), abs(self.__cache_size), self.__cache_limit_org)
|
||||
|
||||
def new_limit(self, limit):
|
||||
def new_limit(self, limit: int):
|
||||
""" Called when cache limit changes """
|
||||
self.__cache_limit_org = limit
|
||||
if limit < 0:
|
||||
@@ -71,21 +68,21 @@ class ArticleCache:
|
||||
# The cache should also not be too small
|
||||
self.decoder_cache_article_limit = max(decoder_cache_limit, MIN_DECODE_QUEUE)
|
||||
|
||||
@synchronized(ARTICLE_LOCK)
|
||||
def reserve_space(self, data_size):
|
||||
@synchronized(ARTICLE_COUNTER_LOCK)
|
||||
def reserve_space(self, data_size: int):
|
||||
""" Reserve space in the cache """
|
||||
self.__cache_size += data_size
|
||||
|
||||
@synchronized(ARTICLE_LOCK)
|
||||
def free_reserved_space(self, data_size):
|
||||
@synchronized(ARTICLE_COUNTER_LOCK)
|
||||
def free_reserved_space(self, data_size: int):
|
||||
""" Remove previously reserved space """
|
||||
self.__cache_size -= data_size
|
||||
|
||||
def space_left(self):
|
||||
def space_left(self) -> bool:
|
||||
""" Is there space left in the set limit? """
|
||||
return self.__cache_size < self.__cache_limit
|
||||
|
||||
def save_article(self, article, data):
|
||||
def save_article(self, article: Article, data: bytes):
|
||||
""" Save article in cache, either memory or disk """
|
||||
nzo = article.nzf.nzo
|
||||
if nzo.is_gone():
|
||||
@@ -108,7 +105,6 @@ class ArticleCache:
|
||||
self.reserve_space(data_size)
|
||||
if self.space_left():
|
||||
# Add new article to the cache
|
||||
self.__article_list.append(article)
|
||||
self.__article_table[article] = data
|
||||
else:
|
||||
# Return the space and save to disk
|
||||
@@ -118,40 +114,52 @@ class ArticleCache:
|
||||
# No data saved in memory, direct to disk
|
||||
self.__flush_article_to_disk(article, data)
|
||||
|
||||
def load_article(self, article):
|
||||
def load_article(self, article: Article):
|
||||
""" Load the data of the article """
|
||||
data = None
|
||||
nzo = article.nzf.nzo
|
||||
|
||||
if article in self.__article_list:
|
||||
data = self.__article_table.pop(article)
|
||||
self.__article_list.remove(article)
|
||||
self.free_reserved_space(len(data))
|
||||
elif article.art_id:
|
||||
data = sabnzbd.load_data(article.art_id, nzo.workpath, remove=True, do_pickle=False, silent=True)
|
||||
nzo.remove_saved_article(article)
|
||||
|
||||
return data
|
||||
|
||||
@synchronized(ARTICLE_LOCK)
|
||||
def flush_articles(self):
|
||||
self.__cache_size = 0
|
||||
while self.__article_list:
|
||||
article = self.__article_list.pop(0)
|
||||
data = self.__article_table.pop(article)
|
||||
self.__flush_article_to_disk(article, data)
|
||||
|
||||
def purge_articles(self, articles):
|
||||
""" Remove all saved articles, from memory and disk """
|
||||
for article in articles:
|
||||
if article in self.__article_list:
|
||||
self.__article_list.remove(article)
|
||||
if article in self.__article_table:
|
||||
try:
|
||||
data = self.__article_table.pop(article)
|
||||
self.free_reserved_space(len(data))
|
||||
if article.art_id:
|
||||
sabnzbd.remove_data(article.art_id, article.nzf.nzo.workpath)
|
||||
except KeyError:
|
||||
# Could fail due the article already being deleted by purge_articles, for example
|
||||
# when post-processing deletes the job while delayed articles still come in
|
||||
logging.debug("Failed to load %s from cache, probably already deleted", article)
|
||||
return data
|
||||
elif article.art_id:
|
||||
data = sabnzbd.load_data(article.art_id, nzo.admin_path, remove=True, do_pickle=False, silent=True)
|
||||
nzo.remove_saved_article(article)
|
||||
return data
|
||||
|
||||
def __flush_article_to_disk(self, article, data):
|
||||
def flush_articles(self):
|
||||
logging.debug("Saving %s cached articles to disk", len(self.__article_table))
|
||||
self.__cache_size = 0
|
||||
while self.__article_table:
|
||||
try:
|
||||
article, data = self.__article_table.popitem()
|
||||
self.__flush_article_to_disk(article, data)
|
||||
except KeyError:
|
||||
# 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]):
|
||||
""" Remove all saved articles, from memory and disk """
|
||||
logging.debug("Purging %s articles from the cache/disk", len(articles))
|
||||
for article in articles:
|
||||
if article in self.__article_table:
|
||||
try:
|
||||
data = self.__article_table.pop(article)
|
||||
self.free_reserved_space(len(data))
|
||||
except KeyError:
|
||||
# Could fail if already deleted by flush_articles or load_data
|
||||
logging.debug("Failed to flush %s from cache, probably already deleted or written to disk", article)
|
||||
elif article.art_id:
|
||||
sabnzbd.remove_data(article.art_id, article.nzf.nzo.admin_path)
|
||||
|
||||
@staticmethod
|
||||
def __flush_article_to_disk(article: Article, data):
|
||||
nzo = article.nzf.nzo
|
||||
if nzo.is_gone():
|
||||
# Don't store deleted jobs
|
||||
@@ -159,8 +167,4 @@ class ArticleCache:
|
||||
|
||||
# Save data, but don't complain when destination folder is missing
|
||||
# because this flush may come after completion of the NZO.
|
||||
sabnzbd.save_data(data, article.get_art_id(), nzo.workpath, do_pickle=False, silent=True)
|
||||
|
||||
|
||||
# Create the instance
|
||||
ArticleCache()
|
||||
sabnzbd.save_data(data, article.get_art_id(), nzo.admin_path, do_pickle=False, silent=True)
|
||||
|
||||
@@ -26,46 +26,43 @@ import re
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
import hashlib
|
||||
from typing import Tuple, Optional, List
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.misc import get_all_passwords
|
||||
from sabnzbd.filesystem import set_permissions, clip_path, has_win_device, diskspace, get_filename, get_ext
|
||||
from sabnzbd.constants import Status, GIGI, MAX_ASSEMBLER_QUEUE
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.articlecache import ArticleCache
|
||||
from sabnzbd.postproc import PostProcessor
|
||||
from sabnzbd.nzbstuff import NzbObject, NzbFile
|
||||
import sabnzbd.downloader
|
||||
import sabnzbd.par2file as par2file
|
||||
import sabnzbd.utils.rarfile as rarfile
|
||||
from sabnzbd.rating import Rating
|
||||
|
||||
|
||||
class Assembler(Thread):
|
||||
do = None # Link to the instance of this method
|
||||
|
||||
def __init__(self):
|
||||
Thread.__init__(self)
|
||||
self.queue = queue.Queue()
|
||||
Assembler.do = self
|
||||
self.queue: queue.Queue[Tuple[Optional[NzbObject], Optional[NzbFile], Optional[bool]]] = queue.Queue()
|
||||
|
||||
def stop(self):
|
||||
self.process(None)
|
||||
self.queue.put((None, None, None))
|
||||
|
||||
def process(self, job):
|
||||
self.queue.put(job)
|
||||
def process(self, nzo: NzbObject, nzf: Optional[NzbFile] = None, file_done: Optional[bool] = None):
|
||||
self.queue.put((nzo, nzf, file_done))
|
||||
|
||||
def queue_full(self):
|
||||
return self.queue.qsize() >= MAX_ASSEMBLER_QUEUE
|
||||
|
||||
def run(self):
|
||||
while 1:
|
||||
job = self.queue.get()
|
||||
if not job:
|
||||
# Set NzbObject and NzbFile objects to None so references
|
||||
# from this thread do not keep the objects alive (see #1628)
|
||||
nzo = nzf = None
|
||||
nzo, nzf, file_done = self.queue.get()
|
||||
if not nzo:
|
||||
logging.info("Shutting down")
|
||||
break
|
||||
|
||||
nzo, nzf, file_done = job
|
||||
|
||||
if nzf:
|
||||
# Check if enough disk space is free after each file is done
|
||||
# If not enough space left, pause downloader and send email
|
||||
@@ -74,10 +71,10 @@ class Assembler(Thread):
|
||||
and diskspace(force=True)["download_dir"][1] < (cfg.download_free.get_float() + nzf.bytes) / GIGI
|
||||
):
|
||||
# Only warn and email once
|
||||
if not sabnzbd.downloader.Downloader.do.paused:
|
||||
if not sabnzbd.Downloader.paused:
|
||||
logging.warning(T("Too little diskspace forcing PAUSE"))
|
||||
# Pause downloader, but don't save, since the disk is almost full!
|
||||
sabnzbd.downloader.Downloader.do.pause()
|
||||
sabnzbd.Downloader.pause()
|
||||
sabnzbd.emailer.diskfull_mail()
|
||||
# Abort all direct unpackers, just to be sure
|
||||
sabnzbd.directunpacker.abort_all()
|
||||
@@ -100,7 +97,7 @@ class Assembler(Thread):
|
||||
# Log traceback
|
||||
logging.info("Traceback: ", exc_info=True)
|
||||
# Pause without saving
|
||||
sabnzbd.downloader.Downloader.do.pause()
|
||||
sabnzbd.Downloader.pause()
|
||||
continue
|
||||
except:
|
||||
logging.error(T("Fatal error in Assembler"), exc_info=True)
|
||||
@@ -121,34 +118,30 @@ class Assembler(Thread):
|
||||
if rar_encrypted:
|
||||
if cfg.pause_on_pwrar() == 1:
|
||||
logging.warning(
|
||||
remove_warning_label(
|
||||
T(
|
||||
'WARNING: Paused job "%s" because of encrypted RAR file (if supplied, all passwords were tried)'
|
||||
)
|
||||
T(
|
||||
'Paused job "%s" because of encrypted RAR file (if supplied, all passwords were tried)'
|
||||
),
|
||||
nzo.final_name,
|
||||
)
|
||||
nzo.pause()
|
||||
else:
|
||||
logging.warning(
|
||||
remove_warning_label(
|
||||
T(
|
||||
'WARNING: Aborted job "%s" because of encrypted RAR file (if supplied, all passwords were tried)'
|
||||
)
|
||||
T(
|
||||
'Aborted job "%s" because of encrypted RAR file (if supplied, all passwords were tried)'
|
||||
),
|
||||
nzo.final_name,
|
||||
)
|
||||
nzo.fail_msg = T("Aborted, encryption detected")
|
||||
sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo)
|
||||
sabnzbd.NzbQueue.end_job(nzo)
|
||||
|
||||
if unwanted_file:
|
||||
logging.warning(
|
||||
remove_warning_label(
|
||||
T('WARNING: In "%s" unwanted extension in RAR file. Unwanted file is %s ')
|
||||
),
|
||||
nzo.final_name,
|
||||
unwanted_file,
|
||||
)
|
||||
# Don't repeat the warning after a user override of an unwanted extension pause
|
||||
if nzo.unwanted_ext == 0:
|
||||
logging.warning(
|
||||
T('In "%s" unwanted extension in RAR file. Unwanted file is %s '),
|
||||
nzo.final_name,
|
||||
unwanted_file,
|
||||
)
|
||||
logging.debug(T("Unwanted extension is in rar file %s"), filepath)
|
||||
if cfg.action_on_unwanted_extensions() == 1 and nzo.unwanted_ext == 0:
|
||||
logging.debug("Unwanted extension ... pausing")
|
||||
@@ -157,7 +150,7 @@ class Assembler(Thread):
|
||||
if cfg.action_on_unwanted_extensions() == 2:
|
||||
logging.debug("Unwanted extension ... aborting")
|
||||
nzo.fail_msg = T("Aborted, unwanted extension detected")
|
||||
sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo)
|
||||
sabnzbd.NzbQueue.end_job(nzo)
|
||||
|
||||
# Add to direct unpack
|
||||
nzo.add_to_direct_unpacker(nzf)
|
||||
@@ -169,28 +162,29 @@ class Assembler(Thread):
|
||||
filter_output, reason = nzo_filtered_by_rating(nzo)
|
||||
if filter_output == 1:
|
||||
logging.warning(
|
||||
remove_warning_label(T('WARNING: Paused job "%s" because of rating (%s)')),
|
||||
T('Paused job "%s" because of rating (%s)'),
|
||||
nzo.final_name,
|
||||
reason,
|
||||
)
|
||||
nzo.pause()
|
||||
elif filter_output == 2:
|
||||
logging.warning(
|
||||
remove_warning_label(T('WARNING: Aborted job "%s" because of rating (%s)')),
|
||||
T('Aborted job "%s" because of rating (%s)'),
|
||||
nzo.final_name,
|
||||
reason,
|
||||
)
|
||||
nzo.fail_msg = T("Aborted, rating filter matched (%s)") % reason
|
||||
sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo)
|
||||
sabnzbd.NzbQueue.end_job(nzo)
|
||||
|
||||
else:
|
||||
sabnzbd.nzbqueue.NzbQueue.do.remove(nzo.nzo_id, add_to_history=False, cleanup=False)
|
||||
PostProcessor.do.process(nzo)
|
||||
sabnzbd.NzbQueue.remove(nzo.nzo_id, cleanup=False)
|
||||
sabnzbd.PostProcessor.process(nzo)
|
||||
|
||||
def assemble(self, nzf, file_done):
|
||||
""" Assemble a NZF from its table of articles
|
||||
1) Partial write: write what we have
|
||||
2) Nothing written before: write all
|
||||
@staticmethod
|
||||
def assemble(nzf: NzbFile, file_done: bool):
|
||||
"""Assemble a NZF from its table of articles
|
||||
1) Partial write: write what we have
|
||||
2) Nothing written before: write all
|
||||
"""
|
||||
# New hash-object needed?
|
||||
if not nzf.md5:
|
||||
@@ -208,7 +202,7 @@ class Assembler(Thread):
|
||||
|
||||
# Write all decoded articles
|
||||
if article.decoded:
|
||||
data = ArticleCache.do.load_article(article)
|
||||
data = sabnzbd.ArticleCache.load_article(article)
|
||||
# Could be empty in case nzo was deleted
|
||||
if data:
|
||||
fout.write(data)
|
||||
@@ -231,14 +225,14 @@ class Assembler(Thread):
|
||||
nzf.md5sum = nzf.md5.digest()
|
||||
|
||||
|
||||
def file_has_articles(nzf):
|
||||
""" Do a quick check to see if any articles are present for this file.
|
||||
Destructive: only to be used to differentiate between unknown encoding and no articles.
|
||||
def file_has_articles(nzf: NzbFile):
|
||||
"""Do a quick check to see if any articles are present for this file.
|
||||
Destructive: only to be used to differentiate between unknown encoding and no articles.
|
||||
"""
|
||||
has = False
|
||||
for article in nzf.decodetable:
|
||||
sleep(0.01)
|
||||
data = ArticleCache.do.load_article(article)
|
||||
data = sabnzbd.ArticleCache.load_article(article)
|
||||
if data:
|
||||
has = True
|
||||
return has
|
||||
@@ -248,7 +242,7 @@ RE_SUBS = re.compile(r"\W+sub|subs|subpack|subtitle|subtitles(?![a-z])", re.I)
|
||||
SAFE_EXTS = (".mkv", ".mp4", ".avi", ".wmv", ".mpg", ".webm")
|
||||
|
||||
|
||||
def is_cloaked(nzo, path, names):
|
||||
def is_cloaked(nzo: NzbObject, path: str, names: List[str]) -> bool:
|
||||
""" Return True if this is likely to be a cloaked encrypted post """
|
||||
fname = os.path.splitext(get_filename(path.lower()))[0]
|
||||
for name in names:
|
||||
@@ -277,7 +271,7 @@ def is_cloaked(nzo, path, names):
|
||||
return False
|
||||
|
||||
|
||||
def check_encrypted_and_unwanted_files(nzo, filepath):
|
||||
def check_encrypted_and_unwanted_files(nzo: NzbObject, filepath: str) -> Tuple[bool, Optional[str]]:
|
||||
""" Combines check for unwanted and encrypted files to save on CPU and IO """
|
||||
encrypted = False
|
||||
unwanted = None
|
||||
@@ -371,9 +365,9 @@ def check_encrypted_and_unwanted_files(nzo, filepath):
|
||||
return encrypted, unwanted
|
||||
|
||||
|
||||
def nzo_filtered_by_rating(nzo):
|
||||
if Rating.do and cfg.rating_enable() and cfg.rating_filter_enable() and (nzo.rating_filtered < 2):
|
||||
rating = Rating.do.get_rating_by_nzo(nzo.nzo_id)
|
||||
def nzo_filtered_by_rating(nzo: NzbObject) -> Tuple[int, str]:
|
||||
if cfg.rating_enable() and cfg.rating_filter_enable() and (nzo.rating_filtered < 2):
|
||||
rating = sabnzbd.Rating.get_rating_by_nzo(nzo.nzo_id)
|
||||
if rating is not None:
|
||||
nzo.rating_filtered = 1
|
||||
reason = rating_filtered(rating, nzo.filename.lower(), True)
|
||||
@@ -417,11 +411,3 @@ def rating_filtered(rating, filename, abort):
|
||||
if any(check_keyword(k) for k in keywords.split(",")):
|
||||
return T("keywords")
|
||||
return None
|
||||
|
||||
|
||||
def remove_warning_label(msg):
|
||||
""" Standardize errors by removing obsolete
|
||||
"WARNING:" part in all languages """
|
||||
if ":" in msg:
|
||||
return msg.split(":")[1].strip()
|
||||
return msg
|
||||
|
||||
@@ -22,6 +22,7 @@ sabnzbd.bpsmeter - bpsmeter
|
||||
import time
|
||||
import logging
|
||||
import re
|
||||
from typing import List, Dict
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.constants import BYTES_FILE_NAME, KIBI
|
||||
@@ -87,8 +88,6 @@ def next_month(t):
|
||||
|
||||
|
||||
class BPSMeter:
|
||||
do = None
|
||||
|
||||
def __init__(self):
|
||||
t = time.time()
|
||||
self.start_time = t
|
||||
@@ -96,20 +95,20 @@ class BPSMeter:
|
||||
self.speed_log_time = t
|
||||
self.last_update = t
|
||||
self.bps = 0.0
|
||||
self.bps_list = []
|
||||
self.bps_list: List[int] = []
|
||||
self.bps_list_max = 275
|
||||
|
||||
self.day_total = {}
|
||||
self.week_total = {}
|
||||
self.month_total = {}
|
||||
self.grand_total = {}
|
||||
self.day_total: Dict[str, int] = {}
|
||||
self.week_total: Dict[str, int] = {}
|
||||
self.month_total: Dict[str, int] = {}
|
||||
self.grand_total: Dict[str, int] = {}
|
||||
|
||||
self.timeline_total = {}
|
||||
self.timeline_total: Dict[str, Dict[str, int]] = {}
|
||||
|
||||
self.day_label = time.strftime("%Y-%m-%d")
|
||||
self.end_of_day = tomorrow(t) # Time that current day will end
|
||||
self.end_of_week = next_week(t) # Time that current day will end
|
||||
self.end_of_month = next_month(t) # Time that current month will end
|
||||
self.day_label: str = time.strftime("%Y-%m-%d")
|
||||
self.end_of_day: float = tomorrow(t) # Time that current day will end
|
||||
self.end_of_week: float = next_week(t) # Time that current day will end
|
||||
self.end_of_month: float = next_month(t) # Time that current month will end
|
||||
self.q_day = 1 # Day of quota reset
|
||||
self.q_period = "m" # Daily/Weekly/Monthly quota = d/w/m
|
||||
self.quota = self.left = 0.0 # Quota and remaining quota
|
||||
@@ -118,32 +117,32 @@ class BPSMeter:
|
||||
self.q_hour = 0 # Quota reset hour
|
||||
self.q_minute = 0 # Quota reset minute
|
||||
self.quota_enabled = True # Scheduled quota enable/disable
|
||||
BPSMeter.do = self
|
||||
|
||||
def save(self):
|
||||
""" Save admin to disk """
|
||||
data = (
|
||||
self.last_update,
|
||||
self.grand_total,
|
||||
self.day_total,
|
||||
self.week_total,
|
||||
self.month_total,
|
||||
self.end_of_day,
|
||||
self.end_of_week,
|
||||
self.end_of_month,
|
||||
self.quota,
|
||||
self.left,
|
||||
self.q_time,
|
||||
self.timeline_total,
|
||||
sabnzbd.save_admin(
|
||||
(
|
||||
self.last_update,
|
||||
self.grand_total,
|
||||
self.day_total,
|
||||
self.week_total,
|
||||
self.month_total,
|
||||
self.end_of_day,
|
||||
self.end_of_week,
|
||||
self.end_of_month,
|
||||
self.quota,
|
||||
self.left,
|
||||
self.q_time,
|
||||
self.timeline_total,
|
||||
),
|
||||
BYTES_FILE_NAME,
|
||||
)
|
||||
sabnzbd.save_admin(data, BYTES_FILE_NAME)
|
||||
|
||||
def defaults(self):
|
||||
""" Get the latest data from the database and assign to a fake server """
|
||||
logging.debug("Setting default BPS meter values")
|
||||
history_db = sabnzbd.database.HistoryDB()
|
||||
grand, month, week = history_db.get_history_size()
|
||||
history_db.close()
|
||||
with sabnzbd.database.HistoryDB() as history_db:
|
||||
grand, month, week = history_db.get_history_size()
|
||||
self.grand_total = {}
|
||||
self.month_total = {}
|
||||
self.week_total = {}
|
||||
@@ -191,12 +190,9 @@ class BPSMeter:
|
||||
self.update()
|
||||
return res
|
||||
|
||||
def update(self, server=None, amount=0, testtime=None):
|
||||
def update(self, server=None, amount=0):
|
||||
""" Update counters for "server" with "amount" bytes """
|
||||
if testtime:
|
||||
t = testtime
|
||||
else:
|
||||
t = time.time()
|
||||
t = time.time()
|
||||
if t > self.end_of_day:
|
||||
# current day passed. get new end of day
|
||||
self.day_label = time.strftime("%Y-%m-%d")
|
||||
@@ -238,8 +234,8 @@ class BPSMeter:
|
||||
if self.have_quota and self.quota_enabled:
|
||||
self.left -= amount
|
||||
if self.left <= 0.0:
|
||||
if sabnzbd.downloader.Downloader.do and not sabnzbd.downloader.Downloader.do.paused:
|
||||
sabnzbd.downloader.Downloader.do.pause()
|
||||
if not sabnzbd.Downloader.paused:
|
||||
sabnzbd.Downloader.pause()
|
||||
logging.warning(T("Quota spent, pausing downloading"))
|
||||
|
||||
# Speedometer
|
||||
@@ -324,9 +320,9 @@ class BPSMeter:
|
||||
return self.bps_list[::refresh_rate]
|
||||
|
||||
def get_stable_speed(self, timespan=10):
|
||||
""" See if there is a stable speed the last <timespan> seconds
|
||||
None: indicates it can't determine yet
|
||||
False: the speed was not stable during <timespan>
|
||||
"""See if there is a stable speed the last <timespan> seconds
|
||||
None: indicates it can't determine yet
|
||||
False: the speed was not stable during <timespan>
|
||||
"""
|
||||
if len(self.bps_list) < timespan:
|
||||
return None
|
||||
@@ -350,16 +346,15 @@ class BPSMeter:
|
||||
return None
|
||||
|
||||
def reset_quota(self, force=False):
|
||||
""" Check if it's time to reset the quota, optionally resuming
|
||||
Return True, when still paused
|
||||
"""Check if it's time to reset the quota, optionally resuming
|
||||
Return True, when still paused or should be paused
|
||||
"""
|
||||
if force or (self.have_quota and time.time() > (self.q_time - 50)):
|
||||
self.quota = self.left = cfg.quota_size.get_float()
|
||||
logging.info("Quota was reset to %s", self.quota)
|
||||
if cfg.quota_resume():
|
||||
logging.info("Auto-resume due to quota reset")
|
||||
if sabnzbd.downloader.Downloader.do:
|
||||
sabnzbd.downloader.Downloader.do.resume()
|
||||
sabnzbd.Downloader.resume()
|
||||
self.next_reset()
|
||||
return False
|
||||
else:
|
||||
@@ -464,10 +459,11 @@ class BPSMeter:
|
||||
if action and not status:
|
||||
self.resume()
|
||||
|
||||
def resume(self):
|
||||
@staticmethod
|
||||
def resume():
|
||||
""" Resume downloading """
|
||||
if cfg.quota_resume() and sabnzbd.downloader.Downloader.do and sabnzbd.downloader.Downloader.do.paused:
|
||||
sabnzbd.downloader.Downloader.do.resume()
|
||||
if cfg.quota_resume() and sabnzbd.Downloader.paused:
|
||||
sabnzbd.Downloader.resume()
|
||||
|
||||
def midnight(self):
|
||||
""" Midnight action: dummy update for all servers """
|
||||
@@ -478,12 +474,4 @@ class BPSMeter:
|
||||
def quota_handler():
|
||||
""" To be called from scheduler """
|
||||
logging.debug("Checking quota")
|
||||
BPSMeter.do.reset_quota()
|
||||
|
||||
|
||||
def midnight_action():
|
||||
if BPSMeter.do:
|
||||
BPSMeter.do.midnight()
|
||||
|
||||
|
||||
BPSMeter()
|
||||
sabnzbd.BPSMeter.reset_quota()
|
||||
|
||||
@@ -34,6 +34,7 @@ from sabnzbd.config import (
|
||||
create_api_key,
|
||||
validate_notempty,
|
||||
clean_nice_ionice_parameters,
|
||||
validate_strip_right_slash,
|
||||
)
|
||||
from sabnzbd.constants import (
|
||||
DEF_HOST,
|
||||
@@ -44,6 +45,7 @@ from sabnzbd.constants import (
|
||||
DEF_NZBBACK_DIR,
|
||||
DEF_SCANRATE,
|
||||
DEF_COMPLETE_DIR,
|
||||
DEF_FOLDER_MAX,
|
||||
)
|
||||
|
||||
##############################################################################
|
||||
@@ -83,11 +85,11 @@ queue_complete_pers = OptionBool("misc", "queue_complete_pers", False)
|
||||
bandwidth_perc = OptionNumber("misc", "bandwidth_perc", 0, 0, 100)
|
||||
refresh_rate = OptionNumber("misc", "refresh_rate", 0)
|
||||
log_level = OptionNumber("logging", "log_level", 1, -1, 2)
|
||||
log_size = OptionStr("logging", "max_log_size", "5242880")
|
||||
log_size = OptionNumber("logging", "max_log_size", 5242880)
|
||||
log_backups = OptionNumber("logging", "log_backups", 5, 1, 1024)
|
||||
queue_limit = OptionNumber("misc", "queue_limit", 20, 0)
|
||||
|
||||
configlock = OptionBool("misc", "config_lock", 0)
|
||||
configlock = OptionBool("misc", "config_lock", False)
|
||||
|
||||
|
||||
##############################################################################
|
||||
@@ -113,7 +115,7 @@ password = OptionPassword("misc", "password")
|
||||
bandwidth_max = OptionStr("misc", "bandwidth_max")
|
||||
cache_limit = OptionStr("misc", "cache_limit")
|
||||
web_dir = OptionStr("misc", "web_dir", DEF_STDINTF)
|
||||
web_color = OptionStr("misc", "web_color", "")
|
||||
web_color = OptionStr("misc", "web_color")
|
||||
https_cert = OptionDir("misc", "https_cert", "server.cert", create=False)
|
||||
https_key = OptionDir("misc", "https_key", "server.key", create=False)
|
||||
https_chain = OptionDir("misc", "https_chain", create=False)
|
||||
@@ -128,7 +130,7 @@ nzb_key = OptionStr("misc", "nzb_key", create_api_key())
|
||||
##############################################################################
|
||||
# Config - Folders
|
||||
##############################################################################
|
||||
umask = OptionStr("misc", "permissions", "", validation=validate_octal)
|
||||
umask = OptionStr("misc", "permissions", validation=validate_octal)
|
||||
download_dir = OptionDir("misc", "download_dir", DEF_DOWNLOAD_DIR, create=False, validation=validate_safedir)
|
||||
download_free = OptionStr("misc", "download_free")
|
||||
complete_dir = OptionDir(
|
||||
@@ -152,14 +154,13 @@ top_only = OptionBool("misc", "top_only", False)
|
||||
sfv_check = OptionBool("misc", "sfv_check", True)
|
||||
quick_check_ext_ignore = OptionList("misc", "quick_check_ext_ignore", ["nfo", "sfv", "srr"])
|
||||
script_can_fail = OptionBool("misc", "script_can_fail", False)
|
||||
ssl_ciphers = OptionStr("misc", "ssl_ciphers", "") # Now per-server setting
|
||||
enable_recursive = OptionBool("misc", "enable_recursive", True)
|
||||
flat_unpack = OptionBool("misc", "flat_unpack", False)
|
||||
par_option = OptionStr("misc", "par_option", "")
|
||||
par_option = OptionStr("misc", "par_option")
|
||||
pre_check = OptionBool("misc", "pre_check", False)
|
||||
nice = OptionStr("misc", "nice", "", validation=clean_nice_ionice_parameters)
|
||||
nice = OptionStr("misc", "nice", validation=clean_nice_ionice_parameters)
|
||||
win_process_prio = OptionNumber("misc", "win_process_prio", 3)
|
||||
ionice = OptionStr("misc", "ionice", "", validation=clean_nice_ionice_parameters)
|
||||
ionice = OptionStr("misc", "ionice", validation=clean_nice_ionice_parameters)
|
||||
fail_hopeless_jobs = OptionBool("misc", "fail_hopeless_jobs", True)
|
||||
fast_fail = OptionBool("misc", "fast_fail", True)
|
||||
autodisconnect = OptionBool("misc", "auto_disconnect", True)
|
||||
@@ -168,6 +169,7 @@ no_series_dupes = OptionNumber("misc", "no_series_dupes", 0)
|
||||
series_propercheck = OptionBool("misc", "series_propercheck", True)
|
||||
pause_on_pwrar = OptionNumber("misc", "pause_on_pwrar", 1)
|
||||
ignore_samples = OptionBool("misc", "ignore_samples", False)
|
||||
deobfuscate_final_filenames = OptionBool("misc", "deobfuscate_final_filenames", False)
|
||||
auto_sort = OptionStr("misc", "auto_sort")
|
||||
direct_unpack = OptionBool("misc", "direct_unpack", False)
|
||||
direct_unpack_threads = OptionNumber("misc", "direct_unpack_threads", 3, 1)
|
||||
@@ -254,17 +256,16 @@ enable_filejoin = OptionBool("misc", "enable_filejoin", True)
|
||||
enable_tsjoin = OptionBool("misc", "enable_tsjoin", True)
|
||||
overwrite_files = OptionBool("misc", "overwrite_files", False)
|
||||
ignore_unrar_dates = OptionBool("misc", "ignore_unrar_dates", False)
|
||||
ignore_wrong_unrar = OptionBool("misc", "ignore_wrong_unrar", False)
|
||||
backup_for_duplicates = OptionBool("misc", "backup_for_duplicates", True)
|
||||
empty_postproc = OptionBool("misc", "empty_postproc", False)
|
||||
wait_for_dfolder = OptionBool("misc", "wait_for_dfolder", False)
|
||||
warn_empty_nzb = OptionBool("misc", "warn_empty_nzb", True)
|
||||
rss_filenames = OptionBool("misc", "rss_filenames", False)
|
||||
api_logging = OptionBool("misc", "api_logging", True)
|
||||
html_login = OptionBool("misc", "html_login", True)
|
||||
osx_menu = OptionBool("misc", "osx_menu", True)
|
||||
osx_speed = OptionBool("misc", "osx_speed", True)
|
||||
warn_dupl_jobs = OptionBool("misc", "warn_dupl_jobs", True)
|
||||
helpfull_warnings = OptionBool("misc", "helpfull_warnings", True)
|
||||
keep_awake = OptionBool("misc", "keep_awake", True)
|
||||
win_menu = OptionBool("misc", "win_menu", True)
|
||||
allow_incomplete_nzb = OptionBool("misc", "allow_incomplete_nzb", False)
|
||||
@@ -288,9 +289,10 @@ size_limit = OptionStr("misc", "size_limit", "0")
|
||||
show_sysload = OptionNumber("misc", "show_sysload", 2, 0, 2)
|
||||
history_limit = OptionNumber("misc", "history_limit", 10, 0)
|
||||
wait_ext_drive = OptionNumber("misc", "wait_ext_drive", 5, 1, 60)
|
||||
marker_file = OptionStr("misc", "nomedia_marker", "")
|
||||
max_foldername_length = OptionNumber("misc", "max_foldername_length", DEF_FOLDER_MAX, 20, 65000)
|
||||
marker_file = OptionStr("misc", "nomedia_marker")
|
||||
ipv6_servers = OptionNumber("misc", "ipv6_servers", 1, 0, 2)
|
||||
url_base = OptionStr("misc", "url_base", "/sabnzbd")
|
||||
url_base = OptionStr("misc", "url_base", "/sabnzbd", validation=validate_strip_right_slash)
|
||||
host_whitelist = OptionList("misc", "host_whitelist", validation=all_lowercase)
|
||||
max_url_retries = OptionNumber("misc", "max_url_retries", 10, 1)
|
||||
|
||||
@@ -314,6 +316,7 @@ ncenter_enable = OptionBool("ncenter", "ncenter_enable", sabnzbd.DARWIN)
|
||||
ncenter_cats = OptionList("ncenter", "ncenter_cats", ["*"])
|
||||
ncenter_prio_startup = OptionBool("ncenter", "ncenter_prio_startup", True)
|
||||
ncenter_prio_download = OptionBool("ncenter", "ncenter_prio_download", False)
|
||||
ncenter_prio_pause_resume = OptionBool("ncenter", "ncenter_prio_pause_resume", False)
|
||||
ncenter_prio_pp = OptionBool("ncenter", "ncenter_prio_pp", False)
|
||||
ncenter_prio_complete = OptionBool("ncenter", "ncenter_prio_complete", True)
|
||||
ncenter_prio_failed = OptionBool("ncenter", "ncenter_prio_failed", True)
|
||||
@@ -329,6 +332,7 @@ acenter_enable = OptionBool("acenter", "acenter_enable", sabnzbd.WIN32)
|
||||
acenter_cats = OptionList("acenter", "acenter_cats", ["*"])
|
||||
acenter_prio_startup = OptionBool("acenter", "acenter_prio_startup", False)
|
||||
acenter_prio_download = OptionBool("acenter", "acenter_prio_download", False)
|
||||
acenter_prio_pause_resume = OptionBool("acenter", "acenter_prio_pause_resume", False)
|
||||
acenter_prio_pp = OptionBool("acenter", "acenter_prio_pp", False)
|
||||
acenter_prio_complete = OptionBool("acenter", "acenter_prio_complete", True)
|
||||
acenter_prio_failed = OptionBool("acenter", "acenter_prio_failed", True)
|
||||
@@ -344,6 +348,7 @@ ntfosd_enable = OptionBool("ntfosd", "ntfosd_enable", not sabnzbd.WIN32 and not
|
||||
ntfosd_cats = OptionList("ntfosd", "ntfosd_cats", ["*"])
|
||||
ntfosd_prio_startup = OptionBool("ntfosd", "ntfosd_prio_startup", True)
|
||||
ntfosd_prio_download = OptionBool("ntfosd", "ntfosd_prio_download", False)
|
||||
ntfosd_prio_pause_resume = OptionBool("ntfosd", "ntfosd_prio_pause_resume", False)
|
||||
ntfosd_prio_pp = OptionBool("ntfosd", "ntfosd_prio_pp", False)
|
||||
ntfosd_prio_complete = OptionBool("ntfosd", "ntfosd_prio_complete", True)
|
||||
ntfosd_prio_failed = OptionBool("ntfosd", "ntfosd_prio_failed", True)
|
||||
@@ -360,6 +365,7 @@ prowl_cats = OptionList("prowl", "prowl_cats", ["*"])
|
||||
prowl_apikey = OptionStr("prowl", "prowl_apikey")
|
||||
prowl_prio_startup = OptionNumber("prowl", "prowl_prio_startup", -3)
|
||||
prowl_prio_download = OptionNumber("prowl", "prowl_prio_download", -3)
|
||||
prowl_prio_pause_resume = OptionNumber("prowl", "prowl_prio_pause_resume", -3)
|
||||
prowl_prio_pp = OptionNumber("prowl", "prowl_prio_pp", -3)
|
||||
prowl_prio_complete = OptionNumber("prowl", "prowl_prio_complete", 0)
|
||||
prowl_prio_failed = OptionNumber("prowl", "prowl_prio_failed", 1)
|
||||
@@ -380,6 +386,7 @@ pushover_enable = OptionBool("pushover", "pushover_enable")
|
||||
pushover_cats = OptionList("pushover", "pushover_cats", ["*"])
|
||||
pushover_prio_startup = OptionNumber("pushover", "pushover_prio_startup", -3)
|
||||
pushover_prio_download = OptionNumber("pushover", "pushover_prio_download", -2)
|
||||
pushover_prio_pause_resume = OptionNumber("pushover", "pushover_prio_pause_resume", -2)
|
||||
pushover_prio_pp = OptionNumber("pushover", "pushover_prio_pp", -3)
|
||||
pushover_prio_complete = OptionNumber("pushover", "pushover_prio_complete", -1)
|
||||
pushover_prio_failed = OptionNumber("pushover", "pushover_prio_failed", -1)
|
||||
@@ -395,17 +402,18 @@ pushbullet_enable = OptionBool("pushbullet", "pushbullet_enable")
|
||||
pushbullet_cats = OptionList("pushbullet", "pushbullet_cats", ["*"])
|
||||
pushbullet_apikey = OptionStr("pushbullet", "pushbullet_apikey")
|
||||
pushbullet_device = OptionStr("pushbullet", "pushbullet_device")
|
||||
pushbullet_prio_startup = OptionNumber("pushbullet", "pushbullet_prio_startup", 0)
|
||||
pushbullet_prio_download = OptionNumber("pushbullet", "pushbullet_prio_download", 0)
|
||||
pushbullet_prio_pp = OptionNumber("pushbullet", "pushbullet_prio_pp", 0)
|
||||
pushbullet_prio_complete = OptionNumber("pushbullet", "pushbullet_prio_complete", 1)
|
||||
pushbullet_prio_failed = OptionNumber("pushbullet", "pushbullet_prio_failed", 1)
|
||||
pushbullet_prio_disk_full = OptionNumber("pushbullet", "pushbullet_prio_disk_full", 1)
|
||||
pushbullet_prio_new_login = OptionNumber("pushbullet", "pushbullet_prio_new_login", 0)
|
||||
pushbullet_prio_warning = OptionNumber("pushbullet", "pushbullet_prio_warning", 0)
|
||||
pushbullet_prio_error = OptionNumber("pushbullet", "pushbullet_prio_error", 0)
|
||||
pushbullet_prio_queue_done = OptionNumber("pushbullet", "pushbullet_prio_queue_done", 0)
|
||||
pushbullet_prio_other = OptionNumber("pushbullet", "pushbullet_prio_other", 0)
|
||||
pushbullet_prio_startup = OptionBool("pushbullet", "pushbullet_prio_startup", False)
|
||||
pushbullet_prio_download = OptionBool("pushbullet", "pushbullet_prio_download", False)
|
||||
pushbullet_prio_pause_resume = OptionBool("pushbullet", "pushbullet_prio_pause_resume", False)
|
||||
pushbullet_prio_pp = OptionBool("pushbullet", "pushbullet_prio_pp", False)
|
||||
pushbullet_prio_complete = OptionBool("pushbullet", "pushbullet_prio_complete", True)
|
||||
pushbullet_prio_failed = OptionBool("pushbullet", "pushbullet_prio_failed", True)
|
||||
pushbullet_prio_disk_full = OptionBool("pushbullet", "pushbullet_prio_disk_full", True)
|
||||
pushbullet_prio_new_login = OptionBool("pushbullet", "pushbullet_prio_new_login", False)
|
||||
pushbullet_prio_warning = OptionBool("pushbullet", "pushbullet_prio_warning", False)
|
||||
pushbullet_prio_error = OptionBool("pushbullet", "pushbullet_prio_error", False)
|
||||
pushbullet_prio_queue_done = OptionBool("pushbullet", "pushbullet_prio_queue_done", False)
|
||||
pushbullet_prio_other = OptionBool("pushbullet", "pushbullet_prio_other", False)
|
||||
|
||||
# [nscript]
|
||||
nscript_enable = OptionBool("nscript", "nscript_enable")
|
||||
@@ -414,6 +422,7 @@ nscript_script = OptionStr("nscript", "nscript_script")
|
||||
nscript_parameters = OptionStr("nscript", "nscript_parameters")
|
||||
nscript_prio_startup = OptionBool("nscript", "nscript_prio_startup", True)
|
||||
nscript_prio_download = OptionBool("nscript", "nscript_prio_download", False)
|
||||
nscript_prio_pause_resume = OptionBool("nscript", "nscript_prio_pause_resume", False)
|
||||
nscript_prio_pp = OptionBool("nscript", "nscript_prio_pp", False)
|
||||
nscript_prio_complete = OptionBool("nscript", "nscript_prio_complete", True)
|
||||
nscript_prio_failed = OptionBool("nscript", "nscript_prio_failed", True)
|
||||
|
||||
@@ -25,12 +25,13 @@ import re
|
||||
import shutil
|
||||
import threading
|
||||
import uuid
|
||||
from typing import List, Dict, Any, Callable, Optional, Union, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import configobj
|
||||
|
||||
import sabnzbd.misc
|
||||
from sabnzbd.constants import CONFIG_VERSION, NORMAL_PRIORITY, DEFAULT_PRIORITY, MAX_WIN_DFOLDER
|
||||
from sabnzbd.constants import CONFIG_VERSION, NORMAL_PRIORITY, DEFAULT_PRIORITY
|
||||
from sabnzbd.decorators import synchronized
|
||||
from sabnzbd.filesystem import clip_path, real_path, create_real_path, renamer, remove_file, is_writable
|
||||
|
||||
@@ -38,7 +39,7 @@ CONFIG_LOCK = threading.Lock()
|
||||
SAVE_CONFIG_LOCK = threading.Lock()
|
||||
|
||||
|
||||
CFG = {} # Holds INI structure
|
||||
CFG: configobj.ConfigObj # Holds INI structure
|
||||
# during re-write this variable is global
|
||||
# to allow direct access to INI structure
|
||||
|
||||
@@ -47,26 +48,26 @@ database = {} # Holds the option dictionary
|
||||
modified = False # Signals a change in option dictionary
|
||||
# Should be reset after saving to settings file
|
||||
|
||||
paramfinder = re.compile(r"""(?:'.*?')|(?:".*?")|(?:[^'",\s][^,]*)""")
|
||||
RE_PARAMFINDER = re.compile(r"""(?:'.*?')|(?:".*?")|(?:[^'",\s][^,]*)""")
|
||||
|
||||
|
||||
class Option:
|
||||
""" Basic option class, basic fields """
|
||||
|
||||
def __init__(self, section, keyword, default_val=None, add=True, protect=False):
|
||||
""" Basic option
|
||||
`section` : single section or comma-separated list of sections
|
||||
a list will be a hierarchy: "foo, bar" --> [foo][[bar]]
|
||||
`keyword` : keyword in the (last) section
|
||||
`default_val` : value returned when no value has been set
|
||||
`callback` : procedure to call when value is successfully changed
|
||||
`protect` : Do not allow setting via the API (specifically set_dict)
|
||||
def __init__(self, section: str, keyword: str, default_val: Any = None, add: bool = True, protect: bool = False):
|
||||
"""Basic option
|
||||
`section` : single section or comma-separated list of sections
|
||||
a list will be a hierarchy: "foo, bar" --> [foo][[bar]]
|
||||
`keyword` : keyword in the (last) section
|
||||
`default_val` : value returned when no value has been set
|
||||
`callback` : procedure to call when value is successfully changed
|
||||
`protect` : Do not allow setting via the API (specifically set_dict)
|
||||
"""
|
||||
self.__sections = section.split(",")
|
||||
self.__keyword = keyword
|
||||
self.__default_val = default_val
|
||||
self.__value = None
|
||||
self.__callback = None
|
||||
self.__keyword: str = keyword
|
||||
self.__default_val: Any = default_val
|
||||
self.__value: Any = None
|
||||
self.__callback: Optional[Callable] = None
|
||||
self.__protect = protect
|
||||
|
||||
# Add myself to the config dictionary
|
||||
@@ -79,34 +80,29 @@ class Option:
|
||||
anchor = anchor[section]
|
||||
anchor[keyword] = self
|
||||
|
||||
def __call__(self):
|
||||
""" get() replacement """
|
||||
return self.get()
|
||||
|
||||
def get(self):
|
||||
def get(self) -> Any:
|
||||
""" Retrieve value field """
|
||||
if self.__value is not None:
|
||||
return self.__value
|
||||
else:
|
||||
return self.__default_val
|
||||
|
||||
def get_string(self):
|
||||
def get_string(self) -> str:
|
||||
return str(self.get())
|
||||
|
||||
def get_dict(self, safe=False):
|
||||
def get_dict(self, safe: bool = False) -> Dict[str, Any]:
|
||||
""" Return value a dictionary """
|
||||
return {self.__keyword: self.get()}
|
||||
|
||||
def set_dict(self, input_dict):
|
||||
def set_dict(self, values: Dict[str, Any]):
|
||||
""" Set value based on dictionary """
|
||||
if self.__protect:
|
||||
return False
|
||||
try:
|
||||
return self.set(input_dict["value"])
|
||||
except KeyError:
|
||||
return False
|
||||
if not self.__protect:
|
||||
try:
|
||||
self.set(values["value"])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def __set(self, value):
|
||||
def set(self, value: Any):
|
||||
""" Set new value, no validation """
|
||||
global modified
|
||||
if value is not None:
|
||||
@@ -115,15 +111,11 @@ class Option:
|
||||
modified = True
|
||||
if self.__callback:
|
||||
self.__callback()
|
||||
return None
|
||||
|
||||
def set(self, value):
|
||||
return self.__set(value)
|
||||
|
||||
def default(self):
|
||||
def default(self) -> Any:
|
||||
return self.__default_val
|
||||
|
||||
def callback(self, callback):
|
||||
def callback(self, callback: Callable):
|
||||
""" Set callback function """
|
||||
self.__callback = callback
|
||||
|
||||
@@ -133,18 +125,26 @@ class Option:
|
||||
|
||||
|
||||
class OptionNumber(Option):
|
||||
""" Numeric option class, int/float is determined from default value """
|
||||
"""Numeric option class, int/float is determined from default value."""
|
||||
|
||||
def __init__(
|
||||
self, section, keyword, default_val=0, minval=None, maxval=None, validation=None, add=True, protect=False
|
||||
self,
|
||||
section: str,
|
||||
keyword: str,
|
||||
default_val: Union[int, float] = 0,
|
||||
minval: Optional[int] = None,
|
||||
maxval: Optional[int] = None,
|
||||
validation: Optional[Callable] = None,
|
||||
add: bool = True,
|
||||
protect: bool = False,
|
||||
):
|
||||
self.__minval = minval
|
||||
self.__maxval = maxval
|
||||
self.__validation = validation
|
||||
self.__int = isinstance(default_val, int)
|
||||
self.__minval: Optional[int] = minval
|
||||
self.__maxval: Optional[int] = maxval
|
||||
self.__validation: Optional[Callable] = validation
|
||||
self.__int: bool = isinstance(default_val, int)
|
||||
super().__init__(section, keyword, default_val, add=add, protect=protect)
|
||||
|
||||
def set(self, value):
|
||||
def set(self, value: Any):
|
||||
""" set new value, limited by range """
|
||||
if value is not None:
|
||||
try:
|
||||
@@ -155,7 +155,7 @@ class OptionNumber(Option):
|
||||
except ValueError:
|
||||
value = super().default()
|
||||
if self.__validation:
|
||||
error, val = self.__validation(value)
|
||||
_, val = self.__validation(value)
|
||||
super().set(val)
|
||||
else:
|
||||
if self.__maxval is not None and value > self.__maxval:
|
||||
@@ -163,39 +163,49 @@ class OptionNumber(Option):
|
||||
elif self.__minval is not None and value < self.__minval:
|
||||
value = self.__minval
|
||||
super().set(value)
|
||||
return None
|
||||
|
||||
def __call__(self) -> Union[int, float]:
|
||||
""" get() replacement """
|
||||
return self.get()
|
||||
|
||||
|
||||
class OptionBool(Option):
|
||||
""" Boolean option class """
|
||||
""" Boolean option class, always returns 0 or 1."""
|
||||
|
||||
def __init__(self, section, keyword, default_val=False, add=True, protect=False):
|
||||
def __init__(self, section: str, keyword: str, default_val: bool = False, add: bool = True, protect: bool = False):
|
||||
super().__init__(section, keyword, int(default_val), add=add, protect=protect)
|
||||
|
||||
def set(self, value):
|
||||
if value is None:
|
||||
value = 0
|
||||
try:
|
||||
super().set(int(value))
|
||||
except ValueError:
|
||||
super().set(0)
|
||||
return None
|
||||
def set(self, value: Any):
|
||||
# Store the value as integer, easier to parse when reading the config.
|
||||
super().set(sabnzbd.misc.int_conv(value))
|
||||
|
||||
def __call__(self) -> int:
|
||||
""" get() replacement """
|
||||
return int(self.get())
|
||||
|
||||
|
||||
class OptionDir(Option):
|
||||
""" Directory option class """
|
||||
|
||||
def __init__(
|
||||
self, section, keyword, default_val="", apply_umask=False, create=True, validation=None, writable=True, add=True
|
||||
self,
|
||||
section: str,
|
||||
keyword: str,
|
||||
default_val: str = "",
|
||||
apply_umask: bool = False,
|
||||
create: bool = True,
|
||||
validation: Optional[Callable] = None,
|
||||
writable: bool = True,
|
||||
add: bool = True,
|
||||
):
|
||||
self.__validation = validation
|
||||
self.__root = "" # Base directory for relative paths
|
||||
self.__apply_umask = apply_umask
|
||||
self.__create = create
|
||||
self.__writable = writable
|
||||
self.__validation: Optional[Callable] = validation
|
||||
self.__root: str = "" # Base directory for relative paths
|
||||
self.__apply_umask: bool = apply_umask
|
||||
self.__create: bool = create
|
||||
self.__writable: bool = writable
|
||||
super().__init__(section, keyword, default_val, add=add)
|
||||
|
||||
def get(self):
|
||||
def get(self) -> str:
|
||||
""" Return value, corrected for platform """
|
||||
p = super().get()
|
||||
if sabnzbd.WIN32:
|
||||
@@ -203,7 +213,7 @@ class OptionDir(Option):
|
||||
else:
|
||||
return p.replace("\\", "/") if "\\" in p else p
|
||||
|
||||
def get_path(self):
|
||||
def get_path(self) -> str:
|
||||
""" Return full absolute path """
|
||||
value = self.get()
|
||||
path = ""
|
||||
@@ -213,11 +223,11 @@ class OptionDir(Option):
|
||||
_, path, _ = create_real_path(self.ident()[1], self.__root, value, self.__apply_umask, self.__writable)
|
||||
return path
|
||||
|
||||
def get_clipped_path(self):
|
||||
def get_clipped_path(self) -> str:
|
||||
""" Return clipped full absolute path """
|
||||
return clip_path(self.get_path())
|
||||
|
||||
def test_path(self):
|
||||
def test_path(self) -> bool:
|
||||
""" Return True if path exists """
|
||||
value = self.get()
|
||||
if value:
|
||||
@@ -225,18 +235,18 @@ class OptionDir(Option):
|
||||
else:
|
||||
return False
|
||||
|
||||
def set_root(self, root):
|
||||
def set_root(self, root: str):
|
||||
""" Set new root, is assumed to be valid """
|
||||
self.__root = root
|
||||
|
||||
def set(self, value, create=False):
|
||||
""" Set new dir value, validate and create if needed
|
||||
Return None when directory is accepted
|
||||
Return error-string when not accepted, value will not be changed
|
||||
'create' means try to create (but don't set permanent create flag)
|
||||
def set(self, value: str, create: bool = False) -> Optional[str]:
|
||||
"""Set new dir value, validate and create if needed
|
||||
Return None when directory is accepted
|
||||
Return error-string when not accepted, value will not be changed
|
||||
'create' means try to create (but don't set permanent create flag)
|
||||
"""
|
||||
error = None
|
||||
if value and (value != self.get() or create):
|
||||
if value is not None and (value != self.get() or create):
|
||||
value = value.strip()
|
||||
if self.__validation:
|
||||
error, value = self.__validation(self.__root, value, super().default())
|
||||
@@ -249,21 +259,33 @@ class OptionDir(Option):
|
||||
super().set(value)
|
||||
return error
|
||||
|
||||
def set_create(self, value):
|
||||
def set_create(self, value: bool):
|
||||
""" Set auto-creation value """
|
||||
self.__create = value
|
||||
|
||||
def __call__(self) -> str:
|
||||
""" get() replacement """
|
||||
return self.get()
|
||||
|
||||
|
||||
class OptionList(Option):
|
||||
""" List option class """
|
||||
|
||||
def __init__(self, section, keyword, default_val=None, validation=None, add=True, protect=False):
|
||||
self.__validation = validation
|
||||
def __init__(
|
||||
self,
|
||||
section: str,
|
||||
keyword: str,
|
||||
default_val: Union[str, List, None] = None,
|
||||
validation: Optional[Callable] = None,
|
||||
add: bool = True,
|
||||
protect: bool = False,
|
||||
):
|
||||
self.__validation: Optional[Callable] = validation
|
||||
if default_val is None:
|
||||
default_val = []
|
||||
super().__init__(section, keyword, default_val, add=add, protect=protect)
|
||||
|
||||
def set(self, value):
|
||||
def set(self, value: Union[str, List]) -> Optional[str]:
|
||||
""" Set the list given a comma-separated string or a list """
|
||||
error = None
|
||||
if value is not None:
|
||||
@@ -271,47 +293,52 @@ class OptionList(Option):
|
||||
if '"' not in value and "," not in value:
|
||||
value = value.split()
|
||||
else:
|
||||
value = paramfinder.findall(value)
|
||||
value = RE_PARAMFINDER.findall(value)
|
||||
if self.__validation:
|
||||
error, value = self.__validation(value)
|
||||
if not error:
|
||||
super().set(value)
|
||||
return error
|
||||
|
||||
def get_string(self):
|
||||
def get_string(self) -> str:
|
||||
""" Return the list as a comma-separated string """
|
||||
lst = self.get()
|
||||
if isinstance(lst, str):
|
||||
return lst
|
||||
else:
|
||||
return ", ".join(lst)
|
||||
return ", ".join(self.get())
|
||||
|
||||
def default_string(self):
|
||||
def default_string(self) -> str:
|
||||
""" Return the default list as a comma-separated string """
|
||||
lst = self.default()
|
||||
if isinstance(lst, str):
|
||||
return lst
|
||||
else:
|
||||
return ", ".join(lst)
|
||||
return ", ".join(self.default())
|
||||
|
||||
def __call__(self) -> List[str]:
|
||||
""" get() replacement """
|
||||
return self.get()
|
||||
|
||||
|
||||
class OptionStr(Option):
|
||||
""" String class """
|
||||
""" String class."""
|
||||
|
||||
def __init__(self, section, keyword, default_val="", validation=None, add=True, strip=True, protect=False):
|
||||
self.__validation = validation
|
||||
self.__strip = strip
|
||||
def __init__(
|
||||
self,
|
||||
section: str,
|
||||
keyword: str,
|
||||
default_val: str = "",
|
||||
validation: Optional[Callable] = None,
|
||||
add: bool = True,
|
||||
strip: bool = True,
|
||||
protect: bool = False,
|
||||
):
|
||||
self.__validation: Optional[Callable] = validation
|
||||
self.__strip: bool = strip
|
||||
super().__init__(section, keyword, default_val, add=add, protect=protect)
|
||||
|
||||
def get_float(self):
|
||||
def get_float(self) -> float:
|
||||
""" Return value converted to a float, allowing KMGT notation """
|
||||
return sabnzbd.misc.from_units(self.get())
|
||||
|
||||
def get_int(self):
|
||||
def get_int(self) -> int:
|
||||
""" Return value converted to an int, allowing KMGT notation """
|
||||
return int(self.get_float())
|
||||
|
||||
def set(self, value):
|
||||
def set(self, value: Any) -> Optional[str]:
|
||||
""" Set stripped value """
|
||||
error = None
|
||||
if isinstance(value, str) and self.__strip:
|
||||
@@ -323,57 +350,43 @@ class OptionStr(Option):
|
||||
super().set(value)
|
||||
return error
|
||||
|
||||
def __call__(self) -> str:
|
||||
""" get() replacement """
|
||||
return self.get()
|
||||
|
||||
|
||||
class OptionPassword(Option):
|
||||
""" Password class """
|
||||
""" Password class. """
|
||||
|
||||
def __init__(self, section, keyword, default_val="", add=True):
|
||||
def __init__(self, section: str, keyword: str, default_val: str = "", add: bool = True):
|
||||
self.get_string = self.get_stars
|
||||
super().__init__(section, keyword, default_val, add=add)
|
||||
|
||||
def get(self):
|
||||
def get(self) -> Optional[str]:
|
||||
""" Return decoded password """
|
||||
return decode_password(super().get(), self.ident())
|
||||
|
||||
def get_stars(self):
|
||||
""" Return decoded password as asterisk string """
|
||||
return "*" * len(self.get())
|
||||
def get_stars(self) -> Optional[str]:
|
||||
""" Return non-descript asterisk string """
|
||||
if self.get():
|
||||
return "*" * 10
|
||||
return ""
|
||||
|
||||
def get_dict(self, safe=False):
|
||||
def get_dict(self, safe: bool = False) -> Dict[str, str]:
|
||||
""" Return value a dictionary """
|
||||
if safe:
|
||||
return {self.ident()[1]: self.get_stars()}
|
||||
else:
|
||||
return {self.ident()[1]: self.get()}
|
||||
|
||||
def set(self, pw):
|
||||
def set(self, pw: str):
|
||||
""" Set password, encode it """
|
||||
if (pw is not None and pw == "") or (pw and pw.strip("*")):
|
||||
super().set(encode_password(pw))
|
||||
return None
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def add_to_database(section, keyword, obj):
|
||||
""" add object as section/keyword to INI database """
|
||||
global database
|
||||
if section not in database:
|
||||
database[section] = {}
|
||||
database[section][keyword] = obj
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def delete_from_database(section, keyword):
|
||||
""" Remove section/keyword from INI database """
|
||||
global database, CFG, modified
|
||||
del database[section][keyword]
|
||||
if section == "servers" and "[" in keyword:
|
||||
keyword = keyword.replace("[", "{").replace("]", "}")
|
||||
try:
|
||||
del CFG[section][keyword]
|
||||
except KeyError:
|
||||
pass
|
||||
modified = True
|
||||
def __call__(self) -> str:
|
||||
""" get() replacement """
|
||||
return self.get()
|
||||
|
||||
|
||||
class ConfigServer:
|
||||
@@ -384,28 +397,28 @@ class ConfigServer:
|
||||
self.__name = name
|
||||
name = "servers," + self.__name
|
||||
|
||||
self.displayname = OptionStr(name, "displayname", "", add=False)
|
||||
self.host = OptionStr(name, "host", "", add=False)
|
||||
self.displayname = OptionStr(name, "displayname", add=False)
|
||||
self.host = OptionStr(name, "host", add=False)
|
||||
self.port = OptionNumber(name, "port", 119, 0, 2 ** 16 - 1, add=False)
|
||||
self.timeout = OptionNumber(name, "timeout", 60, 20, 240, add=False)
|
||||
self.username = OptionStr(name, "username", "", add=False)
|
||||
self.password = OptionPassword(name, "password", "", add=False)
|
||||
self.username = OptionStr(name, "username", add=False)
|
||||
self.password = OptionPassword(name, "password", add=False)
|
||||
self.connections = OptionNumber(name, "connections", 1, 0, 100, add=False)
|
||||
self.ssl = OptionBool(name, "ssl", False, add=False)
|
||||
# 0=No, 1=Normal, 2=Strict (hostname verification)
|
||||
self.ssl_verify = OptionNumber(name, "ssl_verify", 2, add=False)
|
||||
self.ssl_ciphers = OptionStr(name, "ssl_ciphers", "", add=False)
|
||||
self.ssl_ciphers = OptionStr(name, "ssl_ciphers", add=False)
|
||||
self.enable = OptionBool(name, "enable", True, add=False)
|
||||
self.optional = OptionBool(name, "optional", False, add=False)
|
||||
self.retention = OptionNumber(name, "retention", add=False)
|
||||
self.retention = OptionNumber(name, "retention", 0, add=False)
|
||||
self.send_group = OptionBool(name, "send_group", False, add=False)
|
||||
self.priority = OptionNumber(name, "priority", 0, 0, 99, add=False)
|
||||
self.notes = OptionStr(name, "notes", "", add=False)
|
||||
self.notes = OptionStr(name, "notes", add=False)
|
||||
|
||||
self.set_dict(values)
|
||||
add_to_database("servers", self.__name, self)
|
||||
|
||||
def set_dict(self, values):
|
||||
def set_dict(self, values: Dict[str, Any]):
|
||||
""" Set one or more fields, passed as dictionary """
|
||||
for kw in (
|
||||
"displayname",
|
||||
@@ -427,14 +440,13 @@ class ConfigServer:
|
||||
):
|
||||
try:
|
||||
value = values[kw]
|
||||
getattr(self, kw).set(value)
|
||||
except KeyError:
|
||||
continue
|
||||
exec("self.%s.set(value)" % kw)
|
||||
if not self.displayname():
|
||||
self.displayname.set(self.__name)
|
||||
return True
|
||||
if not self.displayname():
|
||||
self.displayname.set(self.__name)
|
||||
|
||||
def get_dict(self, safe=False):
|
||||
def get_dict(self, safe: bool = False) -> Dict[str, Any]:
|
||||
""" Return a dictionary with all attributes """
|
||||
output_dict = {}
|
||||
output_dict["name"] = self.__name
|
||||
@@ -463,23 +475,23 @@ class ConfigServer:
|
||||
""" Remove from database """
|
||||
delete_from_database("servers", self.__name)
|
||||
|
||||
def rename(self, name):
|
||||
def rename(self, name: str):
|
||||
""" Give server new display name """
|
||||
self.displayname.set(name)
|
||||
|
||||
def ident(self):
|
||||
def ident(self) -> Tuple[str, str]:
|
||||
return "servers", self.__name
|
||||
|
||||
|
||||
class ConfigCat:
|
||||
""" Class defining a single category """
|
||||
|
||||
def __init__(self, name, values):
|
||||
def __init__(self, name: str, values: Dict[str, Any]):
|
||||
self.__name = name
|
||||
name = "categories," + name
|
||||
|
||||
self.order = OptionNumber(name, "order", 0, 0, 100, add=False)
|
||||
self.pp = OptionStr(name, "pp", "", add=False)
|
||||
self.pp = OptionStr(name, "pp", add=False)
|
||||
self.script = OptionStr(name, "script", "Default", add=False)
|
||||
self.dir = OptionDir(name, "dir", add=False, create=False)
|
||||
self.newzbin = OptionList(name, "newzbin", add=False, validation=validate_single_tag)
|
||||
@@ -488,17 +500,16 @@ class ConfigCat:
|
||||
self.set_dict(values)
|
||||
add_to_database("categories", self.__name, self)
|
||||
|
||||
def set_dict(self, values):
|
||||
def set_dict(self, values: Dict[str, Any]):
|
||||
""" Set one or more fields, passed as dictionary """
|
||||
for kw in ("order", "pp", "script", "dir", "newzbin", "priority"):
|
||||
try:
|
||||
value = values[kw]
|
||||
getattr(self, kw).set(value)
|
||||
except KeyError:
|
||||
continue
|
||||
exec("self.%s.set(value)" % kw)
|
||||
return True
|
||||
|
||||
def get_dict(self, safe=False):
|
||||
def get_dict(self, safe: bool = False) -> Dict[str, Any]:
|
||||
""" Return a dictionary with all attributes """
|
||||
output_dict = {}
|
||||
output_dict["name"] = self.__name
|
||||
@@ -522,7 +533,7 @@ class OptionFilters(Option):
|
||||
super().__init__(section, keyword, add=add)
|
||||
self.set([])
|
||||
|
||||
def move(self, current, new):
|
||||
def move(self, current: int, new: int):
|
||||
""" Move filter from position 'current' to 'new' """
|
||||
lst = self.get()
|
||||
try:
|
||||
@@ -532,9 +543,9 @@ class OptionFilters(Option):
|
||||
return
|
||||
self.set(lst)
|
||||
|
||||
def update(self, pos, value):
|
||||
""" Update filter 'pos' definition, value is a list
|
||||
Append if 'pos' outside list
|
||||
def update(self, pos: int, value: Tuple):
|
||||
"""Update filter 'pos' definition, value is a list
|
||||
Append if 'pos' outside list
|
||||
"""
|
||||
lst = self.get()
|
||||
try:
|
||||
@@ -543,7 +554,7 @@ class OptionFilters(Option):
|
||||
lst.append(value)
|
||||
self.set(lst)
|
||||
|
||||
def delete(self, pos):
|
||||
def delete(self, pos: int):
|
||||
""" Remove filter 'pos' """
|
||||
lst = self.get()
|
||||
try:
|
||||
@@ -552,34 +563,27 @@ class OptionFilters(Option):
|
||||
return
|
||||
self.set(lst)
|
||||
|
||||
def get_dict(self, safe=False):
|
||||
def get_dict(self, safe: bool = False) -> Dict[str, str]:
|
||||
""" Return filter list as a dictionary with keys 'filter[0-9]+' """
|
||||
output_dict = {}
|
||||
n = 0
|
||||
for filter_name in self.get():
|
||||
output_dict["filter" + str(n)] = filter_name
|
||||
n = n + 1
|
||||
for n, rss_filter in enumerate(self.get()):
|
||||
output_dict[f"filter{n}"] = rss_filter
|
||||
return output_dict
|
||||
|
||||
def set_dict(self, values):
|
||||
def set_dict(self, values: Dict[str, Any]):
|
||||
""" Create filter list from dictionary with keys 'filter[0-9]+' """
|
||||
filters = []
|
||||
# We don't know how many filters there are, so just assume all values are filters
|
||||
for n in range(len(values)):
|
||||
kw = "filter%d" % n
|
||||
val = values.get(kw)
|
||||
if val is not None:
|
||||
val = values[kw]
|
||||
if isinstance(val, list):
|
||||
filters.append(val)
|
||||
else:
|
||||
filters.append(paramfinder.findall(val))
|
||||
while len(filters[-1]) < 7:
|
||||
filters[-1].append("1")
|
||||
if not filters[-1][6]:
|
||||
filters[-1][6] = "1"
|
||||
kw = f"filter{n}"
|
||||
if kw in values:
|
||||
filters.append(values[kw])
|
||||
if filters:
|
||||
self.set(filters)
|
||||
return True
|
||||
|
||||
def __call__(self) -> List[List[str]]:
|
||||
""" get() replacement """
|
||||
return self.get()
|
||||
|
||||
|
||||
class ConfigRSS:
|
||||
@@ -591,7 +595,7 @@ class ConfigRSS:
|
||||
|
||||
self.uri = OptionList(name, "uri", add=False)
|
||||
self.cat = OptionStr(name, "cat", add=False)
|
||||
self.pp = OptionStr(name, "pp", "", add=False)
|
||||
self.pp = OptionStr(name, "pp", add=False)
|
||||
self.script = OptionStr(name, "script", add=False)
|
||||
self.enable = OptionBool(name, "enable", add=False)
|
||||
self.priority = OptionNumber(name, "priority", DEFAULT_PRIORITY, DEFAULT_PRIORITY, 2, add=False)
|
||||
@@ -601,19 +605,17 @@ class ConfigRSS:
|
||||
self.set_dict(values)
|
||||
add_to_database("rss", self.__name, self)
|
||||
|
||||
def set_dict(self, values):
|
||||
def set_dict(self, values: Dict[str, Any]):
|
||||
""" Set one or more fields, passed as dictionary """
|
||||
for kw in ("uri", "cat", "pp", "script", "priority", "enable"):
|
||||
try:
|
||||
value = values[kw]
|
||||
getattr(self, kw).set(value)
|
||||
except KeyError:
|
||||
continue
|
||||
exec("self.%s.set(value)" % kw)
|
||||
|
||||
self.filters.set_dict(values)
|
||||
return True
|
||||
|
||||
def get_dict(self, safe=False):
|
||||
def get_dict(self, safe: bool = False) -> Dict[str, Any]:
|
||||
""" Return a dictionary with all attributes """
|
||||
output_dict = {}
|
||||
output_dict["name"] = self.__name
|
||||
@@ -632,13 +634,36 @@ class ConfigRSS:
|
||||
""" Remove from database """
|
||||
delete_from_database("rss", self.__name)
|
||||
|
||||
def ident(self):
|
||||
def ident(self) -> Tuple[str, str]:
|
||||
return "rss", self.__name
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def add_to_database(section, keyword, obj):
|
||||
""" add object as section/keyword to INI database """
|
||||
global database
|
||||
if section not in database:
|
||||
database[section] = {}
|
||||
database[section][keyword] = obj
|
||||
|
||||
|
||||
@synchronized(CONFIG_LOCK)
|
||||
def delete_from_database(section, keyword):
|
||||
""" Remove section/keyword from INI database """
|
||||
global database, CFG, modified
|
||||
del database[section][keyword]
|
||||
if section == "servers" and "[" in keyword:
|
||||
keyword = keyword.replace("[", "{").replace("]", "}")
|
||||
try:
|
||||
del CFG[section][keyword]
|
||||
except KeyError:
|
||||
pass
|
||||
modified = True
|
||||
|
||||
|
||||
def get_dconfig(section, keyword, nested=False):
|
||||
""" Return a config values dictionary,
|
||||
Single item or slices based on 'section', 'keyword'
|
||||
"""Return a config values dictionary,
|
||||
Single item or slices based on 'section', 'keyword'
|
||||
"""
|
||||
data = {}
|
||||
if not section:
|
||||
@@ -696,7 +721,7 @@ def set_config(kwargs):
|
||||
return True
|
||||
|
||||
|
||||
def delete(section, keyword):
|
||||
def delete(section: str, keyword: str):
|
||||
""" Delete specific config item """
|
||||
try:
|
||||
database[section][keyword].delete()
|
||||
@@ -712,15 +737,15 @@ def delete(section, keyword):
|
||||
##############################################################################
|
||||
@synchronized(SAVE_CONFIG_LOCK)
|
||||
def read_config(path):
|
||||
""" Read the complete INI file and check its version number
|
||||
if OK, pass values to config-database
|
||||
"""Read the complete INI file and check its version number
|
||||
if OK, pass values to config-database
|
||||
"""
|
||||
return _read_config(path)
|
||||
|
||||
|
||||
def _read_config(path, try_backup=False):
|
||||
""" Read the complete INI file and check its version number
|
||||
if OK, pass values to config-database
|
||||
"""Read the complete INI file and check its version number
|
||||
if OK, pass values to config-database
|
||||
"""
|
||||
global CFG, database, modified
|
||||
|
||||
@@ -778,9 +803,16 @@ def _read_config(path, try_backup=False):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
define_categories()
|
||||
define_rss()
|
||||
define_servers()
|
||||
# Define the special settings
|
||||
if "categories" in CFG:
|
||||
for cat in CFG["categories"]:
|
||||
ConfigCat(cat, CFG["categories"][cat])
|
||||
if "rss" in CFG:
|
||||
for rss_feed in CFG["rss"]:
|
||||
ConfigRSS(rss_feed, CFG["rss"][rss_feed])
|
||||
if "servers" in CFG:
|
||||
for server in CFG["servers"]:
|
||||
ConfigServer(server.replace("{", "[").replace("}", "]"), CFG["servers"][server])
|
||||
|
||||
modified = False
|
||||
return True, ""
|
||||
@@ -825,13 +857,7 @@ def save_config(force=False):
|
||||
CFG[sec] = {}
|
||||
value = database[section][option]()
|
||||
# bool is a subclass of int, check first
|
||||
if isinstance(value, bool):
|
||||
# convert bool to int when saving so we store 0 or 1
|
||||
CFG[sec][kw] = str(int(value))
|
||||
elif isinstance(value, int):
|
||||
CFG[sec][kw] = str(value)
|
||||
else:
|
||||
CFG[sec][kw] = value
|
||||
CFG[sec][kw] = value
|
||||
|
||||
res = False
|
||||
filename = CFG.filename
|
||||
@@ -872,27 +898,7 @@ def save_config(force=False):
|
||||
return res
|
||||
|
||||
|
||||
def define_servers():
|
||||
""" Define servers listed in the Setup file
|
||||
return a list of ConfigServer instances
|
||||
"""
|
||||
global CFG
|
||||
try:
|
||||
for server in CFG["servers"]:
|
||||
svr = CFG["servers"][server]
|
||||
s = ConfigServer(server.replace("{", "[").replace("}", "]"), svr)
|
||||
|
||||
# Conversion of global SSL-Ciphers to server ones
|
||||
if sabnzbd.cfg.ssl_ciphers():
|
||||
s.ssl_ciphers.set(sabnzbd.cfg.ssl_ciphers())
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# No longer needed
|
||||
sabnzbd.cfg.ssl_ciphers.set("")
|
||||
|
||||
|
||||
def get_servers():
|
||||
def get_servers() -> Dict[str, ConfigServer]:
|
||||
global database
|
||||
try:
|
||||
return database["servers"]
|
||||
@@ -900,22 +906,9 @@ def get_servers():
|
||||
return {}
|
||||
|
||||
|
||||
def define_categories():
|
||||
""" Define categories listed in the Setup file
|
||||
return a list of ConfigCat instances
|
||||
"""
|
||||
global CFG, categories
|
||||
try:
|
||||
for cat in CFG["categories"]:
|
||||
ConfigCat(cat, CFG["categories"][cat])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
def get_categories(cat=0):
|
||||
""" Return link to categories section.
|
||||
This section will always contain special category '*'
|
||||
When 'cat' is given, a link to that category or to '*' is returned
|
||||
def get_categories() -> Dict[str, ConfigCat]:
|
||||
"""Return link to categories section.
|
||||
This section will always contain special category '*'
|
||||
"""
|
||||
global database
|
||||
if "categories" not in database:
|
||||
@@ -933,17 +926,21 @@ def get_categories(cat=0):
|
||||
|
||||
# Save config for future use
|
||||
save_config(True)
|
||||
if not isinstance(cat, int):
|
||||
try:
|
||||
cats = cats[cat]
|
||||
except KeyError:
|
||||
cats = cats["*"]
|
||||
return cats
|
||||
|
||||
|
||||
def get_ordered_categories():
|
||||
""" Return list-copy of categories section that's ordered
|
||||
by user's ordering including Default-category
|
||||
def get_category(cat: str = "*") -> ConfigCat:
|
||||
"""Get one specific category or if not found the default one"""
|
||||
cats = get_categories()
|
||||
try:
|
||||
return cats[cat]
|
||||
except KeyError:
|
||||
return cats["*"]
|
||||
|
||||
|
||||
def get_ordered_categories() -> List[Dict]:
|
||||
"""Return list-copy of categories section that's ordered
|
||||
by user's ordering including Default-category
|
||||
"""
|
||||
database_cats = get_categories()
|
||||
|
||||
@@ -960,22 +957,10 @@ def get_ordered_categories():
|
||||
return categories
|
||||
|
||||
|
||||
def define_rss():
|
||||
""" Define rss-feeds listed in the Setup file
|
||||
return a list of ConfigRSS instances
|
||||
"""
|
||||
global CFG
|
||||
try:
|
||||
for r in CFG["rss"]:
|
||||
ConfigRSS(r, CFG["rss"][r])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
def get_rss():
|
||||
def get_rss() -> Dict[str, ConfigRSS]:
|
||||
global database
|
||||
try:
|
||||
# We have to remove non-seperator commas by detecting if they are valid URL's
|
||||
# We have to remove non-separator commas by detecting if they are valid URL's
|
||||
for feed_key in database["rss"]:
|
||||
feed = database["rss"][feed_key]
|
||||
# Only modify if we have to, to prevent repeated config-saving
|
||||
@@ -1033,8 +1018,8 @@ def encode_password(pw):
|
||||
|
||||
|
||||
def decode_password(pw, name):
|
||||
""" Decode hexadecimal encoded password
|
||||
but only decode when prefixed
|
||||
"""Decode hexadecimal encoded password
|
||||
but only decode when prefixed
|
||||
"""
|
||||
decPW = ""
|
||||
if pw and pw.startswith(__PW_PREFIX):
|
||||
@@ -1102,12 +1087,8 @@ def validate_no_unc(root, value, default):
|
||||
|
||||
|
||||
def validate_safedir(root, value, default):
|
||||
""" Allow only when queues are empty and no UNC
|
||||
On Windows path should be small
|
||||
"""
|
||||
if sabnzbd.WIN32 and value and len(real_path(root, value)) >= MAX_WIN_DFOLDER:
|
||||
return T("Error: Path length should be below %s.") % MAX_WIN_DFOLDER, None
|
||||
if sabnzbd.empty_queues():
|
||||
"""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)
|
||||
else:
|
||||
return T("Error: Queue not empty, cannot change folder."), None
|
||||
@@ -1121,9 +1102,16 @@ def validate_notempty(root, value, default):
|
||||
return None, default
|
||||
|
||||
|
||||
def validate_strip_right_slash(value):
|
||||
"""Strips the right slash"""
|
||||
if value:
|
||||
return None, value.rstrip("/")
|
||||
return None, value
|
||||
|
||||
|
||||
def validate_single_tag(value):
|
||||
""" Don't split single indexer tags like "TV > HD"
|
||||
into ['TV', '>', 'HD']
|
||||
"""Don't split single indexer tags like "TV > HD"
|
||||
into ['TV', '>', 'HD']
|
||||
"""
|
||||
if len(value) == 3:
|
||||
if value[1] == ">":
|
||||
|
||||
@@ -24,7 +24,7 @@ CONFIG_VERSION = 19
|
||||
QUEUE_VERSION = 10
|
||||
POSTPROC_QUEUE_VERSION = 2
|
||||
|
||||
REC_RAR_VERSION = 500
|
||||
REC_RAR_VERSION = 550
|
||||
|
||||
PNFO = namedtuple(
|
||||
"PNFO",
|
||||
@@ -37,6 +37,10 @@ QNFO = namedtuple("QNFO", "bytes bytes_left bytes_left_previous_page list q_size
|
||||
|
||||
ANFO = namedtuple("ANFO", "article_sum cache_size cache_limit")
|
||||
|
||||
# Leave some space for "_UNPACK_" which we append during post-proc
|
||||
# Or, when extra ".1", ".2" etc. are added for identically named jobs
|
||||
DEF_FOLDER_MAX = 256 - 10
|
||||
|
||||
GIGI = float(2 ** 30)
|
||||
MEBI = float(2 ** 20)
|
||||
KIBI = float(2 ** 10)
|
||||
@@ -48,6 +52,7 @@ QUEUE_FILE_NAME = QUEUE_FILE_TMPL % QUEUE_VERSION
|
||||
POSTPROC_QUEUE_FILE_NAME = "postproc%s.sab" % POSTPROC_QUEUE_VERSION
|
||||
RSS_FILE_NAME = "rss_data.sab"
|
||||
SCAN_FILE_NAME = "watched_data2.sab"
|
||||
RATING_FILE_NAME = "Rating.sab"
|
||||
FUTURE_Q_FOLDER = "future"
|
||||
JOB_ADMIN = "__ADMIN__"
|
||||
VERIFIED_FILE = "__verified__"
|
||||
@@ -83,7 +88,6 @@ DEF_ARTICLE_CACHE_MAX = "1G"
|
||||
DEF_TIMEOUT = 60
|
||||
DEF_SCANRATE = 5
|
||||
MAX_WARNINGS = 20
|
||||
MAX_WIN_DFOLDER = 60
|
||||
MAX_BAD_ARTICLES = 5
|
||||
|
||||
# Constants affecting download performance
|
||||
@@ -93,7 +97,7 @@ DIRECT_WRITE_TRIGGER = 35
|
||||
MAX_ASSEMBLER_QUEUE = 5
|
||||
|
||||
REPAIR_PRIORITY = 3
|
||||
TOP_PRIORITY = 2
|
||||
FORCE_PRIORITY = 2
|
||||
HIGH_PRIORITY = 1
|
||||
NORMAL_PRIORITY = 0
|
||||
LOW_PRIORITY = -1
|
||||
@@ -102,6 +106,14 @@ PAUSED_PRIORITY = -2
|
||||
DUP_PRIORITY = -3
|
||||
STOP_PRIORITY = -4
|
||||
|
||||
INTERFACE_PRIORITIES = {
|
||||
FORCE_PRIORITY: "Force",
|
||||
REPAIR_PRIORITY: "Repair",
|
||||
HIGH_PRIORITY: "High",
|
||||
NORMAL_PRIORITY: "Normal",
|
||||
LOW_PRIORITY: "Low",
|
||||
}
|
||||
|
||||
STAGES = {"Source": 0, "Download": 1, "Servers": 2, "Repair": 3, "Filejoin": 4, "Unpack": 5, "Script": 6}
|
||||
|
||||
VALID_ARCHIVES = (".zip", ".rar", ".7z")
|
||||
@@ -111,6 +123,8 @@ CHEETAH_DIRECTIVES = {"directiveStartToken": "<!--#", "directiveEndToken": "#-->
|
||||
|
||||
IGNORED_FOLDERS = ("@eaDir", ".appleDouble")
|
||||
|
||||
LOCALHOSTS = ("localhost", "127.0.0.1", "[::1]", "::1")
|
||||
|
||||
# (MATCHER, [EXTRA, MATCHERS])
|
||||
series_match = [
|
||||
(compile(r"( [sS]|[\d]+)x(\d+)"), [compile(r"^[-\.]+([sS]|[\d])+x(\d+)"), compile(r"^[-\.](\d+)")]), # 1x01
|
||||
@@ -139,25 +153,10 @@ class Status:
|
||||
GRABBING = "Grabbing" # Q: Getting an NZB from an external site
|
||||
MOVING = "Moving" # PP: Files are being moved
|
||||
PAUSED = "Paused" # Q: Job is paused
|
||||
QUEUED = "Queued" # Q: Job is waiting for its turn to download
|
||||
QUEUED = "Queued" # Q: Job is waiting for its turn to download or post-process
|
||||
QUICK_CHECK = "QuickCheck" # PP: QuickCheck verification is running
|
||||
REPAIRING = "Repairing" # PP: Job is being repaired (by par2)
|
||||
RUNNING = "Running" # PP: User's post processing script is running
|
||||
VERIFYING = "Verifying" # PP: Job is being verified (by par2)
|
||||
DELETED = "Deleted" # Q: Job has been deleted (and is almost gone)
|
||||
PROP = "Propagating" # Q: Delayed download
|
||||
|
||||
|
||||
NOTIFY_KEYS = (
|
||||
"startup",
|
||||
"download",
|
||||
"pp",
|
||||
"complete",
|
||||
"failed",
|
||||
"queue_done",
|
||||
"disk_full",
|
||||
"new_login",
|
||||
"warning",
|
||||
"error",
|
||||
"other",
|
||||
)
|
||||
|
||||
@@ -26,6 +26,7 @@ import logging
|
||||
import sys
|
||||
import threading
|
||||
import sqlite3
|
||||
from typing import Union, Dict
|
||||
|
||||
import sabnzbd
|
||||
import sabnzbd.cfg
|
||||
@@ -34,7 +35,7 @@ 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.filesystem import remove_file
|
||||
from sabnzbd.filesystem import remove_file, clip_path
|
||||
|
||||
DB_LOCK = threading.RLock()
|
||||
|
||||
@@ -61,10 +62,10 @@ def convert_search(search):
|
||||
|
||||
|
||||
class HistoryDB:
|
||||
""" Class to access the History database
|
||||
Each class-instance will create an access channel that
|
||||
can be used in one thread.
|
||||
Each thread needs its own class-instance!
|
||||
"""Class to access the History database
|
||||
Each class-instance will create an access channel that
|
||||
can be used in one thread.
|
||||
Each thread needs its own class-instance!
|
||||
"""
|
||||
|
||||
# These class attributes will be accessed directly because
|
||||
@@ -84,7 +85,7 @@ class HistoryDB:
|
||||
""" Create a connection to the database """
|
||||
create_table = not os.path.exists(HistoryDB.db_path)
|
||||
self.con = sqlite3.connect(HistoryDB.db_path)
|
||||
self.con.row_factory = dict_factory
|
||||
self.con.row_factory = sqlite3.Row
|
||||
self.c = self.con.cursor()
|
||||
if create_table:
|
||||
self.create_history_db()
|
||||
@@ -98,7 +99,7 @@ class HistoryDB:
|
||||
self.execute("PRAGMA user_version;")
|
||||
try:
|
||||
version = self.c.fetchone()["user_version"]
|
||||
except TypeError:
|
||||
except IndexError:
|
||||
version = 0
|
||||
if version < 1:
|
||||
# Add any missing columns added since first DB version
|
||||
@@ -220,7 +221,7 @@ class HistoryDB:
|
||||
"""SELECT path FROM history WHERE name LIKE ? AND status = ?""", (search, Status.FAILED)
|
||||
)
|
||||
if fetch_ok:
|
||||
return [item.get("path") for item in self.c.fetchall()]
|
||||
return [item["path"] for item in self.c.fetchall()]
|
||||
else:
|
||||
return []
|
||||
|
||||
@@ -275,15 +276,15 @@ class HistoryDB:
|
||||
save=True,
|
||||
)
|
||||
|
||||
def add_history_db(self, nzo, storage="", path="", postproc_time=0, script_output="", script_line=""):
|
||||
def add_history_db(self, nzo, storage="", postproc_time=0, script_output="", script_line=""):
|
||||
""" Add a new job entry to the database """
|
||||
t = build_history_info(nzo, storage, path, postproc_time, script_output, script_line, series_info=True)
|
||||
t = build_history_info(nzo, storage, postproc_time, script_output, script_line, series_info=True)
|
||||
|
||||
self.execute(
|
||||
"""INSERT INTO history (completed, name, nzb_name, category, pp, script, report,
|
||||
url, status, nzo_id, storage, path, script_log, script_line, download_time, postproc_time, stage_log,
|
||||
downloaded, completeness, fail_message, url_info, bytes, series, md5sum, password)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
downloaded, fail_message, url_info, bytes, series, md5sum, password)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
t,
|
||||
save=True,
|
||||
)
|
||||
@@ -309,8 +310,8 @@ class HistoryDB:
|
||||
total_items = -1
|
||||
if res:
|
||||
try:
|
||||
total_items = self.c.fetchone().get("COUNT(*)")
|
||||
except AttributeError:
|
||||
total_items = self.c.fetchone()["COUNT(*)"]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
if not start:
|
||||
@@ -342,36 +343,39 @@ class HistoryDB:
|
||||
if series and season and episode:
|
||||
pattern = "%s/%s/%s" % (series, season, episode)
|
||||
res = self.execute(
|
||||
"select count(*) from History WHERE series = ? AND STATUS != ?", (pattern, Status.FAILED)
|
||||
"""SELECT COUNT(*) FROM History WHERE series = ? AND STATUS != ?""", (pattern, Status.FAILED)
|
||||
)
|
||||
if res:
|
||||
try:
|
||||
total = self.c.fetchone().get("count(*)")
|
||||
except AttributeError:
|
||||
total = self.c.fetchone()["COUNT(*)"]
|
||||
except IndexError:
|
||||
pass
|
||||
return total > 0
|
||||
|
||||
def have_name_or_md5sum(self, name, md5sum):
|
||||
""" Check whether this name or md5sum is already in History """
|
||||
total = 0
|
||||
res = self.execute("select count(*) from History WHERE md5sum = ? AND STATUS != ?", (md5sum, Status.FAILED))
|
||||
res = self.execute(
|
||||
"""SELECT COUNT(*) FROM History WHERE ( LOWER(name) = LOWER(?) OR md5sum = ? ) AND STATUS != ?""",
|
||||
(name, md5sum, Status.FAILED),
|
||||
)
|
||||
if res:
|
||||
try:
|
||||
total = self.c.fetchone().get("count(*)")
|
||||
except AttributeError:
|
||||
total = self.c.fetchone()["COUNT(*)"]
|
||||
except IndexError:
|
||||
pass
|
||||
return total > 0
|
||||
|
||||
def get_history_size(self):
|
||||
""" Returns the total size of the history and
|
||||
amounts downloaded in the last month and week
|
||||
"""Returns the total size of the history and
|
||||
amounts downloaded in the last month and week
|
||||
"""
|
||||
# Total Size of the history
|
||||
total = 0
|
||||
if self.execute("""SELECT sum(bytes) FROM history"""):
|
||||
try:
|
||||
total = self.c.fetchone().get("sum(bytes)")
|
||||
except AttributeError:
|
||||
total = self.c.fetchone()["sum(bytes)"]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
# Amount downloaded this month
|
||||
@@ -382,8 +386,8 @@ class HistoryDB:
|
||||
month = 0
|
||||
if self.execute("""SELECT sum(bytes) FROM history WHERE completed > ?""", (month_timest,)):
|
||||
try:
|
||||
month = self.c.fetchone().get("sum(bytes)")
|
||||
except AttributeError:
|
||||
month = self.c.fetchone()["sum(bytes)"]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
# Amount downloaded this week
|
||||
@@ -392,8 +396,8 @@ class HistoryDB:
|
||||
week = 0
|
||||
if self.execute("""SELECT sum(bytes) FROM history WHERE completed > ?""", (week_timest,)):
|
||||
try:
|
||||
week = self.c.fetchone().get("sum(bytes)")
|
||||
except AttributeError:
|
||||
week = self.c.fetchone()["sum(bytes)"]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return total, month, week
|
||||
@@ -402,9 +406,9 @@ class HistoryDB:
|
||||
""" 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 = ?""", t):
|
||||
try:
|
||||
data = ubtou(zlib.decompress(self.c.fetchone().get("script_log")))
|
||||
data = ubtou(zlib.decompress(self.c.fetchone()["script_log"]))
|
||||
except:
|
||||
pass
|
||||
return data
|
||||
@@ -413,10 +417,10 @@ class HistoryDB:
|
||||
""" 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 = ?""", t):
|
||||
try:
|
||||
name = self.c.fetchone().get("name")
|
||||
except AttributeError:
|
||||
name = self.c.fetchone()["name"]
|
||||
except IndexError:
|
||||
pass
|
||||
return name
|
||||
|
||||
@@ -424,9 +428,9 @@ class HistoryDB:
|
||||
""" Return the `incomplete` path of the job `nzo_id` if it is still there """
|
||||
t = (nzo_id,)
|
||||
path = ""
|
||||
if self.execute("SELECT path FROM history WHERE nzo_id = ?", t):
|
||||
if self.execute("""SELECT path FROM history WHERE nzo_id = ?""", t):
|
||||
try:
|
||||
path = self.c.fetchone().get("path")
|
||||
path = self.c.fetchone()["path"]
|
||||
except AttributeError:
|
||||
pass
|
||||
if os.path.exists(path):
|
||||
@@ -436,38 +440,29 @@ class HistoryDB:
|
||||
def get_other(self, nzo_id):
|
||||
""" 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 = ?""", t):
|
||||
try:
|
||||
items = self.c.fetchone()
|
||||
dtype = items.get("report")
|
||||
url = items.get("url")
|
||||
pp = items.get("pp")
|
||||
script = items.get("script")
|
||||
cat = items.get("category")
|
||||
return dtype, url, pp, script, cat
|
||||
item = self.c.fetchone()
|
||||
return item["report"], item["url"], item["pp"], item["script"], item["category"]
|
||||
except (AttributeError, IndexError):
|
||||
pass
|
||||
return "", "", "", "", ""
|
||||
|
||||
def __enter__(self):
|
||||
""" For context manager support """
|
||||
return self
|
||||
|
||||
def dict_factory(cursor, row):
|
||||
""" Return a dictionary for the current database position """
|
||||
d = {}
|
||||
for idx, col in enumerate(cursor.description):
|
||||
d[col[0]] = row[idx]
|
||||
return d
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
""" For context manager support, ignore any exception """
|
||||
self.close()
|
||||
|
||||
|
||||
_PP_LOOKUP = {0: "", 1: "R", 2: "U", 3: "D"}
|
||||
|
||||
|
||||
def build_history_info(
|
||||
nzo, storage="", downpath="", postproc_time=0, script_output="", script_line="", series_info=False
|
||||
):
|
||||
def build_history_info(nzo, workdir_complete="", postproc_time=0, script_output="", script_line="", series_info=False):
|
||||
""" Collects all the information needed for the database """
|
||||
completed = int(time.time())
|
||||
if not downpath:
|
||||
downpath = nzo.downpath
|
||||
pp = _PP_LOOKUP.get(opts_to_pp(*nzo.repair_opts), "X")
|
||||
|
||||
if script_output:
|
||||
@@ -475,7 +470,6 @@ def build_history_info(
|
||||
script_output = sqlite3.Binary(zlib.compress(utob(script_output)))
|
||||
|
||||
download_time = nzo.nzo_info.get("download_time", 0)
|
||||
completeness = 0
|
||||
url_info = nzo.nzo_info.get("details", "") or nzo.nzo_info.get("more_info", "")
|
||||
|
||||
# Get the dictionary containing the stages and their unpack process
|
||||
@@ -507,15 +501,14 @@ def build_history_info(
|
||||
nzo.url,
|
||||
nzo.status,
|
||||
nzo.nzo_id,
|
||||
storage,
|
||||
downpath,
|
||||
clip_path(workdir_complete),
|
||||
clip_path(nzo.download_path),
|
||||
script_output,
|
||||
script_line,
|
||||
download_time,
|
||||
postproc_time,
|
||||
stage_log,
|
||||
nzo.bytes_downloaded,
|
||||
completeness,
|
||||
nzo.fail_msg,
|
||||
url_info,
|
||||
nzo.bytes_downloaded,
|
||||
@@ -525,44 +518,42 @@ def build_history_info(
|
||||
)
|
||||
|
||||
|
||||
def unpack_history_info(item):
|
||||
""" Expands the single line stage_log from the DB
|
||||
into a python dictionary for use in the history display
|
||||
def unpack_history_info(item: Union[Dict, sqlite3.Row]):
|
||||
"""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)
|
||||
|
||||
# Stage Name is separated by ::: stage lines by ; and stages by \r\n
|
||||
lst = item["stage_log"]
|
||||
if lst:
|
||||
parsed_stage_log = []
|
||||
try:
|
||||
lines = lst.split("\r\n")
|
||||
all_stages_lines = lst.split("\r\n")
|
||||
except:
|
||||
logging.error(T("Invalid stage logging in history for %s") + " (\\r\\n)", item["name"])
|
||||
logging.error(T("Invalid stage logging in history for %s"), item["name"])
|
||||
logging.debug("Lines: %s", lst)
|
||||
lines = []
|
||||
lst = [None for x in STAGES]
|
||||
for line in lines:
|
||||
stage = {}
|
||||
all_stages_lines = []
|
||||
|
||||
for stage_lines in all_stages_lines:
|
||||
try:
|
||||
key, logs = line.split(":::")
|
||||
key, logs = stage_lines.split(":::")
|
||||
except:
|
||||
logging.debug('Missing key:::logs "%s"', line)
|
||||
key = line
|
||||
logs = ""
|
||||
stage["name"] = key
|
||||
stage["actions"] = []
|
||||
logging.info('Missing key:::logs "%s"', stage_lines)
|
||||
continue
|
||||
stage = {"name": key, "actions": []}
|
||||
try:
|
||||
logs = logs.split(";")
|
||||
stage["actions"] = logs.split(";")
|
||||
except:
|
||||
logging.error(T("Invalid stage logging in history for %s") + " (;)", item["name"])
|
||||
logging.error(T("Invalid stage logging in history for %s"), item["name"])
|
||||
logging.debug("Logs: %s", logs)
|
||||
logs = []
|
||||
for log in logs:
|
||||
stage["actions"].append(log)
|
||||
try:
|
||||
lst[STAGES[key]] = stage
|
||||
except KeyError:
|
||||
lst.append(stage)
|
||||
# Remove unused stages
|
||||
item["stage_log"] = [x for x in lst if x is not None]
|
||||
parsed_stage_log.append(stage)
|
||||
|
||||
# Sort it so it is more logical
|
||||
parsed_stage_log.sort(key=lambda stage_log: STAGES.get(stage_log["name"], 100))
|
||||
item["stage_log"] = parsed_stage_log
|
||||
|
||||
if item["script_log"]:
|
||||
item["script_log"] = ""
|
||||
@@ -574,6 +565,5 @@ def unpack_history_info(item):
|
||||
|
||||
def midnight_history_purge():
|
||||
logging.info("Scheduled history purge")
|
||||
history_db = HistoryDB()
|
||||
history_db.auto_history_purge()
|
||||
history_db.close()
|
||||
with HistoryDB() as history_db:
|
||||
history_db.auto_history_purge()
|
||||
|
||||
@@ -23,13 +23,12 @@ import logging
|
||||
import hashlib
|
||||
import queue
|
||||
from threading import Thread
|
||||
from typing import Tuple, List, Optional
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.constants import SABYENC_VERSION_REQUIRED
|
||||
from sabnzbd.articlecache import ArticleCache
|
||||
from sabnzbd.downloader import Downloader
|
||||
from sabnzbd.nzbqueue import NzbQueue
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.constants import SABYENC_VERSION_REQUIRED
|
||||
from sabnzbd.nzbstuff import Article
|
||||
from sabnzbd.misc import match_str
|
||||
|
||||
# Check for correct SABYenc version
|
||||
@@ -62,8 +61,6 @@ class BadYenc(Exception):
|
||||
class Decoder:
|
||||
""" Implement thread-like coordinator for the decoders """
|
||||
|
||||
do = None
|
||||
|
||||
def __init__(self):
|
||||
logging.debug("Initializing decoders")
|
||||
# Initialize queue and servers
|
||||
@@ -73,13 +70,12 @@ class Decoder:
|
||||
self.decoder_workers = []
|
||||
for i in range(cfg.num_decoders()):
|
||||
self.decoder_workers.append(DecoderWorker(self.decoder_queue))
|
||||
Decoder.do = self
|
||||
|
||||
def start(self):
|
||||
for decoder_worker in self.decoder_workers:
|
||||
decoder_worker.start()
|
||||
|
||||
def is_alive(self):
|
||||
def is_alive(self) -> bool:
|
||||
# Check all workers
|
||||
for decoder_worker in self.decoder_workers:
|
||||
if not decoder_worker.is_alive():
|
||||
@@ -89,7 +85,7 @@ class Decoder:
|
||||
def stop(self):
|
||||
# Put multiple to stop all decoders
|
||||
for _ in self.decoder_workers:
|
||||
self.decoder_queue.put(None)
|
||||
self.decoder_queue.put((None, None))
|
||||
|
||||
def join(self):
|
||||
# Wait for all decoders to finish
|
||||
@@ -99,14 +95,14 @@ class Decoder:
|
||||
except:
|
||||
pass
|
||||
|
||||
def process(self, article, raw_data):
|
||||
def process(self, article: Article, raw_data: List[bytes]):
|
||||
# We use reported article-size, just like sabyenc does
|
||||
ArticleCache.do.reserve_space(article.bytes)
|
||||
sabnzbd.ArticleCache.reserve_space(article.bytes)
|
||||
self.decoder_queue.put((article, raw_data))
|
||||
|
||||
def queue_full(self):
|
||||
def queue_full(self) -> bool:
|
||||
# Check if the queue size exceeds the limits
|
||||
return self.decoder_queue.qsize() >= ArticleCache.do.decoder_cache_article_limit
|
||||
return self.decoder_queue.qsize() >= sabnzbd.ArticleCache.decoder_cache_article_limit
|
||||
|
||||
|
||||
class DecoderWorker(Thread):
|
||||
@@ -116,30 +112,25 @@ class DecoderWorker(Thread):
|
||||
Thread.__init__(self)
|
||||
logging.debug("Initializing decoder %s", self.name)
|
||||
|
||||
self.decoder_queue = decoder_queue
|
||||
|
||||
def stop(self):
|
||||
# Put multiple to stop all decoders
|
||||
self.decoder_queue.put(None)
|
||||
self.decoder_queue.put(None)
|
||||
self.decoder_queue: queue.Queue[Tuple[Optional[Article], Optional[List[bytes]]]] = decoder_queue
|
||||
|
||||
def run(self):
|
||||
while 1:
|
||||
# Let's get to work!
|
||||
art_tup = self.decoder_queue.get()
|
||||
if not art_tup:
|
||||
# Set Article and NzbObject objects to None so references from this
|
||||
# thread do not keep the parent objects alive (see #1628)
|
||||
decoded_data = raw_data = article = nzo = None
|
||||
article, raw_data = self.decoder_queue.get()
|
||||
if not article:
|
||||
logging.info("Shutting down decoder %s", self.name)
|
||||
break
|
||||
|
||||
article, raw_data = art_tup
|
||||
nzo = article.nzf.nzo
|
||||
art_id = article.article
|
||||
|
||||
# Free space in the decoder-queue
|
||||
ArticleCache.do.free_reserved_space(article.bytes)
|
||||
sabnzbd.ArticleCache.free_reserved_space(article.bytes)
|
||||
|
||||
# Keeping track
|
||||
decoded_data = None
|
||||
article_success = False
|
||||
|
||||
try:
|
||||
@@ -155,12 +146,12 @@ class DecoderWorker(Thread):
|
||||
except MemoryError:
|
||||
logging.warning(T("Decoder failure: Out of memory"))
|
||||
logging.info("Decoder-Queue: %d", self.decoder_queue.qsize())
|
||||
logging.info("Cache: %d, %d, %d", *ArticleCache.do.cache_info())
|
||||
logging.info("Cache: %d, %d, %d", *sabnzbd.ArticleCache.cache_info())
|
||||
logging.info("Traceback: ", exc_info=True)
|
||||
Downloader.do.pause()
|
||||
sabnzbd.Downloader.pause()
|
||||
|
||||
# This article should be fetched again
|
||||
NzbQueue.do.reset_try_lists(article)
|
||||
sabnzbd.NzbQueue.reset_try_lists(article)
|
||||
continue
|
||||
|
||||
except CrcError:
|
||||
@@ -193,7 +184,7 @@ class DecoderWorker(Thread):
|
||||
logme = T("UUencode detected, only yEnc encoding is supported [%s]") % nzo.final_name
|
||||
logging.error(logme)
|
||||
nzo.fail_msg = logme
|
||||
NzbQueue.do.end_job(nzo)
|
||||
sabnzbd.NzbQueue.end_job(nzo)
|
||||
break
|
||||
|
||||
# Pre-check, proper article found so just register
|
||||
@@ -219,12 +210,12 @@ class DecoderWorker(Thread):
|
||||
if decoded_data:
|
||||
# If the data needs to be written to disk due to full cache, this will be slow
|
||||
# Causing the decoder-queue to fill up and delay the downloader
|
||||
ArticleCache.do.save_article(article, decoded_data)
|
||||
sabnzbd.ArticleCache.save_article(article, decoded_data)
|
||||
|
||||
NzbQueue.do.register_article(article, article_success)
|
||||
sabnzbd.NzbQueue.register_article(article, article_success)
|
||||
|
||||
|
||||
def decode(article, raw_data):
|
||||
def decode(article: Article, raw_data: List[bytes]) -> bytes:
|
||||
# Let SABYenc do all the heavy lifting
|
||||
decoded_data, yenc_filename, crc, crc_expected, crc_correct = sabyenc3.decode_usenet_chunks(raw_data, article.bytes)
|
||||
|
||||
@@ -251,7 +242,7 @@ def decode(article, raw_data):
|
||||
return decoded_data
|
||||
|
||||
|
||||
def search_new_server(article):
|
||||
def search_new_server(article: Article) -> bool:
|
||||
""" Shorthand for searching new server or else increasing bad_articles """
|
||||
# Continue to the next one if we found new server
|
||||
if not article.search_new_server():
|
||||
|
||||
172
sabnzbd/deobfuscate_filenames.py
Executable file
172
sabnzbd/deobfuscate_filenames.py
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/python3 -OO
|
||||
# Copyright 2007-2020 The SABnzbd-Team <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.
|
||||
|
||||
"""
|
||||
|
||||
Deobfuscation post-processing script:
|
||||
|
||||
Will check in the completed job folder if maybe there are par2 files,
|
||||
for example "rename.par2", and use those to rename the files.
|
||||
If there is no "rename.par2" available, it will rename large, not-excluded
|
||||
files to the job-name in the queue if the filename looks obfuscated
|
||||
|
||||
Based on work by P1nGu1n
|
||||
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from sabnzbd.filesystem import get_unique_filename, renamer, get_ext
|
||||
from sabnzbd.par2file import is_parfile, parse_par2_file
|
||||
|
||||
# Files to exclude and minimal file size for renaming
|
||||
EXCLUDED_FILE_EXTS = (".vob", ".rar", ".par2", ".mts", ".m2ts", ".cpi", ".clpi", ".mpl", ".mpls", ".bdm", ".bdmv")
|
||||
MIN_FILE_SIZE = 10 * 1024 * 1024
|
||||
|
||||
|
||||
def decode_par2(parfile):
|
||||
""" Parse a par2 file and rename files listed in the par2 to their real name """
|
||||
# Check if really a par2 file
|
||||
if not is_parfile(parfile):
|
||||
logging.info("Par2 file %s was not really a par2 file")
|
||||
return False
|
||||
|
||||
# Parse the par2 file
|
||||
md5of16k = {}
|
||||
parse_par2_file(parfile, md5of16k)
|
||||
|
||||
# Parse all files in the folder
|
||||
dirname = os.path.dirname(parfile)
|
||||
result = False
|
||||
for fn in os.listdir(dirname):
|
||||
filepath = os.path.join(dirname, fn)
|
||||
# Only check files
|
||||
if os.path.isfile(filepath):
|
||||
with open(filepath, "rb") as fileToMatch:
|
||||
first16k_data = fileToMatch.read(16384)
|
||||
|
||||
# Check if we have this hash
|
||||
file_md5of16k = hashlib.md5(first16k_data).digest()
|
||||
if file_md5of16k in md5of16k:
|
||||
new_path = os.path.join(dirname, md5of16k[file_md5of16k])
|
||||
# Make sure it's a unique name
|
||||
renamer(filepath, get_unique_filename(new_path))
|
||||
result = True
|
||||
return result
|
||||
|
||||
|
||||
def is_probably_obfuscated(myinputfilename):
|
||||
"""Returns boolean if filename is likely obfuscated. Default: True
|
||||
myinputfilename (string) can be a plain file name, or a full path"""
|
||||
|
||||
# Find filebasename
|
||||
path, filename = os.path.split(myinputfilename)
|
||||
filebasename, fileextension = os.path.splitext(filename)
|
||||
|
||||
# First fixed patterns that we know of:
|
||||
|
||||
# ...blabla.H.264/b082fa0beaa644d3aa01045d5b8d0b36.mkv is certainly obfuscated
|
||||
if re.findall(r"^[a-f0-9]{32}$", filebasename):
|
||||
logging.debug("Obfuscated: 32 hex digit")
|
||||
# exactly 32 hex digits, so:
|
||||
return True
|
||||
|
||||
# /some/thing/abc.xyz.a4c567edbcbf27.BLA is certainly obfuscated
|
||||
if re.findall(r"^abc\.xyz", filebasename):
|
||||
logging.debug("Obfuscated: starts with 'abc.xyz'")
|
||||
# ... which we consider as obfuscated:
|
||||
return True
|
||||
|
||||
# these are signals for the obfuscation versus non-obfuscation
|
||||
decimals = sum(1 for c in filebasename if c.isnumeric())
|
||||
upperchars = sum(1 for c in filebasename if c.isupper())
|
||||
lowerchars = sum(1 for c in filebasename if c.islower())
|
||||
spacesdots = sum(1 for c in filebasename if c == " " or c == ".")
|
||||
|
||||
# Example: "Great Distro"
|
||||
if upperchars >= 2 and lowerchars >= 2 and spacesdots >= 1:
|
||||
logging.debug("Not obfuscated: upperchars >= 2 and lowerchars >= 2 and spacesdots >= 1")
|
||||
return False
|
||||
|
||||
# Example: "this is a download"
|
||||
if spacesdots >= 3:
|
||||
logging.debug("Not obfuscated: spacesdots >= 3")
|
||||
return False
|
||||
|
||||
# Example: "Beast 2020"
|
||||
if (upperchars + lowerchars >= 4) and decimals >= 4 and spacesdots >= 1:
|
||||
logging.debug("Not obfuscated: (upperchars + lowerchars >= 4) and decimals > 3 and spacesdots > 1")
|
||||
return False
|
||||
|
||||
# Example: "Catullus", starts with a capital, and most letters are lower case
|
||||
if filebasename[0].isupper() and lowerchars > 2 and upperchars / lowerchars <= 0.25:
|
||||
logging.debug("Not obfuscated: starts with a capital, and most letters are lower case")
|
||||
return False
|
||||
|
||||
# If we get here, no trigger for a clear name was found, so let's default to obfuscated
|
||||
logging.debug("Obfuscated (default)")
|
||||
return True # default not obfuscated
|
||||
|
||||
|
||||
def deobfuscate_list(filelist, usefulname):
|
||||
""" Check all files in filelist, and if wanted, deobfuscate """
|
||||
|
||||
# to be sure, only keep really exsiting files:
|
||||
filelist = [f for f in filelist if os.path.exists(f)]
|
||||
|
||||
# Search for par2 files in the filelist
|
||||
par2_files = [f for f in filelist if f.endswith(".par2")]
|
||||
|
||||
# Found any par2 files we can use?
|
||||
run_renamer = True
|
||||
if not par2_files:
|
||||
logging.debug("No par2 files found to process, running renamer.")
|
||||
else:
|
||||
# Run par2 from SABnzbd on them
|
||||
for par2_file in par2_files:
|
||||
# Analyse data and analyse result
|
||||
logging.debug("Deobfuscate par2: handling %s", par2_file)
|
||||
if decode_par2(par2_file):
|
||||
logging.debug("Deobfuscate par2 repair/verify finished.")
|
||||
run_renamer = False
|
||||
else:
|
||||
logging.debug("Deobfuscate par2 repair/verify did not find anything to rename.")
|
||||
|
||||
# No par2 files? Then we try to rename qualifying (big, not-excluded, obfuscated) files to the job-name
|
||||
if run_renamer:
|
||||
logging.debug("Trying to see if there are qualifying files to be deobfuscated")
|
||||
for filename in filelist:
|
||||
logging.debug("Deobfuscate inspecting %s", filename)
|
||||
file_size = os.path.getsize(filename)
|
||||
# Do we need to rename this file?
|
||||
# Criteria: big, not-excluded extension, obfuscated (in that order)
|
||||
if (
|
||||
file_size > MIN_FILE_SIZE
|
||||
and get_ext(filename) not in EXCLUDED_FILE_EXTS
|
||||
and is_probably_obfuscated(filename) # this as last test to avoid unnecessary analysis
|
||||
):
|
||||
# OK, rename
|
||||
path, file = os.path.split(filename)
|
||||
new_name = get_unique_filename("%s%s" % (os.path.join(path, usefulname), get_ext(filename)))
|
||||
logging.info("Deobfuscate renaming %s to %s", filename, new_name)
|
||||
# Rename and make sure the new filename is unique
|
||||
renamer(filename, new_name)
|
||||
else:
|
||||
logging.info("No qualifying files found to deobfuscate")
|
||||
@@ -21,19 +21,20 @@ sabnzbd.directunpacker
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import threading
|
||||
import subprocess
|
||||
import logging
|
||||
from subprocess import Popen
|
||||
from typing import Optional, Dict, Tuple, List
|
||||
|
||||
import sabnzbd
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.misc import int_conv, format_time_string
|
||||
from sabnzbd.filesystem import clip_path, long_path, remove_all, real_path, remove_file
|
||||
from sabnzbd.misc import format_time_string, build_and_run_command
|
||||
from sabnzbd.filesystem import long_path, remove_all, real_path, remove_file, analyze_rar_filename
|
||||
from sabnzbd.nzbstuff import NzbObject, NzbFile
|
||||
from sabnzbd.encoding import platform_btou
|
||||
from sabnzbd.decorators import synchronized
|
||||
from sabnzbd.newsunpack import build_command, EXTRACTFROM_RE, EXTRACTED_RE, rar_volumelist
|
||||
from sabnzbd.newsunpack import EXTRACTFROM_RE, EXTRACTED_RE, rar_volumelist
|
||||
from sabnzbd.postproc import prepare_extraction_path
|
||||
from sabnzbd.utils.rarfile import RarFile
|
||||
from sabnzbd.utils.diskspeed import diskspeedmeasure
|
||||
@@ -44,26 +45,24 @@ START_STOP_LOCK = threading.RLock()
|
||||
|
||||
ACTIVE_UNPACKERS = []
|
||||
|
||||
RAR_NR = re.compile(r"(.*?)(\.part(\d*).rar|\.r(\d*))$", re.IGNORECASE)
|
||||
|
||||
|
||||
class DirectUnpacker(threading.Thread):
|
||||
def __init__(self, nzo):
|
||||
def __init__(self, nzo: NzbObject):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.nzo = nzo
|
||||
self.active_instance = None
|
||||
self.nzo: NzbObject = nzo
|
||||
self.active_instance: Optional[subprocess.Popen] = None
|
||||
self.killed = False
|
||||
self.next_file_lock = threading.Condition(threading.RLock())
|
||||
|
||||
self.unpack_dir_info = None
|
||||
self.rarfile_nzf = None
|
||||
self.rarfile_nzf: Optional[NzbFile] = None
|
||||
self.cur_setname = None
|
||||
self.cur_volume = 0
|
||||
self.total_volumes = {}
|
||||
self.unpack_time = 0.0
|
||||
|
||||
self.success_sets = {}
|
||||
self.success_sets: Dict[str, Tuple[List[str], List[str]]] = {}
|
||||
self.next_sets = []
|
||||
|
||||
self.duplicate_lines = 0
|
||||
@@ -77,9 +76,8 @@ class DirectUnpacker(threading.Thread):
|
||||
pass
|
||||
|
||||
def reset_active(self):
|
||||
# make sure the process and filehandles are closed nicely:
|
||||
# make sure the process and file handlers are closed nicely:
|
||||
try:
|
||||
# Creation was done via "self.active_instance = Popen()", so:
|
||||
if self.active_instance:
|
||||
self.active_instance.stdout.close()
|
||||
self.active_instance.stdin.close()
|
||||
@@ -87,6 +85,7 @@ class DirectUnpacker(threading.Thread):
|
||||
except:
|
||||
logging.debug("Exception in reset_active()", exc_info=True)
|
||||
pass
|
||||
|
||||
self.active_instance = None
|
||||
self.cur_setname = None
|
||||
self.cur_volume = 0
|
||||
@@ -96,6 +95,7 @@ class DirectUnpacker(threading.Thread):
|
||||
if (
|
||||
not cfg.direct_unpack()
|
||||
or self.killed
|
||||
or self.nzo.first_articles
|
||||
or not self.nzo.unpack
|
||||
or self.nzo.bad_articles
|
||||
or sabnzbd.newsunpack.RAR_PROBLEM
|
||||
@@ -123,12 +123,12 @@ class DirectUnpacker(threading.Thread):
|
||||
self.total_volumes = {}
|
||||
|
||||
@synchronized(START_STOP_LOCK)
|
||||
def add(self, nzf):
|
||||
def add(self, nzf: NzbFile):
|
||||
""" Add jobs and start instance of DirectUnpack """
|
||||
if not cfg.direct_unpack_tested():
|
||||
test_disk_performance()
|
||||
|
||||
# Stop if something is wrong
|
||||
# Stop if something is wrong or we shouldn't start yet
|
||||
if not self.check_requirements():
|
||||
return
|
||||
|
||||
@@ -163,8 +163,9 @@ class DirectUnpacker(threading.Thread):
|
||||
|
||||
def run(self):
|
||||
# Input and output
|
||||
linebuf = ""
|
||||
last_volume_linebuf = ""
|
||||
linebuf = b""
|
||||
linebuf_encoded = ""
|
||||
last_volume_linebuf = b""
|
||||
unrar_log = []
|
||||
rarfiles = []
|
||||
extracted = []
|
||||
@@ -176,99 +177,107 @@ class DirectUnpacker(threading.Thread):
|
||||
with START_STOP_LOCK:
|
||||
if not self.active_instance or not self.active_instance.stdout:
|
||||
break
|
||||
char = platform_btou(self.active_instance.stdout.read(1))
|
||||
char = self.active_instance.stdout.read(1)
|
||||
|
||||
if not char:
|
||||
# End of program
|
||||
break
|
||||
linebuf += char
|
||||
|
||||
# Error? Let PP-handle it
|
||||
if linebuf.endswith(
|
||||
(
|
||||
"ERROR: ",
|
||||
"Cannot create",
|
||||
"in the encrypted file",
|
||||
"CRC failed",
|
||||
"checksum failed",
|
||||
"You need to start extraction from a previous volume",
|
||||
"password is incorrect",
|
||||
"Incorrect password",
|
||||
"Write error",
|
||||
"checksum error",
|
||||
"Cannot open",
|
||||
"start extraction from a previous volume",
|
||||
"Unexpected end of archive",
|
||||
)
|
||||
):
|
||||
logging.info("Error in DirectUnpack of %s: %s", self.cur_setname, linebuf.strip())
|
||||
self.abort()
|
||||
# Continue if it's not a space or end of line
|
||||
if char not in (b" ", b"\n"):
|
||||
continue
|
||||
|
||||
if linebuf.endswith("\n"):
|
||||
# List files we used
|
||||
if linebuf.startswith("Extracting from"):
|
||||
filename = re.search(EXTRACTFROM_RE, linebuf.strip()).group(1)
|
||||
# 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""
|
||||
|
||||
# Error? Let PP-handle this job
|
||||
if any(
|
||||
error_text in linebuf_encoded
|
||||
for error_text in (
|
||||
"ERROR: ",
|
||||
"Cannot create",
|
||||
"in the encrypted file",
|
||||
"CRC failed",
|
||||
"checksum failed",
|
||||
"You need to start extraction from a previous volume",
|
||||
"password is incorrect",
|
||||
"Incorrect password",
|
||||
"Write error",
|
||||
"checksum error",
|
||||
"Cannot open",
|
||||
"start extraction from a previous volume",
|
||||
"Unexpected end of archive",
|
||||
)
|
||||
):
|
||||
logging.info("Error in DirectUnpack of %s: %s", self.cur_setname, platform_btou(linebuf.strip()))
|
||||
self.abort()
|
||||
|
||||
elif linebuf_encoded.startswith("All OK"):
|
||||
# Did we reach the end?
|
||||
# Stop timer and finish
|
||||
self.unpack_time += time.time() - start_time
|
||||
ACTIVE_UNPACKERS.remove(self)
|
||||
|
||||
# Add to success
|
||||
rarfile_path = os.path.join(self.nzo.download_path, self.rarfile_nzf.filename)
|
||||
self.success_sets[self.cur_setname] = (
|
||||
rar_volumelist(rarfile_path, self.nzo.password, rarfiles),
|
||||
extracted,
|
||||
)
|
||||
logging.info("DirectUnpack completed for %s", self.cur_setname)
|
||||
self.nzo.set_action_line(T("Direct Unpack"), T("Completed"))
|
||||
|
||||
# List success in history-info
|
||||
msg = T("Unpacked %s files/folders in %s") % (len(extracted), format_time_string(self.unpack_time))
|
||||
msg = "%s - %s" % (T("Direct Unpack"), msg)
|
||||
self.nzo.set_unpack_info("Unpack", msg, self.cur_setname)
|
||||
|
||||
# Write current log and clear
|
||||
logging.debug("DirectUnpack Unrar output %s", "\n".join(unrar_log))
|
||||
unrar_log = []
|
||||
rarfiles = []
|
||||
extracted = []
|
||||
|
||||
# Are there more files left?
|
||||
while self.nzo.files and not self.next_sets:
|
||||
with self.next_file_lock:
|
||||
self.next_file_lock.wait()
|
||||
|
||||
# Is there another set to do?
|
||||
if self.next_sets:
|
||||
# Start new instance
|
||||
nzf = self.next_sets.pop(0)
|
||||
self.reset_active()
|
||||
self.cur_setname = nzf.setname
|
||||
# Wait for the 1st volume to appear
|
||||
self.wait_for_next_volume()
|
||||
self.create_unrar_instance()
|
||||
start_time = time.time()
|
||||
else:
|
||||
self.killed = True
|
||||
break
|
||||
|
||||
elif linebuf_encoded.startswith("Extracting from"):
|
||||
# List files we used
|
||||
filename = re.search(EXTRACTFROM_RE, linebuf_encoded).group(1)
|
||||
if filename not in rarfiles:
|
||||
rarfiles.append(filename)
|
||||
|
||||
# List files we extracted
|
||||
m = re.search(EXTRACTED_RE, linebuf)
|
||||
if m:
|
||||
# In case of flat-unpack, UnRar still prints the whole path (?!)
|
||||
unpacked_file = m.group(2)
|
||||
if cfg.flat_unpack():
|
||||
unpacked_file = os.path.basename(unpacked_file)
|
||||
extracted.append(real_path(self.unpack_dir_info[0], unpacked_file))
|
||||
|
||||
# Did we reach the end?
|
||||
if linebuf.endswith("All OK"):
|
||||
# Stop timer and finish
|
||||
self.unpack_time += time.time() - start_time
|
||||
ACTIVE_UNPACKERS.remove(self)
|
||||
|
||||
# Add to success
|
||||
rarfile_path = os.path.join(self.nzo.downpath, self.rarfile_nzf.filename)
|
||||
self.success_sets[self.cur_setname] = (
|
||||
rar_volumelist(rarfile_path, self.nzo.password, rarfiles),
|
||||
extracted,
|
||||
)
|
||||
logging.info("DirectUnpack completed for %s", self.cur_setname)
|
||||
self.nzo.set_action_line(T("Direct Unpack"), T("Completed"))
|
||||
|
||||
# List success in history-info
|
||||
msg = T("Unpacked %s files/folders in %s") % (len(extracted), format_time_string(self.unpack_time))
|
||||
msg = "%s - %s" % (T("Direct Unpack"), msg)
|
||||
self.nzo.set_unpack_info("Unpack", msg, self.cur_setname)
|
||||
|
||||
# Write current log and clear
|
||||
unrar_log.append(linebuf.strip())
|
||||
linebuf = ""
|
||||
last_volume_linebuf = ""
|
||||
logging.debug("DirectUnpack Unrar output %s", "\n".join(unrar_log))
|
||||
unrar_log = []
|
||||
rarfiles = []
|
||||
extracted = []
|
||||
|
||||
# Are there more files left?
|
||||
while self.nzo.files and not self.next_sets:
|
||||
with self.next_file_lock:
|
||||
self.next_file_lock.wait()
|
||||
|
||||
# Is there another set to do?
|
||||
if self.next_sets:
|
||||
# Start new instance
|
||||
nzf = self.next_sets.pop(0)
|
||||
self.reset_active()
|
||||
self.cur_setname = nzf.setname
|
||||
# Wait for the 1st volume to appear
|
||||
self.wait_for_next_volume()
|
||||
self.create_unrar_instance()
|
||||
start_time = time.time()
|
||||
else:
|
||||
self.killed = True
|
||||
break
|
||||
# List files we extracted
|
||||
m = re.search(EXTRACTED_RE, linebuf_encoded)
|
||||
if m:
|
||||
# In case of flat-unpack, UnRar still prints the whole path (?!)
|
||||
unpacked_file = m.group(2)
|
||||
if cfg.flat_unpack():
|
||||
unpacked_file = os.path.basename(unpacked_file)
|
||||
extracted.append(real_path(self.unpack_dir_info[0], unpacked_file))
|
||||
|
||||
if linebuf.endswith("[C]ontinue, [Q]uit "):
|
||||
if linebuf.endswith(b"[C]ontinue, [Q]uit "):
|
||||
# Stop timer
|
||||
self.unpack_time += time.time() - start_time
|
||||
|
||||
@@ -301,20 +310,16 @@ class DirectUnpacker(threading.Thread):
|
||||
logging.info("DirectUnpack failed due to missing files %s", self.cur_setname)
|
||||
self.abort()
|
||||
else:
|
||||
logging.debug('Duplicate output line detected: "%s"', last_volume_linebuf)
|
||||
logging.debug('Duplicate output line detected: "%s"', platform_btou(last_volume_linebuf))
|
||||
self.duplicate_lines += 1
|
||||
else:
|
||||
self.duplicate_lines = 0
|
||||
last_volume_linebuf = linebuf
|
||||
|
||||
# Show the log
|
||||
if linebuf.endswith("\n"):
|
||||
unrar_log.append(linebuf.strip())
|
||||
linebuf = ""
|
||||
|
||||
# Add last line
|
||||
unrar_log.append(linebuf.strip())
|
||||
logging.debug("DirectUnpack Unrar output %s", "\n".join(unrar_log))
|
||||
# Add last line and write any new output
|
||||
if linebuf:
|
||||
unrar_log.append(platform_btou(linebuf.strip()))
|
||||
logging.debug("DirectUnpack Unrar output %s", "\n".join(unrar_log))
|
||||
|
||||
# Make more space
|
||||
self.reset_active()
|
||||
@@ -325,9 +330,9 @@ class DirectUnpacker(threading.Thread):
|
||||
self.killed = True
|
||||
|
||||
def have_next_volume(self):
|
||||
""" Check if next volume of set is available, start
|
||||
from the end of the list where latest completed files are
|
||||
Make sure that files are 100% written to disk by checking md5sum
|
||||
"""Check if next volume of set is available, start
|
||||
from the end of the list where latest completed files are
|
||||
Make sure that files are 100% written to disk by checking md5sum
|
||||
"""
|
||||
for nzf_search in reversed(self.nzo.finished_files):
|
||||
if nzf_search.setname == self.cur_setname and nzf_search.vol == (self.cur_volume + 1) and nzf_search.md5sum:
|
||||
@@ -335,8 +340,8 @@ class DirectUnpacker(threading.Thread):
|
||||
return False
|
||||
|
||||
def wait_for_next_volume(self):
|
||||
""" Wait for the correct volume to appear
|
||||
But stop if it was killed or the NZB is done
|
||||
"""Wait for the correct volume to appear
|
||||
But stop if it was killed or the NZB is done
|
||||
"""
|
||||
while not self.have_next_volume() and not self.killed and self.nzo.files:
|
||||
with self.next_file_lock:
|
||||
@@ -377,29 +382,32 @@ class DirectUnpacker(threading.Thread):
|
||||
return
|
||||
|
||||
# Generate command
|
||||
rarfile_path = os.path.join(self.nzo.downpath, self.rarfile_nzf.filename)
|
||||
rarfile_path = os.path.join(self.nzo.download_path, self.rarfile_nzf.filename)
|
||||
if sabnzbd.WIN32:
|
||||
# For Unrar to support long-path, we need to cricumvent Python's list2cmdline
|
||||
# For Unrar to support long-path, we need to circumvent Python's list2cmdline
|
||||
# See: https://github.com/sabnzbd/sabnzbd/issues/1043
|
||||
# The -scf forces the output to be UTF8
|
||||
command = [
|
||||
"%s" % sabnzbd.newsunpack.RAR_COMMAND,
|
||||
action,
|
||||
"-vp",
|
||||
"-idp",
|
||||
"-scf",
|
||||
"-o+",
|
||||
"-ai",
|
||||
password_command,
|
||||
"%s" % clip_path(rarfile_path),
|
||||
rarfile_path,
|
||||
"%s\\" % long_path(extraction_path),
|
||||
]
|
||||
|
||||
else:
|
||||
# Don't use "-ai" (not needed for non-Windows)
|
||||
# The -scf forces the output to be UTF8
|
||||
command = [
|
||||
"%s" % sabnzbd.newsunpack.RAR_COMMAND,
|
||||
action,
|
||||
"-vp",
|
||||
"-idp",
|
||||
"-scf",
|
||||
"-o+",
|
||||
password_command,
|
||||
"%s" % rarfile_path,
|
||||
@@ -411,19 +419,10 @@ class DirectUnpacker(threading.Thread):
|
||||
|
||||
# Let's start from the first one!
|
||||
self.cur_volume = 1
|
||||
stup, need_shell, command, creationflags = build_command(command, flatten_command=True)
|
||||
logging.debug("Running unrar for DirectUnpack %s", command)
|
||||
|
||||
# Need to disable buffer to have direct feedback
|
||||
self.active_instance = Popen(
|
||||
command,
|
||||
shell=False,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
startupinfo=stup,
|
||||
creationflags=creationflags,
|
||||
bufsize=0,
|
||||
)
|
||||
self.active_instance = build_and_run_command(command, flatten_command=True, bufsize=0)
|
||||
|
||||
# Add to runners
|
||||
ACTIVE_UNPACKERS.append(self)
|
||||
|
||||
@@ -473,7 +472,7 @@ class DirectUnpacker(threading.Thread):
|
||||
# RarFile can fail for mysterious reasons
|
||||
try:
|
||||
rar_contents = RarFile(
|
||||
os.path.join(self.nzo.downpath, rarfile_nzf.filename), single_file_check=True
|
||||
os.path.join(self.nzo.download_path, rarfile_nzf.filename), single_file_check=True
|
||||
).filelist()
|
||||
for rm_file in rar_contents:
|
||||
# Flat-unpack, so remove foldername from RarFile output
|
||||
@@ -502,23 +501,6 @@ class DirectUnpacker(threading.Thread):
|
||||
return self.cur_volume
|
||||
|
||||
|
||||
def analyze_rar_filename(filename):
|
||||
""" Extract volume number and setname from rar-filenames
|
||||
Both ".part01.rar" and ".r01"
|
||||
"""
|
||||
m = RAR_NR.search(filename)
|
||||
if m:
|
||||
if m.group(4):
|
||||
# Special since starts with ".rar", ".r00"
|
||||
return m.group(1), int_conv(m.group(4)) + 2
|
||||
return m.group(1), int_conv(m.group(3))
|
||||
else:
|
||||
# Detect if first of "rxx" set
|
||||
if filename.endswith(".rar"):
|
||||
return os.path.splitext(filename)[0], 1
|
||||
return None, None
|
||||
|
||||
|
||||
def abort_all():
|
||||
""" Abort all running DirectUnpackers """
|
||||
logging.info("Aborting all DirectUnpackers")
|
||||
@@ -527,8 +509,8 @@ def abort_all():
|
||||
|
||||
|
||||
def test_disk_performance():
|
||||
""" Test the incomplete-dir performance and enable
|
||||
Direct Unpack if good enough (> 40MB/s)
|
||||
"""Test the incomplete-dir performance and enable
|
||||
Direct Unpack if good enough (> 40MB/s)
|
||||
"""
|
||||
if diskspeedmeasure(sabnzbd.cfg.download_dir.get_path()) > 40:
|
||||
cfg.direct_unpack.set(True)
|
||||
|
||||
@@ -46,7 +46,7 @@ def compare_stat_tuple(tup1, tup2):
|
||||
|
||||
def clean_file_list(inp_list, folder, files):
|
||||
""" Remove elements of "inp_list" not found in "files" """
|
||||
for path in sorted(inp_list.keys()):
|
||||
for path in sorted(inp_list):
|
||||
fld, name = os.path.split(path)
|
||||
if fld == folder:
|
||||
present = False
|
||||
@@ -59,14 +59,12 @@ def clean_file_list(inp_list, folder, files):
|
||||
|
||||
|
||||
class DirScanner(threading.Thread):
|
||||
""" Thread that periodically scans a given directory and picks up any
|
||||
valid NZB, NZB.GZ ZIP-with-only-NZB and even NZB.GZ named as .NZB
|
||||
Candidates which turned out wrong, will be remembered and skipped in
|
||||
subsequent scans, unless changed.
|
||||
"""Thread that periodically scans a given directory and picks up any
|
||||
valid NZB, NZB.GZ ZIP-with-only-NZB and even NZB.GZ named as .NZB
|
||||
Candidates which turned out wrong, will be remembered and skipped in
|
||||
subsequent scans, unless changed.
|
||||
"""
|
||||
|
||||
do = None # Access to instance of DirScanner
|
||||
|
||||
def __init__(self):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
@@ -89,7 +87,6 @@ class DirScanner(threading.Thread):
|
||||
self.trigger = False
|
||||
cfg.dirscan_dir.callback(self.newdir)
|
||||
cfg.dirscan_speed.callback(self.newspeed)
|
||||
DirScanner.do = self
|
||||
|
||||
def newdir(self):
|
||||
""" We're notified of a dir change """
|
||||
@@ -105,7 +102,6 @@ class DirScanner(threading.Thread):
|
||||
|
||||
def stop(self):
|
||||
""" Stop the dir scanner """
|
||||
logging.info("Dirscanner shutting down")
|
||||
self.shutdown = True
|
||||
|
||||
def save(self):
|
||||
@@ -213,9 +209,3 @@ class DirScanner(threading.Thread):
|
||||
if os.path.isdir(dpath) and dd.lower() in cats:
|
||||
run_dir(dpath, dd.lower())
|
||||
self.busy = False
|
||||
|
||||
|
||||
def dirscan():
|
||||
""" Wrapper required for scheduler """
|
||||
logging.info("Scheduled or manual watched folder scan")
|
||||
DirScanner.do.scan()
|
||||
|
||||
@@ -27,15 +27,14 @@ from nntplib import NNTPPermanentError
|
||||
import socket
|
||||
import random
|
||||
import sys
|
||||
from typing import List, Dict
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.decorators import synchronized, NzbQueueLocker, DOWNLOADER_CV
|
||||
from sabnzbd.newswrapper import NewsWrapper, request_server_info
|
||||
import sabnzbd.notifier as notifier
|
||||
import sabnzbd.notifier
|
||||
import sabnzbd.config as config
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.bpsmeter import BPSMeter
|
||||
import sabnzbd.scheduler
|
||||
from sabnzbd.misc import from_units, nntp_to_msg, int_conv
|
||||
from sabnzbd.utils.happyeyeballs import happyeyeballs
|
||||
|
||||
@@ -79,10 +78,10 @@ class Server:
|
||||
self.restart = False
|
||||
self.displayname = displayname
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.port: int = port
|
||||
self.timeout = timeout
|
||||
self.threads = threads
|
||||
self.priority = priority
|
||||
self.priority: int = priority
|
||||
self.ssl = ssl
|
||||
self.ssl_verify = ssl_verify
|
||||
self.ssl_ciphers = ssl_ciphers
|
||||
@@ -110,12 +109,12 @@ class Server:
|
||||
|
||||
@property
|
||||
def hostip(self):
|
||||
""" In case a server still has active connections, we use the same IP again
|
||||
If new connection then based on value of load_balancing() and self.info:
|
||||
0 - return the first entry, so all threads use the same IP
|
||||
1 - and self.info has more than 1 entry (read: IP address): Return a random entry from the possible IPs
|
||||
2 - and self.info has more than 1 entry (read: IP address): Return the quickest IP based on the happyeyeballs algorithm
|
||||
In case of problems: return the host name itself
|
||||
"""In case a server still has active connections, we use the same IP again
|
||||
If new connection then based on value of load_balancing() and self.info:
|
||||
0 - return the first entry, so all threads use the same IP
|
||||
1 - and self.info has more than 1 entry (read: IP address): Return a random entry from the possible IPs
|
||||
2 - and self.info has more than 1 entry (read: IP address): Return the quickest IP based on the happyeyeballs algorithm
|
||||
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:
|
||||
@@ -162,14 +161,12 @@ class Server:
|
||||
self.idle_threads = []
|
||||
|
||||
def __repr__(self):
|
||||
return "%s:%s" % (self.host, self.port)
|
||||
return "<Server: %s:%s>" % (self.host, self.port)
|
||||
|
||||
|
||||
class Downloader(Thread):
|
||||
""" Singleton Downloader Thread """
|
||||
|
||||
do = None
|
||||
|
||||
def __init__(self, paused=False):
|
||||
Thread.__init__(self)
|
||||
|
||||
@@ -199,23 +196,21 @@ class Downloader(Thread):
|
||||
|
||||
self.force_disconnect = False
|
||||
|
||||
self.read_fds = {}
|
||||
self.write_fds = {}
|
||||
self.read_fds: Dict[int, NewsWrapper] = {}
|
||||
self.write_fds: Dict[int, NewsWrapper] = {}
|
||||
|
||||
self.servers = []
|
||||
self.server_dict = {} # For faster lookups, but is not updated later!
|
||||
self.servers: List[Server] = []
|
||||
self.server_dict: Dict[str, Server] = {} # For faster lookups, but is not updated later!
|
||||
self.server_nr = 0
|
||||
self._timers = {}
|
||||
|
||||
for server in config.get_servers():
|
||||
self.init_server(None, server)
|
||||
|
||||
Downloader.do = self
|
||||
|
||||
def init_server(self, oldserver, newserver):
|
||||
""" Setup or re-setup single server
|
||||
When oldserver is defined and in use, delay startup.
|
||||
Note that the server names are "host:port" strings!
|
||||
"""Setup or re-setup single server
|
||||
When oldserver is defined and in use, delay startup.
|
||||
Note that the server names are "host:port" strings!
|
||||
"""
|
||||
|
||||
create = False
|
||||
@@ -274,8 +269,6 @@ class Downloader(Thread):
|
||||
# Update server-count
|
||||
self.server_nr = len(self.servers)
|
||||
|
||||
return
|
||||
|
||||
@NzbQueueLocker
|
||||
def set_paused_state(self, state):
|
||||
""" Set downloader to specified paused state """
|
||||
@@ -286,7 +279,7 @@ class Downloader(Thread):
|
||||
# Do not notify when SABnzbd is still starting
|
||||
if self.paused and sabnzbd.WEB_DIR:
|
||||
logging.info("Resuming")
|
||||
notifier.send_notification("SABnzbd", T("Resuming"), "download")
|
||||
sabnzbd.notifier.send_notification("SABnzbd", T("Resuming"), "pause_resume")
|
||||
self.paused = False
|
||||
|
||||
@NzbQueueLocker
|
||||
@@ -295,9 +288,9 @@ class Downloader(Thread):
|
||||
if not self.paused:
|
||||
self.paused = True
|
||||
logging.info("Pausing")
|
||||
notifier.send_notification("SABnzbd", T("Paused"), "download")
|
||||
sabnzbd.notifier.send_notification("SABnzbd", T("Paused"), "pause_resume")
|
||||
if self.is_paused():
|
||||
BPSMeter.do.reset()
|
||||
sabnzbd.BPSMeter.reset()
|
||||
if cfg.autodisconnect():
|
||||
self.disconnect()
|
||||
|
||||
@@ -314,9 +307,9 @@ class Downloader(Thread):
|
||||
self.force_disconnect = True
|
||||
|
||||
def limit_speed(self, value):
|
||||
""" Set the actual download speed in Bytes/sec
|
||||
When 'value' ends with a '%' sign or is within 1-100, it is interpreted as a pecentage of the maximum bandwidth
|
||||
When no '%' is found, it is interpreted as an absolute speed (including KMGT notation).
|
||||
"""Set the actual download speed in Bytes/sec
|
||||
When 'value' ends with a '%' sign or is within 1-100, it is interpreted as a pecentage of the maximum bandwidth
|
||||
When no '%' is found, it is interpreted as an absolute speed (including KMGT notation).
|
||||
"""
|
||||
if value:
|
||||
mx = cfg.bandwidth_max.get_int()
|
||||
@@ -326,7 +319,7 @@ class Downloader(Thread):
|
||||
if mx:
|
||||
self.bandwidth_limit = mx * self.bandwidth_perc / 100
|
||||
else:
|
||||
logging.warning(T("You must set a maximum bandwidth before you can set a bandwidth limit"))
|
||||
logging.warning_helpful(T("You must set a maximum bandwidth before you can set a bandwidth limit"))
|
||||
else:
|
||||
self.bandwidth_limit = from_units(value)
|
||||
if mx:
|
||||
@@ -357,14 +350,14 @@ class Downloader(Thread):
|
||||
if not self.paused:
|
||||
return False
|
||||
else:
|
||||
if sabnzbd.nzbqueue.NzbQueue.do.has_forced_items():
|
||||
if sabnzbd.NzbQueue.has_forced_items():
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def highest_server(self, me):
|
||||
""" Return True when this server has the highest priority of the active ones
|
||||
0 is the highest priority
|
||||
def highest_server(self, me: Server):
|
||||
"""Return True when this server has the highest priority of the active ones
|
||||
0 is the highest priority
|
||||
"""
|
||||
for server in self.servers:
|
||||
if server is not me and server.active and server.priority < me.priority:
|
||||
@@ -403,32 +396,30 @@ class Downloader(Thread):
|
||||
# Make sure server address resolution is refreshed
|
||||
server.info = None
|
||||
|
||||
sabnzbd.nzbqueue.NzbQueue.do.reset_all_try_lists()
|
||||
sabnzbd.NzbQueue.reset_all_try_lists()
|
||||
|
||||
def decode(self, article, raw_data):
|
||||
""" Decode article and check the status of
|
||||
the decoder and the assembler
|
||||
"""Decode article and check the status of
|
||||
the decoder and the assembler
|
||||
"""
|
||||
# Handle broken articles directly
|
||||
if not raw_data:
|
||||
if not article.search_new_server():
|
||||
sabnzbd.nzbqueue.NzbQueue.do.register_article(article, success=False)
|
||||
sabnzbd.NzbQueue.register_article(article, success=False)
|
||||
return
|
||||
|
||||
# Send to decoder-queue
|
||||
sabnzbd.decoder.Decoder.do.process(article, raw_data)
|
||||
sabnzbd.Decoder.process(article, raw_data)
|
||||
|
||||
# See if we need to delay because the queues are full
|
||||
logged = False
|
||||
while not self.shutdown and (
|
||||
sabnzbd.decoder.Decoder.do.queue_full() or sabnzbd.assembler.Assembler.do.queue_full()
|
||||
):
|
||||
while not self.shutdown and (sabnzbd.Decoder.queue_full() or sabnzbd.Assembler.queue_full()):
|
||||
if not logged:
|
||||
# Only log once, to not waste any CPU-cycles
|
||||
logging.debug(
|
||||
"Delaying - Decoder queue: %s - Assembler queue: %s",
|
||||
sabnzbd.decoder.Decoder.do.decoder_queue.qsize(),
|
||||
sabnzbd.assembler.Assembler.do.queue.qsize(),
|
||||
sabnzbd.Decoder.decoder_queue.qsize(),
|
||||
sabnzbd.Assembler.queue.qsize(),
|
||||
)
|
||||
logged = True
|
||||
time.sleep(0.05)
|
||||
@@ -443,12 +434,29 @@ class Downloader(Thread):
|
||||
logging.debug("SSL verification test: %s", sabnzbd.CERTIFICATE_VALIDATION)
|
||||
|
||||
# Kick BPS-Meter to check quota
|
||||
BPSMeter.do.update()
|
||||
sabnzbd.BPSMeter.update()
|
||||
|
||||
# Store when each server last searched for articles
|
||||
last_searched = {}
|
||||
|
||||
# Store when each server last downloaded anything or found an article
|
||||
last_busy = {}
|
||||
|
||||
while 1:
|
||||
now = time.time()
|
||||
for server in self.servers:
|
||||
serverid = server.id
|
||||
if server.busy_threads:
|
||||
last_busy[serverid] = now
|
||||
else:
|
||||
# Skip this server if idle for 1 second and it has already been searched less than 0.5 seconds ago
|
||||
if last_busy.get(serverid, 0) + 1 < now and last_searched.get(serverid, 0) + 0.5 > now:
|
||||
continue
|
||||
|
||||
last_searched[serverid] = now
|
||||
|
||||
for nw in server.busy_threads[:]:
|
||||
if (nw.nntp and nw.nntp.error_msg) or (nw.timeout and time.time() > nw.timeout):
|
||||
if (nw.nntp and nw.nntp.error_msg) or (nw.timeout and now > nw.timeout):
|
||||
if nw.nntp and nw.nntp.error_msg:
|
||||
self.__reset_nw(nw, "", warn=False)
|
||||
else:
|
||||
@@ -463,7 +471,7 @@ class Downloader(Thread):
|
||||
if newid:
|
||||
self.init_server(None, newid)
|
||||
self.__restart -= 1
|
||||
sabnzbd.nzbqueue.NzbQueue.do.reset_all_try_lists()
|
||||
sabnzbd.NzbQueue.reset_all_try_lists()
|
||||
# Have to leave this loop, because we removed element
|
||||
break
|
||||
else:
|
||||
@@ -478,24 +486,26 @@ class Downloader(Thread):
|
||||
|
||||
for nw in server.idle_threads[:]:
|
||||
if nw.timeout:
|
||||
if time.time() < nw.timeout:
|
||||
if now < nw.timeout:
|
||||
continue
|
||||
else:
|
||||
nw.timeout = None
|
||||
|
||||
if not server.info:
|
||||
# Only request info if there's stuff in the queue
|
||||
if not sabnzbd.nzbqueue.NzbQueue.do.is_empty():
|
||||
if not sabnzbd.NzbQueue.is_empty():
|
||||
self.maybe_block_server(server)
|
||||
request_server_info(server)
|
||||
break
|
||||
|
||||
article = sabnzbd.nzbqueue.NzbQueue.do.get_article(server, self.servers)
|
||||
article = sabnzbd.NzbQueue.get_article(server, self.servers)
|
||||
|
||||
if not article:
|
||||
break
|
||||
|
||||
if server.retention and article.nzf.nzo.avg_stamp < time.time() - server.retention:
|
||||
last_busy[serverid] = now
|
||||
|
||||
if server.retention and article.nzf.nzo.avg_stamp < now - server.retention:
|
||||
# Let's get rid of all the articles for this server at once
|
||||
logging.info("Job %s too old for %s, moving on", article.nzf.nzo.final_name, server.host)
|
||||
while article:
|
||||
@@ -562,26 +572,26 @@ class Downloader(Thread):
|
||||
# Need to initialize the check during first 20 seconds
|
||||
if self.can_be_slowed is None or self.can_be_slowed_timer:
|
||||
# Wait for stable speed to start testing
|
||||
if not self.can_be_slowed_timer and BPSMeter.do.get_stable_speed(timespan=10):
|
||||
self.can_be_slowed_timer = time.time()
|
||||
if not self.can_be_slowed_timer and sabnzbd.BPSMeter.get_stable_speed(timespan=10):
|
||||
self.can_be_slowed_timer = now
|
||||
|
||||
# Check 10 seconds after enabling slowdown
|
||||
if self.can_be_slowed_timer and time.time() > self.can_be_slowed_timer + 10:
|
||||
if self.can_be_slowed_timer and now > self.can_be_slowed_timer + 10:
|
||||
# Now let's check if it was stable in the last 10 seconds
|
||||
self.can_be_slowed = BPSMeter.do.get_stable_speed(timespan=10)
|
||||
self.can_be_slowed = sabnzbd.BPSMeter.get_stable_speed(timespan=10)
|
||||
self.can_be_slowed_timer = 0
|
||||
logging.debug("Downloader-slowdown: %r", self.can_be_slowed)
|
||||
|
||||
else:
|
||||
read, write, error = ([], [], [])
|
||||
|
||||
BPSMeter.do.reset()
|
||||
sabnzbd.BPSMeter.reset()
|
||||
|
||||
time.sleep(1.0)
|
||||
|
||||
DOWNLOADER_CV.acquire()
|
||||
while (
|
||||
(sabnzbd.nzbqueue.NzbQueue.do.is_empty() or self.is_paused() or self.postproc)
|
||||
(sabnzbd.NzbQueue.is_empty() or self.is_paused() or self.postproc)
|
||||
and not self.shutdown
|
||||
and not self.__restart
|
||||
):
|
||||
@@ -602,7 +612,7 @@ class Downloader(Thread):
|
||||
self.write_fds.pop(fileno)
|
||||
|
||||
if not read:
|
||||
BPSMeter.do.update()
|
||||
sabnzbd.BPSMeter.update()
|
||||
continue
|
||||
|
||||
for selected in read:
|
||||
@@ -610,16 +620,13 @@ class Downloader(Thread):
|
||||
article = nw.article
|
||||
server = nw.server
|
||||
|
||||
if article:
|
||||
nzo = article.nzf.nzo
|
||||
|
||||
try:
|
||||
bytes_received, done, skip = nw.recv_chunk()
|
||||
except:
|
||||
bytes_received, done, skip = (0, False, False)
|
||||
|
||||
if skip:
|
||||
BPSMeter.do.update()
|
||||
sabnzbd.BPSMeter.update()
|
||||
continue
|
||||
|
||||
if bytes_received < 1:
|
||||
@@ -629,12 +636,12 @@ class Downloader(Thread):
|
||||
else:
|
||||
if self.bandwidth_limit:
|
||||
limit = self.bandwidth_limit
|
||||
if bytes_received + BPSMeter.do.bps > limit:
|
||||
while BPSMeter.do.bps > limit:
|
||||
if bytes_received + sabnzbd.BPSMeter.bps > limit:
|
||||
while sabnzbd.BPSMeter.bps > limit:
|
||||
time.sleep(0.05)
|
||||
BPSMeter.do.update()
|
||||
BPSMeter.do.update(server.id, bytes_received)
|
||||
nzo.update_download_stats(BPSMeter.do.bps, server.id, bytes_received)
|
||||
sabnzbd.BPSMeter.update()
|
||||
sabnzbd.BPSMeter.update(server.id, bytes_received)
|
||||
article.nzf.nzo.update_download_stats(sabnzbd.BPSMeter.bps, server.id, bytes_received)
|
||||
|
||||
if not done and nw.status_code != 222:
|
||||
if not nw.connected or nw.status_code == 480:
|
||||
@@ -716,7 +723,7 @@ class Downloader(Thread):
|
||||
server.active = False
|
||||
if penalty and (block or server.optional):
|
||||
self.plan_server(server, penalty)
|
||||
sabnzbd.nzbqueue.NzbQueue.do.reset_all_try_lists()
|
||||
sabnzbd.NzbQueue.reset_all_try_lists()
|
||||
self.__reset_nw(nw, None, warn=False, send_quit=True)
|
||||
continue
|
||||
except:
|
||||
@@ -756,7 +763,7 @@ class Downloader(Thread):
|
||||
nw.clear_data()
|
||||
|
||||
elif nw.status_code == 500:
|
||||
if nzo.precheck:
|
||||
if article.nzf.nzo.precheck:
|
||||
# Assume "STAT" command is not supported
|
||||
server.have_stat = False
|
||||
logging.debug("Server %s does not support STAT", server.host)
|
||||
@@ -823,7 +830,7 @@ class Downloader(Thread):
|
||||
self.decode(article, None)
|
||||
else:
|
||||
# Allow all servers to iterate over each nzo/nzf again
|
||||
sabnzbd.nzbqueue.NzbQueue.do.reset_try_lists(article)
|
||||
sabnzbd.NzbQueue.reset_try_lists(article)
|
||||
|
||||
if destroy:
|
||||
nw.terminate(quit=send_quit)
|
||||
@@ -833,7 +840,7 @@ class Downloader(Thread):
|
||||
# Empty SSL info, it might change on next connect
|
||||
server.ssl_info = ""
|
||||
|
||||
def __request_article(self, nw):
|
||||
def __request_article(self, nw: NewsWrapper):
|
||||
try:
|
||||
nzo = nw.article.nzf.nzo
|
||||
if nw.server.send_group and nzo.group != nw.group:
|
||||
@@ -877,7 +884,7 @@ class Downloader(Thread):
|
||||
stamp = time.time() + 60.0 * interval
|
||||
self._timers[server.id].append(stamp)
|
||||
if interval:
|
||||
sabnzbd.scheduler.plan_server(self.trigger_server, [server.id, stamp], interval)
|
||||
sabnzbd.Scheduler.plan_server(self.trigger_server, [server.id, stamp], interval)
|
||||
|
||||
@synchronized(TIMER_LOCK)
|
||||
def trigger_server(self, server_id, timestamp):
|
||||
@@ -914,7 +921,8 @@ class Downloader(Thread):
|
||||
# Clean expired timers
|
||||
now = time.time()
|
||||
kicked = []
|
||||
for server_id in self._timers.keys():
|
||||
# Create a copy so we can remove during iteration
|
||||
for server_id in list(self._timers):
|
||||
if not [stamp for stamp in self._timers[server_id] if stamp >= now]:
|
||||
logging.debug("Forcing re-evaluation of server-id %s", server_id)
|
||||
del self._timers[server_id]
|
||||
@@ -928,8 +936,8 @@ class Downloader(Thread):
|
||||
self.init_server(server.id, server.id)
|
||||
|
||||
def update_server(self, oldserver, newserver):
|
||||
""" Update the server and make sure we trigger
|
||||
the update in the loop to do housekeeping """
|
||||
"""Update the server and make sure we trigger
|
||||
the update in the loop to do housekeeping"""
|
||||
self.init_server(oldserver, newserver)
|
||||
self.wakeup()
|
||||
|
||||
@@ -940,18 +948,18 @@ class Downloader(Thread):
|
||||
|
||||
def stop(self):
|
||||
self.shutdown = True
|
||||
notifier.send_notification("SABnzbd", T("Shutting down"), "startup")
|
||||
sabnzbd.notifier.send_notification("SABnzbd", T("Shutting down"), "startup")
|
||||
|
||||
|
||||
def stop():
|
||||
DOWNLOADER_CV.acquire()
|
||||
try:
|
||||
Downloader.do.stop()
|
||||
sabnzbd.Downloader.stop()
|
||||
finally:
|
||||
DOWNLOADER_CV.notify_all()
|
||||
DOWNLOADER_CV.release()
|
||||
try:
|
||||
Downloader.do.join()
|
||||
sabnzbd.Downloader.join()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import glob
|
||||
|
||||
from Cheetah.Template import Template
|
||||
from email.message import EmailMessage
|
||||
from email import policy
|
||||
|
||||
from sabnzbd.constants import *
|
||||
import sabnzbd
|
||||
@@ -296,4 +297,4 @@ def _prepare_message(txt):
|
||||
msg[keyword] = value
|
||||
|
||||
msg.set_content("\n".join(payload))
|
||||
return msg.as_bytes()
|
||||
return msg.as_bytes(policy=msg.policy.clone(linesep="\r\n"))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user