Skip to content

feat(indexers): send an indexer's grabs to a chosen download client - #163

Open
bbutlerau wants to merge 2 commits into
pullboxapp:developfrom
bbutlerau:feature/indexer-download-client
Open

bbutlerau wants to merge 2 commits into
pullboxapp:developfrom
bbutlerau:feature/indexer-download-client

Conversation

@bbutlerau

@bbutlerau bbutlerau commented Oct 3, 2026 •

Copy link
Copy Markdown

Summary

Pullbox sends every torrent grab to the highest-priority torrent client, and every Usenet grab to the highest-priority Usenet client. There is no way to keep one indexer's releases in a particular client.

That matters for private trackers. A common setup is a public-tracker qBittorrent behind a VPN, plus a second qBittorrent outside it whose IP is registered with a private tracker and which follows that tracker's seeding rules. Today, releases from the private tracker land in whichever client has the higher priority.

This adds a per-indexer "Download client" setting, like Sonarr's. Leave it at the default and nothing changes; pick a client and every grab from that indexer goes there.

Related Issues

None filed.

Changes

  • indexer_configs.download_client_id: nullable foreign key to download_client_configs, ON DELETE SET NULL. Migration i6c7d8e9f012 adds it, and existing indexers stay unpinned.
  • DownloadService.send_to_client uses the indexer's pinned client when it has one, and the existing priority-based selection otherwise.
  • A pinned client is never swapped for another one. If it is disabled, or cannot take the release's protocol, the grab fails with an error that names the indexer and the client, instead of quietly sending a private-tracker torrent somewhere else.
  • Retries and status polling need no change, because they already follow download_client_config_id on the download record.
  • Indexer API: download_client_id on create, update and response. The client must exist and take the indexer's protocol: Torznab takes torrent clients, Newznab takes Usenet clients. Sending null clears the pin.
  • Prowlarr and Jackett sync don't touch the field, so a pin survives a re-sync.
  • More than one client of a type is now allowed (second commit). The add-client picker greyed out a type once one existed and the API rejected it, which made pinning useless for the main case: two qBittorrents. AirDC++ already allowed several. Status polling and completed-download path mapping now use the exact client recorded on the download (download_client_config_id) rather than the first client of that type, falling back to the type only for older rows without an ID. Otherwise the second qBittorrent's downloads would be polled and path-mapped through the first one.
  • Settings → Indexers: a "Download client" dropdown under Advanced Settings that lists only clients of the indexer's protocol. Indexer cards show a "Client: name" pill when an indexer is pinned.

Checklist

  • Tests pass locally. New: tests/api/test_indexer_download_client.py, repeat-type cases in tests/tasks/test_download_monitor_poll.py and test_download_post_processing_sources.py, the clients API tests (a second same-type client is now accepted), TestIndexerPinnedClient in tests/integration/test_download_clients_integration.py, a migration round-trip in tests/integration/test_migrations.py, and a settings render test in tests/ui/test_settings_shell_ui_routes.py; the new tests fail on develop without the change. Full non-browser suite: 14,826 passed; 13 failed, all needing tools my test container lacks (ps, Docker smoke, release tooling), none in code this PR touches.
  • Lint clean (ruff check src/ tests/)
  • Type check clean for the touched files (mypy --strict on download_service.py, api/v1/indexers.py, ui/settings_routes.py, models/indexer.py, schemas/indexer.py)
  • Format clean (ruff format --check src/ tests/ alembic/)
  • New code has test coverage
  • Documentation updated: the indexer settings docs on pullbox.app would need a line about the new dropdown
  • Commit messages use documented conventional prefixes
  • CHANGELOG.md updated: left for release prep, per the contributing guide

Screenshots

The new field sits under Advanced Settings in the indexer dialog, below Priority. I haven't attached screenshots.

🤖 Generated with Claude Code

bbutlerau and others added 2 commits October 3, 2026 11:30
Each indexer can now name the download client its grabs go to, like
Sonarr's per-indexer client setting. This lets a private tracker use its
own torrent client (for example one outside a VPN, with the tracker's
seeding rules) while every other indexer keeps using the highest-priority
client for its protocol.

- indexer_configs.download_client_id, nullable, ON DELETE SET NULL, so
  deleting a client returns its indexers to the default selection.
- The API validates the client exists and takes the indexer's protocol
  (Torznab -> torrent, Newznab -> Usenet).
- A pinned client is never swapped for another: if it is disabled or
  unavailable the grab fails with a message naming the indexer and client.
- Retries and status polling already follow the persisted client id.
- Prowlarr and Jackett sync leave the setting alone.
- Indexer settings gain a "Download client" dropdown under Advanced
  Settings, and indexer cards show the pinned client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E9gUFQEryh4gDJRKvjNUZM
Pinning an indexer to a client is only useful if a second client of the
same type can exist, such as a private-tracker qBittorrent next to the
default one. The add-client picker and API refused that.

- Drop the one-per-type check from the API and the picker. AirDC++
  already allowed several.
- Status polling resolves the exact client a download was sent to
  (download_client_config_id), falling back to the type only for older
  rows recorded without it. It used the first client of the type, which
  would poll the wrong qBittorrent.
- Completed-download path mapping and the cleanup boundary use that
  same client's Remote Path and Download Directory, so two clients can
  mount their downloads in different places.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E9gUFQEryh4gDJRKvjNUZM

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant