Add NzbDAV as an alternative Usenet provider#425
Open
Jauntiness wants to merge 7 commits into
Open
Conversation
Adds a second supported usenet backend (https://github.com/nzbdav-dev/nzbdav) alongside Decypharr. Selectable at runtime via a new "Provider" dropdown in the Usenet Provider settings. New files: - usenet/nzbdav_client.py (471 lines, mirrors DecypharrClient 1:1) - usenet/__init__.py (factory + display-name helper) Modified (small targeted diffs): - usenet/decypharr_client.py: factory delegates to NzbdavClient when provider == 'nzbdav' (DecypharrClient class itself unchanged). - routes/program_operation_routes.py: connectivity-check uses the client factory instead of a hardcoded /version URL. - routes/scraper_routes.py + magnet_routes.py: toasts use display name. - routes/web_server.py: context_processor injects usenet_provider_name. - utilities/settings_schema.py: schema description provider-neutral. - templates/base.html: injects window.USENET_PROVIDER_NAME global for JS. - templates/settings_tabs/required_tangerine.html: Provider dropdown, conditional fields (decypharr-only vs nzbdav-only), inline JS toggler. - templates/debrid_manager.html: Decypharr labels become provider-aware. - static/js/scraper.js: dialogs read window.USENET_PROVIDER_NAME. Backwards-compatible: installs without a 'provider' key default to 'decypharr' — zero behaviour change for existing users. All 11 existing call sites that import get_decypharr_client() keep working unchanged.
Previously, all grabs went to a single static SAB category (`download_folder`, default 'cli_debrid'). In a zurg-style Plex setup this folder is not a library location, so grabs stay invisible and cli_debrid loops items in Wanted / Checking. This commit adds a title-based heuristic in nzbdav_client.py that derives the SAB category from the release title before submission: Show + 1080p + H.264 -> shows_1080p_264 Show (else) -> shows Movie + 1080p + H.264 -> movies_1080p_264 Movie (else) -> movies Music markers -> music No match -> default_category (default '__unplayable__') Category names mirror zurg's filter-folder convention so the same Plex library locations can feed from both providers. Centralized in a single helper — covers all 11 add_nzb call sites (torrent_processor x3, repair_engine x2, debrid_manager_routes x2, magnet_routes x1, scraper_routes x3) including manual UI submits. No upstream code touched. The default fallback category renames from 'cli_debrid' to '__unplayable__' to mirror zurg's catch-all bucket name. Backward compatible: an explicit `category=` argument still wins; unmatched titles fall through to `self.default_category` (= identical behavior to before modulo the rename). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-contained docs for the nzbdav integration: config, interface differences vs DecypharrClient, the title-based category-routing heuristic, and optional companion setups (mergerfs union for robust file-detection, Plex Smart Collections for quality views, separate `_1080p` libraries). Includes a "For LLMs / AI agents" section so this can be set up or extended autonomously by AI assistants — patch architecture, code anchors, edge cases, smoke-test commands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…btitle/settings improvements - templates/_nzbdav_setup_helper.html: in-app setup helper — client-side generator for the NzbDAV docker-compose + rclone.conf (+ optional mergerfs union for multi-provider setups) + the cli-debrid config block, plus a live "test connection & ensure categories" tool. Included in the Usenet Provider settings and in onboarding step 2. The setup guide now points at this helper rather than the CLI. - routes/program_operation_routes.py: /api/nzbdav/check and /api/nzbdav/ensure_categories endpoints backing the helper. They use NzbDAV's get-config/update-config API to read and create the categories cli-debrid needs (missing categories otherwise loop grabs in "Wanted"); applied live, no restart. Reachable without the SAB key when DISABLE_FRONTEND_AUTH=true. - usenet/nzbdav_setup.py + nzbdav_migrate.py: optional host-side CLIs. The in-app helper covers the GUI path, but a script is still needed for things a container can't do: nzbdav_setup.py 'migrate-files' replays an existing Decypharr .nzb store into NzbDAV, plus headless 'wizard'/'generate'; nzbdav_migrate.py is a read-only preflight doctor (reachability, missing categories with --fix, mount visibility). stdlib-only, no app imports. - usenet/repair_engine.py + nzbdav_client.py + decypharr_client.py: route health/repair through the client interface so it works on NzbDAV (derives broken items from history status=Failed), scoped to owned categories so it never touches another SAB client's entries; fixes NzbDAV history-delete to use GET (it was a silent no-op via DELETE). - utilities/downsub.py + post_processing.py: storage-agnostic subtitles — write a sidecar where the media dir is writable (Symlinked/Local), otherwise upload via the Plex API keyed on ratingKey (works on read-only WebDAV/rclone mounts, no Plex Pass needed). - Settings clarity: relabel the Usenet "Download Folder" field to "Fallback Category (rarely used)" with an accurate explanation, and document Plex.mounted_file_location for multi-provider / union setups. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
a613d2f to
07a3089
Compare
Author
|
Pushed
Details in the commit message — feedback on the direction welcome. |
Moves stored downloads between usenet backends so users can switch without
re-searching — the .nzb is copied to the target, which re-fetches it from usenet.
HTTP/mounted-path only (cli-debrid has no Docker socket, so it can't docker-cp a
provider's nzb store):
- NzbDAV source/target: list mode=history (each slot has nzb_blob_id), fetch via
GET /api/download-nzb, submit via mode=addfile.
- Decypharr source: read .nzb files from Decypharr's nzb store mounted into
cli-debrid (the existing data_path bind); target: submit via POST /api/add.
- utilities/provider_transfer.py: engine — list/preview, read, submit, with a
throttled background job (target queue-depth cap), skip-already-present, and
progress/cancel. Both directions.
- routes/program_operation_routes.py: /api/provider_transfer/{config,list,start,
status,cancel}.
- templates/_provider_transfer.html: UI panel (direction, source/target, preview,
progress) with guardrail warnings; prefilled from the configured provider.
- templates/debug_functions.html: include the panel in the Library tab.
- templates/settings_tabs/required_tangerine.html: point the "Switching from
Decypharr?" note and the Decypharr Data Path help at the in-app migration and
explain the <data_path>/usenet/nzbs source path.
Best-effort by nature: Decypharr trims old .nzb files, and the target re-downloads
from usenet (subject to retention/connection limits).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
57c93ab to
aba2662
Compare
deletion_manager.py called get_decypharr_client() at both removal sites (delete_single_item and delete_multiple_items). After a Decypharr -> NzbDAV migration the removal silently no-ops — Decypharr is offline and the item's NZB id is in NzbDAV. - usenet/__init__.py: new USENET_ID_PREFIX + is_usenet_id() / usenet_raw_id() / remove_usenet_item(). One source of truth for the 'nzb:<id>' convention and a factory-routed remove that hits whichever provider is configured. - utilities/deletion_manager.py: both removal sites now call remove_usenet_item() instead of the Decypharr client directly. Passes location_basename as the fallback match key — it equals the provider's history entry name and survives migrations better than filled_by_file/title. - usenet/nzbdav_client.py: remove_nzb() refactored — try id-delete when the id exists in history, then fall back to exact-name match for migrated items whose stored id is an old Decypharr UUID. Adds _release_name_key, _id_in_history, _raw_delete_by_id, _delete_by_exact_name helpers and a filtered _history_slots(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- report webdav.enforce-readonly state in /api/nzbdav/check - new POST /api/nzbdav/set_delete_mode to flip it live (no restart) - setup helper: 'Enable delete-from-Plex' button + status line - docs: setup notes step 5b + README 'Delete usenet items natively' - README: reconcile local sections with upstream intro
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add NzbDAV as an alternative Usenet provider
Summary
Adds NzbDAV as a second supported
usenet backend alongside the existing Decypharr integration. Both can be
selected at runtime via a config key / settings UI dropdown.
Why
The current
DecypharrClientis tightly coupled to Decypharr's customendpoints (
POST /api/add,GET /api/browse/nzbs/…,DELETE /api/torrents).NzbDAV exposes a different (standard SABnzbd-style) API
(
GET /api?mode=addfile,?mode=queue,?mode=history, …) and serves filesvia WebDAV. Without a second client it cannot be wired into cli-debrid.
NzbDAV is a useful alternative because:
HealthCheckService(automatic NZB-availability repair)What's changed
New files
app/usenet/nzbdav_client.pyNzbdavClient— mirrorsDecypharrClient's public interface 1:1app/usenet/__init__.pyget_usenet_client()factory + provider-key readerModified files (minimal diffs)
app/usenet/decypharr_client.pyget_decypharr_client()delegates to NzbdavClient whenprovider == 'nzbdav'; class body untouchedapp/routes/program_operation_routes.pycheck_service_connectivity()/versionURL fetch replaced withclient.check_connectivity()so it works for any providerapp/templates/settings_tabs/required_tangerine.htmlapp/static/js/scraper.jsapp/templates/debrid_manager.htmlBackwards compatibility
providerkey inUsenet Providerconfigdefault to
decypharr→ zero behaviour change.from usenet.decypharr_client import get_decypharr_clientcall sites keep working — the function is just provider-aware now.
DecypharrClientclass itself is untouched.Configuration
Settings → Required → Usenet Provider:
http://x:8888http://x:3000api.keyapi.categoriesconfig)Provider-specific differences (documented in nzbdav_client.py)
POST /api/addwitharr+downloadFolderform fieldsPOST /api?mode=addfilewithcatquery parammode=addurlwith the same pre-fetch fallbackGET /api/browse/nzbs/<name>folder lookupmounted_file_location(nzbdav has no browse API)/api/repair/health/<n>/checktriggerHealthCheckServiceDELETE /api/torrents?hashes=<uuid>DELETE /api?mode=history&name=delete&value=<nzo_id>Testing
Tested locally against:
:devimageConnectivity check confirms with provider-aware label:
Follow-ups (not in this PR)
onboarding_step_2.html(initial setupwizard still hardcodes "Decypharr" wording)
connections.htmlprovider label display viaPROVIDER_NAMEget_decypharr_client()toget_usenet_client()for cleaner naming (purely cosmetic, no functional change)Closes / refs: (add issue link here)
Follow-up commits in this PR
Title-based category routing (commit 98c3a98)
The original commit routes every grab to a single static SAB category
(
download_folder, defaultcli_debrid). In a typical Plex setup thatfolder is not a library location, so grabs stay invisible and cli_debrid
loops items between
WantedandChecking.Added a title-based heuristic in
nzbdav_client.pythat derives the SABcategory from the release title before submission:
shows_1080p_264showsmovies_1080p_264moviesmusicdefault_category(default__unplayable__)Category names mirror zurg's filter-folder convention so the same Plex
library locations can feed from both providers. Centralized in a single
helper — covers all 11
add_nzbcall sites including manual UI submits.Backward-compatible: explicit
category=arg still wins; unmatched titlesfall to
default_category(= identical behavior to before, just renamedfrom
cli_debridto__unplayable__).Standalone docs (commit 2f669b7)
usenet/README.md— self-contained docs covering config, interfacedifferences vs DecypharrClient, the category-routing heuristic, and
optional companion setups (mergerfs union for direct file-detection,
Plex Smart Collections for quality views, separate
_1080pPlexlibraries). Includes a "For LLMs / AI agents" section with code
anchors and smoke-test commands so the integration can be set up or
extended autonomously by AI assistants.