mirror of
https://github.com/ManiMatter/decluttarr.git
synced 2026-04-17 23:53:57 +02:00
Added sabnzbd support
This commit is contained in:
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@@ -3,18 +3,18 @@ name: Test, Build, Deploy
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
- "*"
|
||||
paths:
|
||||
- 'src/**'
|
||||
- "src/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
- "*"
|
||||
paths:
|
||||
- 'src/**'
|
||||
- "src/**"
|
||||
workflow_dispatch:
|
||||
create:
|
||||
branches:
|
||||
- '*'
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
validate-branch-name:
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10.13'
|
||||
python-version: "3.10.13"
|
||||
- name: Install testing dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: '0'
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: Bump version and push tag
|
||||
if: ${{ github.ref_name == 'latest' }}
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
WITH_V: true
|
||||
RELEASE_BRANCHES: latest
|
||||
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
# Convert IMAGE_TAG to lowercase
|
||||
IMAGE_TAG=$(echo $IMAGE_TAG | tr '[:upper:]' '[:lower:]')
|
||||
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
|
||||
|
||||
|
||||
# Convert repository owner to lowercase
|
||||
REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')
|
||||
echo "REPO_OWNER=$REPO_OWNER" >> $GITHUB_ENV
|
||||
|
||||
4
.github/workflows/delete.yml
vendored
4
.github/workflows/delete.yml
vendored
@@ -3,7 +3,7 @@ name: Branch Delete
|
||||
on:
|
||||
delete:
|
||||
branches:
|
||||
- '*'
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
delete-branch-docker-image:
|
||||
@@ -30,4 +30,4 @@ jobs:
|
||||
uses: dataaxiom/ghcr-cleanup-action@v1.0.8
|
||||
with:
|
||||
delete-tags: ${{ env.BRANCH_NAME }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -66,3 +66,7 @@ download_clients:
|
||||
# username: xxxx # (optional -> if not provided, assuming not needed)
|
||||
# password: xxxx # (optional -> if not provided, assuming not needed)
|
||||
# name: "qBittorrent" # (optional -> if not provided, assuming "qBittorrent". Must correspond with what is specified in your *arr as download client name)
|
||||
# sabnzbd:
|
||||
# - base_url: "http://sabnzbd:8080" # SABnzbd server URL
|
||||
# api_key: "your_api_key_here" # (required -> SABnzbd API key)
|
||||
# # name: "SABnzbd" # (optional -> if not provided, assuming "SABnzbd". Must correspond with what is specified in your *arr as download client name)
|
||||
|
||||
2
main.py
2
main.py
@@ -44,7 +44,7 @@ async def main():
|
||||
while True:
|
||||
logger.info("-" * 50)
|
||||
|
||||
# Refresh qBit Cookies
|
||||
# Refresh qBit Cookies (SABnzbd doesn't need cookie refresh)
|
||||
for qbit in settings.download_clients.qbittorrent:
|
||||
await qbit.refresh_cookie()
|
||||
|
||||
|
||||
@@ -92,6 +92,17 @@ class JobManager:
|
||||
f">>> qBittorrent is disconnected. Skipping queue cleaning on {self.arr.name}.",
|
||||
)
|
||||
return False
|
||||
|
||||
for sabnzbd in self.settings.download_clients.sabnzbd:
|
||||
logger.debug(
|
||||
f"job_manager.py/_queue_has_items (Before any removal jobs): Checking if SABnzbd is connected"
|
||||
)
|
||||
# Check if any client is disconnected
|
||||
if not await sabnzbd.check_sabnzbd_connected():
|
||||
logger.warning(
|
||||
f">>> SABnzbd is disconnected. Skipping queue cleaning on {self.arr.name}.",
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
def _get_removal_jobs(self):
|
||||
|
||||
@@ -42,6 +42,11 @@ class RemoveBadFiles(RemovalJob):
|
||||
if download_client_type == "qbittorrent":
|
||||
client_items = await self._handle_qbit(download_client, download_ids)
|
||||
affected_items.extend(client_items)
|
||||
elif download_client_type == "sabnzbd":
|
||||
# SABnzbd doesn't support bad file removal in the same way as BitTorrent
|
||||
# Usenet doesn't have the concept of "availability" or individual file selection
|
||||
logger.debug("remove_bad_files: Skipping SABnzbd downloads (not applicable for Usenet)")
|
||||
continue
|
||||
return affected_items
|
||||
|
||||
def _group_download_ids_by_client(self):
|
||||
|
||||
@@ -31,7 +31,7 @@ class RemoveSlow(RemovalJob):
|
||||
|
||||
# Is Usenet -> skip
|
||||
if self._is_usenet(item):
|
||||
continue # No need to check for speed for usenet, since there users pay for speed
|
||||
continue # No need to check for speed for usenet
|
||||
|
||||
# Completed but stuck -> skip
|
||||
if self._is_completed_but_stuck(item):
|
||||
@@ -103,7 +103,7 @@ class RemoveSlow(RemovalJob):
|
||||
|
||||
|
||||
async def _get_download_progress(self, item, download_id):
|
||||
# Grabs the progress from qbit if possible, else calculates it based on progress (imprecise)
|
||||
# Grabs the progress from qbit or SABnzbd if possible, else calculates it based on progress (imprecise)
|
||||
if item["download_client_type"] == "qbittorrent":
|
||||
try:
|
||||
progress = await item["download_client"].fetch_download_progress(download_id)
|
||||
@@ -111,6 +111,14 @@ class RemoveSlow(RemovalJob):
|
||||
return progress
|
||||
except Exception: # noqa: BLE001
|
||||
pass # fall back below
|
||||
elif item["download_client_type"] == "sabnzbd":
|
||||
try:
|
||||
progress = await item["download_client"].get_download_progress(download_id)
|
||||
if progress is not None:
|
||||
# SABnzbd returns percentage, convert to bytes
|
||||
return (progress / 100.0) * item["size"]
|
||||
except Exception: # noqa: BLE001
|
||||
pass # fall back below
|
||||
return item["size"] - item["sizeleft"]
|
||||
|
||||
def _compute_increment_and_speed(self, download_id, current_progress):
|
||||
@@ -135,6 +143,7 @@ class RemoveSlow(RemovalJob):
|
||||
if download_client.bandwidth_usage > DISABLE_OVER_BANDWIDTH_USAGE:
|
||||
self.strikes_handler.pause_entry(download_id, "High Bandwidth Usage")
|
||||
return True
|
||||
# SABnzbd: Users typically pay for their Usenet speed, so bandwidth checking isn't applicable
|
||||
|
||||
return False
|
||||
|
||||
@@ -157,4 +166,5 @@ class RemoveSlow(RemovalJob):
|
||||
continue
|
||||
if item["download_client_type"] == "qbittorrent":
|
||||
await download_client.set_bandwidth_usage()
|
||||
# SABnzbd doesn't need bandwidth usage tracking for the same reason as usenet
|
||||
processed_clients.add(item["download_client"])
|
||||
|
||||
@@ -35,6 +35,7 @@ class MinVersions:
|
||||
readarr = "0.4.15.2787"
|
||||
whisparr = "2.0.0.548"
|
||||
qbittorrent = "4.3.0"
|
||||
sabnzbd = "4.0.0"
|
||||
|
||||
|
||||
class FullQueueParameter:
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
from src.settings._config_as_yaml import get_config_as_yaml
|
||||
from src.settings._download_clients_qbit import QbitClients
|
||||
from src.settings._download_clients_sabnzbd import SabnzbdClients
|
||||
|
||||
DOWNLOAD_CLIENT_TYPES = ["qbittorrent"]
|
||||
DOWNLOAD_CLIENT_TYPES = ["qbittorrent", "sabnzbd"]
|
||||
|
||||
|
||||
class DownloadClients:
|
||||
"""Represents all download clients."""
|
||||
|
||||
qbittorrent = None
|
||||
sabnzbd = None
|
||||
|
||||
def __init__(self, config, settings):
|
||||
self._set_qbit_clients(config, settings)
|
||||
self._set_sabnzbd_clients(config, settings)
|
||||
self.check_unique_download_client_types()
|
||||
|
||||
def _set_qbit_clients(self, config, settings):
|
||||
@@ -28,11 +31,18 @@ class DownloadClients:
|
||||
]:
|
||||
setattr(settings.general, key, None)
|
||||
|
||||
def _set_sabnzbd_clients(self, config, settings):
|
||||
download_clients = config.get("download_clients", {})
|
||||
if isinstance(download_clients, dict):
|
||||
self.sabnzbd = SabnzbdClients(config, settings)
|
||||
if not self.sabnzbd:
|
||||
self.sabnzbd = SabnzbdClients({}, settings) # Initialize empty list
|
||||
|
||||
def config_as_yaml(self):
|
||||
"""Log all download clients."""
|
||||
return get_config_as_yaml(
|
||||
{"qbittorrent": self.qbittorrent},
|
||||
sensitive_attributes={"username", "password", "cookie"},
|
||||
{"qbittorrent": self.qbittorrent, "sabnzbd": self.sabnzbd},
|
||||
sensitive_attributes={"username", "password", "cookie", "api_key"},
|
||||
internal_attributes={"api_url", "cookie", "settings", "min_version"},
|
||||
hide_internal_attr=True,
|
||||
)
|
||||
@@ -101,7 +111,7 @@ class DownloadClients:
|
||||
"""
|
||||
mapping = {
|
||||
"QBittorrent": "qbittorrent",
|
||||
# Only qbit configured for now
|
||||
"SABnzbd": "sabnzbd",
|
||||
}
|
||||
download_client_type = mapping.get(arr_download_client_implementation)
|
||||
return download_client_type
|
||||
|
||||
304
src/settings/_download_clients_sabnzbd.py
Normal file
304
src/settings/_download_clients_sabnzbd.py
Normal file
@@ -0,0 +1,304 @@
|
||||
from packaging import version
|
||||
|
||||
from src.settings._constants import MinVersions
|
||||
from src.utils.common import make_request, wait_and_exit, extract_json_from_response
|
||||
from src.utils.log_setup import logger
|
||||
|
||||
|
||||
class SabnzbdError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SabnzbdClients(list):
|
||||
"""Represents all SABnzbd clients."""
|
||||
|
||||
def __init__(self, config, settings):
|
||||
super().__init__()
|
||||
self._set_sabnzbd_clients(config, settings)
|
||||
|
||||
def _set_sabnzbd_clients(self, config, settings):
|
||||
sabnzbd_config = config.get("download_clients", {}).get("sabnzbd", [])
|
||||
|
||||
if not isinstance(sabnzbd_config, list):
|
||||
logger.error(
|
||||
"Invalid config format for sabnzbd clients. Expected a list.",
|
||||
)
|
||||
return
|
||||
|
||||
for client_config in sabnzbd_config:
|
||||
try:
|
||||
self.append(SabnzbdClient(settings, **client_config))
|
||||
except (TypeError, ValueError) as e:
|
||||
logger.error(f"Error parsing sabnzbd client config: {e}")
|
||||
|
||||
|
||||
class SabnzbdClient:
|
||||
"""Represents a single SABnzbd client."""
|
||||
|
||||
version: str = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
settings,
|
||||
base_url: str = None,
|
||||
api_key: str = None,
|
||||
name: str = None,
|
||||
):
|
||||
self.settings = settings
|
||||
if not base_url:
|
||||
logger.error("Skipping SABnzbd client entry: 'base_url' is required.")
|
||||
error = "SABnzbd client must have a 'base_url'."
|
||||
raise ValueError(error)
|
||||
|
||||
if not api_key:
|
||||
logger.error("Skipping SABnzbd client entry: 'api_key' is required.")
|
||||
error = "SABnzbd client must have an 'api_key'."
|
||||
raise ValueError(error)
|
||||
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.api_url = self.base_url + "/api"
|
||||
self.min_version = MinVersions.sabnzbd
|
||||
self.api_key = api_key
|
||||
self.name = name
|
||||
if not self.name:
|
||||
logger.verbose(
|
||||
"No name provided for sabnzbd client, assuming 'SABnzbd'. If the name used in your *arr is different, please correct either the name in your *arr, or set the name in your config"
|
||||
)
|
||||
self.name = "SABnzbd"
|
||||
|
||||
self._remove_none_attributes()
|
||||
|
||||
def _remove_none_attributes(self):
|
||||
"""Remove attributes that are None to keep the object clean."""
|
||||
for attr in list(vars(self)):
|
||||
if getattr(self, attr) is None:
|
||||
delattr(self, attr)
|
||||
|
||||
async def fetch_version(self):
|
||||
"""Fetch the current SABnzbd version."""
|
||||
logger.debug("_download_clients_sabnzbd.py/fetch_version: Getting SABnzbd Version")
|
||||
params = {
|
||||
"mode": "version",
|
||||
"apikey": self.api_key,
|
||||
"output": "json"
|
||||
}
|
||||
response = await make_request(
|
||||
"get", self.api_url, self.settings, params=params
|
||||
)
|
||||
response_data = response.json()
|
||||
self.version = response_data.get("version", "unknown")
|
||||
logger.debug(
|
||||
f"_download_clients_sabnzbd.py/fetch_version: SABnzbd version={self.version}"
|
||||
)
|
||||
|
||||
async def validate_version(self):
|
||||
"""Check if the SABnzbd version meets minimum requirements."""
|
||||
min_version = self.settings.min_versions.sabnzbd
|
||||
|
||||
if version.parse(self.version) < version.parse(min_version):
|
||||
logger.error(
|
||||
f"Please update SABnzbd to at least version {min_version}. Current version: {self.version}",
|
||||
)
|
||||
error = f"SABnzbd version {self.version} is too old. Please update."
|
||||
raise SabnzbdError(error)
|
||||
|
||||
async def check_sabnzbd_reachability(self):
|
||||
"""Check if the SABnzbd URL is reachable."""
|
||||
try:
|
||||
logger.debug(
|
||||
"_download_clients_sabnzbd.py/check_sabnzbd_reachability: Checking if SABnzbd is reachable"
|
||||
)
|
||||
params = {
|
||||
"mode": "version",
|
||||
"apikey": self.api_key,
|
||||
"output": "json"
|
||||
}
|
||||
await make_request(
|
||||
"get",
|
||||
self.api_url,
|
||||
self.settings,
|
||||
params=params,
|
||||
log_error=False,
|
||||
ignore_test_run=True,
|
||||
)
|
||||
|
||||
except Exception as e: # noqa: BLE001
|
||||
tip = "💡 Tip: Did you specify the URL and API key correctly?"
|
||||
logger.error(f"-- | SABnzbd\n❗️ {e}\n{tip}\n")
|
||||
wait_and_exit()
|
||||
|
||||
async def check_sabnzbd_connected(self):
|
||||
"""Check if SABnzbd is connected and operational."""
|
||||
logger.debug(
|
||||
"_download_clients_sabnzbd.py/check_sabnzbd_connected: Checking if SABnzbd is connected"
|
||||
)
|
||||
params = {
|
||||
"mode": "status",
|
||||
"apikey": self.api_key,
|
||||
"output": "json"
|
||||
}
|
||||
response = await make_request(
|
||||
"get",
|
||||
self.api_url,
|
||||
self.settings,
|
||||
params=params,
|
||||
)
|
||||
status_data = response.json()
|
||||
# SABnzbd doesn't have a direct "disconnected" status like qBittorrent
|
||||
# We check if we can get status successfully
|
||||
return "status" in status_data
|
||||
|
||||
async def setup(self):
|
||||
"""Perform the SABnzbd setup by calling relevant managers."""
|
||||
# Check reachability
|
||||
await self.check_sabnzbd_reachability()
|
||||
|
||||
try:
|
||||
# Fetch version and validate it
|
||||
await self.fetch_version()
|
||||
await self.validate_version()
|
||||
logger.info(f"OK | SABnzbd ({self.base_url})")
|
||||
except SabnzbdError as e:
|
||||
logger.error(f"SABnzbd version check failed: {e}")
|
||||
wait_and_exit() # Exit if version check fails
|
||||
|
||||
async def get_queue_items(self):
|
||||
"""Fetch queue items from SABnzbd."""
|
||||
logger.debug("_download_clients_sabnzbd.py/get_queue_items: Getting queue items")
|
||||
params = {
|
||||
"mode": "queue",
|
||||
"apikey": self.api_key,
|
||||
"output": "json"
|
||||
}
|
||||
response = await make_request(
|
||||
"get",
|
||||
self.api_url,
|
||||
self.settings,
|
||||
params=params,
|
||||
)
|
||||
queue_data = response.json()
|
||||
return queue_data.get("queue", {}).get("slots", [])
|
||||
|
||||
async def get_history_items(self):
|
||||
"""Fetch history items from SABnzbd."""
|
||||
logger.debug("_download_clients_sabnzbd.py/get_history_items: Getting history items")
|
||||
params = {
|
||||
"mode": "history",
|
||||
"apikey": self.api_key,
|
||||
"output": "json"
|
||||
}
|
||||
response = await make_request(
|
||||
"get",
|
||||
self.api_url,
|
||||
self.settings,
|
||||
params=params,
|
||||
)
|
||||
history_data = response.json()
|
||||
return history_data.get("history", {}).get("slots", [])
|
||||
|
||||
async def remove_download(self, nzo_id: str):
|
||||
"""Remove a download from SABnzbd queue."""
|
||||
logger.debug(f"_download_clients_sabnzbd.py/remove_download: Removing download {nzo_id}")
|
||||
params = {
|
||||
"mode": "queue",
|
||||
"name": "delete",
|
||||
"value": nzo_id,
|
||||
"apikey": self.api_key,
|
||||
"output": "json"
|
||||
}
|
||||
await make_request(
|
||||
"get",
|
||||
self.api_url,
|
||||
self.settings,
|
||||
params=params,
|
||||
)
|
||||
|
||||
async def pause_download(self, nzo_id: str):
|
||||
"""Pause a download in SABnzbd queue."""
|
||||
logger.debug(f"_download_clients_sabnzbd.py/pause_download: Pausing download {nzo_id}")
|
||||
params = {
|
||||
"mode": "queue",
|
||||
"name": "pause",
|
||||
"value": nzo_id,
|
||||
"apikey": self.api_key,
|
||||
"output": "json"
|
||||
}
|
||||
await make_request(
|
||||
"get",
|
||||
self.api_url,
|
||||
self.settings,
|
||||
params=params,
|
||||
)
|
||||
|
||||
async def resume_download(self, nzo_id: str):
|
||||
"""Resume a download in SABnzbd queue."""
|
||||
logger.debug(f"_download_clients_sabnzbd.py/resume_download: Resuming download {nzo_id}")
|
||||
params = {
|
||||
"mode": "queue",
|
||||
"name": "resume",
|
||||
"value": nzo_id,
|
||||
"apikey": self.api_key,
|
||||
"output": "json"
|
||||
}
|
||||
await make_request(
|
||||
"get",
|
||||
self.api_url,
|
||||
self.settings,
|
||||
params=params,
|
||||
)
|
||||
|
||||
async def retry_download(self, nzo_id: str):
|
||||
"""Retry a failed download from SABnzbd history."""
|
||||
logger.debug(f"_download_clients_sabnzbd.py/retry_download: Retrying download {nzo_id}")
|
||||
params = {
|
||||
"mode": "retry",
|
||||
"value": nzo_id,
|
||||
"apikey": self.api_key,
|
||||
"output": "json"
|
||||
}
|
||||
await make_request(
|
||||
"get",
|
||||
self.api_url,
|
||||
self.settings,
|
||||
params=params,
|
||||
)
|
||||
|
||||
async def get_download_progress(self, nzo_id: str):
|
||||
"""Get progress of a specific download."""
|
||||
queue_items = await self.get_queue_items()
|
||||
for item in queue_items:
|
||||
if item.get("nzo_id") == nzo_id:
|
||||
# Calculate progress: (size - remaining) / size * 100
|
||||
size_total = float(item.get("size", 0))
|
||||
size_left = float(item.get("sizeleft", 0))
|
||||
if size_total > 0:
|
||||
progress = ((size_total - size_left) / size_total) * 100
|
||||
return progress
|
||||
return None
|
||||
|
||||
async def get_download_speed(self, nzo_id: str = None):
|
||||
"""Get current download speed from SABnzbd status."""
|
||||
params = {
|
||||
"mode": "status",
|
||||
"apikey": self.api_key,
|
||||
"output": "json"
|
||||
}
|
||||
response = await make_request(
|
||||
"get",
|
||||
self.api_url,
|
||||
self.settings,
|
||||
params=params,
|
||||
)
|
||||
status_data = response.json()
|
||||
speed_str = status_data.get("status", {}).get("speed", "0 KB/s")
|
||||
|
||||
# Convert speed string to KB/s
|
||||
# SABnzbd returns speed like "1.2 MB/s", "500 KB/s", etc.
|
||||
if "MB/s" in speed_str:
|
||||
speed_value = float(speed_str.replace(" MB/s", ""))
|
||||
return speed_value * 1024 # Convert MB/s to KB/s
|
||||
elif "KB/s" in speed_str:
|
||||
speed_value = float(speed_str.replace(" KB/s", ""))
|
||||
return speed_value
|
||||
else:
|
||||
return 0.0
|
||||
@@ -62,6 +62,10 @@ async def launch_steps(settings):
|
||||
for qbit in settings.download_clients.qbittorrent:
|
||||
await qbit.setup()
|
||||
|
||||
# Check SABnzbd connections and versions
|
||||
for sabnzbd in settings.download_clients.sabnzbd:
|
||||
await sabnzbd.setup()
|
||||
|
||||
# Setup arrs (apply checks, and store information)
|
||||
settings.instances.check_any_arrs()
|
||||
for arr in settings.instances:
|
||||
|
||||
163
tests/settings/test_sabnzbd_clients.py
Normal file
163
tests/settings/test_sabnzbd_clients.py
Normal file
@@ -0,0 +1,163 @@
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock
|
||||
|
||||
from src.settings._download_clients_sabnzbd import SabnzbdClient, SabnzbdClients, SabnzbdError
|
||||
from src.settings.settings import Settings
|
||||
|
||||
|
||||
class TestSabnzbdClient:
|
||||
def test_init_minimal_config(self):
|
||||
"""Test SabnzbdClient initialization with minimal required config."""
|
||||
settings = Mock()
|
||||
settings.min_versions = Mock()
|
||||
settings.min_versions.sabnzbd = "4.0.0"
|
||||
|
||||
client = SabnzbdClient(
|
||||
settings=settings,
|
||||
base_url="http://sabnzbd:8080",
|
||||
api_key="test_api_key"
|
||||
)
|
||||
|
||||
assert client.base_url == "http://sabnzbd:8080"
|
||||
assert client.api_url == "http://sabnzbd:8080/api"
|
||||
assert client.api_key == "test_api_key"
|
||||
assert client.name == "SABnzbd"
|
||||
|
||||
def test_init_full_config(self):
|
||||
"""Test SabnzbdClient initialization with full config."""
|
||||
settings = Mock()
|
||||
settings.min_versions = Mock()
|
||||
settings.min_versions.sabnzbd = "4.0.0"
|
||||
|
||||
client = SabnzbdClient(
|
||||
settings=settings,
|
||||
base_url="http://sabnzbd:8080/",
|
||||
api_key="test_api_key",
|
||||
name="Custom SABnzbd"
|
||||
)
|
||||
|
||||
assert client.base_url == "http://sabnzbd:8080"
|
||||
assert client.api_url == "http://sabnzbd:8080/api"
|
||||
assert client.api_key == "test_api_key"
|
||||
assert client.name == "Custom SABnzbd"
|
||||
|
||||
def test_init_missing_base_url(self):
|
||||
"""Test SabnzbdClient initialization fails without base_url."""
|
||||
settings = Mock()
|
||||
|
||||
with pytest.raises(ValueError, match="SABnzbd client must have a 'base_url'"):
|
||||
SabnzbdClient(settings=settings, api_key="test_api_key")
|
||||
|
||||
def test_init_missing_api_key(self):
|
||||
"""Test SabnzbdClient initialization fails without api_key."""
|
||||
settings = Mock()
|
||||
|
||||
with pytest.raises(ValueError, match="SABnzbd client must have an 'api_key'"):
|
||||
SabnzbdClient(settings=settings, base_url="http://sabnzbd:8080")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_download_progress(self):
|
||||
"""Test getting download progress for a specific download."""
|
||||
settings = Mock()
|
||||
settings.min_versions = Mock()
|
||||
settings.min_versions.sabnzbd = "4.0.0"
|
||||
|
||||
client = SabnzbdClient(
|
||||
settings=settings,
|
||||
base_url="http://sabnzbd:8080",
|
||||
api_key="test_api_key"
|
||||
)
|
||||
|
||||
# Mock the get_queue_items method
|
||||
client.get_queue_items = AsyncMock(return_value=[
|
||||
{
|
||||
"nzo_id": "test_id_1",
|
||||
"size": "1000",
|
||||
"sizeleft": "200"
|
||||
},
|
||||
{
|
||||
"nzo_id": "test_id_2",
|
||||
"size": "2000",
|
||||
"sizeleft": "1000"
|
||||
}
|
||||
])
|
||||
|
||||
# Test getting progress for existing download
|
||||
progress = await client.get_download_progress("test_id_1")
|
||||
expected_progress = ((1000 - 200) / 1000) * 100 # 80%
|
||||
assert progress == expected_progress
|
||||
|
||||
# Test getting progress for non-existing download
|
||||
progress = await client.get_download_progress("non_existing_id")
|
||||
assert progress is None
|
||||
|
||||
|
||||
class TestSabnzbdClients:
|
||||
def test_init_empty_config(self):
|
||||
"""Test SabnzbdClients initialization with empty config."""
|
||||
config = {"download_clients": {}}
|
||||
settings = Mock()
|
||||
|
||||
clients = SabnzbdClients(config, settings)
|
||||
assert len(clients) == 0
|
||||
|
||||
def test_init_valid_config(self):
|
||||
"""Test SabnzbdClients initialization with valid config."""
|
||||
config = {
|
||||
"download_clients": {
|
||||
"sabnzbd": [
|
||||
{
|
||||
"base_url": "http://sabnzbd1:8080",
|
||||
"api_key": "api_key_1"
|
||||
},
|
||||
{
|
||||
"base_url": "http://sabnzbd2:8080",
|
||||
"api_key": "api_key_2",
|
||||
"name": "SABnzbd 2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
settings = Mock()
|
||||
settings.min_versions = Mock()
|
||||
settings.min_versions.sabnzbd = "4.0.0"
|
||||
|
||||
clients = SabnzbdClients(config, settings)
|
||||
assert len(clients) == 2
|
||||
assert clients[0].base_url == "http://sabnzbd1:8080"
|
||||
assert clients[0].api_key == "api_key_1"
|
||||
assert clients[0].name == "SABnzbd"
|
||||
assert clients[1].base_url == "http://sabnzbd2:8080"
|
||||
assert clients[1].api_key == "api_key_2"
|
||||
assert clients[1].name == "SABnzbd 2"
|
||||
|
||||
def test_init_invalid_config_format(self, caplog):
|
||||
"""Test SabnzbdClients initialization with invalid config format."""
|
||||
config = {
|
||||
"download_clients": {
|
||||
"sabnzbd": "not_a_list"
|
||||
}
|
||||
}
|
||||
settings = Mock()
|
||||
|
||||
clients = SabnzbdClients(config, settings)
|
||||
assert len(clients) == 0
|
||||
assert "Invalid config format for sabnzbd clients" in caplog.text
|
||||
|
||||
def test_init_missing_required_field(self, caplog):
|
||||
"""Test SabnzbdClients initialization with missing required fields."""
|
||||
config = {
|
||||
"download_clients": {
|
||||
"sabnzbd": [
|
||||
{
|
||||
"base_url": "http://sabnzbd:8080"
|
||||
# Missing api_key
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
settings = Mock()
|
||||
|
||||
clients = SabnzbdClients(config, settings)
|
||||
assert len(clients) == 0
|
||||
assert "Error parsing sabnzbd client config" in caplog.text
|
||||
Reference in New Issue
Block a user