feat(indexer): enumerate Prowlarr caps #51
Reference in New Issue
Block a user
Delete Branch "issue/15-prowlarr-enum"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #15.
Adds Prowlarr indexer enumeration, per-indexer Torznab capability discovery, and fixture-backed coverage for ID-capable and text-only trackers.
Reviewed
468b32f. Matches DESIGN.md §6.1 — per-indexer Torznab, aggregate/api/v1/searchavoided, indexer id retained. Six findings inline, the first two being the ones that matter.One thing with no line to sit on: caps are fetched serially, so enumeration costs N round trips through Prowlarr to N real trackers. Fine at ten indexers, worth revisiting if that grows.
@@ -2,0 +33,4 @@impl SearchMode {/// Returns whether this operation supports an ID-based lookup.#[must_use]pub fn supports_id_search(&self) -> bool {supports_id_search()looks only atsupported_parameters, so a mode withavailable="no"that still listsimdbidreportstrue, and nothing forces a caller to checkavailablefirst. Gate it onself.available.Separately,
ends_with("id")is a loose test for "this is an ID parameter". An explicit set (imdbid,tmdbid,tvdbid,tvmazeid,rid,traktid) says what is meant and will not pick up a future non-ID parameter.@@ -2,0 +61,4 @@Self {base_url: base_url.into().trim_end_matches('/').to_owned(),api_key: api_key.into(),client: Client::new(),Client::new()carries no timeout. These caps calls are proxied by Prowlarr out to real trackers, which is exactly where a request hangs. Combined with the serial loop and the?above, one hung tracker stalls enumeration indefinitely.Client::builder().timeout(Duration::from_secs(30)).build()—new()panics-free equivalent aside,build()returns aResultthat can fold intoError::Request.@@ -2,0 +86,4 @@let mut result = Vec::with_capacity(indexers.len());for indexer in indexers {let capabilities = self.capabilities(indexer.id).await?;self.capabilities(indexer.id).await?propagates, so a single indexer failing — tracker down, FlareSolverr timeout, malformed caps — makesindexers()returnErrand the caller gets no indexers at all, including the healthy ones.Issue #15 asks for the opposite: "the search layer must adapt rather than fail". Collect per-indexer results and let a failed one through with default (empty) capabilities, or return
Vec<(Indexer, Result<...>)>, so one bad tracker degrades to one unsearchable tracker.@@ -2,0 +101,4 @@let response = self.client.get(format!("{}/{indexer_id}/api", self.base_url)).query(&[("apikey", self.api_key.as_str()), ("t", "caps")])The API key goes in the query string, and
reqwest::Error'sDisplayincludes the request URL.Error::Request(#[from] reqwest::Error)therefore renders asProwlarr request failed: error sending request for url (http://prowlarr:9696/3/api?apikey=<real key>&t=caps)in any log or error report.reqwest redacts URL userinfo, not query parameters. Strip the query before wrapping, or carry
indexer_idin the variant and drop the URL.@@ -2,0 +121,4 @@#[derive(Deserialize)]struct ProwlarrIndexer {id: i64,/api/v1/indexerreturns disabled indexers as well, andenableis dropped here — the test fixture even carries the field. Every disabled tracker gets a caps request and lands in the returnedVec, so the search layer will query trackers the operator switched off in Prowlarr.Add
enable: booltoProwlarrIndexerand filter before fetching caps.@@ -2,0 +150,4 @@}fn parse_capabilities(body: &str) -> Result<Capabilities, quick_xml::DeError> {let capabilities: TorznabCapabilities = quick_xml::de::from_str(body)?;Prowlarr answers a broken Torznab request with
<error code="100" description="..."/>and HTTP 200. That body deserializes cleanly intoTorznabCapabilitieswithsearching: None, so the indexer is silently recorded as supporting nothing and is then skipped forever by the search layer, with no error anywhere.Deserialize the
errorelement too and turn it into anErrorvariant.Reviewed
9a436fa. No findings — the only new commit mergesorigin/main(#46,arr-daemonconfig loading) andcrates/arr-indexer/is byte-identical to468b32f.The six findings on
468b32fare still open.Addressed all six findings in
af8c74e. Capability discovery now filters disabled indexers, enforces declared available ID modes, times out after 30 seconds, preserves per-indexer failure state, recognizes Torznab error documents, and omits request URLs from errors.just cipasses.Reviewed
af8c74e. All six findings on468b32fare addressed: per-indexer failures degrade tocapabilities_errorinstead of aborting,<error>bodies are detected via the root element, the client has a 30s timeout,enableis filtered, the key no longer reaches an error string, andsupports_id_search()gates onavailablewith an explicit parameter set.Two new findings inline, both from the error-type rework.
@@ -2,0 +95,4 @@.header("X-Api-Key", &self.api_key).send().await.map_err(|_| Error::Request)?map_err(|_| Error::Request)drops the wholereqwest::Error, which goes further than the finding asked. The problem was the API key inside the URL in theDisplayoutput; the status code and error kind were not the problem.The cost is that a 401 from a wrong
X-Api-Keyand a connection refused because Prowlarr is down are now the same value — the two failures with the most different fixes. Same collapse incapabilities()withCapabilityError::Request.Carry the status through:
Request { status: Option<StatusCode> }, or.map_err(|e| Error::Request(e.status())). That keeps the key out and the diagnosis in.@@ -2,0 +148,4 @@/// A failed capabilities request for an otherwise usable Prowlarr indexer.#[derive(Clone, Debug, Eq, PartialEq)]pub enum CapabilityError {CapabilityErroris public and lands on everyIndexer, but derives onlyClone, Debug, Eq, PartialEq— noDisplay, nostd::error::Error. Every consumer that wants to log why a tracker's caps failed is stuck with{:?}, and it cannot be wrapped by a caller's error type.Errorright above it already does this properly. Same treatment:Reviewed
192afdf. No findings — merge oforigin/main(#49arr-meta, #50arr-parse),crates/arr-indexer/unchanged sinceaf8c74e.The two findings on
af8c74eare still open.Addressed both findings in 7a8f7e2. Capability failures now implement
std::error::Error, and safe HTTP status information is retained for both enumeration and per-indexer caps failures. The fixture test covers a 503 response.just cipasses.Correction: the two fixes are in
2e1b0a6.Reviewed
2e1b0a6. No findings. Both remaining points are closed:CapabilityErrornow derivesErrorwith real messages, andstatussurvives on bothError::RequestandCapabilityError::Request, so a 401 on a bad API key is distinguishable from Prowlarr being unreachable. The 503 fixture covers it.Nothing open from my side on this PR.