Compare commits

..

46 Commits

Author SHA1 Message Date
Safihre
a34747fbd5 Update text files for 4.2.0RC1
Yes, I used AI to generate the new release notes
2023-12-08 14:24:40 +01:00
Safihre
6b0380199b Mark test_download_sorting_single as xfail on macOS and Windows 2023-12-07 21:43:43 +01:00
Safihre
39d2f90a84 Trigger duplicate analysis if pre-queue script sets a new name 2023-12-07 21:42:43 +01:00
Safihre
7bff7651f3 Only auto-enable Direct Unpack for >100MB/s drives 2023-12-07 21:34:09 +01:00
Safihre
44bd15d519 Small change to internetspeed measurement 2023-12-07 15:49:07 +01:00
SABnzbd Automation
1ca93b03a0 Update translatable texts
[skip ci]
2023-12-06 15:56:06 +00:00
Safihre
3295142d81 Use sabctools for Internet Bandwidth measurement
Closes #2737
2023-12-06 13:44:12 +01:00
Safihre
f12fdc46dc Improve stability of test_adding_nzbs_nzoids 2023-12-06 13:22:59 +01:00
Safihre
fc01254fe6 Mark test_download_sorting_single as xfail on macOS and Windows 32bit 2023-12-06 12:58:47 +01:00
Safihre
8fb3368601 Add guestimate of performance test duration to hint 2023-12-06 12:46:44 +01:00
SABnzbd Automation
58facc2512 Update translatable texts
[skip ci]
2023-12-05 09:56:24 +00:00
Safihre
b43c2b308b Use correct keys for Season and Episode in Smart Duplicate detection 2023-12-05 10:55:34 +01:00
renovate[bot]
1e89a0af56 Update all dependencies 2023-12-04 02:19:58 +00:00
Safihre
acd3cbbf49 Correct test_validate_safedir 2023-12-03 20:02:41 +01:00
SABnzbd Automation
a806521745 Update translatable texts
[skip ci]
2023-12-02 20:20:33 +00:00
Safihre
0dddaf26e0 Stricter validation on Windows to prevent network drives as Incomplete 2023-12-02 21:19:48 +01:00
Safihre
cdf63a005b Python 3.8 doesn't have functools.cache so use lru_cache 2023-12-02 21:09:44 +01:00
SABnzbd Automation
ca422a0af3 Update translatable texts
[skip ci]
2023-12-02 19:40:31 +00:00
Safihre
a682371a91 Cache result of HappyEyeBalls 10 seconds 2023-12-02 20:39:38 +01:00
Safihre
26ef146526 Use decorator to maintain diskspace cache and HappyEyeBalls cache 2023-12-02 20:36:14 +01:00
Safihre
936ee58abb Reduce waiting time if there are no sockets to read 2023-12-01 15:11:05 +01:00
Safihre
71d8c208bc Micro-optimization of NzbQueue.is_empty 2023-12-01 14:50:07 +01:00
Safihre
2200ffa88e Use Server-specific timeout in final attempt 2023-12-01 14:34:07 +01:00
Safihre
4453316516 Server warnings were not always shown 2023-11-30 22:15:32 +01:00
Safihre
b947207571 Use Server-specific timeout during HappyEyeBalls 2023-11-30 19:50:31 +01:00
SABnzbd Automation
25d29deae6 Update translatable texts
[skip ci]
2023-11-30 12:26:53 +00:00
Safihre
9abe6d6d71 Skip empty HTTP-headers in URLGrabber and skip invalid categories 2023-11-30 13:25:43 +01:00
Safihre
77dbc0a37f Check nzb backup folder only if the job is not still in the queue 2023-11-30 13:19:57 +01:00
Safihre
659117512b Give RSS feed it's own history-stage 2023-11-30 13:04:12 +01:00
SABnzbd Automation
b1dbbc6a69 Update translatable texts
[skip ci]
2023-11-29 20:47:52 +00:00
Safihre
424a1c626e Add name of RSS feed to history Source
Closes #2206
2023-11-29 21:46:19 +01:00
Safihre
522666191b Indexer category was not used anymore 2023-11-29 21:46:19 +01:00
Safihre
78055ef794 Do not show propagation label in case job is Forced 2023-11-29 21:46:19 +01:00
SABnzbd Automation
0fe534c202 Update translatable texts
[skip ci]
2023-11-29 13:58:08 +00:00
Safihre
257179de31 Add ability to search Queue/History for status
Closes #2376
2023-11-29 14:57:15 +01:00
Safihre
65b57112b9 Optimize handling of propagation_delay 2023-11-29 14:57:15 +01:00
renovate[bot]
27f0b1d1f2 Update dependency cryptography to v41.0.6 [SECURITY] 2023-11-29 01:54:31 +00:00
SABnzbd Automation
6e31476c45 Update translatable texts
[skip ci]
2023-11-28 21:07:35 +00:00
Safihre
bc7f0f3fb3 Update text files for 4.2.0Beta1 2023-11-28 22:06:43 +01:00
SABnzbd Automation
13eeb5164f Update translatable texts
[skip ci]
2023-11-28 14:31:50 +00:00
Safihre
fc756ed23d Add smarter duplicate detection (#2736)
Restore pre-queue
2023-11-28 15:30:46 +01:00
SABnzbd Automation
c150365462 Update translatable texts
[skip ci]
2023-11-27 12:18:43 +00:00
renovate[bot]
58d209059e Update dependency setuptools to v69 2023-11-27 13:12:00 +01:00
Safihre
506179b517 Remove unused sort_type from guess_what 2023-11-24 21:17:29 +01:00
SABnzbd Automation
f0f4eb75df Update translatable texts
[skip ci]
2023-11-22 15:55:34 +00:00
Safihre
6c1c025668 Update text files 4.2.0Alpha3 2023-11-22 16:54:50 +01:00
90 changed files with 1342 additions and 1201 deletions

View File

@@ -81,7 +81,7 @@ jobs:
# We need the official Python, because the GA ones only support newer macOS versions
# The deployment target is picked up by the Python build tools automatically
# If updated, make sure to also set LSMinimumSystemVersion in SABnzbd.spec
PYTHON_VERSION: "3.12.0"
PYTHON_VERSION: "3.12.1"
MACOSX_DEPLOYMENT_TARGET: "10.9"
# We need to force compile for universal2 support
CFLAGS: -arch x86_64 -arch arm64

View File

@@ -1,33 +1,55 @@
Release Notes - SABnzbd 4.2.0 Alpha 2
Release Notes - SABnzbd 4.2.0 Release Candidate 1
=========================================================
## Changes since 4.1.0
- Numerous smaller performance improvements were made.
- Reduced recursive unpacking to 2 levels, instead of 5.
- IPv6 addresses are preferred during server address selection.
- Stricter check if `Complete Folder` is inside `Download Folder`.
- Windows: Reduced size of installer.
- Windows/macOS: Updated to Python 3.12.
This is a pre-release build of SABnzbd 4.2.0, which includes several new features and bug fixes.
## Bugfixes since 4.1.0
- Multi-select in the queue was broken for some users.
- Prevent crash during saving of configuration.
- Removing a failed download from the history could break active downloads.
## Key changes since 4.1.0
* **Duplicate detection workflow was overhauled:**
* `Series Duplicate Detection` was replaced by `Smart Duplicate Detection`
that can also detect `Movie` and `Daily Show` duplicates.
* Additionally, duplicates will also be detected if they are still in the queue.
* More information: https://sabnzbd.org/wiki/duplicate-detection
* **Interface changes:**
* Added ability to filter the Queue and History by `status`.
* RSS-feed that provided the download is shown in History details.
* **Performance and usability improvements:**
* Numerous smaller performance improvements were made.
* Server IP-address selection was optimized.
* The `Internet Bandwidth` test was made more reliable.
* Windows/macOS: Updated to Python 3.12.
* **Configuration changes:**
* The `On queue finish script` is now set in Switches.
* Reduced recursive unpacking to 2 levels, instead of 5.
* Stricter check if `Complete Folder` is inside `Download Folder`.
* Windows: Prevent use of network drive as `Download Folder`.
## Bug fixes since 4.1.0
* Fixed an issue where the multi-select option in the queue was not working for some users.
* Prevented a crash that would occur during the saving of configuration settings.
* Ensured that server warnings are always displayed to users.
* If `weblogging` was enabled, output was also written to regular log.
* Fixed an issue where removing a failed download from the History could break active downloads.
## Upgrade notices
- Direct upgrade is possible from version 3.0.0 and newer.
Upgrading from older versions will require `Queue repair`.
- Downgrading from version 4.2.0 or newer to 3.7.2 or older will
require `Queue repair` due to changes in the internal data format.
* You can directly upgrade from version 3.0.0 and newer.
* Upgrading from older versions will require performing a `Queue repair`.
* Downgrading from version 4.2.0 or newer to 3.7.2 or older will require
performing a `Queue repair` due to changes in the internal data format.
## Known problems and solutions
- Read the file "ISSUES.txt"
* Read `ISSUES.txt` or https://sabnzbd.org/wiki/introduction/known-issues
## About
SABnzbd is an open-source cross-platform binary newsreader.
It simplifies the process of downloading from Usenet dramatically, thanks
to its web-based user interface and advanced built-in post-processing options
that automatically verify, repair, extract and clean up posts downloaded
from Usenet.
SABnzbd is an open-source cross-platform binary newsreader.
It simplifies the process of downloading from Usenet dramatically, thanks to its web-based
user interface and advanced built-in post-processing options that automatically verify, repair,
extract and clean up posts downloaded from Usenet.
(c) Copyright 2007-2023 by The SABnzbd-Team (sabnzbd.org)
(c) Copyright 2007-2023 by The SABnzbd-Team (sabnzbd.org)

View File

@@ -1,3 +1,3 @@
# Special requirements for macOS universal2 binary release
# This way dependabot can auto-update them
cryptography==41.0.5
cryptography==41.0.7

View File

@@ -5,11 +5,11 @@ packaging==23.2
pyinstaller-hooks-contrib==2023.10
altgraph==0.17.4
wrapt==1.16.0
setuptools==68.2.2
setuptools==69.0.2
certifi
# Required on 32bit Windows, exclude it based on Python-version
importlib_metadata==6.8.0; python_version < '3.10'
importlib_metadata==7.0.0; python_version < '3.10'
importlib_resources==6.1.1; python_version < '3.10'
zipp==3.17.0; python_version < '3.10'

View File

@@ -202,7 +202,7 @@
<div class="field-pair">
<label class="config" for="nscript_parameters">$T('opt-nscript_parameters')</label>
<input type="text" name="nscript_parameters" id="nscript_parameters" value="$nscript_parameters" />
<span class="desc">$T('Optional') - $T('explain-nscript_parameters')</span>
<span class="desc">$T('Optional') - $T('readwiki')</span>
</div>
$show_notify_checkboxes('nscript')
<div class="field-pair no-field-pair-bg">

View File

@@ -96,23 +96,29 @@
<option value="3" <!--#if int($no_dupes) == 3 then 'selected="selected"' else ""#--> >$T('nodupes-fail')</option>
<option value="1" <!--#if int($no_dupes) == 1 then 'selected="selected"' else ""#--> >$T('nodupes-ignore')</option>
</select>
<span class="desc">$T('explain-no_dupes')</span>
<span class="desc">
$T('explain-no_dupes')<br>
<a href="https://sabnzbd.org/wiki/duplicate-detection" target="_blank">https://sabnzbd.org/wiki/duplicate-detection</a>
</span>
</div>
<div class="field-pair">
<label class="config" for="no_series_dupes">$T('opt-no_series_dupes')</label>
<select name="no_series_dupes" id="no_series_dupes">
<option value="0" <!--#if int($no_series_dupes) == 0 then 'selected="selected"' else ""#--> >$T('nodupes-off')</option>
<option value="4" <!--#if int($no_series_dupes) == 4 then 'selected="selected"' else ""#--> >$T('nodupes-tag')</option>
<option value="2" <!--#if int($no_series_dupes) == 2 then 'selected="selected"' else ""#--> >$T('nodupes-pause')</option>
<option value="3" <!--#if int($no_series_dupes) == 3 then 'selected="selected"' else ""#--> >$T('nodupes-fail')</option>
<option value="1" <!--#if int($no_series_dupes) == 1 then 'selected="selected"' else ""#--> >$T('nodupes-ignore')</option>
<label class="config" for="no_smart_dupes">$T('opt-no_smart_dupes')</label>
<select name="no_smart_dupes" id="no_smart_dupes">
<option value="0" <!--#if int($no_smart_dupes) == 0 then 'selected="selected"' else ""#--> >$T('nodupes-off')</option>
<option value="4" <!--#if int($no_smart_dupes) == 4 then 'selected="selected"' else ""#--> >$T('nodupes-tag')</option>
<option value="2" <!--#if int($no_smart_dupes) == 2 then 'selected="selected"' else ""#--> >$T('nodupes-pause')</option>
<option value="3" <!--#if int($no_smart_dupes) == 3 then 'selected="selected"' else ""#--> >$T('nodupes-fail')</option>
<option value="1" <!--#if int($no_smart_dupes) == 1 then 'selected="selected"' else ""#--> >$T('nodupes-ignore')</option>
</select>
<span class="desc">$T('explain-no_series_dupes')</span>
<span class="desc">
$T('explain-no_smart_dupes')<br>
<a href="https://sabnzbd.org/wiki/duplicate-detection" target="_blank">https://sabnzbd.org/wiki/duplicate-detection</a>
</span>
</div>
<div class="field-pair advanced-settings">
<label class="config" for="series_propercheck">$T('opt-series_propercheck')</label>
<input type="checkbox" name="series_propercheck" id="series_propercheck" value="1" <!--#if int($series_propercheck) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-series_propercheck')</span>
<label class="config" for="dupes_propercheck">$T('opt-dupes_propercheck')</label>
<input type="checkbox" name="dupes_propercheck" id="dupes_propercheck" value="1" <!--#if int($dupes_propercheck) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-dupes_propercheck')</span>
</div>
<div class="field-pair">
<label class="config" for="pause_on_pwrar">$T('opt-pause_on_pwrar')</label>
@@ -187,12 +193,12 @@
<div class="field-pair advanced-settings <!--#if not $have_nice then "disabled" else "" #-->">
<label class="config" for="nice">$T('opt-nice')</label>
<input type="text" name="nice" id="nice" value="$nice" <!--#if not $have_nice then 'readonly="readonly" disabled="disabled"' else "" #--> />
<span class="desc">$T('explain-nice')</span>
<span class="desc">$T('readwiki')</span>
</div>
<div class="field-pair advanced-settings <!--#if not $have_ionice then "disabled" else "" #-->">
<label class="config" for="ionice">$T('opt-ionice')</label>
<input type="text" name="ionice" id="ionice" value="$ionice" <!--#if not $have_ionice then 'readonly="readonly" disabled="disabled"' else "" #--> />
<span class="desc">$T('explain-ionice')</span>
<span class="desc">$T('readwiki')</span>
</div>
<!--#else#-->
<div class="field-pair advanced-settings">
@@ -204,13 +210,13 @@
<option value="2" <!--#if int($win_process_prio) == 2 then 'selected="selected"' else ""#-->>$T('win_process_prio-low')</option>
<option value="1" <!--#if int($win_process_prio) == 1 then 'selected="selected"' else ""#-->>$T('win_process_prio-idle')</option>
</select>
<span class="desc">$T('explain-win_process_prio')</span>
<span class="desc">$T('readwiki')</span>
</div>
<!--#end if#-->
<div class="field-pair advanced-settings">
<label class="config" for="par_option">$T('opt-par_option')</label>
<input type="text" name="par_option" id="par_option" value="$par_option" />
<span class="desc">$T('explain-par_option')</span>
<span class="desc">$T('readwiki')</span>
</div>
<div class="field-pair advanced-settings">
<label class="config" for="sfv_check">$T('opt-sfv_check')</label>

View File

@@ -134,7 +134,7 @@
<div class="col-sm-6">$T('dashboard-systemPerformance') &nbsp; </div>
<div class="col-sm-6 col-dot-overflow" data-bind="visible: hasPerformanceInfo">
<span data-bind="text: statusInfo.pystone"></span>
<a href="#" class="diskspeed-button" data-bind="click: loadStatusInfo" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest')"><span class="glyphicon glyphicon-repeat"></span></a>
<a href="#" class="diskspeed-button" data-bind="click: loadStatusInfo" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest') (~10 $T('seconds'))"><span class="glyphicon glyphicon-repeat"></span></a>
<small title="$cpumodel $cpusimd" data-tooltip="true">$cpumodel $cpusimd</small>
</div>
<div class="col-sm-6 col-loading" data-bind="visible: !hasPerformanceInfo()">$T('Glitter-loading')<span class="loader-dot-one">.</span><span class="loader-dot-two">.</span><span class="loader-dot-three">.</span></div>
@@ -143,7 +143,7 @@
<div class="col-sm-6">$T('dashboard-downloadDirSpeed') &nbsp; </div>
<div class="col-sm-6 col-dot-overflow" data-bind="visible: hasPerformanceInfo">
<span data-bind="text: statusInfo.downloaddirspeed()"></span> MB/s
<a href="#" class="diskspeed-button" data-bind="click: loadStatusInfo" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest')"><span class="glyphicon glyphicon-repeat"></span></a>
<a href="#" class="diskspeed-button" data-bind="click: loadStatusInfo" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest') (~10 $T('seconds'))"><span class="glyphicon glyphicon-repeat"></span></a>
<small data-bind="text: statusInfo.downloaddir, attr: { 'data-original-title': statusInfo.downloaddir }" data-tooltip="true"></small>
</div>
<div class="col-sm-6 col-loading" data-bind="visible: !hasPerformanceInfo()">$T('Glitter-loading')<span class="loader-dot-one">.</span><span class="loader-dot-two">.</span><span class="loader-dot-three">.</span></div>
@@ -152,7 +152,7 @@
<div class="col-sm-6">$T('dashboard-completeDirSpeed') &nbsp; </div>
<div class="col-sm-6 col-dot-overflow" data-bind="visible: hasPerformanceInfo">
<span data-bind="text: statusInfo.completedirspeed()"></span> MB/s
<a href="#" class="diskspeed-button" data-bind="click: loadStatusInfo" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest')"><span class="glyphicon glyphicon-repeat"></span></a>
<a href="#" class="diskspeed-button" data-bind="click: loadStatusInfo" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest') (~10 $T('seconds'))"><span class="glyphicon glyphicon-repeat"></span></a>
<small data-bind="text: statusInfo.completedir, attr: { 'data-original-title': statusInfo.completedir }" data-tooltip="true"></small>
</div>
<div class="col-sm-6 col-loading" data-bind="visible: !hasPerformanceInfo()">$T('Glitter-loading')<span class="loader-dot-one">.</span><span class="loader-dot-two">.</span><span class="loader-dot-three">.</span></div>
@@ -161,7 +161,7 @@
<div class="col-sm-6">$T('dashboard-internetBandwidth') &nbsp; </div>
<div class="col-sm-6" data-bind="visible: hasPerformanceInfo">
<span data-bind="text: statusInfo.internetbandwidth()"></span> MB/s
<a href="#" class="diskspeed-button" data-bind="click: loadStatusInfo" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest')"><span class="glyphicon glyphicon-repeat"></span></a>
<a href="#" class="diskspeed-button" data-bind="click: loadStatusInfo" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest') (~10 $T('seconds'))"><span class="glyphicon glyphicon-repeat"></span></a>
<small><span data-bind="text: statusInfo.internetbandwidth()*8"></span> Mbps</small>
</div>
<div class="col-sm-6 col-loading" data-bind="visible: !hasPerformanceInfo()">$T('Glitter-loading')<span class="loader-dot-one">.</span><span class="loader-dot-two">.</span><span class="loader-dot-three">.</span></div>

View File

@@ -97,6 +97,7 @@
glitterTranslate.status['Unpack'] = "$T('stage-unpack')";
glitterTranslate.status['Deobfuscate'] = "$T('stage-deobfuscate')";
glitterTranslate.status['Script'] = "$T('stage-script')";
glitterTranslate.status['RSS'] = "$T('stage-rss')";
glitterTranslate.status['Source'] = "$T('stage-source')";
glitterTranslate.status['Servers'] = "$T('stage-servers')";
glitterTranslate.status['INFO'] = "$T('log-info')".replace('+', '').toUpperCase();

View File

@@ -338,7 +338,7 @@ function ViewModel() {
limit: parseInt(self.queue.paginationLimit())
}
if (self.queue.searchTerm()) {
parseSearchQuery(api_call, self.queue.searchTerm(), ["cat", "category", "priority"])
parseSearchQuery(api_call, self.queue.searchTerm(), ["cat", "category", "priority", "status"])
}
var queueApi = callAPI(api_call)
.done(self.updateQueue)
@@ -367,7 +367,7 @@ function ViewModel() {
last_history_update: self.history.lastUpdate
}
if (self.history.searchTerm()) {
parseSearchQuery(history_call, self.history.searchTerm(), ["cat", "category"])
parseSearchQuery(history_call, self.history.searchTerm(), ["cat", "category", "status"])
}
// History
@@ -397,7 +397,6 @@ function ViewModel() {
if (keyword === "priority" && api_request["priority"]) {
for (const prio_name in self.queue.priorityName) {
api_request["priority"] = api_request["priority"].replace(prio_name, self.queue.priorityName[prio_name])
}
}
}

View File

@@ -4,7 +4,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: team@sabnzbd.org\n"
"Language-Team: SABnzbd <team@sabnzbd.org>\n"

View File

@@ -3,7 +3,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Language-Team: Czech (https://app.transifex.com/sabnzbd/teams/111101/cs/)\n"
"MIME-Version: 1.0\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Danish (https://app.transifex.com/sabnzbd/teams/111101/da/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: German (https://app.transifex.com/sabnzbd/teams/111101/de/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Spanish (https://app.transifex.com/sabnzbd/teams/111101/es/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Finnish (https://app.transifex.com/sabnzbd/teams/111101/fi/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: French (https://app.transifex.com/sabnzbd/teams/111101/fr/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: ION, 2020\n"
"Language-Team: Hebrew (https://app.transifex.com/sabnzbd/teams/111101/he/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Norwegian Bokmål (https://app.transifex.com/sabnzbd/teams/111101/nb/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Dutch (https://app.transifex.com/sabnzbd/teams/111101/nl/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Polish (https://app.transifex.com/sabnzbd/teams/111101/pl/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Portuguese (Brazil) (https://app.transifex.com/sabnzbd/teams/111101/pt_BR/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Romanian (https://app.transifex.com/sabnzbd/teams/111101/ro/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Russian (https://app.transifex.com/sabnzbd/teams/111101/ru/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Serbian (https://app.transifex.com/sabnzbd/teams/111101/sr/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Swedish (https://app.transifex.com/sabnzbd/teams/111101/sv/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Chinese (China) (https://app.transifex.com/sabnzbd/teams/111101/zh_CN/)\n"

View File

@@ -4,7 +4,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: team@sabnzbd.org\n"
"Language-Team: SABnzbd <team@sabnzbd.org>\n"
@@ -162,11 +162,6 @@ msgstr ""
msgid "Default"
msgstr ""
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr ""
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -259,7 +254,7 @@ msgid "Permissions setting of %s might deny SABnzbd access to the files and fold
msgstr ""
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgid "Network path \"%s\" is not allowed here"
msgstr ""
#: sabnzbd/cfg.py
@@ -1369,6 +1364,11 @@ msgstr ""
msgid "Old queue detected, use Status->Repair to convert the queue"
msgstr ""
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr ""
#. Warning message
#: sabnzbd/postproc.py
msgid "Completed Download Folder %s is on FAT file system, limiting maximum file size to 4GB"
@@ -1642,6 +1642,11 @@ msgstr ""
msgid "Script"
msgstr ""
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr ""
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2014,11 +2019,6 @@ msgstr ""
msgid "Scheduling"
msgstr ""
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr ""
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2427,7 +2427,6 @@ msgstr ""
msgid "Backup"
msgstr ""
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr ""
@@ -2904,19 +2903,19 @@ msgid "In case of \"Pause\", you'll need to set a password and resume the job."
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgid "Identical download detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect identical NZB files (based on items in your History or files in .nzb Backup Folder)"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgid "Smart duplicate detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect identical episodes in series (based on \"name/season/episode\" of items in your History)"
msgid "Detect duplicates based on analysis of the filename."
msgstr ""
#: sabnzbd/skintext.py
@@ -2924,7 +2923,7 @@ msgid "Allow proper releases"
msgstr ""
#: sabnzbd/skintext.py
msgid "Bypass series duplicate detection if PROPER, REAL or REPACK is detected in the download name"
msgid "Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in the download name."
msgstr ""
#. Four way switch for duplicates

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Czech (https://app.transifex.com/sabnzbd/teams/111101/cs/)\n"
@@ -179,11 +179,6 @@ msgstr "Žádný"
msgid "Default"
msgstr "Výchozí"
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr "Nepodařilo se zkompilovat regex pro hledaný výraz: %s"
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -286,8 +281,8 @@ msgid ""
msgstr ""
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgstr "UNC cesta \"%s\" zde není povolena"
msgid "Network path \"%s\" is not allowed here"
msgstr ""
#: sabnzbd/cfg.py
msgid "Queue not empty, cannot change folder."
@@ -1441,6 +1436,11 @@ msgstr ""
msgid "Old queue detected, use Status->Repair to convert the queue"
msgstr "Stará fronta nalezena, použijte Status->Repair pro konverzi fronty"
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr "Nepodařilo se zkompilovat regex pro hledaný výraz: %s"
#. Warning message
#: sabnzbd/postproc.py
msgid ""
@@ -1717,6 +1717,11 @@ msgstr ""
msgid "Script"
msgstr "Skript"
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2089,11 +2094,6 @@ msgstr "Přepínače"
msgid "Scheduling"
msgstr "Plánování"
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2522,7 +2522,6 @@ msgstr ""
msgid "Backup"
msgstr "Záloha"
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr ""
@@ -3047,23 +3046,19 @@ msgid "In case of \"Pause\", you'll need to set a password and resume the job."
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgstr "Detekovat duplicitní stahování"
#: sabnzbd/skintext.py
msgid ""
"Detect identical NZB files (based on items in your History or files in .nzb "
"Backup Folder)"
msgid "Identical download detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Detect identical episodes in series (based on \"name/season/episode\" of "
"items in your History)"
msgid "Smart duplicate detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicates based on analysis of the filename."
msgstr ""
#: sabnzbd/skintext.py
@@ -3072,8 +3067,8 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name"
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name."
msgstr ""
#. Four way switch for duplicates

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Danish (https://app.transifex.com/sabnzbd/teams/111101/da/)\n"
@@ -179,11 +179,6 @@ msgstr "Ingen"
msgid "Default"
msgstr "Standard"
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr "Det lykkedes ikke at kompilere regex for søgestreng: %s"
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -286,8 +281,8 @@ msgid ""
msgstr ""
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgstr "UNC søgning \"%s\" er ikke tilladt her"
msgid "Network path \"%s\" is not allowed here"
msgstr ""
#: sabnzbd/cfg.py
msgid "Queue not empty, cannot change folder."
@@ -1473,6 +1468,11 @@ msgstr "Fejl %s: Du skal angive et gyldigt brugernavn og adgangskode."
msgid "Old queue detected, use Status->Repair to convert the queue"
msgstr "Gamle kø opdaget, brug Status->Reparation for at konvertere kø"
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr "Det lykkedes ikke at kompilere regex for søgestreng: %s"
#. Warning message
#: sabnzbd/postproc.py
msgid ""
@@ -1749,6 +1749,11 @@ msgstr ""
msgid "Script"
msgstr "Script"
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2121,11 +2126,6 @@ msgstr "Parameter"
msgid "Scheduling"
msgstr "Planlægning"
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2563,7 +2563,6 @@ msgstr "Oppetid"
msgid "Backup"
msgstr "Sikkerhedskopi"
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr "Læs mere om dette på Wiki Help!"
@@ -3115,28 +3114,20 @@ msgstr ""
"I tilfælde af \"Pause\", skal du angive en adgangskode og genoptage jobbet."
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgstr "Find identiske downloads"
#: sabnzbd/skintext.py
msgid ""
"Detect identical NZB files (based on items in your History or files in .nzb "
"Backup Folder)"
msgid "Identical download detection"
msgstr ""
"Fundet identiske NZB filer (baseret på elementer i din historik eller filer "
"i. nzb Backup mappe)"
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgstr "Opdage identiske episoder i serier"
#: sabnzbd/skintext.py
msgid ""
"Detect identical episodes in series (based on \"name/season/episode\" of "
"items in your History)"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
#: sabnzbd/skintext.py
msgid "Smart duplicate detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicates based on analysis of the filename."
msgstr ""
"Fundet identiske episoder i serie (baseret på \"navn /sæson /episode\" af "
"elementer i din historik)"
#: sabnzbd/skintext.py
msgid "Allow proper releases"
@@ -3144,8 +3135,8 @@ msgstr "Tillad reelle udgivelser"
#: sabnzbd/skintext.py
msgid ""
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name"
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name."
msgstr ""
#. Four way switch for duplicates

View File

@@ -15,7 +15,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: German (https://app.transifex.com/sabnzbd/teams/111101/de/)\n"
@@ -195,12 +195,6 @@ msgstr "Nichts"
msgid "Default"
msgstr "Standard"
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr ""
"Kompilieren des regulären Ausdrucks für den Suchbegriff %s fehlgeschlagen."
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -307,8 +301,8 @@ msgstr ""
"erstellten Dateien und Ordner von SABnzbd verweigern."
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgstr "UNC-Pfad \"%s\" ist hier nicht erlaubt"
msgid "Network path \"%s\" is not allowed here"
msgstr ""
#: sabnzbd/cfg.py
msgid "Queue not empty, cannot change folder."
@@ -1535,6 +1529,12 @@ msgstr ""
"Alte Warteschlangen-Version erkannt, über Status->Reparieren ins neue Format"
" konvertieren"
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr ""
"Kompilieren des regulären Ausdrucks für den Suchbegriff %s fehlgeschlagen."
#. Warning message
#: sabnzbd/postproc.py
msgid ""
@@ -1814,6 +1814,11 @@ msgstr "Entschleiern"
msgid "Script"
msgstr "Skript"
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2186,11 +2191,6 @@ msgstr "Schalter"
msgid "Scheduling"
msgstr "Planung"
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2641,7 +2641,6 @@ msgstr "Zeit seit Start"
msgid "Backup"
msgstr "Sicherheitskopie"
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr "Lesen Sie dazu die Hilfe im Wiki!"
@@ -3234,28 +3233,20 @@ msgstr ""
"fortsetzen."
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgstr "Doppelte Downloads erkennen"
#: sabnzbd/skintext.py
msgid ""
"Detect identical NZB files (based on items in your History or files in .nzb "
"Backup Folder)"
msgid "Identical download detection"
msgstr ""
"Doppelte NZB Datei entdeckt (basierend auf den Eintragungen in der Historie "
"oder den .nzb Dateien im NZB-Backup-Ordner)"
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgstr "Doppelte Episoden in Serien erkennen"
#: sabnzbd/skintext.py
msgid ""
"Detect identical episodes in series (based on \"name/season/episode\" of "
"items in your History)"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
#: sabnzbd/skintext.py
msgid "Smart duplicate detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicates based on analysis of the filename."
msgstr ""
"Identische Episoden in den Serien entdeckt (basierend auf "
"\"name/season/episode\") der Einträge in der Historie"
#: sabnzbd/skintext.py
msgid "Allow proper releases"
@@ -3263,11 +3254,9 @@ msgstr "Erlaube \"Proper\" Releases"
#: sabnzbd/skintext.py
msgid ""
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name"
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name."
msgstr ""
"Umgehe Serien Duplikat-Erkennung, wenn PROPER, REAL oder REPACK im Download-"
"Namen erkannt wird"
#. Four way switch for duplicates
#: sabnzbd/skintext.py

View File

@@ -8,7 +8,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Spanish (https://app.transifex.com/sabnzbd/teams/111101/es/)\n"
@@ -188,11 +188,6 @@ msgstr "Ninguno"
msgid "Default"
msgstr "Predeterminado"
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr "Compilación de regex para término fallo: %s"
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -301,8 +296,8 @@ msgid ""
msgstr ""
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgstr "Ruta de acceso UNC \"%s\" no permitido aqui"
msgid "Network path \"%s\" is not allowed here"
msgstr ""
#: sabnzbd/cfg.py
msgid "Queue not empty, cannot change folder."
@@ -1525,6 +1520,11 @@ msgstr ""
"Se ha encontrado una cola antigua, utilice Estado->Reparar para convertir la"
" cola"
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr "Compilación de regex para término fallo: %s"
#. Warning message
#: sabnzbd/postproc.py
msgid ""
@@ -1805,6 +1805,11 @@ msgstr ""
msgid "Script"
msgstr "Script"
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2177,11 +2182,6 @@ msgstr "Switches"
msgid "Scheduling"
msgstr "Planificación"
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2629,7 +2629,6 @@ msgstr "Tiempo en Activo"
msgid "Backup"
msgstr "Copia de seguridad"
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr "Lee la ayuda en la Wiki (inglés) acerca de esto!"
@@ -3197,28 +3196,20 @@ msgstr ""
"trabajo."
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgstr "Detectar descargas duplicadas"
#: sabnzbd/skintext.py
msgid ""
"Detect identical NZB files (based on items in your History or files in .nzb "
"Backup Folder)"
msgid "Identical download detection"
msgstr ""
"Detecta archivos NZB idénticos (basándose en los elementos de su historial o"
" los archivos en el directorio de copia de seguridad .nzb)"
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgstr "Detectar episodios duplicadas en serie"
#: sabnzbd/skintext.py
msgid ""
"Detect identical episodes in series (based on \"name/season/episode\" of "
"items in your History)"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
#: sabnzbd/skintext.py
msgid "Smart duplicate detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicates based on analysis of the filename."
msgstr ""
"Detecta episodios idénticos en series (basándose en "
"\"nombre/temporada/episodio\" de los elementos de su historial)"
#: sabnzbd/skintext.py
msgid "Allow proper releases"
@@ -3226,11 +3217,9 @@ msgstr "Permitir comunicados adecuados"
#: sabnzbd/skintext.py
msgid ""
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name"
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name."
msgstr ""
"Evitar detección de duplicado de series si se detecta PROPER, REAL o REPACK "
"en el nombre de la descarga"
#. Four way switch for duplicates
#: sabnzbd/skintext.py

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Finnish (https://app.transifex.com/sabnzbd/teams/111101/fi/)\n"
@@ -181,11 +181,6 @@ msgstr "Ei mitään"
msgid "Default"
msgstr "Oletus"
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr "Regex käännös epäonnistui hakutermille: %s"
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -284,8 +279,8 @@ msgid ""
msgstr ""
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgstr "TUNT polku \"%s\" ei ole sallittu"
msgid "Network path \"%s\" is not allowed here"
msgstr ""
#: sabnzbd/cfg.py
msgid "Queue not empty, cannot change folder."
@@ -1467,6 +1462,11 @@ msgstr "Virhe %s: Syötä kelvollinen käyttäjänimi ja salasana."
msgid "Old queue detected, use Status->Repair to convert the queue"
msgstr "Vanhan version jono havaittiin, käytä Tila->Korjaa muuntaaksesi jonon"
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr "Regex käännös epäonnistui hakutermille: %s"
#. Warning message
#: sabnzbd/postproc.py
msgid ""
@@ -1743,6 +1743,11 @@ msgstr ""
msgid "Script"
msgstr "Skripti"
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2115,11 +2120,6 @@ msgstr "Muuttujat"
msgid "Scheduling"
msgstr "Ajastus"
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2559,7 +2559,6 @@ msgstr "Käynnissäoloaika"
msgid "Backup"
msgstr "Varmuuskopioi"
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr "Lue Wikin ohjeet tähän!"
@@ -3122,23 +3121,19 @@ msgstr ""
" lataamista."
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgstr "Tunnista päällekkäiset lataukset"
#: sabnzbd/skintext.py
msgid ""
"Detect identical NZB files (based on items in your History or files in .nzb "
"Backup Folder)"
msgid "Identical download detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgstr "Tunnista identtiset jaksot sarjassa"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Detect identical episodes in series (based on \"name/season/episode\" of "
"items in your History)"
msgid "Smart duplicate detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicates based on analysis of the filename."
msgstr ""
#: sabnzbd/skintext.py
@@ -3147,8 +3142,8 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name"
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name."
msgstr ""
#. Four way switch for duplicates

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Fred L <88com88@gmail.com>, 2023\n"
"Language-Team: French (https://app.transifex.com/sabnzbd/teams/111101/fr/)\n"
@@ -190,11 +190,6 @@ msgstr "Aucun"
msgid "Default"
msgstr "Par défaut"
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr "Echec de la compilation de regex pour la recherche du terme : %s"
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -305,8 +300,8 @@ msgstr ""
"fichiers et dossiers qu'il crée."
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgstr "Le chemin UNC \"%s\" n'est pas autorisé ici"
msgid "Network path \"%s\" is not allowed here"
msgstr "Le chemin réseau \"%s\" n'est pas autorisé ici"
#: sabnzbd/cfg.py
msgid "Queue not empty, cannot change folder."
@@ -1249,7 +1244,7 @@ msgstr "DOUBLON"
#: sabnzbd/nzbstuff.py
msgid "ALTERNATIVE"
msgstr ""
msgstr "ALTERNATIVE"
#: sabnzbd/nzbstuff.py
msgid "ENCRYPTED"
@@ -1531,6 +1526,11 @@ msgstr ""
"Ancienne file d'attente détectée, utiliser Statut-> Réparation pour "
"convertir la file d'attente"
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr "Echec de la compilation de regex pour la recherche du terme : %s"
#. Warning message
#: sabnzbd/postproc.py
msgid ""
@@ -1812,6 +1812,11 @@ msgstr "Désobfuscation"
msgid "Script"
msgstr "Script"
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2184,11 +2189,6 @@ msgstr "Switches"
msgid "Scheduling"
msgstr "Planification"
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2641,7 +2641,6 @@ msgstr "Temps de fonctionnement"
msgid "Backup"
msgstr "Secours"
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr "Consultez le Wiki pour plus d'info à ce sujet (en anglais) !"
@@ -3235,28 +3234,21 @@ msgstr ""
"tâche."
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgstr "Détecter les doublons de téléchargement"
msgid "Identical download detection"
msgstr "Détection des téléchargements identiques"
#: sabnzbd/skintext.py
msgid ""
"Detect identical NZB files (based on items in your History or files in .nzb "
"Backup Folder)"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
"Détecter les fichiers NZB identiques (en fonction des éléments de votre "
"historique ou des fichiers .nzb du dossier de backup)"
"Détecter les téléchargements identiques basés sur le nom ou le contenu NZB."
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgstr "Détecter les doublons d'épisodes de séries"
msgid "Smart duplicate detection"
msgstr "Détection intelligente des doublons"
#: sabnzbd/skintext.py
msgid ""
"Detect identical episodes in series (based on \"name/season/episode\" of "
"items in your History)"
msgstr ""
"Détecter les épisodes de série identiques (en fonction du modèle "
"\"nom/saison/épisode\" des éléments de votre historique)"
msgid "Detect duplicates based on analysis of the filename."
msgstr "Détecter les doublons basés sur l'analyse du nom de fichier."
#: sabnzbd/skintext.py
msgid "Allow proper releases"
@@ -3264,11 +3256,11 @@ msgstr "Autoriser les versions corrigées (proper)"
#: sabnzbd/skintext.py
msgid ""
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name"
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name."
msgstr ""
"Contourner la détection des doublons si PROPER, REAL ou REPACK est détecté "
"dans l'intitulé du téléchargement"
"dans l'intitulé du téléchargement."
#. Four way switch for duplicates
#: sabnzbd/skintext.py

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Hebrew (https://app.transifex.com/sabnzbd/teams/111101/he/)\n"
@@ -178,11 +178,6 @@ msgstr "אין"
msgid "Default"
msgstr "ברירת מחדל"
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr "עבור מונח חיפוש regex נכשל בליקוט: %s"
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -283,8 +278,8 @@ msgstr ""
"הגדרת הרשאות של %s עשויה לדחות גישה מן SABnzbd אל הקבצים והתיקיות שהוא יוצר."
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgstr "נתיב UNC \"%s\" אינו מותר כאן"
msgid "Network path \"%s\" is not allowed here"
msgstr ""
#: sabnzbd/cfg.py
msgid "Queue not empty, cannot change folder."
@@ -1472,6 +1467,11 @@ msgstr "שגיאה %s: אתה צריך לספק שם משתמש וסיסמה ת
msgid "Old queue detected, use Status->Repair to convert the queue"
msgstr "תור ישן התגלה, השתמש במעמד->תיקון כדי להמיר את התור"
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr "עבור מונח חיפוש regex נכשל בליקוט: %s"
#. Warning message
#: sabnzbd/postproc.py
msgid ""
@@ -1750,6 +1750,11 @@ msgstr "אי־האפלה"
msgid "Script"
msgstr "תסריט"
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2122,11 +2127,6 @@ msgstr "מתגים"
msgid "Scheduling"
msgstr "תזמון"
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2568,7 +2568,6 @@ msgstr "זמן פעולה"
msgid "Backup"
msgstr "גיבוי"
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr "קרא את עזרת וויקי על זה!"
@@ -3126,25 +3125,20 @@ msgid "In case of \"Pause\", you'll need to set a password and resume the job."
msgstr "במקרה של \"השהיה\", תצטרך לקבוע סיסמה ולהמשיך את העבודה."
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgstr "גלה הורדות כפולות"
#: sabnzbd/skintext.py
msgid ""
"Detect identical NZB files (based on items in your History or files in .nzb "
"Backup Folder)"
msgid "Identical download detection"
msgstr ""
"גלה קבצי NZB זהים (על סמך פריטים בהיסטוריה שלך או קבצים בתיקיית גיבוי .nzb)"
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgstr "גלה פרקים כפולים בסדרות"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Detect identical episodes in series (based on \"name/season/episode\" of "
"items in your History)"
msgstr "גלה פרקים זהים בסדרות (על סמך \"שם/עונה/פרק\" של פריטים בהיסטוריה שלך)"
msgid "Smart duplicate detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicates based on analysis of the filename."
msgstr ""
#: sabnzbd/skintext.py
msgid "Allow proper releases"
@@ -3152,9 +3146,9 @@ msgstr "התר שחרורים תקינים"
#: sabnzbd/skintext.py
msgid ""
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name"
msgstr "עקוף גילוי כפילויות סדרה אם PROPER, REAL או REPACK מתגלים בשם ההורדה"
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name."
msgstr ""
#. Four way switch for duplicates
#: sabnzbd/skintext.py

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Norwegian Bokmål (https://app.transifex.com/sabnzbd/teams/111101/nb/)\n"
@@ -177,11 +177,6 @@ msgstr "Ingen"
msgid "Default"
msgstr "Standard"
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr "Kunne ikke lage regex for søkestreng: %s"
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -280,8 +275,8 @@ msgid ""
msgstr ""
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgstr "UNC-sti \"%s\" er ikke tillatt her"
msgid "Network path \"%s\" is not allowed here"
msgstr ""
#: sabnzbd/cfg.py
msgid "Queue not empty, cannot change folder."
@@ -1463,6 +1458,11 @@ msgstr "Feil %s: Du må oppgi et gyldig brukernavn og passord."
msgid "Old queue detected, use Status->Repair to convert the queue"
msgstr "Gammel kø oppdaget. Bruk Status -> Reparer for å konvertere køen"
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr "Kunne ikke lage regex for søkestreng: %s"
#. Warning message
#: sabnzbd/postproc.py
msgid ""
@@ -1739,6 +1739,11 @@ msgstr ""
msgid "Script"
msgstr "Skript"
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2111,11 +2116,6 @@ msgstr "Svitsjer"
msgid "Scheduling"
msgstr "Nedlastingsplan"
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2553,7 +2553,6 @@ msgstr "Oppetid"
msgid "Backup"
msgstr "Sikkerhetskopi"
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr "Les Wiki Help fer dette!"
@@ -3103,23 +3102,19 @@ msgstr ""
"I tilfelle \"Pause\", så trenger du å sette et passord og gjenoppta jobben."
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgstr "Oppdag duplikatnedlastinger"
#: sabnzbd/skintext.py
msgid ""
"Detect identical NZB files (based on items in your History or files in .nzb "
"Backup Folder)"
msgid "Identical download detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgstr "Oppdag duplikat episoder i serie"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Detect identical episodes in series (based on \"name/season/episode\" of "
"items in your History)"
msgid "Smart duplicate detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicates based on analysis of the filename."
msgstr ""
#: sabnzbd/skintext.py
@@ -3128,8 +3123,8 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name"
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name."
msgstr ""
#. Four way switch for duplicates

View File

@@ -8,7 +8,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Dutch (https://app.transifex.com/sabnzbd/teams/111101/nl/)\n"
@@ -185,11 +185,6 @@ msgstr "Geen"
msgid "Default"
msgstr "Standaard"
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr "Het compileren van 'regex' voor de zoekterm lukt niet: %s"
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -298,8 +293,8 @@ msgstr ""
"tot de aangemaakte bestanden en mappen."
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgstr "UNC-pad '%s' hier niet toegestaan."
msgid "Network path \"%s\" is not allowed here"
msgstr "Netwerk-pad \"%s\" hier niet toegestaan."
#: sabnzbd/cfg.py
msgid "Queue not empty, cannot change folder."
@@ -319,6 +314,9 @@ msgid ""
"Do not use a folder in the application folder as your Scripts Folder, it "
"might be emptied during updates."
msgstr ""
"Als de Map voor scripts zich in de SABnzbd installatie-map bevindt kan deze "
"automatisch verwijderd worden tijdens updates. We adviseren een andere "
"locatie te gebruiken voor je scripts."
#. Warning message
#: sabnzbd/config.py
@@ -472,7 +470,7 @@ msgstr "Initialisatie van %s@%s mislukt, vanwege: %s"
#. Error message
#: sabnzbd/downloader.py
msgid "Fatal error in Downloader"
msgstr ""
msgstr "Onherstelbare fout in de Downloader"
#: sabnzbd/downloader.py
msgid "Too many connections to server %s [%s]"
@@ -646,11 +644,15 @@ msgstr ""
#: sabnzbd/filesystem.py
msgid "Cannot write a long filename to %s. This can cause problems."
msgstr ""
"Bestanden met lange bestandsnamen kunnen niet worden opgeslagen in %s. Dit "
"kan voor problemen zorgen tijdens het downloaden."
#. Warning message
#: sabnzbd/filesystem.py
msgid "Cannot write a unicode filename to %s. This can cause problems."
msgstr ""
"Bestanden met speciale karakters in de bestandsnaam kunnen niet worden "
"opgeslagen in %s. Dit kan voor problemen zorgen tijdens het downloaden."
#. Warning message
#: sabnzbd/filesystem.py
@@ -1235,7 +1237,7 @@ msgstr "DUBBEL"
#: sabnzbd/nzbstuff.py
msgid "ALTERNATIVE"
msgstr ""
msgstr "ALTERNATIEF"
#: sabnzbd/nzbstuff.py
msgid "ENCRYPTED"
@@ -1511,6 +1513,11 @@ msgstr "Fout %s: Je moet een geldige gebruikersnaam en wachtwoord invullen."
msgid "Old queue detected, use Status->Repair to convert the queue"
msgstr "Oude wachtrij gevonden, gebruik Status->Reparatie om te converteren"
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr "Het compileren van 'regex' voor de zoekterm lukt niet: %s"
#. Warning message
#: sabnzbd/postproc.py
msgid ""
@@ -1790,6 +1797,11 @@ msgstr "Bestandsnaam verbeteren"
msgid "Script"
msgstr "Script"
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2162,11 +2174,6 @@ msgstr "Opties"
msgid "Scheduling"
msgstr "Taakplanner"
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2600,6 +2607,8 @@ msgid ""
"Speed up repairs by installing par2cmdline-turbo, it is available for many "
"platforms."
msgstr ""
"Versnel reparaties door par2cmdline-turbo te installeren. Beschikbaar voor "
"veel besturingssystemen."
#: sabnzbd/skintext.py
msgid "Version"
@@ -2614,7 +2623,6 @@ msgstr "Tijd in de lucht"
msgid "Backup"
msgstr "Reserve"
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr "Lees de Wiki pagina over dit onderwerp"
@@ -2982,6 +2990,8 @@ msgstr "(kan aangepast worden door de categorieën)."
msgid ""
"Use Sorting to automatically organize and rename your completed downloads."
msgstr ""
"Gebruik Sorteren om automatisch je voltooide downloads te organiseren en "
"hernoemen."
#: sabnzbd/skintext.py
msgid "Minimum Free Space for Completed Download Folder"
@@ -3066,7 +3076,7 @@ msgstr "Systeemmappen"
#: sabnzbd/skintext.py
msgid "Hidden Folders"
msgstr ""
msgstr "Verborgen mappen"
#: sabnzbd/skintext.py
msgid "Administrative Folder"
@@ -3113,7 +3123,7 @@ msgstr ""
#: sabnzbd/skintext.py
msgid "Purge Logs"
msgstr ""
msgstr "Logs wissen"
#: sabnzbd/skintext.py
msgid ".nzb Backup Folder"
@@ -3192,28 +3202,22 @@ msgstr ""
"download vrij te geven"
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgstr "Detecteer dubbele downloads"
msgid "Identical download detection"
msgstr "Identieke downloaddetectie"
#: sabnzbd/skintext.py
msgid ""
"Detect identical NZB files (based on items in your History or files in .nzb "
"Backup Folder)"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
"Detecteer identieke downloads (op basis van downloads in je Geschiedenis of "
"bestanden in je .nzb backup map)."
"Detecteer identieke downloads op basis van de naam of de inhoud van de NZB."
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgstr "Detecteer dubbele afleveringen in series"
msgid "Smart duplicate detection"
msgstr "Slimme detectie van dubbele downloads"
#: sabnzbd/skintext.py
msgid ""
"Detect identical episodes in series (based on \"name/season/episode\" of "
"items in your History)"
msgid "Detect duplicates based on analysis of the filename."
msgstr ""
"Detecteer identieke afleveringen in series (gebaseerd op "
"\"naam/seizoen/aflevering\" van downloads in je Geschiedenis)."
"Detecteer dubbele downloads op basis van de analyse van de bestandsnaam."
#: sabnzbd/skintext.py
msgid "Allow proper releases"
@@ -3221,11 +3225,11 @@ msgstr "Sta verbeterde downloads toe"
#: sabnzbd/skintext.py
msgid ""
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name"
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name."
msgstr ""
"Sla dubbele download detectie over als er in de naam van de download PROPER,"
" REAL of REPACK bevat"
"Sla slimme detectie van dubbele downloads over als de naam van de download "
"PROPER, REAL of REPACK bevat."
#. Four way switch for duplicates
#: sabnzbd/skintext.py
@@ -3317,11 +3321,11 @@ msgstr "Word uitgevoerd vóór een download aan de wachtrij word toegevoegd"
#: sabnzbd/skintext.py
msgid "On queue finish script"
msgstr ""
msgstr "Script voor na het afronden van de wachtrij"
#: sabnzbd/skintext.py
msgid "Executed after the queue finishes downloading."
msgstr ""
msgstr "Script wordt uitgevoerd nadat de wachtrij is gedownload."
#: sabnzbd/skintext.py
msgid "Extra PAR2 Parameters"
@@ -4293,15 +4297,15 @@ msgstr "Sorteertekst"
#: sabnzbd/skintext.py
msgid "Multi-part Label"
msgstr ""
msgstr "Meervoudig label"
#: sabnzbd/skintext.py
msgid "Show folder"
msgstr ""
msgstr "Map per serie"
#: sabnzbd/skintext.py
msgid "Season folder"
msgstr ""
msgstr "Map per seizoen"
#: sabnzbd/skintext.py
msgid "In folders"
@@ -4317,7 +4321,7 @@ msgstr "Downloadnaam als Bestandsnaam"
#: sabnzbd/skintext.py
msgid "Series"
msgstr ""
msgstr "Series"
#. Note for title expression in Sorting that does case adjustment
#: sabnzbd/skintext.py
@@ -4354,7 +4358,7 @@ msgstr "Minimale bestandsgrootte"
#: sabnzbd/skintext.py
msgid "Affected Job Types"
msgstr ""
msgstr "Type downloads"
#: sabnzbd/skintext.py
msgid "All"
@@ -4362,15 +4366,15 @@ msgstr "alles"
#: sabnzbd/skintext.py
msgid "Series with air dates"
msgstr ""
msgstr "Series met datums"
#: sabnzbd/skintext.py
msgid "Movies"
msgstr ""
msgstr "Films"
#: sabnzbd/skintext.py
msgid "Other / Unknown"
msgstr ""
msgstr "Anders / Onbekend"
#: sabnzbd/skintext.py
msgid ""
@@ -4382,34 +4386,43 @@ msgid ""
"applied.</p><p>More options are available when Advanced Settings is "
"checked.<br/>Detailed information can be found on the Wiki.</p>"
msgstr ""
"<p>Gebruik Sorteren om automatisch je voltooide downloads te organiseren. Bijvoorbeeld het automatisch verplaatsen van alle afleveringen van een serie in een seizoensspecifieke map. Of plaats films in een map met de naam van de film.</p>\n"
"\n"
"<p>Sorteringen worden in de getoonde volgorde geprobeerd en kunnen worden herschikt door ze te slepen.<br/> De eerste actieve Sortering die overeenkomt met zowel de betreffende categorie als het type taak wordt toegepast.</p>\n"
"\n"
"<p>Meer opties zijn beschikbaar wanneer Geavanceerde instellingen zijn aangevinkt.<br/> Gedetailleerde informatie is te vinden op de Wiki.</p>"
#: sabnzbd/skintext.py
msgid "Add Sorter"
msgstr ""
msgstr "Sortering toevoegen"
#: sabnzbd/skintext.py
msgid "Remove Sorter"
msgstr ""
msgstr "Sortering verwijderen"
#: sabnzbd/skintext.py
msgid "Test Data"
msgstr ""
msgstr "Testgegevens"
#: sabnzbd/skintext.py
msgid "Quick start"
msgstr ""
msgstr "Snel beginnen"
#: sabnzbd/skintext.py
msgid ""
"Move and rename all episodes in the \"tv\" category to a show-specific "
"folder"
msgstr ""
"Verplaats en hernoem alle afleveringen in de \"tv\" categorie naar een "
"specifieke map voor de serie."
#: sabnzbd/skintext.py
msgid ""
"Move and rename all movies in the \"movies\" category to a movie-specific "
"folder"
msgstr ""
"Verplaats en hernoem alle films in de categorie \"films\" naar een "
"specifieke map voor de film."
#: sabnzbd/skintext.py
msgid ""
@@ -4652,7 +4665,7 @@ msgstr "Sneltoetsen"
#: sabnzbd/skintext.py
msgid "Shift+Arrow key: Browse Queue and History pages"
msgstr ""
msgstr "Shift+Pijltoets: Blader door de wachtrij- en geschiedenispagina's"
#: sabnzbd/skintext.py
msgid "How long or untill when do you want to pause? (in English!)"
@@ -4831,7 +4844,7 @@ msgstr ""
#. Error message
#: sabnzbd/sorting.py
msgid "Failed to rename %s to %s"
msgstr ""
msgstr "Hernoemen van %s naar %s mislukt"
#. Error message
#: sabnzbd/sorting.py

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Polish (https://app.transifex.com/sabnzbd/teams/111101/pl/)\n"
@@ -173,11 +173,6 @@ msgstr "Brak"
msgid "Default"
msgstr "Domyślne"
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr "Błąd kompilacji wyrażenia regularnego dla wyszukiwania: %s"
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -276,8 +271,8 @@ msgid ""
msgstr ""
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgstr "Ścieżka UNC \"%s\" niedozwolona"
msgid "Network path \"%s\" is not allowed here"
msgstr ""
#: sabnzbd/cfg.py
msgid "Queue not empty, cannot change folder."
@@ -1469,6 +1464,11 @@ msgstr ""
"Wykryto kolejkę w starszej wersji, użyj funkcji Status->Naprawa, aby ją "
"przekonwertować"
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr "Błąd kompilacji wyrażenia regularnego dla wyszukiwania: %s"
#. Warning message
#: sabnzbd/postproc.py
msgid ""
@@ -1745,6 +1745,11 @@ msgstr ""
msgid "Script"
msgstr "Skrypt"
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2117,11 +2122,6 @@ msgstr "Przełączniki"
msgid "Scheduling"
msgstr "Harmonogram"
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2558,7 +2558,6 @@ msgstr "Czas działania"
msgid "Backup"
msgstr "Zapasowy"
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr "Przeczytaj o tym w Wiki!"
@@ -3111,23 +3110,19 @@ msgstr ""
"Jeśli wybrano \"Wstrzymaj\", będzie trzeba ustawić hasło i wznowić zadanie"
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgstr "Działanie dla duplikatów"
#: sabnzbd/skintext.py
msgid ""
"Detect identical NZB files (based on items in your History or files in .nzb "
"Backup Folder)"
msgid "Identical download detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgstr "Wykryj zduplikowane odcinki seriali"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Detect identical episodes in series (based on \"name/season/episode\" of "
"items in your History)"
msgid "Smart duplicate detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicates based on analysis of the filename."
msgstr ""
#: sabnzbd/skintext.py
@@ -3136,8 +3131,8 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name"
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name."
msgstr ""
#. Four way switch for duplicates

View File

@@ -2,11 +2,12 @@
# Copyright 2007-2023 The SABnzbd-Team
#
# Translators:
# Henrique Moreno, 2023
# Safihre <safihre@sabnzbd.org>, 2023
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Portuguese (Brazil) (https://app.transifex.com/sabnzbd/teams/111101/pt_BR/)\n"
@@ -67,7 +68,7 @@ msgstr "aplicativo 7za... NÃO encontrado!"
#. Error message
#: SABnzbd.py
msgid "Essential modules are missing, downloading cannot start."
msgstr ""
msgstr "Módulos essenciais estão faltando, não é possível baixar."
#. Warning message
#: SABnzbd.py
@@ -89,6 +90,8 @@ msgid ""
"SABnzbd was started with encoding %s, this should be UTF-8. Expect problems "
"with Unicoded file and directory names in downloads."
msgstr ""
"SABnzbd iniciou com codificado %s, deveria ser UFT-8. Esperado problemas com"
" arquivos e nomes de diretórios Unicode nos downloades."
#. Warning message
#: SABnzbd.py
@@ -96,11 +99,13 @@ msgid ""
"Current umask (%o) might deny SABnzbd access to the files and folders it "
"creates."
msgstr ""
"Mascara atual (%o) pode negar ao SABnzbd acesso aos arquivos e diretórios "
"criados."
#. Warning message
#: SABnzbd.py
msgid "Could not load additional certificates from certifi package"
msgstr ""
msgstr "Não foi possível carregar certificado do pacote certifi."
#. Warning message
#: SABnzbd.py
@@ -110,7 +115,7 @@ msgstr "HTTPS desabilitado pela falta de arquivos CERT e KEY"
#. Warning message
#: SABnzbd.py
msgid "Disabled HTTPS because of invalid CERT and KEY files"
msgstr ""
msgstr "HTTPs desabilitado por caus de arquivo CERT e KEY invalidos"
#. Error message
#: SABnzbd.py
@@ -138,22 +143,22 @@ msgstr "Erro fatal ao salvar estado"
#. Warning message
#: sabnzbd/__init__.py
msgid "Restarting because of crashed postprocessor"
msgstr ""
msgstr "Reiniciado por falha de pós processamento."
#. Warning message
#: sabnzbd/__init__.py
msgid "Restarting because of crashed downloader"
msgstr ""
msgstr "Reiniciado por falha de download"
#. Warning message
#: sabnzbd/__init__.py
msgid "Restarting because of crashed assembler"
msgstr ""
msgstr "Reiniciado por falha de assembler"
#. Warning message
#: sabnzbd/__init__.py
msgid "Cannot access PID file %s"
msgstr ""
msgstr "Não é possível acessar arquivo PID %s"
#: sabnzbd/api.py, sabnzbd/emailer.py
msgid "Email succeeded"
@@ -177,11 +182,6 @@ msgstr "Nenhum"
msgid "Default"
msgstr "Padrão"
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr "Falha ao compilar a expressão para o termo pesquisado: %s"
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -208,6 +208,8 @@ msgid ""
"Paused job \"%s\" because of encrypted RAR file (if supplied, all passwords "
"were tried)"
msgstr ""
"Tarefa \"%s\" pausado por causa de arquivo RAR encripitado (se fornecido, "
"todos as senhas foram tentadas)"
#. Warning message
#: sabnzbd/assembler.py
@@ -215,6 +217,8 @@ msgid ""
"Aborted job \"%s\" because of encrypted RAR file (if supplied, all passwords"
" were tried)"
msgstr ""
"Tarefa \"%s\" abortado por causa de arquivo RAR encripitado (se fornecido, "
"todos as senhas foram tentadas)"
#: sabnzbd/assembler.py
msgid "Aborted, encryption detected"
@@ -224,6 +228,8 @@ msgstr "Cancelado, criptografia detectada"
#: sabnzbd/assembler.py
msgid "In \"%s\" unwanted extension in RAR file. Unwanted file is %s "
msgstr ""
"Em \"%s\" extensão não necessária em arquivo RAR. Arquivo não necessário é "
"%s "
#: sabnzbd/assembler.py
msgid "Unwanted extension is in rar file %s"
@@ -280,8 +286,8 @@ msgid ""
msgstr ""
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgstr "O caminho UNC \"%s\" não é permitido aqui"
msgid "Network path \"%s\" is not allowed here"
msgstr ""
#: sabnzbd/cfg.py
msgid "Queue not empty, cannot change folder."
@@ -1470,6 +1476,11 @@ msgstr ""
"Fila antiga detectada, use \"Situação -> Reparação da fila\" para converter "
"a fila"
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr "Falha ao compilar a expressão para o termo pesquisado: %s"
#. Warning message
#: sabnzbd/postproc.py
msgid ""
@@ -1748,6 +1759,11 @@ msgstr ""
msgid "Script"
msgstr "Script"
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2120,11 +2136,6 @@ msgstr "Opções"
msgid "Scheduling"
msgstr "Agendamento"
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2561,7 +2572,6 @@ msgstr "Tempo ativo"
msgid "Backup"
msgstr "Backup"
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr "Leia a sessão ajuda no Wiki sobre isso!"
@@ -3113,23 +3123,19 @@ msgid "In case of \"Pause\", you'll need to set a password and resume the job."
msgstr "Em caso de \"Pausa\", você precisa definir uma senha e retomar a tarefa."
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgstr "Detectar Downloads Duplicados"
#: sabnzbd/skintext.py
msgid ""
"Detect identical NZB files (based on items in your History or files in .nzb "
"Backup Folder)"
msgid "Identical download detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgstr "Detecta episódios duplicados em séries"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Detect identical episodes in series (based on \"name/season/episode\" of "
"items in your History)"
msgid "Smart duplicate detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicates based on analysis of the filename."
msgstr ""
#: sabnzbd/skintext.py
@@ -3138,8 +3144,8 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name"
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name."
msgstr ""
#. Four way switch for duplicates

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Romanian (https://app.transifex.com/sabnzbd/teams/111101/ro/)\n"
@@ -182,11 +182,6 @@ msgstr "Niciunul"
msgid "Default"
msgstr "Implicit"
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr "Compilarea unei căutări regex nereuşită: %s"
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -291,8 +286,8 @@ msgid ""
msgstr ""
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgstr "cale UNC \"%s\" nu este premisă aici"
msgid "Network path \"%s\" is not allowed here"
msgstr ""
#: sabnzbd/cfg.py
msgid "Queue not empty, cannot change folder."
@@ -1497,6 +1492,11 @@ msgstr ""
"Coadă de descărcare veche detectată, utilizează Stare->Reparare pentru a "
"converti coada"
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr "Compilarea unei căutări regex nereuşită: %s"
#. Warning message
#: sabnzbd/postproc.py
msgid ""
@@ -1775,6 +1775,11 @@ msgstr ""
msgid "Script"
msgstr "Script"
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2147,11 +2152,6 @@ msgstr "Comutatoare"
msgid "Scheduling"
msgstr "Planificare"
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2591,7 +2591,6 @@ msgstr "Durata Funcţionării"
msgid "Backup"
msgstr "Server Secundar"
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr "Citeşte Ajutorul Wiki despre asta !"
@@ -3140,23 +3139,19 @@ msgstr ""
"reluați sarcina."
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgstr "Detectează Descărcări Duplicate"
#: sabnzbd/skintext.py
msgid ""
"Detect identical NZB files (based on items in your History or files in .nzb "
"Backup Folder)"
msgid "Identical download detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgstr "Detectează episoade duplicate în seriale"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Detect identical episodes in series (based on \"name/season/episode\" of "
"items in your History)"
msgid "Smart duplicate detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicates based on analysis of the filename."
msgstr ""
#: sabnzbd/skintext.py
@@ -3165,8 +3160,8 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name"
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name."
msgstr ""
#. Four way switch for duplicates

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Russian (https://app.transifex.com/sabnzbd/teams/111101/ru/)\n"
@@ -177,11 +177,6 @@ msgstr "Ничего"
msgid "Default"
msgstr "по умолчанию"
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr "Не удалось составить регулярное выражение поиска: %s"
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -280,8 +275,8 @@ msgid ""
msgstr ""
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgstr "UNC-путь «%s» здесь не допускается"
msgid "Network path \"%s\" is not allowed here"
msgstr ""
#: sabnzbd/cfg.py
msgid "Queue not empty, cannot change folder."
@@ -1466,6 +1461,11 @@ msgstr "Ошибка %s: укажите действительное имя по
msgid "Old queue detected, use Status->Repair to convert the queue"
msgstr ""
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr "Не удалось составить регулярное выражение поиска: %s"
#. Warning message
#: sabnzbd/postproc.py
msgid ""
@@ -1742,6 +1742,11 @@ msgstr ""
msgid "Script"
msgstr "Сценарий"
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2114,11 +2119,6 @@ msgstr "Переключатели"
msgid "Scheduling"
msgstr "Расписание"
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2555,7 +2555,6 @@ msgstr "Время работы"
msgid "Backup"
msgstr "Резервный"
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr "Описание см. на вики-странице."
@@ -3107,23 +3106,19 @@ msgid "In case of \"Pause\", you'll need to set a password and resume the job."
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgstr "Обнаруживать повторяющиеся загрузки"
#: sabnzbd/skintext.py
msgid ""
"Detect identical NZB files (based on items in your History or files in .nzb "
"Backup Folder)"
msgid "Identical download detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Detect identical episodes in series (based on \"name/season/episode\" of "
"items in your History)"
msgid "Smart duplicate detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicates based on analysis of the filename."
msgstr ""
#: sabnzbd/skintext.py
@@ -3132,8 +3127,8 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name"
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name."
msgstr ""
#. Four way switch for duplicates

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Serbian (https://app.transifex.com/sabnzbd/teams/111101/sr/)\n"
@@ -175,11 +175,6 @@ msgstr "Ниједно"
msgid "Default"
msgstr "Подразумевано"
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr "Neuspešna kompilacija regularne ekspresije za termin pretrage: %s"
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -278,8 +273,8 @@ msgid ""
msgstr ""
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgstr "UNC путања \"%s\" није дозвољена"
msgid "Network path \"%s\" is not allowed here"
msgstr ""
#: sabnzbd/cfg.py
msgid "Queue not empty, cannot change folder."
@@ -1460,6 +1455,11 @@ msgstr "Грешка %s: Требате да унесете важеће име/
msgid "Old queue detected, use Status->Repair to convert the queue"
msgstr "Стари ред је нађен, употребити Статус->Поправи за претварање реда"
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr "Neuspešna kompilacija regularne ekspresije za termin pretrage: %s"
#. Warning message
#: sabnzbd/postproc.py
msgid ""
@@ -1736,6 +1736,11 @@ msgstr ""
msgid "Script"
msgstr "Скрипт"
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2108,11 +2113,6 @@ msgstr "Прекидачи"
msgid "Scheduling"
msgstr "Планирање"
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2547,7 +2547,6 @@ msgstr "; Ради"
msgid "Backup"
msgstr "Резервно"
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr "За више информација, читајте Вики!"
@@ -3094,23 +3093,19 @@ msgid "In case of \"Pause\", you'll need to set a password and resume the job."
msgstr "Ако је \"Пауза\", требате да поставите лозинку и да наставите рад."
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgstr "Откриј дупликатна преузимања"
#: sabnzbd/skintext.py
msgid ""
"Detect identical NZB files (based on items in your History or files in .nzb "
"Backup Folder)"
msgid "Identical download detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgstr "Откриј дупле епизоде у серије"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Detect identical episodes in series (based on \"name/season/episode\" of "
"items in your History)"
msgid "Smart duplicate detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicates based on analysis of the filename."
msgstr ""
#: sabnzbd/skintext.py
@@ -3119,8 +3114,8 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name"
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name."
msgstr ""
#. Four way switch for duplicates

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Language-Team: Swedish (https://app.transifex.com/sabnzbd/teams/111101/sv/)\n"
@@ -175,11 +175,6 @@ msgstr "Ingen"
msgid "Default"
msgstr "Standard"
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr "Det gick inte att kompilera regex för sök-sträng: %s"
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -278,8 +273,8 @@ msgid ""
msgstr ""
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgstr "UNC sökväg \"%s\" är inte tillåten här"
msgid "Network path \"%s\" is not allowed here"
msgstr ""
#: sabnzbd/cfg.py
msgid "Queue not empty, cannot change folder."
@@ -1466,6 +1461,11 @@ msgstr "Error %s: Du måste ange ett giltigt användarnamn och lösenord."
msgid "Old queue detected, use Status->Repair to convert the queue"
msgstr "Gammal kö hittad, använd Status -> Reparera för att konvertera kön"
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr "Det gick inte att kompilera regex för sök-sträng: %s"
#. Warning message
#: sabnzbd/postproc.py
msgid ""
@@ -1742,6 +1742,11 @@ msgstr ""
msgid "Script"
msgstr "Skript"
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2114,11 +2119,6 @@ msgstr "Switchar"
msgid "Scheduling"
msgstr "Schemaläggare"
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2555,7 +2555,6 @@ msgstr "Upptid"
msgid "Backup"
msgstr "Säkerhetskopiera"
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr "Läs Wiki Help för detta!"
@@ -3104,23 +3103,19 @@ msgid "In case of \"Pause\", you'll need to set a password and resume the job."
msgstr "Om \"Pausad\", så behöver du ange ett lösenord för att återuppta jobbet."
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgstr "Upptäck dubbletter av nedladdningar"
#: sabnzbd/skintext.py
msgid ""
"Detect identical NZB files (based on items in your History or files in .nzb "
"Backup Folder)"
msgid "Identical download detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgstr "Hitta dublettavsnitt i serier"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Detect identical episodes in series (based on \"name/season/episode\" of "
"items in your History)"
msgid "Smart duplicate detection"
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicates based on analysis of the filename."
msgstr ""
#: sabnzbd/skintext.py
@@ -3129,8 +3124,8 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name"
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name."
msgstr ""
#. Four way switch for duplicates

View File

@@ -3,12 +3,13 @@
#
# Translators:
# Safihre <safihre@sabnzbd.org>, 2023
# Kangwei Li <lkw20010211@gmail.com>, 2023
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:49+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2023\n"
"Last-Translator: Kangwei Li <lkw20010211@gmail.com>, 2023\n"
"Language-Team: Chinese (China) (https://app.transifex.com/sabnzbd/teams/111101/zh_CN/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -173,11 +174,6 @@ msgstr "无"
msgid "Default"
msgstr "默认"
#. Error message
#: sabnzbd/api.py
msgid "Failed to compile regex for search term: %s"
msgstr "为搜索关键词编译正则表达式失败: %s"
#. Error message
#: sabnzbd/assembler.py
msgid "Disk full! Forcing Pause"
@@ -276,8 +272,8 @@ msgid ""
msgstr ""
#: sabnzbd/cfg.py
msgid "UNC path \"%s\" not allowed here"
msgstr "此处不允许使用 UNC 路径 \"%s\""
msgid "Network path \"%s\" is not allowed here"
msgstr ""
#: sabnzbd/cfg.py
msgid "Queue not empty, cannot change folder."
@@ -1453,6 +1449,11 @@ msgstr "错误 %s: 您需要提供有效的用户名与密码。"
msgid "Old queue detected, use Status->Repair to convert the queue"
msgstr "侦测到旧版队列,请使用“状态”→“修复”转换队列"
#. Error message
#: sabnzbd/postproc.py
msgid "Failed to compile regex for search term: %s"
msgstr "为搜索关键词编译正则表达式失败: %s"
#. Warning message
#: sabnzbd/postproc.py
msgid ""
@@ -1729,6 +1730,11 @@ msgstr ""
msgid "Script"
msgstr "脚本"
#. PP RSS feed of the NZB - Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. PP Source of the NZB (path or URL) - Where to find the SABnzbd sourcecode
#: sabnzbd/skintext.py
msgid "Source"
@@ -2101,11 +2107,6 @@ msgstr "参数"
msgid "Scheduling"
msgstr "定时任务"
#. Main menu item
#: sabnzbd/skintext.py
msgid "RSS"
msgstr "RSS"
#. Main menu item
#: sabnzbd/skintext.py
msgid "Notifications"
@@ -2534,7 +2535,6 @@ msgstr "启动时间"
msgid "Backup"
msgstr "备份"
#. Notification Script settings
#: sabnzbd/skintext.py
msgid "Read the Wiki Help on this!"
msgstr "关于该项请参阅 Wiki 帮助!"
@@ -2875,11 +2875,11 @@ msgstr ""
#: sabnzbd/skintext.py
msgid "Minimum Free Space for Completed Download Folder"
msgstr ""
msgstr "完成下载文件夹的最小剩余空间"
#: sabnzbd/skintext.py
msgid "Will not work if a category folder is on a different disk."
msgstr ""
msgstr "当某分类的路径位于另一磁盘上时不生效。"
#. Auto-resume download on the reset day
#: sabnzbd/skintext.py
@@ -2963,14 +2963,14 @@ msgstr "队列管理及历史数据库的存放位置。<br /><i>仅当队列为
#: sabnzbd/skintext.py
msgid "Backup Folder"
msgstr ""
msgstr "备份文件夹"
#: sabnzbd/skintext.py
msgid ""
"Location where the backups of the configuration file and databases are "
"stored.<br />If left empty, the backup will be created in the Completed "
"Download Folder."
msgstr ""
msgstr "备份配置文件和数据库的位置。<br />如果留空,备份将存放于完成下载文件夹中。"
#: sabnzbd/skintext.py
msgid "<i>Data will <b>not</b> be moved. Requires SABnzbd restart!</i>"
@@ -2987,7 +2987,7 @@ msgstr "SABnzbd 日志文件的位置。<br /><i>需要重启 SABnzbd 才能生
#: sabnzbd/skintext.py
msgid "Purge Logs"
msgstr ""
msgstr "清除日志"
#: sabnzbd/skintext.py
msgid ".nzb Backup Folder"
@@ -3057,24 +3057,20 @@ msgid "In case of \"Pause\", you'll need to set a password and resume the job."
msgstr "若选择“暂停”,您将需要设置密码并手动续传对应任务。"
#: sabnzbd/skintext.py
msgid "Detect Duplicate Downloads"
msgstr "侦测重复下载"
msgid "Identical download detection"
msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Detect identical NZB files (based on items in your History or files in .nzb "
"Backup Folder)"
msgstr "检测相同的 NZB 文件 (基于您的历史项目或 .nzb 备份文件夹中的文件)"
msgid "Detect identical downloads based on name or NZB contents."
msgstr ""
#: sabnzbd/skintext.py
msgid "Detect duplicate episodes in series"
msgstr "侦测同季的重复剧集"
msgid "Smart duplicate detection"
msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Detect identical episodes in series (based on \"name/season/episode\" of "
"items in your History)"
msgstr "在剧目中检测相同的剧集 (基于您的历史项目,参照 \"name/season/episode\" 的规则)"
msgid "Detect duplicates based on analysis of the filename."
msgstr ""
#: sabnzbd/skintext.py
msgid "Allow proper releases"
@@ -3082,8 +3078,8 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name"
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in "
"the download name."
msgstr ""
#. Four way switch for duplicates
@@ -3440,7 +3436,7 @@ msgstr "超时"
#: sabnzbd/skintext.py
msgid "Account expiration date"
msgstr ""
msgstr "账户到期时间"
#: sabnzbd/skintext.py
msgid "Warn 5 days in advance of account expiration date."
@@ -3620,7 +3616,7 @@ msgstr "应用过滤器"
#. Config->RSS edit button
#: sabnzbd/skintext.py
msgid "Edit"
msgstr ""
msgstr "编辑"
#. Config->RSS when will be the next RSS scan
#: sabnzbd/skintext.py

View File

@@ -4,7 +4,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: team@sabnzbd.org\n"
"Language-Team: SABnzbd <team@sabnzbd.org>\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Pavel C <quoing_transifex@mess.cz>, 2022\n"
"Language-Team: Czech (https://app.transifex.com/sabnzbd/teams/111101/cs/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Danish (https://app.transifex.com/sabnzbd/teams/111101/da/)\n"

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Ester Molla Aragones <moarages@gmail.com>, 2020\n"
"Language-Team: Spanish (https://app.transifex.com/sabnzbd/teams/111101/es/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Finnish (https://app.transifex.com/sabnzbd/teams/111101/fi/)\n"

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Fred L <88com88@gmail.com>, 2021\n"
"Language-Team: French (https://app.transifex.com/sabnzbd/teams/111101/fr/)\n"

View File

@@ -7,7 +7,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: ION, 2021\n"
"Language-Team: Hebrew (https://app.transifex.com/sabnzbd/teams/111101/he/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Norwegian Bokmål (https://app.transifex.com/sabnzbd/teams/111101/nb/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Polish (https://app.transifex.com/sabnzbd/teams/111101/pl/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Portuguese (Brazil) (https://app.transifex.com/sabnzbd/teams/111101/pt_BR/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Romanian (https://app.transifex.com/sabnzbd/teams/111101/ro/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Russian (https://app.transifex.com/sabnzbd/teams/111101/ru/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Serbian (https://app.transifex.com/sabnzbd/teams/111101/sr/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Swedish (https://app.transifex.com/sabnzbd/teams/111101/sv/)\n"

View File

@@ -6,7 +6,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: SABnzbd-4.2.0Alpha2\n"
"Project-Id-Version: SABnzbd-4.2.0Beta1\n"
"PO-Revision-Date: 2020-06-27 15:56+0000\n"
"Last-Translator: Safihre <safihre@sabnzbd.org>, 2020\n"
"Language-Team: Chinese (China) (https://app.transifex.com/sabnzbd/teams/111101/zh_CN/)\n"

View File

@@ -30,7 +30,7 @@ rebulk==3.2.0
# Recent cryptography versions require Rust. If you run into issues compiling this
# SABnzbd will also work with older pre-Rust versions such as cryptography==3.3.2
cryptography==41.0.5
cryptography==41.0.7
# We recommend using "orjson" as it is 2x as fast as "ujson". However, it requires
# Rust so SABnzbd works just as well with "ujson" or the Python built in "json" module

View File

@@ -285,6 +285,11 @@ def initialize(pause_downloader=False, clean_up=False, repair=0):
misc.convert_sorter_settings()
cfg.sorters_converted.set(True)
# Convert duplicate settings
if cfg.no_series_dupes():
cfg.no_smart_dupes.set(cfg.no_series_dupes())
cfg.no_series_dupes.set(0)
# Add hostname to the whitelist
if not cfg.host_whitelist():
cfg.host_whitelist.set(socket.gethostname())

View File

@@ -58,7 +58,7 @@ import sabnzbd.config as config
import sabnzbd.cfg as cfg
from sabnzbd.skintext import SKIN_TEXT
from sabnzbd.utils.diskspeed import diskspeedmeasure
from sabnzbd.utils.internetspeed import internetspeed
from sabnzbd.internetspeed import internetspeed
from sabnzbd.utils.pathbrowser import folders_at_path
from sabnzbd.utils.getperformance import getpystone
from sabnzbd.misc import (
@@ -270,6 +270,7 @@ def _api_queue_default(value, kwargs):
search = kwargs.get("search")
categories = kwargs.get("cat") or kwargs.get("category")
priorities = kwargs.get("priority")
statuses = kwargs.get("status")
nzo_ids = kwargs.get("nzo_ids")
if categories and not isinstance(categories, list):
@@ -277,13 +278,21 @@ def _api_queue_default(value, kwargs):
if priorities and not isinstance(priorities, list):
# Make sure it's an integer
priorities = [int_conv(prio) for prio in priorities.split(",")]
if statuses and not isinstance(statuses, list):
statuses = statuses.split(",")
if nzo_ids and not isinstance(nzo_ids, list):
nzo_ids = nzo_ids.split(",")
return report(
keyword="queue",
data=build_queue(
start=start, limit=limit, search=search, categories=categories, priorities=priorities, nzo_ids=nzo_ids
start=start,
limit=limit,
search=search,
categories=categories,
priorities=priorities,
statuses=statuses,
nzo_ids=nzo_ids,
),
)
@@ -483,8 +492,9 @@ def _api_history(name, kwargs):
limit = int_conv(kwargs.get("limit"))
last_history_update = int_conv(kwargs.get("last_history_update", 0))
search = kwargs.get("search")
failed_only = int_conv(kwargs.get("failed_only"))
categories = kwargs.get("cat") or kwargs.get("category")
statuses = kwargs.get("status")
failed_only = int_conv(kwargs.get("failed_only"))
nzo_ids = kwargs.get("nzo_ids")
# Do we need to send anything?
@@ -494,6 +504,13 @@ def _api_history(name, kwargs):
if categories and not isinstance(categories, list):
categories = categories.split(",")
if statuses and not isinstance(statuses, list):
statuses = statuses.split(",")
if failed_only:
# We ignore any other statuses, having both doesn't make sense
statuses = [Status.FAILED]
if nzo_ids and not isinstance(nzo_ids, list):
nzo_ids = nzo_ids.split(",")
@@ -537,7 +554,12 @@ def _api_history(name, kwargs):
to_units(day),
)
history["slots"], history["ppslots"], history["noofslots"] = build_history(
start=start, limit=limit, search=search, failed_only=failed_only, categories=categories, nzo_ids=nzo_ids
start=start,
limit=limit,
search=search,
categories=categories,
statuses=statuses,
nzo_ids=nzo_ids,
)
history["last_history_update"] = sabnzbd.LAST_HISTORY_UPDATE
history["version"] = sabnzbd.__version__
@@ -1250,9 +1272,6 @@ def build_status(calculate_performance: bool = False, skip_dashboard: bool = Fal
# Calculate performance measures, if requested
if int_conv(calculate_performance):
# Perform the internetspeed measure in separate thread
internetspeed_future = sabnzbd.THREAD_POOL.submit(internetspeed)
# PyStone
sabnzbd.PYSTONE_SCORE = getpystone()
@@ -1261,7 +1280,7 @@ def build_status(calculate_performance: bool = False, skip_dashboard: bool = Fal
sabnzbd.COMPLETE_DIR_SPEED = round(diskspeedmeasure(sabnzbd.cfg.complete_dir.get_path()), 1)
# Internet bandwidth
sabnzbd.INTERNET_BANDWIDTH = round(internetspeed_future.result(), 1)
sabnzbd.INTERNET_BANDWIDTH = round(internetspeed(), 1)
# How often did we delay?
info["delayed_assembler"] = sabnzbd.BPSMeter.delayed_assembler
@@ -1343,6 +1362,7 @@ def build_queue(
search: Optional[str] = None,
categories: Optional[List[str]] = None,
priorities: Optional[List[str]] = None,
statuses: Optional[List[str]] = None,
nzo_ids: Optional[List[str]] = None,
):
info = build_header(for_template=False)
@@ -1354,7 +1374,13 @@ def build_queue(
queue_fullsize,
nzos_matched,
) = sabnzbd.NzbQueue.queue_info(
search=search, categories=categories, priorities=priorities, nzo_ids=nzo_ids, start=start, limit=limit
search=search,
categories=categories,
priorities=priorities,
statuses=statuses,
nzo_ids=nzo_ids,
start=start,
limit=limit,
)
info["kbpersec"] = "%.2f" % (sabnzbd.BPSMeter.bps / KIBI)
@@ -1384,7 +1410,6 @@ def build_queue(
for nzo in nzo_list:
mbleft = nzo.remaining / MEBI
mb = nzo.bytes / MEBI
is_propagating = (nzo.avg_stamp + float(cfg.propagation_delay() * 60)) > time.time()
slot = {}
slot["index"] = n
@@ -1405,8 +1430,8 @@ def build_queue(
slot["direct_unpack"] = nzo.direct_unpack_progress
if not sabnzbd.Downloader.paused and nzo.status not in (Status.PAUSED, Status.FETCHING, Status.GRABBING):
if is_propagating:
slot["status"] = Status.PROP
if nzo.propagation_delay_left:
slot["status"] = Status.PROPAGATING
elif nzo.status == Status.CHECKING:
slot["status"] = Status.CHECKING
else:
@@ -1420,7 +1445,7 @@ def build_queue(
if (
sabnzbd.Downloader.paused
or sabnzbd.Downloader.paused_for_postproc
or is_propagating
or nzo.propagation_delay_left
or nzo.status not in (Status.DOWNLOADING, Status.FETCHING, Status.QUEUED)
) and nzo.priority != FORCE_PRIORITY:
slot["timeleft"] = "0:00:00"
@@ -1631,36 +1656,20 @@ def build_header(webdir: str = "", for_template: bool = True, trans_functions: b
def build_history(
start: int = 0,
limit: int = 0,
limit: int = 1000000,
search: Optional[str] = None,
failed_only: int = 0,
categories: Optional[List[str]] = None,
statuses: Optional[List[str]] = None,
nzo_ids: Optional[List[str]] = None,
) -> Tuple[List[Dict[str, Any]], int, int]:
"""Combine the jobs still in post-processing and the database history"""
if not limit:
limit = 1000000
# Grab any items that are active or queued in postproc
postproc_queue = sabnzbd.PostProcessor.get_queue()
# 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(categories, 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)
if nzo_ids:
postproc_queue = [nzo for nzo in postproc_queue if nzo.nzo_id in nzo_ids]
postproc_queue = sabnzbd.PostProcessor.get_queue(
search=search,
categories=categories,
statuses=statuses,
nzo_ids=nzo_ids,
)
# Multi-page support for postproc items
postproc_queue_size = len(postproc_queue)
@@ -1669,13 +1678,10 @@ def build_history(
postproc_queue = []
database_history_limit = limit
else:
try:
if limit:
postproc_queue = postproc_queue[start : start + limit]
else:
postproc_queue = postproc_queue[start:]
except:
pass
if limit:
postproc_queue = postproc_queue[start : start + limit]
else:
postproc_queue = postproc_queue[start:]
# Remove the amount of postproc items from the db request for history items
database_history_limit = max(limit - len(postproc_queue), 0)
database_history_start = max(start - postproc_queue_size, 0)
@@ -1692,12 +1698,22 @@ def build_history(
# Fetch history items
if not database_history_limit:
items, total_items = history_db.fetch_history(
database_history_start, 1, search, failed_only, categories, nzo_ids
start=database_history_start,
limit=1,
search=search,
categories=categories,
statuses=statuses,
nzo_ids=nzo_ids,
)
items = []
else:
items, total_items = history_db.fetch_history(
database_history_start, database_history_limit, search, failed_only, categories, nzo_ids
start=database_history_start,
limit=database_history_limit,
search=search,
categories=categories,
statuses=statuses,
nzo_ids=nzo_ids,
)
# Add the postproc items to the top of the history
@@ -1742,6 +1758,7 @@ def add_active_history(postproc_queue: List[NzbObject], items: List[Dict[str, An
"size": to_units(nzo.bytes_downloaded, "B"),
"meta": None,
"series": "",
"duplicate_key": nzo.duplicate_key,
"md5sum": "",
"password": nzo.correct_password,
"action_line": nzo.action_line,

View File

@@ -51,7 +51,7 @@ from sabnzbd.constants import (
DEF_HTTPS_CERT_FILE,
DEF_HTTPS_KEY_FILE,
)
from sabnzbd.filesystem import same_directory, real_path
from sabnzbd.filesystem import same_directory, real_path, is_valid_script, is_network_path
# Validators currently only are made for string/list-of-strings
# and return those on success or an error message.
@@ -188,7 +188,7 @@ def validate_host(value: str) -> ValidateResult:
def validate_script(value: str) -> ValidateResult:
"""Check if value is a valid script"""
if not sabnzbd.__INITIALIZED__ or (value and sabnzbd.filesystem.is_valid_script(value)):
if not sabnzbd.__INITIALIZED__ or (value and is_valid_script(value)):
return None, value
elif (value and value == "None") or not value:
return None, "None"
@@ -217,10 +217,10 @@ def validate_permissions(value: str) -> ValidateResult:
def validate_safedir(root: str, value: str, default: str) -> ValidateResult:
"""Allow only when queues are empty and no UNC"""
"""Allow only when queues are empty and not a network-path"""
if not sabnzbd.__INITIALIZED__ or (sabnzbd.PostProcessor.empty() and sabnzbd.NzbQueue.is_empty()):
if value.startswith(r"\\"):
return T('UNC path "%s" not allowed here') % value, None
if is_network_path(real_path(root, value)):
return T('Network path "%s" is not allowed here') % value, None
else:
return validate_default_if_empty(root, value, default)
else:
@@ -378,14 +378,15 @@ autodisconnect = OptionBool("misc", "auto_disconnect", True)
pre_script = OptionStr("misc", "pre_script", "None", validation=validate_script)
end_queue_script = OptionStr("misc", "end_queue_script", "None", validation=validate_script)
no_dupes = OptionNumber("misc", "no_dupes", 0)
no_series_dupes = OptionNumber("misc", "no_series_dupes", 0)
series_propercheck = OptionBool("misc", "series_propercheck", True)
no_series_dupes = OptionNumber("misc", "no_series_dupes", 0) # Kept for converting to no_smart_dupes
no_smart_dupes = OptionNumber("misc", "no_smart_dupes", 0)
dupes_propercheck = OptionBool("misc", "dupes_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", True)
auto_sort = OptionStr("misc", "auto_sort")
direct_unpack = OptionBool("misc", "direct_unpack", False)
propagation_delay = OptionNumber("misc", "propagation_delay", 0)
propagation_delay = OptionNumber("misc", "propagation_delay", 0, minval=0)
folder_rename = OptionBool("misc", "folder_rename", True)
replace_spaces = OptionBool("misc", "replace_spaces", False)
replace_underscores = OptionBool("misc", "replace_underscores", False)

View File

@@ -120,14 +120,15 @@ INTERFACE_PRIORITIES = {
}
STAGES = {
"Source": 0,
"Download": 1,
"Servers": 2,
"Repair": 3,
"Filejoin": 4,
"Unpack": 5,
"Deobfuscate": 6,
"Script": 7,
"RSS": 0,
"Source": 1,
"Download": 2,
"Servers": 3,
"Repair": 4,
"Filejoin": 5,
"Unpack": 6,
"Deobfuscate": 7,
"Script": 8,
}
VALID_ARCHIVES = (".zip", ".rar", ".7z")
@@ -162,14 +163,14 @@ class Status:
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
PROPAGATING = "Propagating" # Q: Delayed download
class DuplicateStatus:
DUPLICATE = "Duplicate" # Simple duplicate
DUPLICATE_ALTERNATIVE = "Duplicate Alternative" # Alternative duplicate for a queued job
SERIES_DUPLICATE = "Series Duplicate" # Simple Series duplicate
SERIES_DUPLICATE_ALTERNATIVE = "Series Duplicate Alternative" # Alternative duplicate for a queued job
SMART_DUPLICATE = "Smart Duplicate" # Simple Series duplicate
SMART_DUPLICATE_ALTERNATIVE = "Smart Duplicate Alternative" # Alternative duplicate for a queued job
class AddNzbFileResult:

View File

@@ -82,19 +82,25 @@ class HistoryDB:
version = self.cursor.fetchone()["user_version"]
except IndexError:
version = 0
# Add any new columns added since last DB version
# Use "and" to stop when database has been reset due to corruption
if version < 1:
# Add any missing columns added since first DB version
# Use "and" to stop when database has been reset due to corruption
_ = (
self.execute("PRAGMA user_version = 1;")
and self.execute('ALTER TABLE "history" ADD COLUMN series TEXT;')
and self.execute('ALTER TABLE "history" ADD COLUMN md5sum TEXT;')
and self.execute("ALTER TABLE history ADD COLUMN series TEXT;")
and self.execute("ALTER TABLE history ADD COLUMN md5sum TEXT;")
)
if version < 2:
# Add any missing columns added since second DB version
# Use "and" to stop when database has been reset due to corruption
_ = self.execute("PRAGMA user_version = 2;") and self.execute(
'ALTER TABLE "history" ADD COLUMN password TEXT;'
"ALTER TABLE history ADD COLUMN password TEXT;"
)
if version < 3:
# Transfer data to new column (requires WHERE-hack), original column should be removed later
_ = (
self.execute("PRAGMA user_version = 3;")
and self.execute("ALTER TABLE history ADD COLUMN duplicate_key TEXT;")
and self.execute("UPDATE history SET duplicate_key = series WHERE 1 = 1;")
)
def execute(self, command: str, args: Sequence = (), save: bool = False) -> bool:
@@ -142,7 +148,7 @@ class HistoryDB:
"""Create a new (empty) database file"""
self.execute(
"""
CREATE TABLE "history" (
CREATE TABLE history (
"id" INTEGER PRIMARY KEY,
"completed" INTEGER NOT NULL,
"name" TEXT NOT NULL,
@@ -169,11 +175,12 @@ class HistoryDB:
"meta" TEXT,
"series" TEXT,
"md5sum" TEXT,
"password" TEXT
"password" TEXT,
"duplicate_key" TEXT
)
"""
)
self.execute("PRAGMA user_version = 2;")
self.execute("PRAGMA user_version = 3;")
def close(self):
"""Close database connection"""
@@ -263,7 +270,7 @@ class HistoryDB:
self.execute(
"""INSERT INTO history (completed, name, nzb_name, category, pp, script, report,
url, status, nzo_id, storage, path, script_log, script_line, download_time, postproc_time, stage_log,
downloaded, fail_message, url_info, bytes, series, md5sum, password)
downloaded, fail_message, url_info, bytes, duplicate_key, md5sum, password)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
t,
save=True,
@@ -275,8 +282,8 @@ class HistoryDB:
start: Optional[int] = None,
limit: Optional[int] = None,
search: Optional[str] = None,
failed_only: int = 0,
categories: Optional[List[str]] = None,
statuses: Optional[List[str]] = None,
nzo_ids: Optional[List[str]] = None,
) -> Tuple[List[Dict[str, Any]], int]:
"""Return records for specified jobs"""
@@ -285,18 +292,20 @@ class HistoryDB:
post = ""
if categories:
categories = ["*" if c == "Default" else c for c in categories]
post = " AND (CATEGORY = ?"
post += " OR CATEGORY = ? " * (len(categories) - 1)
post = " AND (category = ?"
post += " OR category = ? " * (len(categories) - 1)
post += ")"
command_args.extend(categories)
if statuses:
post += " AND (status = ?"
post += " OR status = ? " * (len(statuses) - 1)
post += ")"
command_args.extend(statuses)
if nzo_ids:
post += " AND (NZO_ID = ?"
post += " OR NZO_ID = ? " * (len(nzo_ids) - 1)
post += " AND (nzo_id = ?"
post += " OR nzo_id = ? " * (len(nzo_ids) - 1)
post += ")"
command_args.extend(nzo_ids)
if failed_only:
post += " AND STATUS = ?"
command_args.append(Status.FAILED)
cmd = "SELECT COUNT(*) FROM history WHERE name LIKE ?"
total_items = -1
@@ -321,11 +330,17 @@ class HistoryDB:
return items, total_items
def have_episode(self, series_key: str) -> bool:
"""Check whether History contains this series episode"""
def have_duplicate_key(self, duplicate_key: str) -> bool:
"""Check whether History contains this duplicate key"""
total = 0
if self.execute(
"""SELECT COUNT(*) FROM History WHERE series = ? AND STATUS != ?""", (series_key, Status.FAILED)
"""
SELECT COUNT(*)
FROM History
WHERE
duplicate_key = ? AND
STATUS != ?""",
(duplicate_key, Status.FAILED),
):
total = self.cursor.fetchone()["COUNT(*)"]
return total > 0
@@ -334,7 +349,12 @@ class HistoryDB:
"""Check whether this name or md5sum is already in History"""
total = 0
if self.execute(
"""SELECT COUNT(*) FROM History WHERE ( LOWER(name) = LOWER(?) OR md5sum = ? ) AND STATUS != ?""",
"""
SELECT COUNT(*)
FROM History
WHERE
( LOWER(name) = LOWER(?) OR md5sum = ? ) AND
STATUS != ?""",
(name, md5sum, Status.FAILED),
):
total = self.cursor.fetchone()["COUNT(*)"]
@@ -422,7 +442,7 @@ class HistoryDB:
def convert_search(search: str) -> str:
"""Convert classic wildcard to SQL wildcard"""
if not search:
if not search or not isinstance(search, str):
# Default value
search = ""
else:
@@ -466,7 +486,7 @@ def build_history_info(nzo, workdir_complete: str, postproc_time: int, script_ou
report = "future" if nzo.futuretype else ""
# Make sure we have the duplicate key
nzo.set_duplicate_series_key()
nzo.set_duplicate_key()
return (
completed,
@@ -490,7 +510,7 @@ def build_history_info(nzo, workdir_complete: str, postproc_time: int, script_ou
nzo.fail_msg,
url_info,
nzo.bytes_downloaded,
nzo.duplicate_series_key,
nzo.duplicate_key,
nzo.md5sum,
nzo.correct_password,
)

View File

@@ -18,6 +18,8 @@
##############################################################################
# Decorators
##############################################################################
import time
import functools
from typing import Union, Callable
from threading import Lock, RLock, Condition
@@ -60,3 +62,24 @@ def NzbQueueLocker(func: Callable):
DOWNLOADER_CV.release()
return call_func
def cache_maintainer(clear_time: int):
"""
A function decorator that clears functools.cache or functools.lru_cache clear_time seconds
:param clear_time: In seconds, how often to clear cache (only checks when called)
"""
def inner(func):
def wrapper(*args, **kwargs):
if hasattr(func, "next_clear"):
if time.time() > func.next_clear or kwargs.get("force"):
func.cache_clear()
func.next_clear = time.time() + clear_time
else:
func.next_clear = time.time() + clear_time
return func(*args, **kwargs)
return wrapper
return inner

View File

@@ -565,9 +565,9 @@ def abort_all():
def test_disk_performance():
"""Test the incomplete-dir performance and enable
Direct Unpack if good enough (> 40MB/s)
Direct Unpack if good enough (> 100MB/s)
"""
if diskspeedmeasure(sabnzbd.cfg.download_dir.get_path()) > 40:
if diskspeedmeasure(sabnzbd.cfg.download_dir.get_path()) > 100:
cfg.direct_unpack.set(True)
logging.warning(
T("Direct Unpack was automatically enabled.")

View File

@@ -223,7 +223,7 @@ class Server:
def request_addrinfo_blocking(self):
"""Blocking attempt to run getaddrinfo() and Happy Eyeballs for specified server"""
logging.debug("Retrieving server address information for %s", self.host)
self.addrinfo = happyeyeballs(self.host, self.port)
self.addrinfo = happyeyeballs(self.host, self.port, self.timeout)
if not self.addrinfo:
self.bad_cons += self.threads
# Notify next call to maybe_block_server
@@ -666,7 +666,7 @@ class Downloader(Thread):
else:
read = []
BPSMeter.reset()
time.sleep(1.0)
time.sleep(0.1)
self.max_chunk_size = _DEFAULT_CHUNK_SIZE
with DOWNLOADER_CV:
while (
@@ -888,6 +888,13 @@ class Downloader(Thread):
errormsg = T("Cannot connect to server %s [%s]") % (server.host, error.msg)
penalty = _PENALTY_UNKNOWN
block = True
# Set error for server and warn user if it was first time thrown
if errormsg and server.active and server.errormsg != errormsg:
server.errormsg = errormsg
logging.warning(errormsg)
# Take action on the problem
if block or (penalty and server.optional):
retry_article = False
if server.active:
@@ -900,11 +907,6 @@ class Downloader(Thread):
self.plan_server(server, penalty)
# Note that the article is discard for this server if the server is not required
self.__reset_nw(nw, retry_article=retry_article)
# Set error for server and warn user if it was first time thrown
if errormsg and server.active and server.errormsg != errormsg:
server.errormsg = errormsg
logging.warning(errormsg)
return False
except Exception as err:
logging.error(

View File

@@ -33,6 +33,7 @@ import fnmatch
import stat
import ctypes
import random
import functools
from typing import Union, List, Tuple, Any, Dict, Optional, BinaryIO
try:
@@ -42,7 +43,7 @@ except ImportError:
pass
import sabnzbd
from sabnzbd.decorators import synchronized
from sabnzbd.decorators import synchronized, cache_maintainer
from sabnzbd.constants import FUTURE_Q_FOLDER, JOB_ADMIN, GIGI, DEF_FILE_MAX, IGNORED_FILES_AND_FOLDERS, DEF_LOG_FILE
from sabnzbd.encoding import correct_unknown_encoding, utob, ubtou
from sabnzbd.utils import rarfile
@@ -452,9 +453,23 @@ def same_directory(a: str, b: str) -> int:
return is_subfolder
def check_mount(path: str) -> bool:
"""Return False if volume isn't mounted on Linux or macOS
Retry 6 times with an interval of 1 sec.
def is_network_path(path: str) -> bool:
"""Check weither a path is a network path.
On Windows, use win32 functions to detect users that try to avoid this detection by using a mapped drive letter.
We don't check on Linux for mnt or media, since those could also be used for internal drives."""
path = clip_path(path)
if path.startswith(r"\\"):
return True
if sabnzbd.WIN32:
drive_letter, _ = os.path.splitdrive(path)
return win32file.GetDriveType(drive_letter) == win32file.DRIVE_REMOTE
return False
def mount_is_available(path: str) -> bool:
"""Return False if volume isn't mounted on Linux or macOS or
the network path isn't available on Windows.
Retry wait_ext_drive times with an interval of 1 sec.
"""
if sabnzbd.MACOS:
m = re.search(r"^(/Volumes/[^/]+)", path, re.I)
@@ -748,7 +763,7 @@ def create_all_dirs(path: str, apply_permissions: bool = False) -> Union[str, bo
@synchronized(DIR_LOCK)
def get_unique_dir(path: str, n: int = 0, create_dir: bool = True) -> Union[str, bool]:
"""Determine a unique folder or filename"""
if not check_mount(path):
if not mount_is_available(path):
return path
new_path = path
@@ -1068,43 +1083,15 @@ def diskspace_base(dir_to_check: str) -> Tuple[float, float]:
return 20.0, 10.0
# Store all results to speed things up
__DIRS_CHECKED = []
__DISKS_SAME = None
__LAST_DISK_RESULT = {"download_dir": (0.0, 0.0), "complete_dir": (0.0, 0.0)}
__LAST_DISK_CALL = 0
@cache_maintainer(clear_time=10)
@functools.lru_cache(maxsize=None)
def diskspace(force: bool = False) -> Dict[str, Tuple[float, float]]:
"""Wrapper to cache results"""
global __DIRS_CHECKED, __DISKS_SAME, __LAST_DISK_RESULT, __LAST_DISK_CALL
# Reset everything when folders changed
dirs_to_check = [sabnzbd.cfg.download_dir.get_path(), sabnzbd.cfg.complete_dir.get_path()]
if __DIRS_CHECKED != dirs_to_check:
__DIRS_CHECKED = dirs_to_check
__DISKS_SAME = None
__LAST_DISK_RESULT = {"download_dir": [], "complete_dir": []}
__LAST_DISK_CALL = 0
# When forced, ignore any cache to avoid problems in UI
if force:
__LAST_DISK_CALL = 0
# Check against cache
if time.time() > __LAST_DISK_CALL + 10.0:
# Same disk? Then copy-paste
__LAST_DISK_RESULT["download_dir"] = diskspace_base(sabnzbd.cfg.download_dir.get_path())
__LAST_DISK_RESULT["complete_dir"] = (
__LAST_DISK_RESULT["download_dir"] if __DISKS_SAME else diskspace_base(sabnzbd.cfg.complete_dir.get_path())
)
__LAST_DISK_CALL = time.time()
# Do we know if it's same disk?
if __DISKS_SAME is None:
__DISKS_SAME = __LAST_DISK_RESULT["download_dir"] == __LAST_DISK_RESULT["complete_dir"]
return __LAST_DISK_RESULT
"""Wrapper to keep results cached by cache_maintainer
If called with force=True, the wrapper will clear the results"""
return {
"download_dir": diskspace_base(sabnzbd.cfg.download_dir.get_path()),
"complete_dir": diskspace_base(sabnzbd.cfg.complete_dir.get_path()),
}
def get_new_id(prefix, folder, check_list=None):

View File

@@ -28,20 +28,20 @@ import threading
import time
import logging
import queue
import functools
from dataclasses import dataclass
from typing import Tuple, Union, Optional
from more_itertools import roundrobin
from sabnzbd import cfg as cfg
from sabnzbd.constants import DEF_TIMEOUT
from sabnzbd.decorators import cache_maintainer
# How long to delay between connection attempts? The RFC suggests 250ms, but this is
# quite long and might give us a slow host that just happened to be on top of the list.
# The absolute minium specified in RFC 8305 is 10ms, so we use that.
CONNECTION_ATTEMPT_DELAY = 0.01
# The total time we want to wait for any result
MAXIMUM_RESOLUTION_TIME = 3
# While providers are afraid to add IPv6 to their standard hostnames
# we map a number of well known hostnames to their IPv6 alternatives.
# WARNING: Only add if the SSL-certificate allows both hostnames!
@@ -78,12 +78,12 @@ class AddrInfo:
# Called by each thread
def do_socket_connect(result_queue: queue.Queue, addrinfo: AddrInfo):
def do_socket_connect(result_queue: queue.Queue, addrinfo: AddrInfo, timeout: int):
"""Connect to the ip, and put the result into the queue"""
try:
start = time.time()
s = socket.socket(addrinfo.family, addrinfo.type)
s.settimeout(MAXIMUM_RESOLUTION_TIME)
s.settimeout(timeout)
try:
s.connect(addrinfo.sockaddr)
result_queue.put(addrinfo)
@@ -106,7 +106,9 @@ def do_socket_connect(result_queue: queue.Queue, addrinfo: AddrInfo):
pass
def happyeyeballs(host: str, port: int) -> Optional[AddrInfo]:
@cache_maintainer(clear_time=10)
@functools.lru_cache(maxsize=None)
def happyeyeballs(host: str, port: int, timeout: int = DEF_TIMEOUT) -> Optional[AddrInfo]:
"""Return the fastest result of getaddrinfo() based on RFC 6555/8305 (Happy Eyeballs),
including IPv6 addresses if desired. Returns None in case no addresses were returned
by getaddrinfo or if no connection could be made to any of the addresses"""
@@ -165,7 +167,7 @@ def happyeyeballs(host: str, port: int) -> Optional[AddrInfo]:
addr_tried = 0
result: Optional[AddrInfo] = None
for addrinfo in roundrobin(ipv6_addrinfo, ipv4_addrinfo):
threading.Thread(target=do_socket_connect, args=(result_queue, addrinfo), daemon=True).start()
threading.Thread(target=do_socket_connect, args=(result_queue, addrinfo, timeout), daemon=True).start()
addr_tried += 1
try:
result = result_queue.get(timeout=CONNECTION_ATTEMPT_DELAY)
@@ -179,7 +181,7 @@ def happyeyeballs(host: str, port: int) -> Optional[AddrInfo]:
if not result:
try:
# Reduce waiting time by time already spent
result = result_queue.get(timeout=MAXIMUM_RESOLUTION_TIME - addr_tried * CONNECTION_ATTEMPT_DELAY)
result = result_queue.get(timeout=timeout - addr_tried * CONNECTION_ATTEMPT_DELAY)
except queue.Empty:
raise ConnectionError("No addresses could be resolved")

View File

@@ -80,6 +80,7 @@ from sabnzbd.constants import (
GUESSIT_SORT_TYPES,
VALID_NZB_FILES,
VALID_ARCHIVES,
DEF_TIMEOUT,
)
from sabnzbd.lang import list_languages
from sabnzbd.api import (
@@ -781,8 +782,8 @@ SWITCH_LIST = (
"fail_hopeless_jobs",
"enable_all_par",
"enable_recursive",
"no_series_dupes",
"series_propercheck",
"no_smart_dupes",
"dupes_propercheck",
"script_can_fail",
"unwanted_extensions",
"action_on_unwanted_extensions",
@@ -1151,7 +1152,7 @@ def handle_server(kwargs, root=None, new_svr=False):
kwargs["connections"] = "1"
if kwargs.get("enable") == "1":
if not happyeyeballs(host, int_conv(port)):
if not happyeyeballs(host, int_conv(port), int_conv(kwargs.get("timeout"), default=DEF_TIMEOUT)):
return badParameterResponse(T('Server address "%s:%s" is not valid.') % (host, port), ajax)
# Default server name is just the host name
@@ -1485,16 +1486,24 @@ class ConfigRss:
"""Download NZB from provider (Download button)"""
feed = kwargs.get("feed")
url = kwargs.get("url")
nzbname = kwargs.get("nzbname")
att = sabnzbd.RSSReader.lookup_url(feed, url)
if att:
if att := sabnzbd.RSSReader.lookup_url(feed, url):
nzbname = kwargs.get("nzbname")
pp = att.get("pp")
cat = att.get("cat")
script = att.get("script")
prio = att.get("prio")
priority = att.get("prio")
if url:
sabnzbd.urlgrabber.add_url(url, pp, script, cat, prio, nzbname)
logging.info("Adding %s (%s) to queue", url, nzbname)
sabnzbd.urlgrabber.add_url(
url,
pp=pp,
script=script,
cat=cat,
priority=priority,
nzbname=nzbname,
nzo_info={"RSS": feed},
)
# Need to pass the title instead
sabnzbd.RSSReader.flag_downloaded(feed, url)
raise rssRaiser(self.__root, kwargs)

110
sabnzbd/internetspeed.py Normal file
View File

@@ -0,0 +1,110 @@
#!/usr/bin/python3 -OO
# Copyright 2007-2023 The SABnzbd-Team (sabnzbd.org)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""
sabnzbd.internetspeed - Measure internet bandwidth using sabctools routines
"""
import sys
import logging
import socket
import ssl
import time
import threading
from typing import Dict
import sabctools
import sabnzbd
from sabnzbd.happyeyeballs import happyeyeballs
TEST_HOSTNAME = "sabnzbd.org"
TEST_PORT = 443
TEST_FILE = "/tests/internetspeed/100MB.bin"
TEST_FILE_SIZE = 100 * 1024 * 1024
TEST_REQUEST = f"GET {TEST_FILE} HTTP/1.1\nHost: {TEST_HOSTNAME}\nUser-Agent: SABnzbd/{sabnzbd.__version__}\n\n"
SOCKET_TIMEOUT = 3
BUFFER_SIZE = 5 * 1024 * 1024 # Each connection will allocate its own buffer, so mind the memory usage!
NR_CONNECTIONS = 5
TIME_LIMIT = 3
def internetspeed_worker(secure_sock: ssl.SSLSocket, socket_speed: Dict[ssl.SSLSocket, float]):
"""Worker to perform the requests in parallel"""
secure_sock.sendall(TEST_REQUEST.encode())
empty_buffer = memoryview(sabctools.bytearray_malloc(BUFFER_SIZE))
start_time = time.perf_counter()
diff_time = 0
data_received = 0
while diff_time < TIME_LIMIT:
if data_received < TEST_FILE_SIZE:
try:
if new_bytes := sabctools.unlocked_ssl_recv_into(secure_sock, empty_buffer):
# Update the speed after every loop
diff_time = time.perf_counter() - start_time
data_received += new_bytes
socket_speed[secure_sock] = data_received / diff_time
else:
break
except ssl.SSLWantReadError:
time.sleep(0)
else:
break
try:
secure_sock.close()
except socket.error:
# In case socket was closed unexpectedly already
pass
def internetspeed(test_time_limit: int = TIME_LIMIT) -> float:
"""Measure internet speed from a test-download using our optimized SSL-code"""
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
socket_speed = {}
try:
for _ in range(NR_CONNECTIONS):
addrinfo = happyeyeballs(TEST_HOSTNAME, TEST_PORT, SOCKET_TIMEOUT)
sock = socket.socket(addrinfo.family, addrinfo.type)
sock.settimeout(SOCKET_TIMEOUT)
sock.connect(addrinfo.sockaddr)
secure_sock = context.wrap_socket(sock, server_hostname=TEST_HOSTNAME)
secure_sock.setblocking(False)
socket_speed[secure_sock] = 0
for secure_sock in socket_speed:
threading.Thread(target=internetspeed_worker, args=(secure_sock, socket_speed), daemon=True).start()
except Exception:
logging.info("Internet Bandwidth connection failure", exc_info=True)
return 0.0
# We let the workers finish
time.sleep(test_time_limit + 0.5)
speed = sum(socket_speed.values()) / 1024 / 1024
logging.debug("Internet Bandwidth = %.2f MB/s - %.2f Mbps", speed, speed * 8.05)
return speed
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
internetspeed()

View File

@@ -36,6 +36,7 @@ import sabnzbd.utils.rarfile as rarfile
from sabnzbd.misc import (
format_time_string,
find_on_path,
int_conv,
get_all_passwords,
calc_age,
cmp,
@@ -44,6 +45,7 @@ from sabnzbd.misc import (
format_time_left,
)
from sabnzbd.filesystem import (
make_script_path,
real_path,
globber,
globber_full,
@@ -2132,6 +2134,89 @@ def add_time_left(perc: float, start_time: Optional[float] = None, time_used: Op
return " - %s %s" % (format_time_left(int((100 - perc) / (perc / time_used)), short_format=True), T("left"))
return ""
def pre_queue(nzo: NzbObject, pp, cat):
"""Run pre-queue script (if any) and process results.
pp and cat are supplied separate since they can change.
"""
def fix(p):
# If added via API, some items can still be "None" (as a string)
if not p or str(p).lower() == "none":
return ""
return str(p)
values = [1, nzo.final_name_with_password, pp, cat, nzo.script, nzo.priority, None]
script_path = make_script_path(cfg.pre_script())
if script_path:
# Basic command-line parameters
command = [
script_path,
nzo.final_name_with_password,
pp,
cat,
nzo.script,
nzo.priority,
str(nzo.bytes),
" ".join(nzo.groups),
]
command = [fix(arg) for arg in command]
# Fields not in the NZO directly
show_analysis = sabnzbd.sorting.BasicAnalyzer(nzo.final_name)
extra_env_fields = {
"groups": " ".join(nzo.groups),
"title": show_analysis.info.get("title", ""),
"season": show_analysis.info.get("season_num", ""),
"episode": show_analysis.info.get("episode_num", ""),
"episode_name": show_analysis.info.get("ep_name", ""),
"is_proper": show_analysis.is_proper(),
"resolution": show_analysis.info.get("resolution", ""),
"decade": show_analysis.info.get("decade", ""),
"year": show_analysis.info.get("year", ""),
"month": show_analysis.info.get("month", ""),
"day": show_analysis.info.get("day", ""),
"job_type": show_analysis.type,
}
try:
p = build_and_run_command(command, env=create_env(nzo, extra_env_fields))
except:
logging.debug("Failed script %s, Traceback: ", script_path, exc_info=True)
return values
output = p.stdout.read()
ret = p.wait()
logging.info("Pre-queue script returned %s and output=\n%s", ret, output)
if ret == 0:
split_output = output.splitlines()
try:
# Extract category line from pre-queue output
pre_queue_category = split_output[3].strip(" '\"")
except IndexError:
pre_queue_category = None
for index, line in enumerate(split_output):
line = line.strip(" '\"")
if index < len(values):
if line:
values[index] = line
elif pre_queue_category and index in (2, 4, 5):
# Preserve empty pp, script, and priority lines to prevent
# pre-existing values from overriding category-based settings
values[index] = ""
accept = int_conv(values[0])
if accept < 1:
logging.info("Pre-Q refuses %s", nzo.final_name)
elif accept == 2:
logging.info("Pre-Q accepts&fails %s", nzo.final_name)
else:
logging.info("Pre-Q accepts %s", nzo.final_name)
return values
def is_sevenfile(path: str) -> bool:
"""Return True if path has 7Zip-signature and 7Zip is detected"""
with open(path, "rb") as sevenzip:

View File

@@ -696,15 +696,13 @@ class NzbQueue:
"""Get next article for jobs in the queue
Not locked for performance, since it only reads the queue
"""
# Pre-calculate propagation delay
propagation_delay = float(cfg.propagation_delay() * 60)
for nzo in self.__nzo_list:
# Not when queue paused, individually paused, or when waiting for propagation
# Force items will always download
if (
not sabnzbd.Downloader.paused
and nzo.status not in (Status.PAUSED, Status.GRABBING)
and (not propagation_delay or (nzo.avg_stamp + propagation_delay) < time.time())
and not nzo.propagation_delay_left
) or nzo.priority == FORCE_PRIORITY:
if not nzo.server_in_try_list(server):
if articles := nzo.get_articles(server, servers, fetch_limit):
@@ -807,12 +805,12 @@ class NzbQueue:
search: Optional[str] = None,
categories: Optional[List[str]] = None,
priorities: Optional[List[str]] = None,
statuses: Optional[List[str]] = None,
nzo_ids: Optional[List[str]] = None,
start: int = 0,
limit: int = 0,
) -> Tuple[int, int, int, List[NzbObject], int, int]:
"""Return list of queued jobs,
optionally filtered by 'search' and 'nzo_ids', and limited by start and limit.
"""Return list of queued jobs, optionally filtered and limited by start and limit.
Not locked for performance, only reads the queue
"""
if search:
@@ -841,6 +839,10 @@ class NzbQueue:
continue
if priorities and nzo.priority not in priorities:
continue
if statuses and nzo.status not in statuses:
# Propagation status is set only by the API-code, so has to be filtered specially
if not (Status.PROPAGATING in statuses and nzo.propagation_delay_left):
continue
if nzo_ids and nzo.nzo_id not in nzo_ids:
continue
@@ -863,12 +865,10 @@ class NzbQueue:
return bytes_left
def is_empty(self) -> bool:
empty = True
for nzo in self.__nzo_list:
if not nzo.futuretype and nzo.status != Status.PAUSED:
empty = False
break
return empty
return False
return True
def stop_idle_jobs(self):
"""Detect jobs that have zero files left and send them to post processing"""
@@ -953,28 +953,28 @@ class NzbQueue:
lname = name.lower()
for nzo in self.__nzo_list + sabnzbd.PostProcessor.get_queue():
# Skip any jobs already marked as duplicate, to prevent double-triggers
if not nzo.duplicate:
# URL's do not have an MD5!
if nzo.final_name.lower() == lname or (nzo.md5sum and md5sum and nzo.md5sum == md5sum):
return True
# URL's do not have an MD5!
if not nzo.duplicate and (
nzo.final_name.lower() == lname or (nzo.md5sum and md5sum and nzo.md5sum == md5sum)
):
return True
return False
@NzbQueueLocker
def have_episode(self, series_key: str) -> bool:
"""Check whether this episode of the series is already
def have_duplicate_key(self, duplicate_key: str) -> bool:
"""Check whether this duplicate key is already
in the queue or the post-processing queue"""
for nzo in self.__nzo_list:
for nzo in self.__nzo_list + sabnzbd.PostProcessor.get_queue():
# Skip any jobs already marked as duplicate, to prevent double-triggers
if not nzo.duplicate:
if nzo.duplicate_series_key == series_key:
return True
if not nzo.duplicate and nzo.duplicate_key == duplicate_key:
return True
return False
@NzbQueueLocker
def handle_duplicate_alternatives(self, finished_nzo: NzbObject, success: bool):
"""Remove matching duplicates if the first job succeeded,
or start the next alternative if the job failed"""
if not cfg.no_dupes() and not cfg.no_series_dupes():
if not cfg.no_dupes() and not cfg.no_smart_dupes():
return
# Unfortunately we need a copy, since we might remove items from the list
@@ -986,16 +986,16 @@ class NzbQueue:
if (
nzo.final_name.lower() == finished_nzo.final_name.lower()
or (nzo.md5sum and finished_nzo.md5sum and nzo.md5sum == finished_nzo.md5sum)
) or (
nzo.duplicate_series_key
and finished_nzo.duplicate_series_key
and nzo.duplicate_series_key == finished_nzo.duplicate_series_key
):
) or (nzo.duplicate_key and finished_nzo.duplicate_key and nzo.duplicate_key == finished_nzo.duplicate_key):
# Start the next alternative
if not success:
logging.info("Resuming duplicate alternative %s for ", nzo.final_name, finished_nzo.final_name)
# Don't just resume if only set to tag
if (nzo.duplicate == DuplicateStatus.DUPLICATE_ALTERNATIVE and cfg.no_dupes() != 4) or (
nzo.duplicate == DuplicateStatus.SMART_DUPLICATE_ALTERNATIVE and cfg.no_smart_dupes() != 4
):
logging.info("Resuming duplicate alternative %s for ", nzo.final_name, finished_nzo.final_name)
nzo.resume()
nzo.duplicate = None
nzo.resume()
return
# Take action on the alternatives to the duplicate
@@ -1003,13 +1003,11 @@ class NzbQueue:
# 2 = Pause
# 3 = Fail (move to History)
# 4 = Tag
series_duplicate = nzo.duplicate == DuplicateStatus.SERIES_DUPLICATE_ALTERNATIVE
if (not series_duplicate and cfg.no_dupes() == 1) or (series_duplicate and cfg.no_series_dupes() == 1):
smart_duplicate = nzo.duplicate == DuplicateStatus.SMART_DUPLICATE_ALTERNATIVE
if (not smart_duplicate and cfg.no_dupes() == 1) or (smart_duplicate and cfg.no_smart_dupes() == 1):
duplicate_warning(T('Ignoring duplicate NZB "%s"'), nzo.final_name)
self.remove(nzo.nzo_id)
elif (not series_duplicate and cfg.no_dupes() == 3) or (
series_duplicate and cfg.no_series_dupes() == 3
):
elif (not smart_duplicate and cfg.no_dupes() == 3) or (smart_duplicate and cfg.no_smart_dupes() == 3):
duplicate_warning(T('Failing duplicate NZB "%s"'), nzo.final_name)
nzo.fail_msg = T("Duplicate NZB")
self.fail_to_history(nzo)
@@ -1019,7 +1017,7 @@ class NzbQueue:
if nzo.duplicate == DuplicateStatus.DUPLICATE_ALTERNATIVE:
nzo.duplicate = DuplicateStatus.DUPLICATE
else:
nzo.duplicate = DuplicateStatus.SERIES_DUPLICATE
nzo.duplicate = DuplicateStatus.SMART_DUPLICATE
return
def __repr__(self):

View File

@@ -537,6 +537,7 @@ NzbObjectSaver = (
"url",
"groups",
"avg_date",
"propagation_delay",
"md5of16k",
"extrapars",
"par2packs",
@@ -563,7 +564,7 @@ NzbObjectSaver = (
"encrypted",
"bad_articles",
"duplicate",
"duplicate_series_key",
"duplicate_key",
"oversized",
"precheck",
"incomplete",
@@ -655,6 +656,7 @@ class NzbObject(TryList):
self.groups = []
self.avg_date = datetime.datetime(1970, 1, 1, 1, 0)
self.avg_stamp = 0.0 # Avg age in seconds (calculated from avg_age)
self.propagation_delay: Optional[float] = None # Set during parsing
self.correct_password: Optional[str] = None
# Bookkeeping values
@@ -691,7 +693,7 @@ class NzbObject(TryList):
self.nzo_id: Optional[str] = None
self.duplicate: Optional[str] = None
self.duplicate_series_key: Optional[str] = None
self.duplicate_key: Optional[str] = None
self.futuretype = futuretype
self.removed_from_queue = False
@@ -826,15 +828,64 @@ class NzbObject(TryList):
# Determine category and find pp/script values
self.cat, pp_tmp, self.script, priority = cat_to_opts(cat, pp, script, self.priority)
self.set_priority(priority)
self.set_pp(pp_tmp)
self.repair, self.unpack, self.delete = pp_to_opts(pp_tmp)
# Show first meta-password (if any), when there's no explicit password
if not self.password and self.meta.get("password"):
self.password = self.meta.get("password", [None])[0]
# Run user pre-queue script
if not reuse:
self.run_pre_queue(input_priority, input_pp, input_script)
# Check if we expect propagation delay
if (propagation_delay := self.avg_stamp + float(cfg.propagation_delay() * 60)) > time.time():
self.propagation_delay = propagation_delay
# Run user pre-queue script if set and valid
if not reuse and make_script_path(cfg.pre_script()):
# Call the script
accept, name, pq_pp, pq_cat, pq_script, pq_priority, pq_group = sabnzbd.newsunpack.pre_queue(self, pp, cat)
if pq_cat:
# An explicit pp/script/priority set upon adding the job takes precedence
# over an implicit setting based on the category set by pre-queue
if input_priority and not pq_priority:
pq_priority = input_priority
if input_pp and not pq_pp:
pq_pp = input_pp
if input_script and not pq_script:
pq_script = input_script
# Accept or reject
accept = int_conv(accept)
if accept < 1:
self.purge_data()
raise NzbRejected
if accept == 2:
raise NzbRejectToHistory(self, T("Pre-queue script marked job as failed"))
# Process all options, only over-write if set by script
# Beware that cannot do "if priority/pp", because those can
# also have a valid value of 0, which shouldn't be ignored
if name:
self.set_final_name_and_scan_password(name)
self.duplicate_check(repeat=True)
try:
pp = int(pq_pp)
except:
pp = None
if pq_cat:
cat = pq_cat
try:
priority = int(pq_priority)
except:
priority = DEFAULT_PRIORITY
if pq_script and is_valid_script(pq_script):
script = pq_script
if pq_group:
self.groups = [str(pq_group)]
# Re-evaluate results from pre-queue script
self.cat, pp, self.script, priority = cat_to_opts(cat, pp, script, priority)
self.set_priority(priority)
self.repair, self.unpack, self.delete = pp_to_opts(pp)
# Pause if requested by the NZB-adding or the pre-queue script
if self.priority == PAUSED_PRIORITY:
@@ -1247,6 +1298,17 @@ class NzbObject(TryList):
else:
return opts_to_pp(self.repair, self.unpack, self.delete)
@property
def propagation_delay_left(self) -> int:
"""Returns number of propagation minutes remaining, if any.
It could return seconds, but the numerical value is only used in the queue."""
if self.propagation_delay:
if (time_left := self.propagation_delay - time.time()) > 0:
return int(time_left / 60 + 0.5)
# We can remove the value, to skip any further calculations
self.propagation_delay = None
return 0
def set_pp(self, value: int):
self.repair, self.unpack, self.delete = pp_to_opts(value)
logging.info("Set pp=%s for job %s", value, self.final_name)
@@ -1300,9 +1362,12 @@ class NzbObject(TryList):
def labels(self):
"""Return (translated) labels of job"""
labels = []
if self.duplicate in (DuplicateStatus.DUPLICATE, DuplicateStatus.SERIES_DUPLICATE):
if self.duplicate in (DuplicateStatus.DUPLICATE, DuplicateStatus.SMART_DUPLICATE):
labels.append(T("DUPLICATE"))
if self.duplicate in (DuplicateStatus.DUPLICATE_ALTERNATIVE, DuplicateStatus.SERIES_DUPLICATE_ALTERNATIVE):
if self.duplicate in (
DuplicateStatus.DUPLICATE_ALTERNATIVE,
DuplicateStatus.SMART_DUPLICATE_ALTERNATIVE,
):
labels.append(T("ALTERNATIVE"))
if self.encrypted > 0:
labels.append(T("ENCRYPTED"))
@@ -1320,10 +1385,8 @@ class NzbObject(TryList):
labels.append(T("WAIT %s sec") % dif)
# Propagation delay label
propagation_delay = float(cfg.propagation_delay() * 60)
if propagation_delay and self.avg_stamp + propagation_delay > time.time() and self.priority != FORCE_PRIORITY:
wait_time = int((self.avg_stamp + propagation_delay - time.time()) / 60 + 0.5)
labels.append(T("PROPAGATING %s min") % wait_time) # Queue indicator while waiting for propagation of post
if self.propagation_delay_left and self.priority != FORCE_PRIORITY:
labels.append(T("PROPAGATING %s min") % self.propagation_delay_left) # Queue indicator: propagation of post
return labels
@@ -1521,6 +1584,10 @@ class NzbObject(TryList):
if dups:
download_msgs.append(T("%s articles had non-matching duplicates") % dups)
self.set_unpack_info("Download", "<br/>".join(download_msgs), unique=True)
# Add RSS source
if rss_feed := self.nzo_info.get("RSS"):
self.set_unpack_info("RSS", rss_feed, unique=True)
self.set_unpack_info("Source", self.url or self.filename, unique=True)
@synchronized(NZO_LOCK)
@@ -1841,28 +1908,45 @@ class NzbObject(TryList):
else:
nzf_ids.remove(nzf_id)
def set_duplicate_series_key(self):
def set_duplicate_key(self):
"""Shorthand to set the key once"""
if not self.duplicate_series_key:
show_analysis = sabnzbd.sorting.analyse_show(self.final_name)
if show_analysis["job_type"] == "tv":
series, season, episode, is_proper = (
show_analysis[key] for key in ("title", "season", "episode", "is_proper")
)
# Ignore proper results if not desired
if not cfg.series_propercheck():
is_proper = False
if not self.duplicate_key:
show_analysis = sabnzbd.sorting.BasicAnalyzer(self.final_name)
# We allow 1 proper result to bypass duplicate detection
self.duplicate_series_key = f"{series.lower()}/{season}/{episode}{f'/{is_proper}' if is_proper else ''}"
# We can only set a duplicate key for these types
if show_analysis.type not in ("tv", "movie", "date"):
return
def duplicate_check(self):
# The key always includes the title, for movies we don't add anything else
duplicate_key_items = [show_analysis.info.get("title", "")]
if show_analysis.type == "tv":
# For TV-shows we add the season and episode
duplicate_key_items.append(str(show_analysis.info.get("season_num", "")))
duplicate_key_items.append(str(show_analysis.info.get("episode_num", "")))
elif show_analysis.type == "date":
# Add date
duplicate_key_items.append(str(show_analysis.info.get("year", "")))
duplicate_key_items.append(str(show_analysis.info.get("month", "")))
duplicate_key_items.append(str(show_analysis.info.get("day", "")))
# We allow 1 proper result to bypass the detection, if desired
if show_analysis.is_proper() and cfg.dupes_propercheck():
duplicate_key_items.append("proper")
self.duplicate_key = "/".join(duplicate_key_items).lower()
def duplicate_check(self, repeat: bool = False):
"""Set the correct duplicate status"""
if not cfg.no_dupes() and not cfg.no_series_dupes():
if not cfg.no_dupes() and not cfg.no_smart_dupes():
return
duplicate_in_history = series_duplicate_in_history = False
duplicate_in_queue = series_duplicate_in_queue = False
# Reset status in case of a repeat analysis
if repeat:
self.duplicate = None
self.duplicate_key = None
duplicate_in_history = smart_duplicate_in_history = False
duplicate_in_queue = smart_duplicate_in_queue = False
with HistoryDB() as history_db:
# Dupe check off just name or nzb contents
@@ -1872,33 +1956,34 @@ class NzbObject(TryList):
duplicate_in_history = history_db.have_name_or_md5sum(self.final_name, self.md5sum)
logging.debug("Duplicate in history: %s", duplicate_in_history)
if not duplicate_in_history and cfg.backup_for_duplicates():
duplicate_in_history = backup_exists(self.filename)
logging.debug("Duplicate in backup: %s", duplicate_in_history)
duplicate_in_queue = sabnzbd.NzbQueue.have_name_or_md5sum(self.final_name, self.md5sum)
logging.debug("Duplicate in queue: %s", duplicate_in_queue)
# Dupe check off nzb filename
if not duplicate_in_history and not duplicate_in_queue and cfg.no_series_dupes():
logging.debug("Duplicate episode checking (%s): %s", self.final_name, self.duplicate_series_key)
self.set_duplicate_series_key()
if self.duplicate_series_key:
series_duplicate_in_history = history_db.have_episode(self.duplicate_series_key)
logging.debug("Duplicate episode in history: %s", series_duplicate_in_history)
# The nzb can already be in the backup while the job is still in the queue, so skip on repeat
if not repeat and not duplicate_in_history and not duplicate_in_queue and cfg.backup_for_duplicates():
duplicate_in_history = backup_exists(self.filename)
logging.debug("Duplicate in backup: %s", duplicate_in_history)
series_duplicate_in_queue = sabnzbd.NzbQueue.have_episode(self.duplicate_series_key)
logging.debug("Duplicate episode in queue: %s", series_duplicate_in_queue)
# Dupe check off nzb filename
if not duplicate_in_history and not duplicate_in_queue and cfg.no_smart_dupes():
self.set_duplicate_key()
logging.debug("Smart duplicate checking (%s): %s", self.final_name, self.duplicate_key)
if self.duplicate_key:
smart_duplicate_in_history = history_db.have_duplicate_key(self.duplicate_key)
logging.debug("Duplicate in history: %s", smart_duplicate_in_history)
smart_duplicate_in_queue = sabnzbd.NzbQueue.have_duplicate_key(self.duplicate_key)
logging.debug("Duplicate in queue: %s", smart_duplicate_in_queue)
else:
logging.debug("Not an episode, skipping duplicate episode check")
logging.debug("Unknown type, skipping smart duplicate check")
# Set the correct status
if series_duplicate_in_queue:
self.duplicate = DuplicateStatus.SERIES_DUPLICATE_ALTERNATIVE
if smart_duplicate_in_queue:
self.duplicate = DuplicateStatus.SMART_DUPLICATE_ALTERNATIVE
elif duplicate_in_queue:
self.duplicate = DuplicateStatus.DUPLICATE_ALTERNATIVE
elif series_duplicate_in_history:
self.duplicate = DuplicateStatus.SERIES_DUPLICATE
elif smart_duplicate_in_history:
self.duplicate = DuplicateStatus.SMART_DUPLICATE
elif duplicate_in_history:
self.duplicate = DuplicateStatus.DUPLICATE
@@ -1913,18 +1998,18 @@ class NzbObject(TryList):
# 2 = Pause
# 3 = Fail (move to History)
# 4 = Tag
if self.duplicate in (DuplicateStatus.DUPLICATE, DuplicateStatus.SERIES_DUPLICATE):
series_duplicate = self.duplicate == DuplicateStatus.SERIES_DUPLICATE
if (not series_duplicate and cfg.no_dupes() == 1) or (series_duplicate and cfg.no_series_dupes() == 1):
if self.duplicate in (DuplicateStatus.DUPLICATE, DuplicateStatus.SMART_DUPLICATE):
smart_duplicate = self.duplicate == DuplicateStatus.SMART_DUPLICATE
if (not smart_duplicate and cfg.no_dupes() == 1) or (smart_duplicate and cfg.no_smart_dupes() == 1):
# Discard
duplicate_warning(T('Ignoring duplicate NZB "%s"'), self.final_name)
self.purge_data()
raise NzbRejected
elif (not series_duplicate and cfg.no_dupes() == 3) or (series_duplicate and cfg.no_series_dupes() == 3):
elif (not smart_duplicate and cfg.no_dupes() == 3) or (smart_duplicate and cfg.no_smart_dupes() == 3):
# Fail (move to History)
duplicate_warning(T('Failing duplicate NZB "%s"'), self.final_name)
raise NzbRejectToHistory(self, T("Duplicate NZB"))
elif (not series_duplicate and cfg.no_dupes() == 2) or (series_duplicate and cfg.no_series_dupes() == 2):
elif (not smart_duplicate and cfg.no_dupes() == 2) or (smart_duplicate and cfg.no_smart_dupes() == 2):
# Pause
duplicate_warning(T('Pausing duplicate NZB "%s"'), self.final_name)
self.pause()
@@ -1932,101 +2017,13 @@ class NzbObject(TryList):
# Tag job
duplicate_warning('%s: "%s"', T("Duplicate NZB"), self.final_name)
# In case of alternative, just pause
if self.duplicate in (DuplicateStatus.DUPLICATE_ALTERNATIVE, DuplicateStatus.SERIES_DUPLICATE_ALTERNATIVE):
# In case of alternative, just pause (unless only tagging is desired)
if (self.duplicate == DuplicateStatus.DUPLICATE_ALTERNATIVE and cfg.no_dupes() != 4) or (
self.duplicate == DuplicateStatus.SMART_DUPLICATE_ALTERNATIVE and cfg.no_smart_dupes() != 4
):
logging.info("Pausing duplicate alternative %s", self.final_name)
self.pause()
def run_pre_queue(
self,
input_priority: Optional[Union[int, str]],
input_pp: Optional[int],
input_script: Optional[str],
):
"""Run pre-queue script (if any) and process results."""
if script_path := make_script_path(cfg.pre_script()):
def fix_parameter(parameter: Any) -> str:
# If added via API, some items can still be "None" (as a string)
if not parameter or str(parameter).lower() == "none":
return ""
return str(parameter)
# Basic parameters
command = [script_path, self.final_name_with_password, self.cat, self.priority, self.pp, self.script]
command = [fix_parameter(arg) for arg in command]
# Fields not in the NZO directly
extra_env_fields = sabnzbd.newsunpack.analyse_show(self.final_name_with_password)
extra_env_fields["groups"] = " ".join(self.groups)
try:
p = sabnzbd.newsunpack.build_and_run_command(
command, env=sabnzbd.newsunpack.create_env(self, extra_env_fields)
)
except:
logging.debug("Failed script %s, Traceback: ", script_path, exc_info=True)
return
output = p.stdout.read()
ret = p.wait()
logging.info("Pre-queue script returned %s and output=\n%s", ret, output)
if ret == 0:
# Base values
pq_cat = pq_pp = pq_script = pq_priority = None
for index, line in enumerate(output.splitlines(), start=1):
# Make sure to keep this in line with the documentation!
# 1: Accept
# 2: Name
# 3: Category
# 4: Priority
# 5: Post-processing
# 6: Script
# 7: Duplicate
# 8: Duplicate key
if line := line.strip(" '\""):
if index == 1:
# Accept or reject
accept = int_conv(line)
if accept < 1:
logging.info("Pre-queue script refuses %s", self.final_name)
self.purge_data()
raise NzbRejected
if accept == 2:
logging.info("Pre-queue marking as failed %s", self.final_name)
raise NzbRejectToHistory(self, T("Pre-queue script marked job as failed"))
logging.info("Pre-queue accepts %s", self.final_name)
elif index == 2:
self.set_final_name_and_scan_password(line)
elif index == 3:
pq_cat = line
elif index == 4:
pq_priority = int_conv(line, default=DEFAULT_PRIORITY)
elif index == 5:
pq_pp = int_conv(line, default=None)
elif index == 6:
if is_valid_script(line):
pq_script = line
elif index == 7:
self.duplicate = line
elif index == 8:
self.duplicate_series_key = line
if pq_cat:
# An explicit pp/script/priority set upon adding the job takes precedence
# over an implicit setting based on the category set by pre-queue
if input_priority and pq_priority is None:
pq_priority = input_priority
if input_pp and pq_pp is None:
pq_pp = input_pp
if input_script and not pq_script:
pq_script = input_script
# Re-evaluate results from pre-queue script
self.cat, pp, self.script, priority = cat_to_opts(pq_cat, pq_pp, pq_script, pq_priority)
self.set_priority(priority)
self.set_pp(pp)
def __getstate__(self):
"""Save to pickle file, selecting attributes"""
dict_ = {}

View File

@@ -216,9 +216,39 @@ class PostProcessor(Thread):
"""Return True if pp queue is empty"""
return self.slow_queue.empty() and self.fast_queue.empty() and not self.__busy
def get_queue(self) -> List[NzbObject]:
"""Return list of NZOs that still need to be processed"""
return [nzo for nzo in self.history_queue if nzo.work_name]
def get_queue(
self,
search: Optional[str] = None,
categories: Optional[List[str]] = None,
statuses: Optional[List[str]] = None,
nzo_ids: Optional[List[str]] = None,
) -> List[NzbObject]:
"""Return list of NZOs that still need to be processed.
Optionally filtered by the search terms"""
re_search = None
if isinstance(search, str):
# Replace * with .* and ' ' with .
search_text = search.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)
# Need a copy to prevent race conditions
filtered_queue = []
for nzo in self.history_queue[:]:
if not nzo.work_name:
continue
if re_search and not re_search.search(nzo.final_name):
continue
if categories and nzo.cat not in categories:
continue
if statuses and nzo.status not in statuses:
continue
if nzo_ids and nzo.nzo_id not in nzo_ids:
continue
filtered_queue.append(nzo)
return filtered_queue
def get_path(self, nzo_id: str) -> Optional[str]:
"""Return download path for given nzo_id or None when not found"""

View File

@@ -308,8 +308,9 @@ class RSSReader:
myPrio = defPrio
n = 0
if ("F" in reTypes or "S" in reTypes) and (not season or not episode):
show_analysis = sabnzbd.sorting.analyse_show(title)
season, episode = show_analysis["season"], show_analysis["episode"]
show_analysis = sabnzbd.sorting.BasicAnalyzer(title)
season = show_analysis.info.get("season")
episode = show_analysis.info.get("episode")
# Match against all filters until an positive or negative match
logging.debug("Size %s", size)
@@ -336,12 +337,7 @@ class RSSReader:
logging.debug("Filter requirement match on rule %d", n)
result = False
break
elif (
reTypes[n] == "S"
and season
and episode
and ep_match(season, episode, regexes[n], title)
):
elif reTypes[n] == "S" and ep_match(season, episode, regexes[n], title):
logging.debug("Filter matched on rule %d", n)
result = True
break
@@ -400,6 +396,7 @@ class RSSReader:
star = first
if result:
_HandleLink(
feed,
jobs,
link,
infourl,
@@ -422,6 +419,7 @@ class RSSReader:
new_downloads.append(title)
else:
_HandleLink(
feed,
jobs,
link,
infourl,
@@ -580,6 +578,7 @@ def patch_feedparser():
def _HandleLink(
feed,
jobs,
link,
infourl,
@@ -628,8 +627,17 @@ def _HandleLink(
if download:
jobs[link]["status"] = "D"
jobs[link]["time_downloaded"] = time.localtime()
logging.info("Adding %s (%s) to queue", link, title)
sabnzbd.urlgrabber.add_url(link, pp=pp, script=script, cat=cat, priority=priority, nzbname=nzbname)
sabnzbd.urlgrabber.add_url(
link,
pp=pp,
script=script,
cat=cat,
priority=priority,
nzbname=nzbname,
nzo_info={"RSS": feed},
)
else:
if star:
jobs[link]["status"] = flag + "*"

View File

@@ -28,6 +28,7 @@ SKIN_TEXT = {
"stage-unpack": TT("Unpack"), #: PP phase "unpack"
"stage-deobfuscate": TT("Deobfuscate"), #: PP phase "deobfuscate"
"stage-script": TT("Script"), #: PP phase "script"
"stage-rss": TT("RSS"), #: PP RSS feed of the NZB
"stage-source": TT("Source"), #: PP Source of the NZB (path or URL)
"stage-servers": TT("Servers"), #: PP Distribution over servers
"post-Completed": TT("Completed"), #: PP status
@@ -414,17 +415,13 @@ SKIN_TEXT = {
),
"opt-pause_on_pwrar": TT("Action when encrypted RAR is downloaded"),
"explain-pause_on_pwrar": TT('In case of "Pause", you\'ll need to set a password and resume the job.'),
"opt-no_dupes": TT("Detect Duplicate Downloads"),
"explain-no_dupes": TT(
"Detect identical NZB files (based on items in your History or files in .nzb Backup Folder)"
),
"opt-no_series_dupes": TT("Detect duplicate episodes in series"),
"explain-no_series_dupes": TT(
'Detect identical episodes in series (based on "name/season/episode" of items in your History)'
),
"opt-series_propercheck": TT("Allow proper releases"),
"explain-series_propercheck": TT(
"Bypass series duplicate detection if PROPER, REAL or REPACK is detected in the download name"
"opt-no_dupes": TT("Identical download detection"),
"explain-no_dupes": TT("Detect identical downloads based on name or NZB contents."),
"opt-no_smart_dupes": TT("Smart duplicate detection"),
"explain-no_smart_dupes": TT("Detect duplicates based on analysis of the filename."),
"opt-dupes_propercheck": TT("Allow proper releases"),
"explain-dupes_propercheck": TT(
"Bypass smart duplicate detection if PROPER, REAL or REPACK is detected in the download name."
),
"nodupes-off": TT("Off"), #: Three way switch for duplicates
"nodupes-ignore": TT("Discard"), #: Four way switch for duplicates
@@ -455,13 +452,9 @@ SKIN_TEXT = {
"opt-end_queue_script": TT("On queue finish script"),
"explain-end_queue_script": TT("Executed after the queue finishes downloading."),
"opt-par_option": TT("Extra PAR2 Parameters"),
"explain-par_option": TT("Read the Wiki Help on this!"),
"opt-nice": TT("Nice Parameters"),
"explain-nice": TT("Read the Wiki Help on this!"),
"opt-ionice": TT("IONice Parameters"),
"explain-ionice": TT("Read the Wiki Help on this!"),
"opt-win_process_prio": TT("External process priority"),
"explain-win_process_prio": TT("Read the Wiki Help on this!"),
"win_process_prio-high": TT("High"),
"win_process_prio-normal": TT("Normal"),
"win_process_prio-low": TT("Low"),
@@ -690,7 +683,6 @@ SKIN_TEXT = {
"opt-nscript_parameters": TT("Parameters"), #: Notification Script settings
"explain-nscript_enable": TT("Executes a custom script"), #: Notification Scriptsettings
"explain-nscript_script": TT("Which script should we execute for notification?"), #: Notification Scriptsettings
"explain-nscript_parameters": TT("Read the Wiki Help on this!"), #: Notification Script settings
# Config->Cat
"explain-catTags": TT(
'Indexers can supply a category inside the NZB which SABnzbd will try to match to the categories defined below. Additionally, you can add terms to "Indexer Categories / Groups" to match more categories. Use commas to separate terms. Wildcards in the terms are supported. <br>More information can be found on the Wiki.'

View File

@@ -585,10 +585,12 @@ class Sorter:
return move_to_parent_directory(base_path)
class SeriesAnalyzer(Sorter):
class BasicAnalyzer(Sorter):
def __init__(self, job_name: str):
"""Very basic sorter that doesn't require a config"""
super().__init__(nzo=None, job_name=job_name)
# Directly trigger setting all values
self.get_values()
def match_sorters(self):
"""Much more basic matching"""
@@ -600,25 +602,6 @@ class SeriesAnalyzer(Sorter):
self.type = "date" if self.guess.get("date") else "tv"
def analyse_show(job_name: str) -> Dict[str, str]:
"""Use the Sorter to collect some basic info on series"""
job = SeriesAnalyzer(job_name)
job.get_values()
return {
"title": job.info.get("title", ""),
"season": job.info.get("season_num", ""),
"episode": job.info.get("episode_num", ""),
"episode_name": job.info.get("ep_name", ""),
"is_proper": job.is_proper(),
"resolution": job.info.get("resolution", ""),
"decade": job.info.get("decade", ""),
"year": job.info.get("year", ""),
"month": job.info.get("month", ""),
"day": job.info.get("day", ""),
"job_type": job.type,
}
def ends_in_file(path: str) -> bool:
"""Return True when path ends with '.%ext' or '%fn' while allowing for a lowercase marker"""
return bool(RE_ENDEXT.search(path) or RE_ENDFN.search(path))
@@ -649,9 +632,8 @@ def move_to_parent_directory(workdir: str) -> Tuple[str, bool]:
return dest, True
def guess_what(name: str, sort_type: Optional[str] = None) -> MatchesDict:
"""Guess metadata for movies or episodes from their name. The sort_type ('movie' or 'episode')
is passed as a hint to guessit, if given."""
def guess_what(name: str) -> MatchesDict:
"""Guess metadata for movies or episodes from their name."""
if not name:
raise ValueError("Need a name for guessing")
@@ -670,9 +652,6 @@ def guess_what(name: str, sort_type: Optional[str] = None) -> MatchesDict:
"excludes": EXCLUDED_GUESSIT_PROPERTIES,
"date_year_first": True, # Make sure also short-dates are detected as YY-MM-DD
}
if sort_type:
# Hint the type if known
guessit_options["type"] = sort_type
guess = guessit.api.guessit(digit_fix + name, options=guessit_options)
logging.debug("Initial guess for %s is %s", digit_fix + name, guess)
@@ -687,7 +666,7 @@ def guess_what(name: str, sort_type: Optional[str] = None) -> MatchesDict:
# Try to avoid setting the type to movie on arbitrary jobs (e.g. 'Setup.exe') just because guessit defaults to that
table = str.maketrans({char: "" for char in whitespace + "_.-()[]{}"})
if guess.get("type") == "movie" and not sort_type == "movie": # No movie hint
if guess.get("type") == "movie":
if (
guess.get("title", "").translate(table) == name.translate(table) # Check for full name used as title
or any(
@@ -697,7 +676,9 @@ def guess_what(name: str, sort_type: Optional[str] = None) -> MatchesDict:
[key in guess for key in ("year", "screen_size", "video_codec")]
) # No typical movie properties set
or (
name.lower().startswith("http://") and name.lower().endswith(".nzb") and guess.get("container" == "nzb")
name.lower().startswith(("http://", "https://"))
and name.lower().endswith(".nzb")
and guess.get("container" == "nzb")
) # URL to an nzb file, can happen when pre-queue script rejects a job
):
guess["type"] = "unknown"

View File

@@ -31,7 +31,7 @@ from http.client import IncompleteRead, HTTPResponse
from mailbox import Message
from threading import Thread
import base64
from typing import Tuple, Optional, Union, List
from typing import Tuple, Optional, Union, List, Dict, Any
import sabnzbd
from sabnzbd.constants import DEF_TIMEOUT, FUTURE_Q_FOLDER, VALID_NZB_FILES, Status, VALID_ARCHIVES, DEFAULT_PRIORITY
@@ -112,8 +112,7 @@ class URLGrabber(Thread):
continue
filename = None
category = None
nzo_info = {}
nzo_info = future_nzo.nzo_info
wait = 0
retry = True
fetch_request = None
@@ -150,11 +149,19 @@ class URLGrabber(Thread):
value = fetch_request.headers[hdr]
except:
continue
# Skip empty values
if not value:
continue
if item in ("category_id", "x-dnzb-category"):
category = value
elif item in ("x-dnzb-moreinfo",):
# Use indexer category in case no specific one was set
if value and future_nzo.cat in (None, "*"):
if indexer_cat := misc.cat_convert(value):
future_nzo.cat = indexer_cat
elif item == "x-dnzb-moreinfo":
nzo_info["more_info"] = value
elif item in ("x-dnzb-name",):
elif item == "x-dnzb-name":
filename = value
if not filename.endswith(".nzb"):
filename += ".nzb"
@@ -172,10 +179,10 @@ class URLGrabber(Thread):
nzo_info["password"] = value
elif item == "retry-after":
wait = misc.int_conv(value)
# Get filename from Content-Disposition header
if not filename and "filename" in value:
filename = filename_from_content_disposition(value)
elif item == "content-disposition":
# Get filename from Content-Disposition header
if not filename and "filename" in value:
filename = filename_from_content_disposition(value)
if wait:
# For sites that have a rate-limiting attribute
@@ -292,6 +299,10 @@ class URLGrabber(Thread):
# Failed fetch
msg = T("URL Fetching failed; %s") % msg
# Add RSS source
if rss_feed := nzo.nzo_info.get("RSS"):
nzo.set_unpack_info("RSS", rss_feed, unique=True)
# Mark as failed and set the info why
nzo.set_unpack_info("Source", url)
nzo.set_unpack_info("Source", msg)
@@ -363,11 +374,9 @@ def filename_from_content_disposition(content_disposition: str) -> Optional[str]
filename_from_content_disposition('attachment; filename=jakubroztocil-httpie-0.4.1-20-g40bd8f6.tar.gz')
should return: 'jakubroztocil-httpie-0.4.1-20-g40bd8f6.tar.gz'
"""
filename = Message(f"Content-Disposition: attachment; {content_disposition}").get_filename()
if filename:
if filename := Message(f"Content-Disposition: attachment; {content_disposition}").get_filename():
# Basic sanitation
filename = os.path.basename(filename).lstrip(".").strip()
if filename:
if filename := os.path.basename(filename).lstrip(".").strip():
return filename
@@ -379,6 +388,7 @@ def add_url(
priority: Optional[Union[int, str]] = None,
nzbname: Optional[str] = None,
password: Optional[str] = None,
nzo_info: Optional[Dict[str, Any]] = None,
dup_check: bool = True,
) -> Tuple[AddNzbFileResult, List[str]]:
"""Add NZB based on a URL, attributes optional"""
@@ -403,6 +413,7 @@ def add_url(
password=password,
nzbname=nzbname,
status=Status.GRABBING,
nzo_info=nzo_info,
dup_check=dup_check,
)
except NzbRejected:

View File

@@ -1,96 +0,0 @@
#!/usr/bin/python3
"""
Module to measure and report Internet speed
Method: get one small and then a bigger reference file, and measure how long it takes, then calculate speed
Reports in MB/s (so mega BYTES per seconds), not to be confused with Mbps
"""
import time
import logging
import urllib.request
SIZE_URL_LIST = [
[5, "https://sabnzbd.org/tests/internetspeed/5MB.bin"],
[10, "https://sabnzbd.org/tests/internetspeed/10MB.bin"],
[20, "https://sabnzbd.org/tests/internetspeed/20MB.bin"],
[50, "https://sabnzbd.org/tests/internetspeed/50MB.bin"],
[100, "https://sabnzbd.org/tests/internetspeed/100MB.bin"],
]
def measure_speed_from_url(url: str) -> float:
"""Download the specified url (pointing to a file), and report back MB/s (as a float)"""
logging.debug("URL is %s", url)
start = time.time()
downloaded_bytes = 0 # default
try:
req = urllib.request.Request(url, data=None, headers={"User-Agent": "Mozilla/5.0 (Macintosh)"})
downloaded_bytes = len(urllib.request.urlopen(req, timeout=4).read())
except:
# No connection at all?
pass
time_granularity_worst_case = 0.008 # Windows has worst case 16 milliseconds
duration = max(time.time() - start, time_granularity_worst_case) # max() to avoid 0.0 divide error later on
logging.debug("Downloaded bytes: %d", downloaded_bytes)
logging.debug("Duration in seconds: %f", duration)
return downloaded_bytes / 1024**2 / duration
def bytes_to_bits(megabytes_per_second: float) -> float:
"""convert bytes (per second) to bits (per second), taking into a account network overhead"""
return 8.05 * megabytes_per_second # bits
def internetspeed() -> float:
"""Report Internet speed in MB/s as a float"""
# Do basic test with a small download
logging.debug("Basic measurement, with small download:")
start = time.time()
urlbasic = SIZE_URL_LIST[0][1] # get first URL, which is smallest download
base_megabytes_per_second = measure_speed_from_url(urlbasic)
logging.debug("Speed in MB/s: %.2f", base_megabytes_per_second)
if base_megabytes_per_second == 0:
# no Internet connection, or other problem
return 0.0
"""
Based on this first, small download, do a bigger download; the biggest download that still fits in total 8 seconds
Rationale: a bigger download could yield higher MB/s because the 'starting delay' is relatively less
We do two downloads, so one download must fit in 4 seconds
Note: a slow DNS lookup does influence the total time, so the measured speed (read: seemingly lower download speed)
"""
# Determine the biggest URL that can be downloaded within timeframe
maxtime = 4 # seconds
url_to_do = None
for size, sizeurl in SIZE_URL_LIST:
expectedtime = size / base_megabytes_per_second
if expectedtime < maxtime:
# ok, this one is feasible, so keep it in mind
url_to_do = sizeurl
max_megabytes_per_second = base_megabytes_per_second
# Execute it twice, and get the best result
for _ in range(2):
if url_to_do:
logging.debug(url_to_do)
measured_megabytes_per_second = measure_speed_from_url(url_to_do)
logging.debug("Speed in MB/s: %.2f", measured_megabytes_per_second)
max_megabytes_per_second = max(max_megabytes_per_second, measured_megabytes_per_second)
logging.debug("Internet Bandwidth = %.2f MB/s (in %.2f seconds)", max_megabytes_per_second, time.time() - start)
return max_megabytes_per_second
# MAIN
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
logging.debug("Log level is DEBUG")
print("Starting speed test:")
maxMBps = internetspeed()
print("Speed in MB/s: %.2f" % maxMBps)
print("Speed in Mbps: %.2f" % bytes_to_bits(maxMBps))

View File

@@ -6,5 +6,5 @@
# You MUST use double quotes (so " and not ')
# Do not forget to update the appdata file for every major release!
__version__ = "4.2.0Alpha2"
__version__ = "4.2.0RC1"
__baseline__ = "unknown"

View File

@@ -52,6 +52,7 @@ stages:
bytes: !anyint
meta: null
series: !anything
duplicate_key: !anything
md5sum: !re_match "[0-9a-fA-F]+"
password: !anystr
action_line: !anything
@@ -92,7 +93,7 @@ stages:
# so parameters match regardless of their order of appearance. Note
# that the content of 'slots' is checked in a separate expression.
expression: "^{{(?=.*'version': '{SAB_VERSION}')(?=.*'noofslots': [0-9]+)(?=.*'ppslots': [0-9]+)(?=.*'last_history_update': '?[0-9]+'?)(?=.*'total_size': '[0-9][0-9.]*.?(\ [A-Z])?')(?=.*'month_size': '[0-9][0-9.]*.?(\ [A-Z])?')(?=.*'week_size': '[0-9][0-9.]*.?(\ [A-Z])?')(?=.*'day_size': '[0-9][0-9.]*.?(\ [A-Z])?')(?=.*'slots': .+).*}}$"
expression: ".*'slots': \\[{{(?=.*'completed': [0-9]+)(?=.*'name': '.+')(?=.*'nzb_name': '.+')(?=.*'category': '.+')(?=.*'pp': '.?')(?=.*'script': '.+')(?=.*'report': '.+')(?=.*'url': '.+')(?=.*'status': '.+')(?=.*'nzo_id': 'SAB.+')(?=.*'storage': '.+')(?=.*'path': '.+')(?=.*'script_line': '.*')(?=.*'download_time': [0-9]+)(?=.*'postproc_time': [0-9]*)(?=.*'stage_log': \\[.*\\])(?=.*'downloaded': [0-9]+)(?=.*'completeness': None)(?=.*'fail_message': '.*')(?=.*'url_info': '.*')(?=.*'bytes': [0-9]+)(?=.*'meta': None)(?=.*'series': '?.*'?)(?=.*'md5sum': '[0-9a-fA-F]+')(?=.*'password': '.*')(?=.*'action_line': '.*')(?=.*'size': '[0-9].*')(?=.*'loaded': (True|False))(?=.*'retry': [0-9]+).*}}\\].*"
expression: ".*'slots': \\[{{(?=.*'completed': [0-9]+)(?=.*'name': '.+')(?=.*'nzb_name': '.+')(?=.*'category': '.+')(?=.*'pp': '.?')(?=.*'script': '.+')(?=.*'report': '.+')(?=.*'url': '.+')(?=.*'status': '.+')(?=.*'nzo_id': 'SAB.+')(?=.*'storage': '.+')(?=.*'path': '.+')(?=.*'script_line': '.*')(?=.*'download_time': [0-9]+)(?=.*'postproc_time': [0-9]*)(?=.*'stage_log': \\[.*\\])(?=.*'downloaded': [0-9]+)(?=.*'completeness': None)(?=.*'fail_message': '.*')(?=.*'url_info': '.*')(?=.*'bytes': [0-9]+)(?=.*'meta': None)(?=.*'series': '?.*'?)(?=.*'duplicate_key': '?.*'?)(?=.*'md5sum': '[0-9a-fA-F]+')(?=.*'password': '.*')(?=.*'action_line': '.*')(?=.*'size': '[0-9].*')(?=.*'loaded': (True|False))(?=.*'retry': [0-9]+).*}}\\].*"
---
@@ -150,6 +151,7 @@ stages:
<bytes>!anyint</bytes>
<meta>!anystr</meta>
<series>!anystr</series>
<duplicate_key>!anystr</duplicate_key>
<md5sum>!anystr</md5sum>
<password>!anystr</password>
<action_line>!anystr</action_line>

View File

@@ -18,6 +18,8 @@
"""
tests.test_cfg - Testing functions in cfg.py
"""
import sys
import pytest
import sabnzbd.cfg as cfg
@@ -108,7 +110,10 @@ class TestValidators:
def test_validate_safedir(self):
assert cfg.validate_safedir("", "", "def") == (None, "def")
assert cfg.validate_safedir("", "C:\\", "") == (None, "C:\\")
assert "UNC path" in cfg.validate_safedir("", "\\\\NAS\\foo", "")[0]
@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows tests")
def test_validate_safedir_win(self):
assert "Network path" in cfg.validate_safedir("", "\\\\NAS\\foo", "")[0]
def test_validate_host(self):
# valid input

View File

@@ -424,36 +424,36 @@ class TestCheckMountLinux(ffs.TestCase):
@set_platform("linux")
def test_bare_mountpoint_linux(self):
assert filesystem.check_mount("/media") is True
assert filesystem.check_mount("/media/") is True
assert filesystem.check_mount("/mnt") is True
assert filesystem.check_mount("/mnt/") is True
assert filesystem.mount_is_available("/media") is True
assert filesystem.mount_is_available("/media/") is True
assert filesystem.mount_is_available("/mnt") is True
assert filesystem.mount_is_available("/mnt/") is True
@set_platform("linux")
def test_existing_dir_linux(self):
assert filesystem.check_mount("/media/test") is True
assert filesystem.check_mount("/media/test/dir/") is True
assert filesystem.check_mount("/media/test/DIR/") is True
assert filesystem.check_mount("/mnt/TEST") is True
assert filesystem.check_mount("/mnt/TEST/dir/") is True
assert filesystem.check_mount("/mnt/TEST/DIR/") is True
assert filesystem.mount_is_available("/media/test") is True
assert filesystem.mount_is_available("/media/test/dir/") is True
assert filesystem.mount_is_available("/media/test/DIR/") is True
assert filesystem.mount_is_available("/mnt/TEST") is True
assert filesystem.mount_is_available("/mnt/TEST/dir/") is True
assert filesystem.mount_is_available("/mnt/TEST/DIR/") is True
@set_platform("linux")
# Cut down a bit on the waiting time
@set_config({"wait_ext_drive": 1})
def test_dir_nonexistent_linux(self):
# Filesystem is case-sensitive on this platform
assert filesystem.check_mount("/media/TEST") is False # Issue #1457
assert filesystem.check_mount("/media/TesT/") is False
assert filesystem.check_mount("/mnt/TeSt/DIR") is False
assert filesystem.check_mount("/mnt/test/DiR/") is False
assert filesystem.mount_is_available("/media/TEST") is False # Issue #1457
assert filesystem.mount_is_available("/media/TesT/") is False
assert filesystem.mount_is_available("/mnt/TeSt/DIR") is False
assert filesystem.mount_is_available("/mnt/test/DiR/") is False
@set_platform("linux")
def test_dir_outsider_linux(self):
# Outside of /media and /mnt
assert filesystem.check_mount("/test/that/") is True
assert filesystem.mount_is_available("/test/that/") is True
# Root directory
assert filesystem.check_mount("/") is True
assert filesystem.mount_is_available("/") is True
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Non-Windows tests")
@@ -470,33 +470,33 @@ class TestCheckMountMacOS(ffs.TestCase):
@set_platform("macos")
def test_bare_mountpoint_macos(self):
assert filesystem.check_mount("/Volumes") is True
assert filesystem.check_mount("/Volumes/") is True
assert filesystem.mount_is_available("/Volumes") is True
assert filesystem.mount_is_available("/Volumes/") is True
@set_platform("macos")
def test_existing_dir_macos(self):
assert filesystem.check_mount("/Volumes/test") is True
assert filesystem.check_mount("/Volumes/test/dir/") is True
assert filesystem.mount_is_available("/Volumes/test") is True
assert filesystem.mount_is_available("/Volumes/test/dir/") is True
# Filesystem is set case-insensitive for this platform
assert filesystem.check_mount("/VOLUMES/test") is True
assert filesystem.check_mount("/volumes/Test/dir/") is True
assert filesystem.mount_is_available("/VOLUMES/test") is True
assert filesystem.mount_is_available("/volumes/Test/dir/") is True
@set_platform("macos")
# Cut down a bit on the waiting time
@set_config({"wait_ext_drive": 1})
def test_dir_nonexistent_macos(self):
# Within /Volumes
assert filesystem.check_mount("/Volumes/nosuchdir") is False # Issue #1457
assert filesystem.check_mount("/Volumes/noSuchDir/") is False
assert filesystem.check_mount("/Volumes/nosuchDIR/subdir") is False
assert filesystem.check_mount("/Volumes/NOsuchdir/subdir/") is False
assert filesystem.mount_is_available("/Volumes/nosuchdir") is False # Issue #1457
assert filesystem.mount_is_available("/Volumes/noSuchDir/") is False
assert filesystem.mount_is_available("/Volumes/nosuchDIR/subdir") is False
assert filesystem.mount_is_available("/Volumes/NOsuchdir/subdir/") is False
@set_platform("macos")
def test_dir_outsider_macos(self):
# Outside of /Volumes
assert filesystem.check_mount("/test/that/") is True
assert filesystem.mount_is_available("/test/that/") is True
# Root directory
assert filesystem.check_mount("/") is True
assert filesystem.mount_is_available("/") is True
class TestCheckMountWin(ffs.TestCase):
@@ -512,39 +512,39 @@ class TestCheckMountWin(ffs.TestCase):
@set_platform("win32")
def test_existing_dir_win(self):
assert filesystem.check_mount("F:\\test") is True
assert filesystem.check_mount("F:\\test\\dir\\") is True
assert filesystem.mount_is_available("F:\\test") is True
assert filesystem.mount_is_available("F:\\test\\dir\\") is True
# Filesystem and drive letters are case-insensitive on this platform
assert filesystem.check_mount("f:\\Test") is True
assert filesystem.check_mount("f:\\test\\DIR\\") is True
assert filesystem.mount_is_available("f:\\Test") is True
assert filesystem.mount_is_available("f:\\test\\DIR\\") is True
@set_platform("win32")
def test_bare_mountpoint_win(self):
assert filesystem.check_mount("F:\\") is True
assert filesystem.check_mount("Z:\\") is False
assert filesystem.mount_is_available("F:\\") is True
assert filesystem.mount_is_available("Z:\\") is False
@set_platform("win32")
def test_dir_nonexistent_win(self):
# The existence of the drive letter is what really matters
assert filesystem.check_mount("F:\\NoSuchDir") is True
assert filesystem.check_mount("F:\\NoSuchDir\\") is True
assert filesystem.check_mount("F:\\NOsuchdir\\subdir") is True
assert filesystem.check_mount("F:\\nosuchDIR\\subdir\\") is True
assert filesystem.mount_is_available("F:\\NoSuchDir") is True
assert filesystem.mount_is_available("F:\\NoSuchDir\\") is True
assert filesystem.mount_is_available("F:\\NOsuchdir\\subdir") is True
assert filesystem.mount_is_available("F:\\nosuchDIR\\subdir\\") is True
@set_platform("win32")
# Cut down a bit on the waiting time
@set_config({"wait_ext_drive": 1})
def test_dir_on_nonexistent_drive_win(self):
# Non-existent drive-letter
assert filesystem.check_mount("H:\\NoSuchDir") is False
assert filesystem.check_mount("E:\\NoSuchDir\\") is False
assert filesystem.check_mount("L:\\NOsuchdir\\subdir") is False
assert filesystem.check_mount("L:\\nosuchDIR\\subdir\\") is False
assert filesystem.mount_is_available("H:\\NoSuchDir") is False
assert filesystem.mount_is_available("E:\\NoSuchDir\\") is False
assert filesystem.mount_is_available("L:\\NOsuchdir\\subdir") is False
assert filesystem.mount_is_available("L:\\nosuchDIR\\subdir\\") is False
@set_platform("win32")
def test_dir_outsider_win(self):
# Outside the local filesystem
assert filesystem.check_mount("//test/that/") is True
assert filesystem.mount_is_available("//test/that/") is True
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Non-Windows tests")

View File

@@ -174,17 +174,9 @@ class TestAddingNZBs:
try:
script_path = os.path.join(VAR.SCRIPT_DIR, script_name)
with open(script_path, "w") as f:
# Lines:
# 1: Accept
# 2: Name
# 3: Category
# 4: Priority
# 5: Post-processing
# 6: Script
# 7: Duplicate
# 8: Duplicate key
# line 1 = accept; 4 = category; 6 = priority
f.write(
"#!%s\n\nprint('1\\n\\n%s\\n%s\\n')"
"#!%s\n\nprint('1\\n\\n\\n%s\\n\\n%s\\n')"
% (
sys.executable,
(category if category else ""),
@@ -414,8 +406,8 @@ class TestAddingNZBs:
@pytest.mark.parametrize("prio_def_cat", sample(VALID_DEFAULT_PRIORITIES, 2))
@pytest.mark.parametrize("prio_add", sample(PRIO_OPTS_ADD, 3))
@pytest.mark.parametrize("prio_add_cat", sample(PRIO_OPTS_ADD_CAT, 2))
@pytest.mark.parametrize("prio_preq", PRIO_OPTS_PREQ)
@pytest.mark.parametrize("prio_preq_cat", PRIO_OPTS_PREQ_CAT)
@pytest.mark.parametrize("prio_preq", sample(PRIO_OPTS_PREQ, 2))
@pytest.mark.parametrize("prio_preq_cat", sample(PRIO_OPTS_PREQ_CAT, 2))
def test_adding_nzbs_priority_sample(
self, prio_def_cat, prio_add, prio_add_cat, prio_preq, prio_preq_cat, prio_meta_cat
):

View File

@@ -69,9 +69,15 @@ class TestAddingNZBsClean:
mode="queue", extra_arguments={"name": "priority", "value": job1["nzo_ids"][0], "value2": STOP_PRIORITY}
)
# Wait for the job to be removed
time.sleep(2)
assert get_api_result(mode="history", extra_arguments={"nzo_ids": job1["nzo_ids"][0]})["history"]["slots"]
# Wait for the job to be removed and appear in the history
for _ in range(10):
try:
history = get_api_result(mode="history", extra_arguments={"nzo_ids": job1["nzo_ids"][0]})["history"]
assert history["slots"][0]["nzo_id"] == job1["nzo_ids"][0]
assert history["slots"][0]["status"] == "Failed"
break
except (IndexError, AssertionError):
time.sleep(1)
# Now the second job should no longer be paused and labelled
queue = get_api_result(mode="queue", extra_arguments={"nzo_ids": job2["nzo_ids"][0]})
@@ -88,9 +94,16 @@ class TestAddingNZBsClean:
job = get_api_result(mode="addlocalfile", extra_arguments={"name": nzbfile})
assert job["status"]
assert job["nzo_ids"]
time.sleep(1)
assert not get_api_result(mode="queue", extra_arguments={"nzo_ids": job["nzo_ids"][0]})["queue"]["slots"]
assert get_api_result(mode="history", extra_arguments={"nzo_ids": job["nzo_ids"][0]})["history"]["slots"]
# Wait for the job to be removed and appear in the history
for _ in range(10):
try:
assert not get_api_result(mode="queue", extra_arguments={"nzo_ids": job["nzo_ids"][0]})["queue"]
history = get_api_result(mode="history", extra_arguments={"nzo_ids": job["nzo_ids"][0]})["history"]
assert history["slots"][0]["nzo_id"] == job["nzo_ids"][0]
assert history["slots"][0]["status"] == "Failed"
except (IndexError, AssertionError):
time.sleep(1)
# Reset and clean up
get_api_result(

View File

@@ -86,9 +86,13 @@ class TestDownloadSorting(DownloadFlowBasics):
"SINGLE_sort_s23e06_480i-SABnzbd",
["Single.Sort.S23E06.1.mov"],
), # Repeat to verify a unique filename is applied
(
pytest.param(
"single-ep_sort_s06e66_4k_uhd-SABnzbd",
["Single-Ep.Sort.S06E66." + ext for ext in ("avi", "srt")],
marks=pytest.mark.xfail(
sabnzbd.MACOS or sabnzbd.WIN32,
reason="Unreliable on macOS and Windows",
),
), # Single episode with associated smaller file
(
"single-ep_sort_s06e66_4k_uhd-SABnzbd",

View File

@@ -54,7 +54,7 @@ class TestHappyEyeballs:
assert "google" in addrinfo.canonname
def test_google_unreachable_port(self):
assert happyeyeballs("www.google.com", port=33333) is None
assert happyeyeballs("www.google.com", port=33333, timeout=1) is None
@pytest.mark.xfail(reason="CI sometimes blocks this")
def test_nntp(self):

View File

@@ -16,30 +16,15 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""
tests.test_utils.test_internetspeed - Testing SABnzbd internetspeed
tests.test_internetspeed - Testing SABnzbd internetspeed
"""
import pytest
from sabnzbd.utils.internetspeed import internetspeed, measure_speed_from_url, SIZE_URL_LIST
from sabnzbd.internetspeed import internetspeed
@pytest.mark.usefixtures("clean_cache_dir")
class TestInternetSpeed:
"""This class contains tests to measure internet speed
with an active and inactive connection
"""
def test_measurespeed_invalid_url(self):
speed = measure_speed_from_url("www.fake-url-9999999.test")
assert not speed
def test_measurespeed_valid_url(self):
speed = measure_speed_from_url(SIZE_URL_LIST[0][1])
assert isinstance(speed, float)
assert speed > 0
def test_internet_speed(self):
curr_speed_mbps = internetspeed()

View File

@@ -219,7 +219,7 @@ class FakeHistoryDB(db.HistoryDB):
nzo.repair, nzo.unpack, nzo.delete = pp_to_opts(choice(list(PP_LOOKUP.keys()))) # for "pp"
nzo.nzo_info = {"download_time": randint(1, 10**4)}
nzo.unpack_info = {"unpack_info": "placeholder unpack_info line\r\n" * 3}
nzo.duplicate_series_key = "show/season/episode"
nzo.duplicate_key = "show/season/episode"
nzo.futuretype = False # for "report", only True when fetching an URL
nzo.download_path = os.path.join(os.path.dirname(db.HistoryDB.db_path), "placeholder_downpath")