Summary
CLU can currently acquire issues from GetComics, Usenet (SABnzbd/NZBGet via Newznab indexers), and DC++ (AirDC++). This adds torrents as a fourth source, using the same open, standard protocols the Usenet integration already uses — no vendor-specific code:
- A Torznab indexer adapter (the open torrent-indexer protocol; this is exactly what Prowlarr exposes per tracker, but the client only speaks the open protocol — same relationship the existing Newznab client already has with Prowlarr's Newznab endpoints).
- A qBittorrent download-client adapter to submit and track the results.
Why
Torznab/qBittorrent is the natural counterpart to the existing Newznab/SABnzbd+NZBGet Usenet path, and to DC++/AirDC++. Users who already run Prowlarr for torrent trackers get the same "configure an indexer + a client, then CLU finds and grabs wanted issues automatically" experience already available for Usenet.
Design
The existing indexer/download-client architecture is already built for this: indexers.indexer_type and download_clients.client_group are free-text columns (no schema change needed to add new values), and set_active_download_client/get_active_download_client already scope "single active client" per client_group, so a torrent client can be active alongside a Usenet client. The plan:
models/indexers/torznab_indexer.py — new IndexerType.TORZNAB adapter, mirrors newznab_indexer.py (t=caps connection test, t=search result parsing, plus Torznab's seeders/peers attrs).
models/download_clients/qbittorrent_client.py — new ClientType.QBITTORRENT adapter (client_group="torrent"), using qBittorrent's cookie-session auth and torrents/add/torrents/info API. A submitted torrent's hash is resolved via a unique tags value passed on submission (avoids hand-rolling bencode/magnet-hash parsing).
models/torrent.py — new orchestration module mirroring models/usenet.py (indexer-driven search + scoring) and models/dcpp.py (own crash-recovery job ledger + poller, since qBittorrent is a separate long-running daemon like AirDC++). Reuses the existing score_getcomics_result/accept_result scorer, the shared notify_download_terminal hook, and the shared WATCH-folder import mover already used by Usenet and DC++.
- A new
torrent_jobs table (mirrors dcpp_jobs) for crash recovery.
- Wiring into
models/download_sources.py (KNOWN_SOURCES, get_external_sources()) so the existing nightly sweep, per-series "Check for Missing Issues", and manual search modal all pick up the new source automatically, plus the small set of hand-written routes (/api/torrent/search, /grab, /downloads) that mirror the existing Usenet/DC++ ones.
- Bundles a small existing-code fix:
search_usenet_for_issue currently doesn't filter indexers by indexer_type, so once Torznab indexers exist they'd otherwise get fed to the NZB search too.
Full test coverage mirrors the existing Usenet/DC++/Newznab/AirDC++ test files (mocked adapter tests, DB integration tests, route tests).
Scope
New: Torznab indexer, qBittorrent client, models/torrent.py, torrent_jobs table, /api/torrent/* routes, config UI + manual-search-modal entries for the new source, tests.
Not in scope: any integration with Prowlarr's "Applications" indexer-push feature — this only consumes the open Torznab protocol, the same way the existing code consumes Newznab.
Summary
CLU can currently acquire issues from GetComics, Usenet (SABnzbd/NZBGet via Newznab indexers), and DC++ (AirDC++). This adds torrents as a fourth source, using the same open, standard protocols the Usenet integration already uses — no vendor-specific code:
Why
Torznab/qBittorrent is the natural counterpart to the existing Newznab/SABnzbd+NZBGet Usenet path, and to DC++/AirDC++. Users who already run Prowlarr for torrent trackers get the same "configure an indexer + a client, then CLU finds and grabs wanted issues automatically" experience already available for Usenet.
Design
The existing indexer/download-client architecture is already built for this:
indexers.indexer_typeanddownload_clients.client_groupare free-text columns (no schema change needed to add new values), andset_active_download_client/get_active_download_clientalready scope "single active client" perclient_group, so a torrent client can be active alongside a Usenet client. The plan:models/indexers/torznab_indexer.py— newIndexerType.TORZNABadapter, mirrorsnewznab_indexer.py(t=capsconnection test,t=searchresult parsing, plus Torznab'sseeders/peersattrs).models/download_clients/qbittorrent_client.py— newClientType.QBITTORRENTadapter (client_group="torrent"), using qBittorrent's cookie-session auth andtorrents/add/torrents/infoAPI. A submitted torrent's hash is resolved via a uniquetagsvalue passed on submission (avoids hand-rolling bencode/magnet-hash parsing).models/torrent.py— new orchestration module mirroringmodels/usenet.py(indexer-driven search + scoring) andmodels/dcpp.py(own crash-recovery job ledger + poller, since qBittorrent is a separate long-running daemon like AirDC++). Reuses the existingscore_getcomics_result/accept_resultscorer, the sharednotify_download_terminalhook, and the shared WATCH-folder import mover already used by Usenet and DC++.torrent_jobstable (mirrorsdcpp_jobs) for crash recovery.models/download_sources.py(KNOWN_SOURCES,get_external_sources()) so the existing nightly sweep, per-series "Check for Missing Issues", and manual search modal all pick up the new source automatically, plus the small set of hand-written routes (/api/torrent/search,/grab,/downloads) that mirror the existing Usenet/DC++ ones.search_usenet_for_issuecurrently doesn't filter indexers byindexer_type, so once Torznab indexers exist they'd otherwise get fed to the NZB search too.Full test coverage mirrors the existing Usenet/DC++/Newznab/AirDC++ test files (mocked adapter tests, DB integration tests, route tests).
Scope
New: Torznab indexer, qBittorrent client,
models/torrent.py,torrent_jobstable,/api/torrent/*routes, config UI + manual-search-modal entries for the new source, tests.Not in scope: any integration with Prowlarr's "Applications" indexer-push feature — this only consumes the open Torznab protocol, the same way the existing code consumes Newznab.