Compare commits

...

9 Commits

Author SHA1 Message Date
Safihre
efe17ca3bb Changes to fix the tests 2025-10-10 15:10:38 +02:00
Safihre
d4995e3120 Only clean up files created by the specific download 2025-10-10 14:15:47 +02:00
Safihre
90989b374a Let Sorter track file changes 2025-10-10 13:44:47 +02:00
SABnzbd Automation
fb2d412c97 Update translatable texts
[skip ci]
2025-10-08 21:01:13 +00:00
Safihre
1c0b1205b2 Add quota notifications
Closes #2926
2025-10-08 22:58:08 +02:00
Safihre
f556cea488 Use release version of Python 3.14 in CI 2025-10-08 20:07:39 +02:00
Sander
a2447253a0 Local ipv4 with socks5 proxy (#3161)
* Update all dependencies

* use socks5 server as test server

* make black happy

* improved active_socks5_proxy(): default port = 1080

* improved local_ipv4()

* use int_conv

* black

* use socks.socksocket.default_proxy directly

* active_socks5_proxy cleaner with int_conv

* correct to windows-2022

* socks.socksocket.default_proxy as check

* uniform naming socks5host/port

Closes #3154
---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: sanderjo <sander.jonkers+github@github.com>
2025-10-08 19:10:47 +02:00
Safihre
3393d7c976 Changing server name shows button "failure" instead of "saving..."
Closes #1551
2025-10-06 15:54:11 +02:00
renovate[bot]
06572bdf7d Update all dependencies (#3159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 02:33:57 +00:00
32 changed files with 562 additions and 230 deletions

View File

@@ -31,7 +31,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14.0-rc.3"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
name: ["Linux"]
os: [ubuntu-latest]
include:

View File

@@ -465,13 +465,10 @@
**/
jQuery(document).ready(function(){
// Reload form in case we change items that make the servers appear different
jQuery('input[name="priority"], input[name="displayname"], textarea[name="notes"]').on('change', function() {
jQuery('.fullform').submit(function() {
// No ajax this time
jQuery('input[name="ajax"]').val('')
// Skip the fancy stuff, just submit
this.submit()
})
jQuery('input[name="priority"], input[name="displayname"], textarea[name="notes"]').on('change', function(event) {
var parentForm = jQuery(event.target).parents("form")
parentForm.unbind("submit")
parentForm.find('input[name="ajax"]').val('')
})
/**

View File

@@ -298,6 +298,19 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr ""
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py, sabnzbd/skintext.py
msgid "Quota"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr ""
@@ -3302,10 +3315,6 @@ msgstr ""
msgid "Naming"
msgstr ""
#: sabnzbd/skintext.py
msgid "Quota"
msgstr ""
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr ""
@@ -3403,7 +3412,7 @@ msgid "Warn 5 days in advance of account expiration date."
msgstr ""
#: sabnzbd/skintext.py
msgid "Quota for this account, counted from the time it is set. In bytes, optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few minutes."
msgid "Quota for this server, counted from the time it is set. In bytes, optionally follow with K,M,G.<br />Checked every few minutes. Notification is sent when quota is spent."
msgstr ""
#. Server's retention time in days

View File

@@ -335,6 +335,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Kvóta přesažena, pozastavuji stahování"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Nesprávný parametr"
@@ -3474,10 +3488,6 @@ msgstr ""
msgid "Naming"
msgstr ""
#: sabnzbd/skintext.py
msgid "Quota"
msgstr ""
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr ""
@@ -3580,9 +3590,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days

View File

@@ -352,6 +352,20 @@ msgstr "Job \"%s\" er sandsynligvis krypteret: \"password\" i filnavnet \"%s\""
msgid "Quota spent, pausing downloading"
msgstr "Kvote brugt, pause downloading"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kvota"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Fejl parameter"
@@ -3601,10 +3615,6 @@ msgstr "Efterbehandling"
msgid "Naming"
msgstr "Navngivning"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kvota"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Hvor meget der kan downloades i denne måned (K/M/G)"
@@ -3712,9 +3722,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days

View File

@@ -376,6 +376,20 @@ msgstr "Aufgabe \"%s\" ist wahrscheinlich verschlüsselt: \"Passwort\" im Datein
msgid "Quota spent, pausing downloading"
msgstr "Kontingent aufgebraucht, Downloads werden angehalten"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kontingent"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Fehlerhafter Parameter"
@@ -3730,10 +3744,6 @@ msgstr "Nachbearbeitung"
msgid "Naming"
msgstr "Benennung"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kontingent"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Wie viel kann in diesem Monat heruntergeladen werden (K/M/G)?"
@@ -3847,13 +3857,10 @@ msgstr "5 Tage vor dem Ablauf des Accounts warnen."
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
"Kontingent für dieses Konto, gezählt ab dem Zeitpunkt, an dem es festgelegt "
"wird. In Bytes, optional gefolgt von K, M, G.<br />Warne, wenn es 0 "
"erreicht, wird alle paar Minuten überprüft."
#. Server's retention time in days
#: sabnzbd/skintext.py

View File

@@ -365,6 +365,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Quota gastado, pausando cola"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Cuota"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Parámetro incorrecto"
@@ -3704,10 +3718,6 @@ msgstr "Post procesado"
msgid "Naming"
msgstr "Nombrado"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Cuota"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Cantidad de descarga permitida este mes (K/M/G)"
@@ -3817,9 +3827,9 @@ msgstr "Advertir 5 días antes de la fecha de expiración de la cuenta."
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days

View File

@@ -334,6 +334,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Latausrajoitus saavutettu, keskeytetään lataukset"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Latausrajoitus"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Virheellinen parametri"
@@ -3562,10 +3576,6 @@ msgstr "Jälkikäsittely"
msgid "Naming"
msgstr "Nimeäminen"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Latausrajoitus"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Kuinka paljon voidaan ladata tässä kuussa (K/M/G)"
@@ -3671,9 +3681,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days

View File

@@ -370,6 +370,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Quota atteint, téléchargement mis en pause"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Quota"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Paramètre incorrect"
@@ -3714,10 +3728,6 @@ msgstr "Post-traitement"
msgid "Naming"
msgstr "Appellation"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Quota"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Combien peut-être télécharger ce mois (K/M/G)"
@@ -3828,13 +3838,10 @@ msgstr "Avertir 5 jours avant la date d'expiration du compte."
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
"Quota pour ce compte calculé à partir du moment où il est défini. En octets,"
" éventuellement suivi de K,M,G.<br />Avertir quand il atteint 0, vérifié "
"toutes les quelques minutes."
#. Server's retention time in days
#: sabnzbd/skintext.py

View File

@@ -335,6 +335,20 @@ msgstr "העבודה \"%s\" כנראה מוצפנת: \"סיסמה\" בשם הק
msgid "Quota spent, pausing downloading"
msgstr "מכסה נוצלה, משהה הורדה"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "מכסה"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "פרמטר שגוי"
@@ -3566,10 +3580,6 @@ msgstr "בתר־עיבוד"
msgid "Naming"
msgstr "מתן שמות"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "מכסה"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "כמה ניתן להוריד החודש (ק״ב/מ״ב/ג״ב)"
@@ -3674,12 +3684,10 @@ msgstr "הזהר 5 ימים טרם תאריך תפוגת החשבון."
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
"מכסה עבור חשבון זה, נספרת מהזמן שהיא הוגדרה. בבתים, יכולה לבוא עם K,M,G.<br "
"/>הזהר כאשר המכסה מגיעה אל 0, היא נבדקת כל כמה דקות."
#. Server's retention time in days
#: sabnzbd/skintext.py

View File

@@ -361,6 +361,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Quota esaurita, download in pausa"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Quota"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Parametro non corretto"
@@ -3673,10 +3687,6 @@ msgstr "Post-elaborazione"
msgid "Naming"
msgstr "Denominazione"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Quota"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Quanto può essere scaricato questo mese (K/M/G)"
@@ -3786,13 +3796,10 @@ msgstr "Avvisa 5 giorni prima della data di scadenza dell'account."
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
"Quota per questo account, contata dal momento in cui è impostata. In byte, "
"opzionalmente seguito da K,M,G.<br />Avvisa quando raggiunge 0, controllato "
"ogni pochi minuti."
#. Server's retention time in days
#: sabnzbd/skintext.py

View File

@@ -332,6 +332,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Kvote oppbrukt, setter nedlasting på pause"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kvote"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Feil parameter"
@@ -3539,10 +3553,6 @@ msgstr "Postprosessering"
msgid "Naming"
msgstr "Filnavn"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kvote"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Hvor mye can lastes ned denne måneden (K/M/G)"
@@ -3650,9 +3660,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days

View File

@@ -358,6 +358,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Quotum verbruikt, download is gestopt"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Quotum"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Incorrecte parameter"
@@ -3673,10 +3687,6 @@ msgstr "Nabewerking"
msgid "Naming"
msgstr "Naamgeving"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Quotum"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Hoeval mag deze maand worden gedownload (K/M/G)"
@@ -3788,14 +3798,10 @@ msgstr "Ontvang 5 dagen voor de verloopdatum een waarschuwing."
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
"Quotum voor dit account, wordt geteld vanaf het moment dat het voor het "
"eerst ingesteld wordt. In bytes, in K,M,G notatie.<br />Er wordt een "
"waarschuwing gegeven als het quotum bereikt is, dit wordt elke paar minuten "
"gecontroleerd."
#. Server's retention time in days
#: sabnzbd/skintext.py

View File

@@ -331,6 +331,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Przekroczono limit, wstrzymywanie pobierania"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Limit pobierania"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Błędny parametr"
@@ -3550,10 +3564,6 @@ msgstr "Przetwarzanie końcowe"
msgid "Naming"
msgstr "Nazwy"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Limit pobierania"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Ile danych można pobrać w miesiącu (K/M/G)"
@@ -3662,9 +3672,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days

View File

@@ -343,6 +343,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Quota esgotada, pausando o download"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Quota"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Parâmetro incorreto"
@@ -3562,10 +3576,6 @@ msgstr "Pós-processamento"
msgid "Naming"
msgstr "Nomeando"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Quota"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Quanto pode ser baixado neste mês (K/M/G)"
@@ -3673,9 +3683,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days

View File

@@ -347,6 +347,20 @@ msgstr "Sarcina „%s” este probabil criptată: „parolă” în fișierul
msgid "Quota spent, pausing downloading"
msgstr "Cotă epuizată, întrerupem descărcarea"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Cotă"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Parametru Incorect"
@@ -3581,10 +3595,6 @@ msgstr "Post procesare"
msgid "Naming"
msgstr "Redenumire"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Cotă"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Cât de mult poate fi descărcat în acestă lună (K/M/G)"
@@ -3693,9 +3703,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days

View File

@@ -331,6 +331,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Квота исчерпана. Загрузка приостановлена"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Квота"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Неправильный параметр"
@@ -3541,10 +3555,6 @@ msgstr "Пост-обработка"
msgid "Naming"
msgstr "Именование"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Квота"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Объем, который можно загрузить в месяц (K/M/G)"
@@ -3651,9 +3661,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days

View File

@@ -328,6 +328,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Kvota utrošena, pauziram preuzimanja"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Квота"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Погрешан параметар"
@@ -3526,10 +3540,6 @@ msgstr "Накнадна обрада"
msgid "Naming"
msgstr "Именовање"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Квота"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Колико може да се преузме овог месеца (К/М/Г)"
@@ -3637,9 +3647,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days

View File

@@ -328,6 +328,20 @@ msgstr ""
msgid "Quota spent, pausing downloading"
msgstr "Din kvot är uppnådd, pausar nerladdning"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kvot"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Fel parameter"
@@ -3538,10 +3552,6 @@ msgstr "Efterbehandling"
msgid "Naming"
msgstr "Döpning"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kvot"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Hur mycket kan laddas ner denna månad (K/M/G)"
@@ -3649,9 +3659,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days

View File

@@ -362,6 +362,20 @@ msgstr "\"%s\" işi muhtemelen şifrelenmiştir: \"parola\", \"%s\" dosya ismind
msgid "Quota spent, pausing downloading"
msgstr "Kota kullanıldı, indirme duraklatılıyor"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kota"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "Yanlış parametre"
@@ -3661,10 +3675,6 @@ msgstr "Post processing"
msgid "Naming"
msgstr "İsimlendirme"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "Kota"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "Bu ay ne kadar indirme yapılabileceği (K/M/G)"
@@ -3772,13 +3782,10 @@ msgstr "Sonlanma tarihinden 5 gün evvel ikaz et."
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
"Bu hesap için kota, bu seçeneğin ayarlanmasından itibaren hesaplanır. Bayt "
"olarak, seçime dayalı bir şekilde K,M,G takip edebilir.<br />0 değerine "
"ulaştığında ikazda bulun, her birkaç dakikada bir kontrol edilir."
#. Server's retention time in days
#: sabnzbd/skintext.py

View File

@@ -327,6 +327,20 @@ msgstr "任务 \"%s\" 可能受加密保护:文件名 \"%s\" 中有 \"password
msgid "Quota spent, pausing downloading"
msgstr "配额已耗尽,暂停下载"
#. Warning message - Notification
#: sabnzbd/bpsmeter.py, sabnzbd/downloader.py, sabnzbd/notifier.py,
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "配额"
#: sabnzbd/bpsmeter.py
msgid "Quota limit warning (%d%%)"
msgstr ""
#: sabnzbd/bpsmeter.py
msgid "Downloading resumed after quota reset"
msgstr ""
#: sabnzbd/cfg.py, sabnzbd/interface.py
msgid "Incorrect parameter"
msgstr "参数不正确"
@@ -3486,10 +3500,6 @@ msgstr "后期处理"
msgid "Naming"
msgstr "命名"
#: sabnzbd/skintext.py
msgid "Quota"
msgstr "配额"
#: sabnzbd/skintext.py
msgid "How much can be downloaded this month (K/M/G)"
msgstr "本月能下载多少数据量 (K/M/G)"
@@ -3592,9 +3602,9 @@ msgstr ""
#: sabnzbd/skintext.py
msgid ""
"Quota for this account, counted from the time it is set. In bytes, "
"optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few"
" minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally"
" follow with K,M,G.<br />Checked every few minutes. Notification is sent "
"when quota is spent."
msgstr ""
#. Server's retention time in days

View File

@@ -1,6 +1,6 @@
# Main requirements
# Note that not all sub-dependencies are listed, but only ones we know could cause trouble
apprise==1.9.4
apprise==1.9.5
sabctools==8.2.6
CT3==3.4.0
cffi==2.0.0
@@ -32,7 +32,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==46.0.1
cryptography==46.0.2
# 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
@@ -67,7 +67,7 @@ paho-mqtt==1.6.1 # Pinned, newer versions don't work with AppRise yet
charset_normalizer==3.4.3
idna==3.10
urllib3==2.5.0
certifi==2025.8.3
certifi==2025.10.5
oauthlib==3.3.1
PyJWT==2.10.1
blinker==1.9.0

View File

@@ -122,6 +122,7 @@ class BPSMeter:
"q_hour",
"q_minute",
"quota_enabled",
"quota_notifications_sent",
)
def __init__(self):
@@ -161,6 +162,7 @@ class BPSMeter:
self.q_hour = 0 # Quota reset hour
self.q_minute = 0 # Quota reset minute
self.quota_enabled: bool = True # Scheduled quota enable/disable
self.quota_notifications_sent: int = 0 # Track highest quota threshold that has been notified
def save(self):
"""Save admin to disk"""
@@ -323,10 +325,7 @@ class BPSMeter:
# Quota check
if self.have_quota and self.quota_enabled:
self.left -= self.sum_cached_amount
if self.left <= 0.0:
if not sabnzbd.Downloader.paused:
sabnzbd.Downloader.pause()
logging.warning(T("Quota spent, pausing downloading"))
self.check_quota()
# Speedometer
try:
@@ -431,15 +430,47 @@ class BPSMeter:
# We record every second, but display at the user's refresh-rate
return self.bps_list[::refresh_rate]
def check_quota(self):
"""Pause the queue when all quota is spent
Notify at specific quota usages (75%, 90%, 100%)
"""
if self.left <= 0.0:
if not sabnzbd.Downloader.paused:
sabnzbd.Downloader.pause()
logging.warning(T("Quota spent, pausing downloading"))
# Guard against zero division
if self.quota:
# Check for quota notifications (75%, 90%, 100%)
# Only send notification for the highest applicable threshold that hasn't been notified yet
used_percentage = ((self.quota - self.left) / self.quota) * 100
if used_percentage >= 100 and self.quota_notifications_sent < 100:
sabnzbd.notifier.send_notification(T("Quota"), T("Quota spent, pausing downloading"), "quota")
elif used_percentage >= 90 and self.quota_notifications_sent < 90:
sabnzbd.notifier.send_notification(
T("Quota"),
T("Quota limit warning (%d%%)") % used_percentage,
"quota",
)
elif used_percentage >= 75 and self.quota_notifications_sent < 75:
sabnzbd.notifier.send_notification(
T("Quota"),
T("Quota limit warning (%d%%)") % used_percentage,
"quota",
)
self.quota_notifications_sent = used_percentage
def reset_quota(self, force: bool = False):
"""Check if it's time to reset the quota, optionally resuming
Return True, when still paused or should be paused
"""
if force or (self.have_quota and time.time() > (self.q_time - 50)):
self.quota = self.left = cfg.quota_size.get_float()
self.quota_notifications_sent = 0
logging.info("Quota was reset to %s", self.quota)
if cfg.quota_resume():
logging.info("Auto-resume due to quota reset")
sabnzbd.notifier.send_notification(T("Quota"), T("Downloading resumed after quota reset"), "quota")
sabnzbd.Downloader.resume()
self.next_reset()
return False

View File

@@ -559,6 +559,7 @@ ncenter_prio_pp = OptionBool("ncenter", "ncenter_prio_pp", False)
ncenter_prio_complete = OptionBool("ncenter", "ncenter_prio_complete", True)
ncenter_prio_failed = OptionBool("ncenter", "ncenter_prio_failed", True)
ncenter_prio_disk_full = OptionBool("ncenter", "ncenter_prio_disk_full", True)
ncenter_prio_quota = OptionBool("ncenter", "ncenter_prio_quota", True)
ncenter_prio_new_login = OptionBool("ncenter", "ncenter_prio_new_login", False)
ncenter_prio_warning = OptionBool("ncenter", "ncenter_prio_warning", False)
ncenter_prio_error = OptionBool("ncenter", "ncenter_prio_error", False)
@@ -575,6 +576,7 @@ acenter_prio_pp = OptionBool("acenter", "acenter_prio_pp", False)
acenter_prio_complete = OptionBool("acenter", "acenter_prio_complete", True)
acenter_prio_failed = OptionBool("acenter", "acenter_prio_failed", True)
acenter_prio_disk_full = OptionBool("acenter", "acenter_prio_disk_full", True)
acenter_prio_quota = OptionBool("acenter", "acenter_prio_quota", True)
acenter_prio_new_login = OptionBool("acenter", "acenter_prio_new_login", False)
acenter_prio_warning = OptionBool("acenter", "acenter_prio_warning", False)
acenter_prio_error = OptionBool("acenter", "acenter_prio_error", False)
@@ -591,6 +593,7 @@ ntfosd_prio_pp = OptionBool("ntfosd", "ntfosd_prio_pp", False)
ntfosd_prio_complete = OptionBool("ntfosd", "ntfosd_prio_complete", True)
ntfosd_prio_failed = OptionBool("ntfosd", "ntfosd_prio_failed", True)
ntfosd_prio_disk_full = OptionBool("ntfosd", "ntfosd_prio_disk_full", True)
ntfosd_prio_quota = OptionBool("ntfosd", "ntfosd_prio_quota", True)
ntfosd_prio_new_login = OptionBool("ntfosd", "ntfosd_prio_new_login", False)
ntfosd_prio_warning = OptionBool("ntfosd", "ntfosd_prio_warning", False)
ntfosd_prio_error = OptionBool("ntfosd", "ntfosd_prio_error", False)
@@ -608,6 +611,7 @@ prowl_prio_pp = OptionNumber("prowl", "prowl_prio_pp", -3)
prowl_prio_complete = OptionNumber("prowl", "prowl_prio_complete", 0)
prowl_prio_failed = OptionNumber("prowl", "prowl_prio_failed", 1)
prowl_prio_disk_full = OptionNumber("prowl", "prowl_prio_disk_full", 1)
prowl_prio_quota = OptionNumber("prowl", "prowl_prio_quota", 0)
prowl_prio_new_login = OptionNumber("prowl", "prowl_prio_new_login", -3)
prowl_prio_warning = OptionNumber("prowl", "prowl_prio_warning", -3)
prowl_prio_error = OptionNumber("prowl", "prowl_prio_error", -3)
@@ -629,6 +633,7 @@ pushover_prio_pp = OptionNumber("pushover", "pushover_prio_pp", -3)
pushover_prio_complete = OptionNumber("pushover", "pushover_prio_complete", -1)
pushover_prio_failed = OptionNumber("pushover", "pushover_prio_failed", -1)
pushover_prio_disk_full = OptionNumber("pushover", "pushover_prio_disk_full", 1)
pushover_prio_quota = OptionNumber("pushover", "pushover_prio_quota", -1)
pushover_prio_new_login = OptionNumber("pushover", "pushover_prio_new_login", -3)
pushover_prio_warning = OptionNumber("pushover", "pushover_prio_warning", 1)
pushover_prio_error = OptionNumber("pushover", "pushover_prio_error", 1)
@@ -647,6 +652,7 @@ pushbullet_prio_pp = OptionBool("pushbullet", "pushbullet_prio_pp", False)
pushbullet_prio_complete = OptionBool("pushbullet", "pushbullet_prio_complete", True)
pushbullet_prio_failed = OptionBool("pushbullet", "pushbullet_prio_failed", True)
pushbullet_prio_disk_full = OptionBool("pushbullet", "pushbullet_prio_disk_full", True)
pushbullet_prio_quota = OptionBool("pushbullet", "pushbullet_prio_quota", True)
pushbullet_prio_new_login = OptionBool("pushbullet", "pushbullet_prio_new_login", False)
pushbullet_prio_warning = OptionBool("pushbullet", "pushbullet_prio_warning", False)
pushbullet_prio_error = OptionBool("pushbullet", "pushbullet_prio_error", False)
@@ -671,6 +677,8 @@ apprise_target_failed = OptionStr("apprise", "apprise_target_failed")
apprise_target_failed_enable = OptionBool("apprise", "apprise_target_failed_enable", True)
apprise_target_disk_full = OptionStr("apprise", "apprise_target_disk_full")
apprise_target_disk_full_enable = OptionBool("apprise", "apprise_target_disk_full_enable", False)
apprise_target_quota = OptionStr("apprise", "apprise_target_quota")
apprise_target_quota_enable = OptionBool("apprise", "apprise_target_quota_enable", True)
apprise_target_new_login = OptionStr("apprise", "apprise_target_new_login")
apprise_target_new_login_enable = OptionBool("apprise", "apprise_target_new_login_enable", True)
apprise_target_warning = OptionStr("apprise", "apprise_target_warning")
@@ -694,6 +702,7 @@ nscript_prio_pp = OptionBool("nscript", "nscript_prio_pp", False)
nscript_prio_complete = OptionBool("nscript", "nscript_prio_complete", True)
nscript_prio_failed = OptionBool("nscript", "nscript_prio_failed", True)
nscript_prio_disk_full = OptionBool("nscript", "nscript_prio_disk_full", True)
nscript_prio_quota = OptionBool("nscript", "nscript_prio_quota", True)
nscript_prio_new_login = OptionBool("nscript", "nscript_prio_new_login", False)
nscript_prio_warning = OptionBool("nscript", "nscript_prio_warning", False)
nscript_prio_error = OptionBool("nscript", "nscript_prio_error", False)

View File

@@ -1143,6 +1143,11 @@ def check_server_quota():
if server.quota():
if server.quota.get_int() + server.usage_at_start() < sabnzbd.BPSMeter.grand_total.get(srv, 0):
logging.warning(T("Server %s has used the specified quota"), server.displayname())
sabnzbd.notifier.send_notification(
T("Quota"),
T("Server %s has used the specified quota") % server.displayname(),
"quota",
)
server.quota.set("")
config.save_config()

View File

@@ -73,9 +73,11 @@ def addresslookup6(myhost):
def active_socks5_proxy() -> Optional[str]:
"""Return the active proxy"""
if socket.socket == socks.socksocket:
return "%s:%s" % socks.socksocket.default_proxy[1:3]
"""Return the active proxy. And None if no proxy is set"""
if socks.socksocket.default_proxy:
socks5host = socks.socksocket.default_proxy[1]
socks5port = sabnzbd.misc.int_conv(socks.socksocket.default_proxy[2], default=1080)
return f"{socks5host}:{socks5port}"
return None
@@ -92,11 +94,21 @@ def dnslookup() -> bool:
def local_ipv4() -> Optional[str]:
"""return IPv4 address of default local LAN interface"""
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s_ipv4:
# Option: use 100.64.1.1 (IANA-Reserved IPv4 Prefix for Shared Address Space)
s_ipv4.connect(("10.255.255.255", 80))
ipv4 = s_ipv4.getsockname()[0]
if not socks.socksocket.default_proxy:
# No socks5 proxy, so we can use UDP (SOCK_DGRAM) and a non-reachable host
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s_ipv4:
s_ipv4.connect(("10.255.255.255", 80))
ipv4 = s_ipv4.getsockname()[0]
else:
# socks5 proxy set, so we must use TCP (SOCK_STREAM) and a reachable host: the proxy server
socks5host = socks.socksocket.default_proxy[1]
socks5port = sabnzbd.misc.int_conv(socks.socksocket.default_proxy[2], default=1080)
logging.debug(f"Using proxy {socks5host} on port {socks5port} to determine local IPv4 address")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s_ipv4:
s_ipv4.connect((socks5host, socks5port))
ipv4 = s_ipv4.getsockname()[0]
except socket.error:
ipv4 = None

View File

@@ -2029,6 +2029,7 @@ NOTIFY_OPTIONS = {
"ncenter_prio_complete",
"ncenter_prio_failed",
"ncenter_prio_disk_full",
"ncenter_prio_quota",
"ncenter_prio_warning",
"ncenter_prio_error",
"ncenter_prio_queue_done",
@@ -2045,6 +2046,7 @@ NOTIFY_OPTIONS = {
"acenter_prio_complete",
"acenter_prio_failed",
"acenter_prio_disk_full",
"acenter_prio_quota",
"acenter_prio_warning",
"acenter_prio_error",
"acenter_prio_queue_done",
@@ -2061,6 +2063,7 @@ NOTIFY_OPTIONS = {
"ntfosd_prio_complete",
"ntfosd_prio_failed",
"ntfosd_prio_disk_full",
"ntfosd_prio_quota",
"ntfosd_prio_warning",
"ntfosd_prio_error",
"ntfosd_prio_queue_done",
@@ -2078,6 +2081,7 @@ NOTIFY_OPTIONS = {
"prowl_prio_complete",
"prowl_prio_failed",
"prowl_prio_disk_full",
"prowl_prio_quota",
"prowl_prio_warning",
"prowl_prio_error",
"prowl_prio_queue_done",
@@ -2097,6 +2101,7 @@ NOTIFY_OPTIONS = {
"pushover_prio_complete",
"pushover_prio_failed",
"pushover_prio_disk_full",
"pushover_prio_quota",
"pushover_prio_warning",
"pushover_prio_error",
"pushover_prio_queue_done",
@@ -2117,6 +2122,7 @@ NOTIFY_OPTIONS = {
"pushbullet_prio_complete",
"pushbullet_prio_failed",
"pushbullet_prio_disk_full",
"pushbullet_prio_quota",
"pushbullet_prio_warning",
"pushbullet_prio_error",
"pushbullet_prio_queue_done",
@@ -2141,6 +2147,8 @@ NOTIFY_OPTIONS = {
"apprise_target_failed_enable",
"apprise_target_disk_full",
"apprise_target_disk_full_enable",
"apprise_target_quota",
"apprise_target_quota_enable",
"apprise_target_warning",
"apprise_target_warning_enable",
"apprise_target_error",
@@ -2164,6 +2172,7 @@ NOTIFY_OPTIONS = {
"nscript_prio_complete",
"nscript_prio_failed",
"nscript_prio_disk_full",
"nscript_prio_quota",
"nscript_prio_warning",
"nscript_prio_error",
"nscript_prio_queue_done",

View File

@@ -89,6 +89,7 @@ NOTIFICATION_TYPES = {
"warning": TT("Warning"), #: Notification
"error": TT("Error"), #: Notification
"disk_full": TT("Disk full"), #: Notification
"quota": TT("Quota"), #: Notification
"queue_done": TT("Queue finished"), #: Notification
"new_login": TT("User logged in"), #: Notification
"other": TT("Other Messages"), #: Notification
@@ -323,6 +324,8 @@ def send_apprise(title, msg, notification_type, force=False, test=None):
"error": apprise.common.NotifyType.FAILURE,
# Disk full
"disk_full": apprise.common.NotifyType.WARNING,
# Quota
"quota": apprise.common.NotifyType.WARNING,
# Queue finished
"queue_done": apprise.common.NotifyType.INFO,
# User logged in

View File

@@ -488,7 +488,7 @@ def process_job(nzo: NzbObject) -> bool:
if all_ok:
# Remove files matching the cleanup list
cleanup_list(tmp_workdir_complete, skip_nzb=True)
newfiles = cleanup_list(newfiles, skip_nzb=True)
# Check if this is an NZB-only download, if so redirect to queue
# except when PP was Download-only
@@ -501,7 +501,7 @@ def process_job(nzo: NzbObject) -> bool:
cleanup_empty_directories(tmp_workdir_complete)
else:
# Full cleanup including nzb's
cleanup_list(tmp_workdir_complete, skip_nzb=False)
newfiles = cleanup_list(newfiles, skip_nzb=False)
script_ret = 0
script_error = False
@@ -536,7 +536,7 @@ def process_job(nzo: NzbObject) -> bool:
# TV/Movie/Date Renaming code part 2 - rename and move files to parent folder
if all_ok and file_sorter.sorter_active:
if newfiles:
workdir_complete, ok = file_sorter.rename(newfiles, workdir_complete)
workdir_complete, ok, newfiles = file_sorter.rename(newfiles, workdir_complete)
if not ok:
nzo.set_unpack_info("Unpack", T("Failed to move files"))
nzo.fail_msg = T("Failed to move files")
@@ -607,9 +607,9 @@ def process_job(nzo: NzbObject) -> bool:
unique=True,
)
# Cleanup again, including NZB files
# Cleanup again, any changes made by the script will not be handled
if all_ok and os.path.isdir(workdir_complete):
cleanup_list(workdir_complete, False)
newfiles = cleanup_list(newfiles, False)
# Force error for empty result
all_ok = all_ok and not empty
@@ -1101,27 +1101,34 @@ def handle_empty_queue():
sabnzbd.LIBC.malloc_trim(0)
def cleanup_list(wdir: str, skip_nzb: bool):
def cleanup_list(file_paths: List[str], skip_nzb: bool) -> List[str]:
"""Remove all files whose extension matches the cleanup list,
optionally ignoring the nzb extension
optionally ignoring the nzb extension.
Returns the updated list of files (excluding removed files).
"""
if cfg.cleanup_list():
try:
with os.scandir(wdir) as files:
for entry in files:
if entry.is_dir():
cleanup_list(entry.path, skip_nzb)
cleanup_empty_directories(entry.path)
else:
if on_cleanup_list(entry.name, skip_nzb):
try:
logging.info("Removing unwanted file %s", entry.path)
remove_file(entry.path)
except Exception:
logging.error(T("Removing %s failed"), clip_path(entry.path))
logging.info("Traceback: ", exc_info=True)
except Exception:
logging.info("Traceback: ", exc_info=True)
if not cfg.cleanup_list():
return file_paths
logging.info("Checking for extensions to clean up: %s", cfg.cleanup_list.get_string())
remaining_files = []
for file_path in file_paths:
filename = os.path.basename(file_path)
if on_cleanup_list(filename, skip_nzb):
try:
logging.info("Removing unwanted file %s", file_path)
remove_file(file_path)
# File was removed, don't add to remaining_files
except Exception:
logging.error(T("Removing %s failed"), clip_path(file_path))
logging.info("Traceback: ", exc_info=True)
# File removal failed, keep it in the list
remaining_files.append(file_path)
else:
# File not on cleanup list, keep it
remaining_files.append(file_path)
return remaining_files
def prefix(path: str, pre: str) -> str:
@@ -1274,6 +1281,7 @@ def del_marker(path: str):
def remove_from_list(name: Optional[str], lst: List[str]):
"""Removes item from list, modifies list in place"""
if name:
for n in range(len(lst)):
if lst[n].endswith(name):

View File

@@ -545,7 +545,8 @@ SKIN_TEXT = {
"srv-expire_date": TT("Account expiration date"),
"srv-explain-expire_date": TT("Warn 5 days in advance of account expiration date."),
"srv-explain-quota": TT(
"Quota for this account, counted from the time it is set. In bytes, optionally follow with K,M,G.<br />Warn when it reaches 0, checked every few minutes."
"Quota for this server, counted from the time it is set. In bytes, optionally follow with K,M,G.<br />"
"Checked every few minutes. Notification is sent when quota is spent."
),
"srv-retention": TT("Retention time"), #: Server's retention time in days
"srv-ssl": TT("SSL"), #: Server SSL tickbox

View File

@@ -501,6 +501,39 @@ class Sorter:
logging.info("Traceback: ", exc_info=True)
return success
def _update_files_after_renames(self, base_path: str, original_files: List[str]) -> List[str]:
"""Update files list to reflect any renames that may have occurred in the base_path"""
updated_files = []
renamed_files = set() # Track files that no longer exist at their original paths
for file_path in original_files:
# Convert to absolute path for checking
if os.path.isabs(file_path):
abs_file_path = file_path
else:
abs_file_path = os.path.join(base_path, file_path)
abs_file_path = os.path.normpath(abs_file_path)
# If the original file still exists, keep it
if os.path.exists(abs_file_path):
updated_files.append(file_path)
else:
renamed_files.add(os.path.basename(file_path))
# If any files were renamed, add all current files in the base_path (excluding originals)
if renamed_files:
try:
for item in os.listdir(base_path):
item_path = os.path.join(base_path, item)
if os.path.isfile(item_path) and item not in renamed_files:
# Only add if not already in the list (to avoid duplicates)
if item_path not in updated_files:
updated_files.append(item_path)
except (OSError, FileNotFoundError):
pass
return updated_files
def _to_filepath(self, f: str, base_path: str) -> str:
if not is_full_path(f):
f = os.path.join(base_path, f)
@@ -515,23 +548,24 @@ class Sorter:
and os.stat(filepath).st_size >= self.rename_limit
)
def rename(self, files: List[str], base_path: str) -> Tuple[str, bool]:
def rename(self, files: List[str], base_path: str) -> Tuple[str, bool, List[str]]:
if not self.rename_files:
return move_to_parent_directory(base_path)
return move_to_parent_directory(base_path, files)
# Log the minimum filesize for renaming
if self.rename_limit > 0:
logging.debug("Minimum filesize for renaming set to %s bytes", self.rename_limit)
# Store the list of all files for later use
all_files = files
all_files = files[:]
updated_files = files[:]
# Filter files to remove nonexistent, undersized, samples, and excluded extensions
files = [f for f in files if self._filter_files(f, base_path)]
if len(files) == 0:
logging.debug("No files left to rename after applying filter")
return move_to_parent_directory(base_path)
return move_to_parent_directory(base_path, updated_files)
# Check for season packs or sequential filenames and handle their renaming separately;
# if neither applies or succeeds, fall back to going with the single largest file instead.
@@ -541,7 +575,9 @@ class Sorter:
logging.debug("Trying to rename season pack files %s", files)
if self._rename_season_pack(files, base_path, all_files):
cleanup_empty_directories(base_path)
return move_to_parent_directory(base_path)
# Update the files list to reflect any renames that happened
updated_files = self._update_files_after_renames(base_path, updated_files)
return move_to_parent_directory(base_path, updated_files)
else:
logging.debug("Season pack sorting didn´t rename any files")
@@ -550,7 +586,9 @@ class Sorter:
logging.debug("Trying to rename sequential files %s", sequential_files)
if self._rename_sequential(sequential_files, base_path):
cleanup_empty_directories(base_path)
return move_to_parent_directory(base_path)
# Update the files list to reflect any renames that happened
updated_files = self._update_files_after_renames(base_path, updated_files)
return move_to_parent_directory(base_path, updated_files)
else:
logging.debug("Sequential file handling didn't rename any files")
@@ -575,14 +613,16 @@ class Sorter:
renamer(filepath, new_filepath)
renamed_files.append(new_filepath)
except Exception:
logging.error(T("Failed to rename %s to %s"), clip_path(base_path), clip_path(new_filepath))
logging.error(T("Failed to rename %s to %s"), clip_path(filepath), clip_path(new_filepath))
logging.info("Traceback: ", exc_info=True)
rename_similar(base_path, f_ext, self.filename_set, renamed_files)
else:
logging.debug("Cannot rename %s, new path %s already exists.", largest_file.get("name"), new_filepath)
return move_to_parent_directory(base_path)
# Update the files list to reflect any renames that happened
updated_files = self._update_files_after_renames(base_path, updated_files)
return move_to_parent_directory(base_path, updated_files)
class BasicAnalyzer(Sorter):
@@ -607,29 +647,55 @@ def ends_in_file(path: str) -> bool:
return bool(RE_ENDEXT.search(path) or RE_ENDFN.search(path))
def move_to_parent_directory(workdir: str) -> Tuple[str, bool]:
"""Move all files under 'workdir' into 'workdir/..'"""
# Determine 'folder'/..
def move_to_parent_directory(workdir: str, files: List[str]) -> Tuple[str, bool, List[str]]:
"""Move specified files from workdir to workdir's parent directory and track file movements"""
if not files:
return workdir, True, []
# Determine 'workdir/..' as destination
workdir = os.path.abspath(os.path.normpath(workdir))
dest = os.path.abspath(os.path.normpath(os.path.join(workdir, "..")))
logging.debug("Moving files from %s to parent directory: %s", workdir, dest)
logging.debug("Moving all files from %s to %s", workdir, dest)
updated_files = []
# Check for DVD folders and bail out if found
for item in os.listdir(workdir):
if item.lower() in IGNORED_MOVIE_FOLDERS:
return workdir, True
try:
for item in os.listdir(workdir):
if os.path.isdir(os.path.join(workdir, item)) and item.lower() in IGNORED_MOVIE_FOLDERS:
return workdir, True, files
except (OSError, FileNotFoundError):
# Skip directory listing if directory doesn't exist
pass
for root, dirs, files in os.walk(workdir):
for _file in files:
path = os.path.join(root, _file)
new_path = path.replace(workdir, dest)
ok, new_path = move_to_path(path, new_path)
if not ok:
return dest, False
# Move each file to the parent directory
for file_path in files:
# Convert relative paths to absolute paths within workdir
if os.path.isabs(file_path):
abs_file_path = file_path
else:
abs_file_path = os.path.join(workdir, file_path)
abs_file_path = os.path.normpath(abs_file_path)
if not os.path.exists(abs_file_path):
# Skip files that don't exist
continue
filename = os.path.basename(abs_file_path)
new_path = os.path.join(dest, filename)
ok, new_path = move_to_path(abs_file_path, new_path)
if not ok:
return dest, False, files
# Track this file as it was moved
updated_files.append(new_path)
# Clean up empty directories in the workdir
cleanup_empty_directories(workdir)
return dest, True
# Return the parent directory and list of files that were actually moved
return dest, True, updated_files
def guess_what(name: str) -> MatchesDict:

View File

@@ -351,13 +351,15 @@ class TestSortingFunctions:
ffs.fs.create_file(base_dir + "/" + test_file, int("0644", 8))
assert os.path.exists(base_dir + "/" + test_file) is True
return_path, return_status = sorting.move_to_parent_directory(base_dir + "/TEST")
# Create the file list to move
files_to_move = [base_dir + "/TEST/DIR/FILE"]
return_path, return_status, return_files = sorting.move_to_parent_directory(base_dir + "/TEST", files_to_move)
# Affected by move
assert not os.path.exists(base_dir + "/TEST/DIR/FILE") # Moved to subdir
assert not os.path.exists(base_dir + "/TEST/DIR2") # Deleted empty directory
assert not os.path.exists(base_dir + "/DIR2") # Dirs don't get moved, only their file content
assert os.path.exists(base_dir + "/DIR/FILE") # Moved file
assert os.path.exists(base_dir + "/FILE") # Moved file
# Not moved
assert not os.path.exists(base_dir + "/some.file")
assert not os.path.exists(base_dir + "/2")
@@ -366,6 +368,8 @@ class TestSortingFunctions:
# Function return values
assert (return_path) == base_dir
assert (return_status) is True
assert len(return_files) == 1
assert return_files[0] == base_dir + "/FILE"
# Exception for DVD directories
with pyfakefs.fake_filesystem_unittest.Patcher() as ffs:
@@ -380,13 +384,15 @@ class TestSortingFunctions:
ffs.fs.create_file(base_dir + "/" + test_file, int("0644", 8))
assert os.path.exists(base_dir + "/" + test_file) is True
return_path, return_status = sorting.move_to_parent_directory(base_dir + "/TEST")
# Create the file list to move (includes file in DVD directory)
files_to_move = [base_dir + "/TEST/" + dvd + "/FILE"]
return_path, return_status, return_files = sorting.move_to_parent_directory(base_dir + "/TEST", files_to_move)
# Nothing should move in the presence of a DVD directory structure
assert os.path.exists(base_dir + "/TEST/" + dvd + "/FILE")
assert os.path.exists(base_dir + "/TEST/DIR2")
assert not os.path.exists(base_dir + "/DIR2")
assert not os.path.exists(base_dir + "/DIR/FILE")
assert not os.path.exists(base_dir + "/FILE")
assert not os.path.exists(base_dir + "/some.file")
assert not os.path.exists(base_dir + "/2")
assert os.path.exists(base_dir + "/dir/some.file")
@@ -394,6 +400,8 @@ class TestSortingFunctions:
# Function return values
assert (return_path) == base_dir + "/TEST"
assert (return_status) is True
# Files should be returned as-is when DVD structure prevents moving
assert return_files == files_to_move
@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows tests")
def test_move_to_parent_directory_win(self):
@@ -409,13 +417,15 @@ class TestSortingFunctions:
ffs.fs.create_file(base_dir + "\\" + test_file, int("0644", 8))
assert os.path.exists(base_dir + "\\" + test_file) is True
return_path, return_status = sorting.move_to_parent_directory(base_dir + "\\TEST")
# Create the file list to move
files_to_move = [base_dir + "\\TEST\\DIR\\FILE"]
return_path, return_status, return_files = sorting.move_to_parent_directory(base_dir + "\\TEST", files_to_move)
# Affected by move
assert not os.path.exists(base_dir + "\\TEST\\DIR\\FILE") # Moved to subdir
assert not os.path.exists(base_dir + "\\TEST\\DIR2") # Deleted empty directory
assert not os.path.exists(base_dir + "\\DIR2") # Dirs don't get moved, only their file content
assert os.path.exists(base_dir + "\\DIR\\FILE") # Moved file
assert os.path.exists(base_dir + "\\FILE") # Moved file
# Not moved
assert not os.path.exists(base_dir + "\\some.file")
assert not os.path.exists(base_dir + "\\2")
@@ -424,6 +434,8 @@ class TestSortingFunctions:
# Function return values
assert (return_path) == base_dir
assert (return_status) is True
assert len(return_files) == 1
assert return_files[0] == base_dir + "\\FILE"
# Exception for DVD directories
with pyfakefs.fake_filesystem_unittest.Patcher() as ffs:
@@ -438,20 +450,24 @@ class TestSortingFunctions:
ffs.fs.create_file(base_dir + "\\" + test_file, int("0644", 8))
assert os.path.exists(base_dir + "\\" + test_file) is True
return_path, return_status = sorting.move_to_parent_directory(base_dir + "\\TEST")
# Create the file list to move (includes file in DVD directory)
files_to_move = [base_dir + "\\TEST\\" + dvd + "\\FILE"]
return_path, return_status, return_files = sorting.move_to_parent_directory(base_dir + "\\TEST", files_to_move)
# Nothing should move in the presence of a DVD directory structure
assert os.path.exists(base_dir + "\\TEST\\" + dvd + "\\FILE")
assert os.path.exists(base_dir + "\\TEST\\DIR2")
assert not os.path.exists(base_dir + "\\DIR2")
assert not os.path.exists(base_dir + "\\DIR\\FILE")
assert not os.path.exists(base_dir + "\\FILE")
assert not os.path.exists(base_dir + "\\some.file")
assert not os.path.exists(base_dir + "\\2")
assert os.path.exists(base_dir + "\\dir\\some.file")
assert os.path.exists(base_dir + "\\dir\\2")
# Function return values
# Function return values - should return original directory when DVD structure found
assert (return_path) == base_dir + "\\TEST"
assert (return_status) is True
# Files should be returned as-is when DVD structure prevents moving
assert return_files == files_to_move
@pytest.mark.usefixtures("clean_cache_dir")
@@ -766,6 +782,10 @@ class TestSortingSorter:
):
"""Test the file renaming of the Sorter class"""
with pyfakefs.fake_filesystem_unittest.Patcher() as ffs:
# Add guessit package directory to real paths so it can access its config files
import guessit
guessit_path = os.path.dirname(guessit.__file__)
ffs.fs.add_real_paths([guessit_path])
# Make up a job name
job_name = "Simulated.Job." + job_tag + ".2160p.Web.x264-SAB"
@@ -816,7 +836,7 @@ class TestSortingSorter:
)
sorter.get_values()
sorter.construct_path()
sort_dest, is_ok = sorter.rename(all_files, job_dir)
sort_dest, is_ok, updated_files = sorter.rename(all_files, job_dir)
# Check the result
try:
@@ -1314,7 +1334,7 @@ class TestSortingSorter:
sorted_path = sorter.construct_path()
# Check season pack status again after constructing the path
assert sorter.is_season_pack is result_is_season_pack_later
sorted_dest, sorted_ok = sorter.rename(globber(job_dir), job_dir)
sorted_dest, sorted_ok, updated_files = sorter.rename(globber(job_dir), job_dir)
# Verify the results
for pattern, number in result_globs.items():