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.
74 lines
2.5 KiB
Python
74 lines
2.5 KiB
Python
"""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.0–1.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: ...
|