Add per-indexer Torznab search client #58
Reference in New Issue
Block a user
Delete Branch "issue/16-torznab"
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?
Summary
Issue
Closes #16
Test plan
just ci(64 tests passed after merging currentorigin/main)Reviewed
93149ce. Five findings on the code, all inline.Nothing calls
search_indexersyet, which is expected at this phase.@@ -0,0 +71,4 @@request: &SearchRequest,) -> Vec<IndexerSearch> {let mut searches = Vec::with_capacity(indexer_ids.len());for &indexer_id in indexer_ids {search_indexersloops sequentially. RSS sync (§6.2) hits every indexer on a ~10 minute cadence, and the client timeout is 30s, so a handful of hanging indexers serialises past that cadence — 20 indexers is a 10 minute worst case on its own.ProwlarrClientisClone, so atokio::task::JoinSetfan-out works without adding a dependency; sort the results back intoindexer_idsorder.@@ -0,0 +172,4 @@impl ReleaseBuilder {fn finish(self, indexer_id: i64) -> Option<SearchRelease> {let name = self.title.filter(|title| !title.is_empty())?;let download_url = self.enclosure_url.or(self.link);An item with a
guidbut no<enclosure>and no<link>survivesfinish()withdownload_url: None.arr_core::Release.download_urlis a non-optionalString, and a release with no URL can never be grabbed. Makedownload_urlrequired here the same waynameis, so the item is dropped at parse time rather than downstream.@@ -0,0 +190,4 @@}fn set_text(&mut self, field: Field, value: &str) {let value = value.trim();set_texttrims each chunk andappend_textjoins them with no separator, so any value split across events loses the whitespace at the split.<title>Movie <![CDATA[& More]]> 2026</title>yieldsMovie& More2026— quick-xml emits Text/CData/Text, each trimmed, then concatenated. Trim once infinish()instead of per chunk.@@ -0,0 +299,4 @@) -> Result<(), SearchError> {match element.local_name().as_ref() {b"attr" => {let name = attribute(reader, element, b"name")?;attribute()returnsErr(InvalidResponse)on a malformed attribute and the?here propagates out ofparse_releases, so one bad attribute anywhere in the feed discards every item from that indexer.The issue's requirement is isolation between indexers, which this meets, but a partial feed is more useful than none — treat an unreadable attribute as absent.
@@ -0,0 +483,4 @@.await;assert_eq!(searches.len(), 4);assert_eq!(searches[0].releases[0].indexer_id, 3);Nothing in the test asserts
name,guidordownload_url.The fixtures were built to exercise exactly those: a CDATA title (beyond-hd), a CDATA link carrying a token and an
<enclosure>/<link>pair that tests the precedence infinish()(torrentleech), and anisPermaLink="true"guid (iptorrents). None of it is checked, so the CDATA and enclosure-over-link paths are untested.Addressed all five findings in
0c766ca: concurrent per-indexer fan-out with stable ordering, required download URLs, whitespace-safe mixed text/CDATA parsing, tolerant optional attributes, and assertions for the recorded fixture fields.just cipasses all 94 tests. The earlier movie API failure was a stale localarr-dbartifact after merging migration0003; rebuilding that package fixed the workspace binary without a source change.Reviewed
3e2ed5d. No findings.All five from the previous pass are addressed in
0c766ca, and I checked the fixes rather than just the intent:JoinSetfan-out keeps the result order and maps a lost task toSearchError::Task.download_urlis required,guidfalls back to it, so an ungrabbable item is dropped at parse time.trim_textand trimming once incleaned()fixes the split-text case; the new test pinsMovie & More 2026.<link>now a details URL) makes enclosure-over-link precedence actually testable, and it is tested.attribute()skipping a malformed attribute is correct, not a silent truncation: quick-xml'sIterState::nextsetsState::Next(offset)onExpectedEq, so iteration resumes at the next attribute — which is what the<enclosure broken url=… />case in the new test proves.