Repository navigation
feat(indexer): send an indexer's grabs to a pinned download client (#3080) - #3283
Merged
Merged
Conversation
Storage only: migration 109 and the column plumbing in the indexer repo. Routing and validation follow in later commits. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com>
…3080) An indexer can be pinned to one download client. Every grab from it (scheduled, interactive, pending, retry) goes to that client, ahead of priority and media type routing. A disabled, missing or wrong protocol client fails the grab with a message naming the indexer and client, and never falls back. Deleting a client an indexer is pinned to is refused with 409 listing the indexers. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com>
#3080) Download client dropdown on the indexer forms listing only enabled clients of the indexer's protocol, a row warning while the pinned client is disabled, and the user guide, API, troubleshooting and changelog entries. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…nt pins (#3080) A grab or retry with no indexer id is now tied to a pinned indexer by the release URL's host, and refused when several indexers share the host and one is pinned. A scheduled search skips a pinned indexer whose client is down and still grabs from the others, records the failure once a day per book and indexer, and notifies with the indexer name only. Saving no longer trips over a pinned client that changed protocol unless the pin or type is being changed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com>
…ts fail to load (#3080) Also corrects the API, troubleshooting and user guide text about bulk retry, retry rows and who sees the client name, and lists the known limits. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com>
…in (#3080) A pending release refused by its pinned client stays pending and the next approved row is tried. The host check now runs whenever the caller named no indexer, refuses a contradicting supplied id when a pinned indexer owns the host, and compares host names ignoring case, a trailing dot and the port. An empty type in an update is no longer read as a change of type. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com>
Replaces the interacting heuristics (signing's pick, the host check and the caller id branch) with one pure function, resolveGrabPin, and a table test of the whole matrix. Signing can no longer override the decision, an unknown id is treated as no id, and the row records the decided indexer. An explicit empty type in an indexer update keeps the stored type. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com>
francisrath
added a commit
to francisrath/bindery
that referenced
this pull request
Oct 15, 2026
Resolves IndexersTab.tsx against the pinned download client feature (vavallee#3283): keeps its clients prop and protocol-change handler, and the label/id pairs from this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Francis Rath <fr@ncis.no>
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
Each indexer gets an optional Download client setting. Unset (the default) keeps today's routing unchanged. When set, every grab from that indexer (scheduled search, search page, Pending force grab, Retry download, bulk retry) goes to that client, and if the client cannot take it the grab fails with a message naming the indexer (and the client, for admins) and is never sent to another client. Closes #3080.
Implementation notes
109_indexer_download_client.sql) addsindexers.download_client_id, nullable,REFERENCES download_clients(id) ON DELETE RESTRICT. Existing rows stay NULL. Number check: origin/main tops out at 107, open PR fix(series): cover split edition parts listed in another series by the same author (#3324) #3239 holds 108, fix(importer): restore Skipped status on undoing library adoption (#2885) #2928 holds 098 and Feature/download client media eligibility #2633 holds 096 (both long taken on main). 109 was claimed by pushing the migration alone in the first commit. A test applies migrations 1 to 108 with the real routine, adds an indexer and a client, lets the real runner apply 109, and checks the rows read back unpinned and the foreign key is enforced.QueueHandler.clientForGrab(API) andScheduler.checkPin(auto grab), both callingDownloadClientRepo.ResolvePinnedClient. Unpinned indexers, or no indexer, still go throughselectClientandPickClientForMediaTypeuntouched.grab()selects the client after the signing and host match step.resolveGrabPin(internal/api/grab_pin.go) is a pure function of the caller's id, the release URL and the indexer list, and it runs before signing. Rules: (a) a supplied id that exists and is pinned always wins; (b) a supplied unpinned id is trusted when its host matches the URL, and refused when it contradicts the URL's host while a pinned indexer matches that host; (c) an id that matches no indexer is treated as no id; (d) with no usable id, the indexers on the URL's host decide: none gives no information (unpinned), exactly one is the release's indexer (this finds a recreated indexer and keyless indexers), several are refused when any is pinned. Signing only fills in what the decision left open and never overrides it; the row'sindexer_idis the decided indexer. The retry and the pending grab use the same path. A table test covers the whole matrix for keyed and keyless pinned indexers.prowlarragainstprowlarr:9696matches. Ignoring a differing port is deliberate: a spurious refusal is safer than a leak.*db.PinnedClientError(missing, disabled, wrong protocol). API answers 400 with codeindexer_client_unavailable, deliberately notno_download_client. An enabled but unreachable pinned client keeps the usual 502 with text added saying the indexer is pinned and no other client was tried.IndexerHandler): the client must exist (400). A wrong protocol is refused only when the request sets the pin or changes the indexer type, and a disabled client only when the pin is new or changed, so enabling, disabling or reprioritising an indexer still works while its client is down. Grab time fails loudly.DownloadClientRepo.Deleterefuses while any indexer is pinned, the handler answers 409 with anindexersarray. The foreign key is the backstop. Disabling a client is allowed.Scope decisions
RequireAdmin, existing route tests cover them); the new field is only in those responses. Pin failure messages name the client only for admins. History and notifications name the indexer only.Known limits
localhost,127.0.0.1and a DNS name),www.and other subdomains, scheme-less indexer URLs and punycode spellings. SupplyingindexerIdfixes it; the web app and the scheduler always send it.typekeeps the stored type.Checklist
docs/User-Guide-Wiki.md,docs/API.md,docs/Troubleshooting-Wiki.md,changelog.d/3080-per-indexer-download-client.md)Test plan
go build ./...go test -count=1 -p 2 ./internal/api/... ./internal/db/... ./internal/scheduler/... ./internal/indexer/... ./internal/migrate/... ./internal/prowlarr/... ./cmd/...make smokenot run🤖 Generated with Claude Code
https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9