soulsync/core/usenet_clients/base.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

74 lines
2.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Usenet client adapter contract.
``UsenetClientAdapter`` mirrors ``TorrentClientAdapter`` in shape so
the download plugin layer can reuse the same dispatch pattern.
Differences from the torrent side:
- No magnet URI equivalent — usenet jobs are always ``.nzb`` files
or URLs that resolve to one.
- No seed/peer counts — usenet is a download-only protocol.
- Status values reflect usenet semantics: ``downloading`` /
``extracting`` / ``verifying`` / ``repairing`` / ``completed`` /
``failed`` / ``paused``.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Optional, Protocol, runtime_checkable
@dataclass
class UsenetStatus:
"""Adapter-uniform view of one usenet job.
Field semantics:
- ``state`` is one of: ``queued`` | ``downloading`` | ``extracting``
| ``verifying`` | ``repairing`` | ``completed`` | ``failed`` |
``paused``. Each adapter maps its native names to this set.
- ``progress`` is 0.01.0 across the entire job (download + par2 +
unpack), so a job stalled at the verify step still shows < 1.0.
"""
id: str # SAB nzo_id / NZBGet NZBID
name: str
state: str
progress: float
size: int # total size in bytes
downloaded: int # bytes downloaded so far
download_speed: int # bytes/sec
eta: Optional[int] = None # seconds, None if unknown
save_path: Optional[str] = None
category: Optional[str] = None
files: Optional[List[str]] = None
error: Optional[str] = None
@runtime_checkable
class UsenetClientAdapter(Protocol):
"""Structural contract every usenet-client adapter implements."""
def is_configured(self) -> bool: ...
async def check_connection(self) -> bool: ...
async def add_nzb(
self,
url_or_bytes,
category: str = "soulsync",
save_path: Optional[str] = None,
) -> Optional[str]:
"""Hand the usenet client either a ``.nzb`` HTTP URL (``str``)
or the raw payload (``bytes``). Returns the client-side job id
on success, ``None`` on failure."""
...
async def get_status(self, job_id: str) -> Optional[UsenetStatus]: ...
async def get_all(self) -> List[UsenetStatus]: ...
async def remove(self, job_id: str, delete_files: bool = False) -> bool: ...
async def pause(self, job_id: str) -> bool: ...
async def resume(self, job_id: str) -> bool: ...