Skip to content

Commit e66b39c

Browse files
authored
Merge pull request #279 from Baretsky:add-sabnzbd-support
Add sabnzbd support
2 parents 1820526 + 7bb4190 commit e66b39c

File tree

14 files changed

+586
-51
lines changed

14 files changed

+586
-51
lines changed

README.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Looking to **upgrade from V1 to V2**? Look [here](#upgrading-from-v1-to-v2)
4747
- [WHISPARR](#whisparr)
4848
- [Downloaders](#download-clients)
4949
- [QBITTORRENT](#qbittorrent)
50+
- [SABNZBD](#sabnzbd)
5051
- [Disclaimer](#disclaimer)
5152

5253
## Overview
@@ -518,10 +519,13 @@ This is the interesting section. It defines which job you want decluttarr to run
518519
- Steers whether slow downloads are removed from the queue
519520
- Blocklisted: Yes
520521
- Note:
521-
- Does not apply to usenet downloads (since there users pay for certain speed, slowness should not occur)
522-
- Applies only if qBittorrent is configured: The remove_slow check is automatically temporarily disabled if qBittorrent is already using more than 80% of your available download bandwidth.
523-
For this to work, you must set a Global Download Rate Limit in qBittorrent. Otherwise, unlimited capacity is assumed, and the auto-disable feature will never trigger.
524-
Make sure to configure the limit in the correct place — either the standard or the alternative limits, depending on which one is active in your setup.
522+
Radarr, Sonarr, etc. only update the info about progress and speed of the queue items periodically.
523+
Therefore, relying only on that information is imprecise to establish whether a download is slow.
524+
It is advised that you configure qBittorrent (for torrents) and or SABnzbd (for Usenet), so that decluttarr can query those information real-time.
525+
- Additional benefit when having qBittorrent configured:
526+
- The remove_slow check is automatically temporarily disabled if qBittorrent is already using more than 80% of your available download bandwidth.
527+
- For this to work, you must set a Global Download Rate Limit in qBittorrent. Otherwise, unlimited capacity is assumed, and the auto-disable feature will never trigger.
528+
- Make sure to configure the limit in the correct place — either the standard or the alternative limits, depending on which one is active in your setup.
525529
- Type: Boolean or Dict
526530
- Permissible Values:
527531
If bool: True, False
@@ -602,7 +606,7 @@ Defines arr-instances on which download queue should be decluttered
602606
Certain jobs need access directly to the download clients, as the arr instances don't offer all the relevant APIs / data.
603607
You can perfectly use decluttarr without this; just certain features won't be available (as documented above).
604608

605-
For time being, only qbittorrent is supported.
609+
Supported download clients: **qBittorrent** and **SABnzbd**.
606610

607611
#### QBITTORRENT
608612
- List of qbittorrent instances
@@ -613,6 +617,14 @@ For time being, only qbittorrent is supported.
613617
- password: Optional - see above
614618
- name: Optional. Needs to correspond with the name that you have set up in your Arr instance. Defaults to "qBittorrent"
615619

620+
#### SABNZBD
621+
- List of SABnzbd instances
622+
- Type: List of SABnzbd instances
623+
- Keys per instance
624+
- base_url: URL under which SABnzbd can be reached (mandatory)
625+
- api_key: SABnzbd API key (mandatory)
626+
- name: Optional. Needs to correspond with the name that you have set up in your Arr instance. Defaults to "SABnzbd"
627+
616628

617629
## Disclaimer
618630

config/config_example.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,7 @@ download_clients:
6666
# username: xxxx # (optional -> if not provided, assuming not needed)
6767
# password: xxxx # (optional -> if not provided, assuming not needed)
6868
# name: "qBittorrent" # (optional -> if not provided, assuming "qBittorrent". Must correspond with what is specified in your *arr as download client name)
69+
# sabnzbd:
70+
# - base_url: "http://sabnzbd:8080" # SABnzbd server URL
71+
# api_key: "your_api_key_here" # (required -> SABnzbd API key)
72+
# # name: "SABnzbd" # (optional -> if not provided, assuming "SABnzbd". Must correspond with what is specified in your *arr as download client name)

main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ async def main():
4444
while True:
4545
logger.info("-" * 50)
4646

47-
# Refresh qBit Cookies
47+
# Refresh qBit Cookies (SABnzbd doesn't need cookie refresh)
4848
for qbit in settings.download_clients.qbittorrent:
4949
await qbit.refresh_cookie()
5050

src/job_manager.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ async def removal_jobs(self):
3535
if not await self._queue_has_items():
3636
return
3737

38-
if not await self._qbit_connected():
38+
if not await self._download_clients_connected():
3939
return
4040

4141
# Refresh trackers
@@ -57,11 +57,17 @@ async def search_jobs(self):
5757
return
5858
if self.settings.jobs.search_missing.enabled:
5959
await SearchHandler(
60-
arr=self.arr, settings=self.settings, missing_or_cutoff="missing", job_name="search_missing"
60+
arr=self.arr,
61+
settings=self.settings,
62+
missing_or_cutoff="missing",
63+
job_name="search_missing",
6164
).handle_search()
6265
if self.settings.jobs.search_unmet_cutoff.enabled:
6366
await SearchHandler(
64-
arr=self.arr, settings=self.settings, missing_or_cutoff="cutoff", job_name="search_cutoff_unmet"
67+
arr=self.arr,
68+
settings=self.settings,
69+
missing_or_cutoff="cutoff",
70+
job_name="search_cutoff_unmet",
6571
).handle_search()
6672

6773
async def _queue_has_items(self):
@@ -81,16 +87,22 @@ async def _queue_has_items(self):
8187
logger.verbose("Removal Jobs: None triggered (Queue is empty)")
8288
return False
8389

84-
async def _qbit_connected(self):
85-
for qbit in self.settings.download_clients.qbittorrent:
90+
async def _download_clients_connected(self):
91+
for clients in [
92+
self.settings.download_clients.qbittorrent,
93+
self.settings.download_clients.sabnzbd,
94+
]:
95+
if not await self._check_client_connection_status(clients):
96+
return False
97+
return True
98+
99+
async def _check_client_connection_status(self, clients):
100+
for client in clients:
86101
logger.debug(
87-
f"job_manager.py/_queue_has_items (Before any removal jobs): Checking if qbit is connected to the internet"
102+
f"job_manager.py/_check_client_connection_status: Checking if {client.name} is connected"
88103
)
89-
# Check if any client is disconnected
90-
if not await qbit.check_qbit_connected():
91-
logger.warning(
92-
f">>> qBittorrent is disconnected. Skipping queue cleaning on {self.arr.name}.",
93-
)
104+
if not await client.check_connected():
105+
logger.warning(f">>> {client.name} is disconnected. Skipping queue cleaning on {self.arr.name}.")
94106
return False
95107
return True
96108

src/jobs/remove_bad_files.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ async def _find_affected_items(self):
4242
if download_client_type == "qbittorrent":
4343
client_items = await self._handle_qbit(download_client, download_ids)
4444
affected_items.extend(client_items)
45+
elif download_client_type == "sabnzbd":
46+
# SABnzbd doesn't support bad file removal in the same way as BitTorrent
47+
# Usenet doesn't have the concept of "availability" or individual file selection
48+
continue
4549
return affected_items
4650

4751
def _group_download_ids_by_client(self):

src/jobs/remove_slow.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,6 @@ async def _find_affected_items(self):
2929
if self._not_downloading(item):
3030
continue
3131

32-
# Is Usenet -> skip
33-
if self._is_usenet(item):
34-
continue # No need to check for speed for usenet, since there users pay for speed
35-
3632
# Completed but stuck -> skip
3733
if self._is_completed_but_stuck(item):
3834
logger.info(
@@ -75,9 +71,6 @@ def _missing_keys(item) -> bool:
7571
required_keys = {"downloadId", "size", "sizeleft", "status", "protocol", "download_client", "download_client_type"}
7672
return not required_keys.issubset(item)
7773

78-
@staticmethod
79-
def _is_usenet(item) -> bool:
80-
return item.get("protocol") == "usenet"
8174

8275
@staticmethod
8376
def _not_downloading(item) -> bool:
@@ -98,19 +91,35 @@ async def _get_progress_stats(self, item):
9891
download_id, download_progress,
9992
)
10093

94+
# For SABnzbd, use calculated speed from API data
95+
if item["download_client_type"] == "sabnzbd":
96+
try:
97+
api_speed = await item["download_client"].get_item_download_speed(download_id)
98+
if api_speed is not None:
99+
speed = api_speed
100+
logger.debug(f"SABnzbd API speed for {item['title']}: {speed} KB/s")
101+
except Exception as e: # noqa: BLE001
102+
logger.debug(f"SABnzbd get_item_download_speed failed: {e}")
101103
self.arr.tracker.download_progress[download_id] = download_progress
102104
return download_progress, previous_progress, increment, speed
103105

104106

105107
async def _get_download_progress(self, item, download_id):
106-
# Grabs the progress from qbit if possible, else calculates it based on progress (imprecise)
108+
# Grabs the progress from qbit or SABnzbd if possible, else calculates it based on progress (imprecise)
107109
if item["download_client_type"] == "qbittorrent":
108110
try:
109111
progress = await item["download_client"].fetch_download_progress(download_id)
110112
if progress is not None:
111113
return progress
112114
except Exception: # noqa: BLE001
113115
pass # fall back below
116+
elif item["download_client_type"] == "sabnzbd":
117+
try:
118+
progress = await item["download_client"].fetch_download_progress(download_id)
119+
if progress is not None:
120+
return progress
121+
except Exception: # noqa: BLE001
122+
pass # fall back below
114123
return item["size"] - item["sizeleft"]
115124

116125
def _compute_increment_and_speed(self, download_id, current_progress):
@@ -135,6 +144,7 @@ def _high_bandwidth_usage(self, item):
135144
if download_client.bandwidth_usage > DISABLE_OVER_BANDWIDTH_USAGE:
136145
self.strikes_handler.pause_entry(download_id, "High Bandwidth Usage")
137146
return True
147+
# SABnzbd: Bandwidth checking isn't applicable to usenet usage
138148

139149
return False
140150

@@ -157,4 +167,5 @@ async def update_bandwidth_usage(self):
157167
continue
158168
if item["download_client_type"] == "qbittorrent":
159169
await download_client.set_bandwidth_usage()
170+
# SABnzbd: Since bandwith checking isn't applicable, setting bandwidth usage is irrelevant
160171
processed_clients.add(item["download_client"])

src/settings/_constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class MinVersions:
3535
readarr = "0.4.15.2787"
3636
whisparr = "2.0.0.548"
3737
qbittorrent = "4.3.0"
38+
sabnzbd = "4.0.0"
3839

3940

4041
class FullQueueParameter:

src/settings/_download_clients.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
from src.settings._config_as_yaml import get_config_as_yaml
22
from src.settings._download_clients_qbit import QbitClients
3+
from src.settings._download_clients_sabnzbd import SabnzbdClients
34

4-
DOWNLOAD_CLIENT_TYPES = ["qbittorrent"]
5+
DOWNLOAD_CLIENT_TYPES = ["qbittorrent", "sabnzbd"]
56

67

78
class DownloadClients:
89
"""Represents all download clients."""
910

1011
qbittorrent = None
12+
sabnzbd = None
1113

1214
def __init__(self, config, settings):
1315
self._set_qbit_clients(config, settings)
16+
self._set_sabnzbd_clients(config, settings)
1417
self.check_unique_download_client_types()
1518

1619
def _set_qbit_clients(self, config, settings):
@@ -28,11 +31,18 @@ def _set_qbit_clients(self, config, settings):
2831
]:
2932
setattr(settings.general, key, None)
3033

34+
def _set_sabnzbd_clients(self, config, settings):
35+
download_clients = config.get("download_clients", {})
36+
if isinstance(download_clients, dict):
37+
self.sabnzbd = SabnzbdClients(config, settings)
38+
if not self.sabnzbd:
39+
self.sabnzbd = SabnzbdClients({}, settings) # Initialize empty list
40+
3141
def config_as_yaml(self):
3242
"""Log all download clients."""
3343
return get_config_as_yaml(
34-
{"qbittorrent": self.qbittorrent},
35-
sensitive_attributes={"username", "password", "cookie"},
44+
{"qbittorrent": self.qbittorrent, "sabnzbd": self.sabnzbd},
45+
sensitive_attributes={"username", "password", "cookie", "api_key"},
3646
internal_attributes={"api_url", "cookie", "settings", "min_version"},
3747
hide_internal_attr=True,
3848
)
@@ -101,7 +111,7 @@ def get_download_client_type_from_implementation(
101111
"""
102112
mapping = {
103113
"QBittorrent": "qbittorrent",
104-
# Only qbit configured for now
114+
"SABnzbd": "sabnzbd",
105115
}
106116
download_client_type = mapping.get(arr_download_client_implementation)
107117
return download_client_type

src/settings/_download_clients_qbit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ async def check_qbit_reachability(self):
220220
logger.error(f"-- | qBittorrent\n❗️ {e}\n{tip}\n")
221221
wait_and_exit()
222222

223-
async def check_qbit_connected(self):
223+
async def check_connected(self):
224224
"""Check if the qBittorrent is connected to internet."""
225225
logger.debug(
226226
"_download_clients_qBit.py/check_qbit_reachability: Checking if qbit is connected to the internet"

0 commit comments

Comments
 (0)