Add torrent support: Torznab indexers + qBittorrent client - #551
Draft
andreasorbelli wants to merge 2 commits into
Draft
Add torrent support: Torznab indexers + qBittorrent client#551andreasorbelli wants to merge 2 commits into
andreasorbelli wants to merge 2 commits into
Conversation
Adds torrents as a fourth download source, alongside GetComics, Usenet
and DC++, using the same open protocol pattern the existing Newznab/
SABnzbd integration already uses:
- models/indexers/torznab_indexer.py: a Torznab indexer adapter
(IndexerType.TORZNAB), mirroring the Newznab adapter's t=caps/
t=search shape, plus parsing seeders/peers.
- models/download_clients/qbittorrent_client.py: a qBittorrent client
adapter (ClientType.QBITTORRENT, client_group="torrent"), using
qBittorrent's cookie-session auth. A submitted torrent's hash is
resolved via a unique `tags` value passed on submission, avoiding a
hand-rolled magnet/bencode hash parser.
- models/torrent.py: the orchestration module wiring search (via
enabled Torznab indexers), scoring (reusing the existing GetComics
scorer), submission, a crash-recovery job ledger + poller (mirroring
models/dcpp.py, since qBittorrent survives a CLU restart like
AirDC++), and the shared WATCH-folder import/notification hooks
already used by Usenet and DC++.
- A new torrent_jobs table (core/database.py), mirroring dcpp_jobs.
- Wiring into models/download_sources.py so the nightly sweep,
per-series "Check for Missing Issues", and the manual search modal
all pick up the new source automatically; hand-written
/api/torrent/{downloads,search,grab} routes mirroring the existing
Usenet/DC++ ones; config UI and manual-search-modal entries for the
new source.
- Bug fix bundled in: indexer searches now filter get_enabled_indexers()
by indexer_type, so a Torznab-configured indexer is never fed to the
Newznab/Usenet search and vice versa.
No new dependencies — qBittorrent's Web API is consumed with the
`requests` library already used throughout the codebase.
Full test coverage mirrors the existing Newznab/Usenet/AirDC++/DC++
test files: mocked adapter tests, DB integration tests, and route
tests.
Closes allaboutduncan#550
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JmSgyJ3Tqnf4TbVvk2fTaX
andreasorbelli
marked this pull request as draft
September 6, 2026 21:15
Contributor
Author
|
Marking this as a draft for now — I want to spend some time actually testing it end-to-end (real Torznab indexer + qBittorrent instance, a full search → grab → complete → import cycle, and a restart-mid-download recovery check) before I'm confident in it. If anyone wants to try it out in the meantime and give feedback, you're most welcome — I'd genuinely appreciate it. |
_login() required exactly HTTP 200 with body "Ok." for a successful login, per the WebAPI 4.1 doc this file links to. Tested against a real qBittorrent 5.2.3 (WebAPI 2.15.1) instance on the NAS: it answers 204 with an empty body on success and 401 on bad credentials instead, so every login -- valid credentials included -- was rejected as an HTTP error before the body/cookie was ever checked, breaking the entire grab/status pipeline for current qBittorrent versions. Accept both response shapes: 200 (+"Ok."/"Fails." body check) for older WebAPI, or 204/401 for 2.11+. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kd4eey159qpMoFvQVovSAw
Owner
|
I want to review this from a use case perspective and focus on file handling. Currently all files download to WATCH, get processed and moved to TARGET. This method could get you banned from some trackers because it would be considered leaching as you wouldn't be sharing the files you downloaded. I'll put some additional thought into this next week as well. |
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.
Summary
Adds torrents as a fourth download source (alongside GetComics, Usenet, and DC++), using the same open-protocol pattern the existing Newznab/SABnzbd integration already uses — no vendor-specific code, no Prowlarr integration. A Torznab indexer (the open torrent-indexer protocol; this is what an indexer manager like Prowlarr happens to expose per-tracker, but CLU only speaks the open protocol) is searched, scored with the existing GetComics scorer, and submitted to qBittorrent.
Closes #550.
What's new
models/indexers/torznab_indexer.py—IndexerType.TORZNABadapter, mirrorsnewznab_indexer.py'st=caps/t=searchshape, plus Torznab'sseeders/peersattributes.models/download_clients/qbittorrent_client.py—ClientType.QBITTORRENTadapter (client_group="torrent"), using qBittorrent's cookie-session auth. A submitted torrent's hash is resolved via a uniquetagsvalue passed on submission, rather than hand-rolling a magnet/bencode hash parser.models/torrent.py— the orchestration module: search via enabled Torznab indexers, score with the existingscore_getcomics_result/accept_result, submit, and a crash-recovery job ledger + poller mirroringmodels/dcpp.py(qBittorrent, like AirDC++, is a separate process that survives a CLU restart). Reuses the shared WATCH-folder import mover andnotify_download_terminalhook already used by Usenet and DC++.torrent_jobstable (core/database.py), identical shape todcpp_jobs.models/download_sources.py(KNOWN_SOURCES,get_external_sources()) so the nightly sweep, per-series "Check for Missing Issues", and the manual search modal all pick up the new source automatically./api/torrent/{downloads,search,grab}(+ dismiss) routes mirroring the existing Usenet/DC++ ones, since those aren't registry-derived.client_group/source-priority labels, and atorrententry in the shared search-modal JS (static/js/clu-source-search.js) showing seeders/peers alongside score.get_enabled_indexers()byindexer_type, so a Torznab-configured indexer is never fed to the Newznab/Usenet search and vice versa (this only became a real risk once a second indexer type exists).No new dependencies — qBittorrent's Web API is consumed with the
requestslibrary already used throughout the codebase.Testing
Mirrors the existing Newznab/Usenet/AirDC++/DC++ test files:
tests/mocked/test_torznab_indexer.py,tests/mocked/test_qbittorrent_client.py,tests/mocked/test_torrent.pytests/integration/test_database_clients.pyandtest_database_schema.py(torrent_jobs ledger CRUD,client_group="torrent"scoping,indexer_typeround-trip)tests/routes/test_download_clients_routes.pytests/unit/test_monitor.pymacOS tmp-path permission flake reproduces identically on a cleanmaincheckout and is untouched by this change)Verification steps
torrent_jobsledger and still imports on completion.🤖 Generated with Claude Code