soulsync/core/usenet_clients/__init__.py
Broque Thomas 7a3ce50f71 feat(usenet): add adapter layer for SABnzbd and NZBGet
Third commit in the torrent + usenet rollout. SoulSync now also
speaks the two big usenet downloaders through a sibling adapter
contract that mirrors the torrent adapter set. All three layers are
now stood up — Prowlarr finds releases, the torrent adapter and the
usenet adapter each know how to ship work to the underlying client.
A later commit wires Prowlarr search results through the adapters
and through the archive-extract-match pipeline.

- core/usenet_clients/base.py: UsenetClientAdapter Protocol +
  UsenetStatus dataclass. Uniform state set covers usenet-specific
  phases (queued / downloading / extracting / verifying / repairing /
  completed / failed / paused).
- core/usenet_clients/__init__.py: adapter_for_type factory +
  get_active_adapter that reads usenet_client.type each call.
- core/usenet_clients/sabnzbd.py: REST adapter. ?apikey=... auth,
  mode=addurl and mode=addfile (multipart) for add_nzb. Reads both
  the active queue and the recent history so completed / failed
  jobs surface in get_all. Parses SAB's HH:MM:SS ``timeleft`` into
  seconds.
- core/usenet_clients/nzbget.py: JSON-RPC adapter. HTTP Basic auth,
  ``append`` method for add_nzb (auto-detects URL vs base64 NZB),
  ``editqueue`` with GroupPause/GroupResume/GroupDelete/GroupFinalDelete
  for state changes. Reads NZBGet's 64-bit split size fields
  (FileSizeHi + FileSizeLo) preferentially over the legacy
  FileSizeMB aggregate.
- core/connection_test.py: 'usenet_client' branch picks the right
  adapter, runs check_connection, surfaces per-client error
  messages (different credentials needed).
- config/settings.py: usenet_client.{type, url, api_key, username,
  password, category} defaults + both api_key and password marked
  encrypted-at-rest.
- web_server.py: 'usenet_client' added to the /api/settings POST
  allow-list.
- webui/index.html: new Usenet Client panel on the Indexers &
  Downloaders tab. Type picker swaps the credential fields between
  API-key (SABnzbd) and username+password (NZBGet).
- webui/static/settings.js: load/save wiring, updateUsenetClientUI
  for the credential field swap, testUsenetClientConnection.
- webui/static/helper.js: WHATS_NEW + VERSION_MODAL_SECTIONS entry.
2026-05-20 15:17:22 -07:00

49 lines
1.5 KiB
Python

"""Usenet client adapters.
Each adapter wraps one Usenet downloader (SABnzbd, NZBGet) behind
the ``UsenetClientAdapter`` Protocol so the rest of SoulSync can
talk to whichever client the user picked through one uniform
surface.
The active adapter is selected at runtime by the
``usenet_client.type`` config key. See ``get_active_adapter()``
for the factory.
"""
from __future__ import annotations
from typing import Optional
from config.settings import config_manager
from core.usenet_clients.base import UsenetClientAdapter, UsenetStatus
from core.usenet_clients.nzbget import NZBGetAdapter
from core.usenet_clients.sabnzbd import SABnzbdAdapter
__all__ = [
"UsenetClientAdapter",
"UsenetStatus",
"SABnzbdAdapter",
"NZBGetAdapter",
"get_active_adapter",
"adapter_for_type",
]
def adapter_for_type(client_type: str) -> Optional[UsenetClientAdapter]:
"""Build a fresh adapter instance for the given client type string.
``None`` for unknown types."""
if client_type == "sabnzbd":
return SABnzbdAdapter()
if client_type == "nzbget":
return NZBGetAdapter()
return None
def get_active_adapter() -> Optional[UsenetClientAdapter]:
"""Return an adapter for whichever usenet client the user has
selected in Settings. Reads ``usenet_client.type`` each call."""
client_type = (config_manager.get('usenet_client.type', '') or '').strip().lower()
if not client_type:
return None
return adapter_for_type(client_type)