Commit graph

2 commits

Author SHA1 Message Date
Tyler Richardson-LaPlume
b8384beef9 Fix: Usenet bundle stuck at 99%/100% — SAB reports post-processing in History as non-terminal (#721)
The earlier #721 fix tolerated a ~10s "completed but no save_path"
window, but the real production stall sits upstream of that: SABnzbd
removes a finished download from the queue and runs par2 verify /
repair / unpack *in History*, exposing the live stage in the slot
`status` ('Verifying' / 'Repairing' / 'Extracting' / 'Moving' / ...)
with `storage` empty until the final move. `_parse_history_slot` mapped
EVERY non-'Failed' status to 'completed', so a still-extracting 1.7 GB
FLAC album looked "completed with no save_path" the instant download hit
100%. The poll burned its completed-no-path budget mid-PP and bailed,
freezing the UI on the last download emit (the stuck-at-99%/100%
signature). SAB then finished fine — which is why the job shows
Completed in History but SoulSync never staged it.

Root fix
- `_parse_history_slot` routes `status` through `_map_state`, so PP
  stages stay NON-terminal: the poll keeps waiting (as 'downloading')
  for as long as post-processing takes and only a real 'Completed'
  flips to terminal success. `save_path` is trusted only on true
  completion (mid-PP path fields may point at the incomplete dir).

Supporting / defensive
- `UsenetStatus.incomplete_path`: surfaced separately from save_path
  (SAB `incomplete_path`) and used by the poll loops as a LAST RESORT
  after the completed-no-path window, to recover the case where
  `storage` never lands but the files are physically on disk.
- `poll_album_download`: dedicated, configurable completed-no-path
  window (~120s via `download_source.album_bundle_completed_no_path_seconds`)
  decoupled from the ~10s transient-miss window; incomplete_path
  fallback; a 30s heartbeat log so the previously-silent poll loop is
  diagnosable.
- `usenet.py` `_download_thread`: per-track parity — it was erroring
  immediately on the first completed-no-path read.
- `album_bundle_dispatch.py` / `status.py` / `monitor.py`: use the
  project `get_logger` so download-flow logs land in app.log under the
  `soulsync.*` namespace (they were console-only before, which hid the
  `[Album Bundle] flow failed` line during triage).

Tests
- PP-history state mapping; end-to-end Hunky Dory PP regression
  (download -> Verifying/Extracting in History past both budgets ->
  Completed+storage -> success); completed-no-path window +
  incomplete_path fallback; per-track thread parity. ruff + compileall +
  pytest all green (the only local failures are environmental: missing
  tzdata + local tools/ffmpeg.exe, neither present on CI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 00:49:02 -04:00
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