From dd9c0881eef76364a253e00a6e406521bebbaa0f Mon Sep 17 00:00:00 2001 From: rays Date: Thu, 10 Sep 2026 18:01:25 +0000 Subject: [PATCH] Keep every enclosure on an item, and view without downloading The rss crate keeps at most one enclosure per item and, when a feed ships several, silently keeps the last -- so a two-file item lost its first file. enclosures_by_item reads them from the XML in document order, unescaping attributes so a URL's & survives. The row summarises the one you would act on and counts the rest; the pane below lists them all. Non-media enclosures gain a View link opening in a new tab: the publisher's URL, or the local copy once downloaded. A direct link, not a proxy, so the daemon does not become a fetch-anything relay. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh --- Cargo.lock | 16 ++++- Cargo.toml | 1 + PROGRESS.md | 29 ++++++++ README.md | 4 ++ src/feed.rs | 134 ++++++++++++++++++++++++++++++++---- tests/ui/app.spec.js | 21 +++++- tests/ui/fixtures/art2.jpg | Bin 0 -> 2022 bytes tests/ui/fixtures/ep2.mp3 | Bin 0 -> 40000 bytes tests/ui/fixtures/multi.xml | 9 +++ tests/ui/global-setup.js | 4 ++ web/index.html | 15 +++- 11 files changed, 212 insertions(+), 21 deletions(-) create mode 100644 tests/ui/fixtures/art2.jpg create mode 100644 tests/ui/fixtures/ep2.mp3 create mode 100644 tests/ui/fixtures/multi.xml diff --git a/Cargo.lock b/Cargo.lock index fd9d8e8..da92361 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,7 +187,7 @@ dependencies = [ "chrono", "derive_builder", "diligent-date-parser", - "quick-xml", + "quick-xml 0.41.0", ] [[package]] @@ -1568,6 +1568,7 @@ dependencies = [ "librqbit", "opml", "percent-encoding", + "quick-xml 0.42.0", "reqwest", "rss", "rusqlite", @@ -2037,7 +2038,7 @@ dependencies = [ "httparse", "librqbit-dualstack-sockets", "network-interface", - "quick-xml", + "quick-xml 0.41.0", "reqwest", "serde", "serde_derive", @@ -2537,6 +2538,15 @@ dependencies = [ "serde", ] +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.11" @@ -2858,7 +2868,7 @@ checksum = "f505d3e5e7b06b4dc0245b13294f8ef9a1a0f70284708be1e11c5b7b7441034e" dependencies = [ "atom_syndication", "derive_builder", - "quick-xml", + "quick-xml 0.41.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e1bb06c..c98620e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ infer = "0.22.0" librqbit = { version = "9.0.1", default-features = false, features = ["rust-tls", "http-api-client"] } opml = "1.1.6" percent-encoding = "2.3.2" +quick-xml = "0.42.0" reqwest = { version = "0.13.5", default-features = false, features = ["rustls", "http2", "gzip", "stream", "json", "charset", "system-proxy"] } rss = "2.1.1" rusqlite = { version = "0.40.2", features = ["bundled"] } diff --git a/PROGRESS.md b/PROGRESS.md index a917748..0273a5b 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -56,6 +56,35 @@ and until now nothing set them. --- +## 2026-09-10 — Multiple enclosures per item, and viewing without downloading + +**View without downloading.** A non-audio/video enclosure now carries a View link opening in a new +tab: the publisher's own URL when nothing is downloaded, the local copy at `/media/` when it is. +Deliberately a direct link rather than a proxy — relaying arbitrary URLs through the daemon would +make it a fetch-anything service. + +**Multiple enclosures.** Probed before assuming, and the result was worse than expected: the `rss` +crate models an item as having at most one enclosure (which is what RSS 2.0 says) and when a feed +carries several it silently keeps **the last**, dropping the rest. So a two-file item lost its first +file entirely. + +`enclosures_by_item()` reads them straight from the XML with quick-xml, in document order, +unescaping attribute values — a feed URL's `&` arrives as `&`, so skipping that would corrupt +every query string. It falls back to the parsed enclosure if the scan and the parser disagree on +item count. Atom already collected all `rel="enclosure"` links. The row now summarises the enclosure +you would act on (playable, else downloaded, else first) and says "+N more files"; the pane below +lists them all. + +**A real limitation surfaced by a broken fixture.** Two browser tests failed with zero enclosures in +the detail pane. Not the new scanner — verified by running it against the fixture files directly, +which was right every time. The cause: my fixtures pointed two feeds at the *same* enclosure URL, +and `enclosures.url` is UNIQUE across the whole database, so whichever feed is scanned first claims +it and the other's entry gets nothing. That is the dedupe working as designed, but it means **two +feeds legitimately sharing a media URL will only ever show it under one of them** — worth knowing, +and worth revisiting if a network feed and a show feed ever overlap. + +--- + ## 2026-09-10 — A file is not the same as a playable file Reported: "Abort Retry Fail still shows downloaded and audio playback UI". The media-type filter diff --git a/README.md b/README.md index 9809172..2289550 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,10 @@ its enclosures in the pane below, which is where you play, download or delete th between the two panes drags and the position is remembered. An OPML subscription's page instead lists the feeds inside it. +An item may carry several enclosures. All of them appear in the pane below; the row summarises the +one you would act on and notes how many others there are. Anything that is not audio or video gets a +View link — opening the publisher's copy, or the local one once downloaded — rather than a player. + ## Log view The **Log** button in the sidebar shows the running daemon's output live, in four tabs: diff --git a/src/feed.rs b/src/feed.rs index 0d001ad..f65cc4e 100644 --- a/src/feed.rs +++ b/src/feed.rs @@ -32,7 +32,7 @@ pub struct Entry { pub enclosures: Vec, } -#[derive(Debug, Default, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct Enclosure { pub url: String, pub mime: Option, @@ -120,7 +120,7 @@ pub fn opml_title(bytes: &[u8]) -> Option { /// RSS first, then Atom -- the same split the original made on `parsedFeed.version`. pub fn parse(bytes: &[u8]) -> Result { match rss::Channel::read_from(bytes) { - Ok(ch) => Ok(from_rss(ch)), + Ok(ch) => Ok(from_rss(ch, bytes)), Err(rss_err) => match atom_syndication::Feed::read_from(bytes) { Ok(feed) => Ok(from_atom(feed)), Err(atom_err) => Err(anyhow!("not RSS ({rss_err}) and not Atom ({atom_err})")), @@ -128,7 +128,78 @@ pub fn parse(bytes: &[u8]) -> Result { } } -fn from_rss(ch: rss::Channel) -> ParsedFeed { +/// Every `` of every ``, in document order. +/// +/// The `rss` crate models an item as having at most one enclosure -- which is what RSS 2.0 +/// says -- and when a feed carries several it keeps only the *last*, silently losing the +/// rest. Feeds do ship several, so read them from the XML directly. +fn enclosures_by_item(bytes: &[u8]) -> Vec> { + use quick_xml::events::Event; + + let mut reader = quick_xml::Reader::from_reader(bytes); + reader.config_mut().trim_text(true); + let mut buf = Vec::new(); + let mut out: Vec> = Vec::new(); + let mut current: Option> = None; + + let read_enclosure = |e: &quick_xml::events::BytesStart| -> Option { + let (mut url, mut mime, mut length) = (String::new(), None, None); + for attr in e.attributes().flatten() { + // Values arrive escaped: a feed URL's "&" is "&" in the document. + let val = quick_xml::escape::unescape(&attr.value) + .map(|v| v.trim().to_string()) + .unwrap_or_default(); + match attr.key.local_name().as_ref() { + "url" => url = val, + "type" => mime = Some(val).filter(|v| !v.is_empty()), + "length" => length = val.parse().ok(), + _ => {} + } + } + (!url.is_empty()).then_some(Enclosure { url, mime, length }) + }; + + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Start(e)) => match e.name().local_name().as_ref() { + "item" => current = Some(Vec::new()), + "enclosure" => { + if let (Some(list), Some(enc)) = (current.as_mut(), read_enclosure(&e)) { + list.push(enc); + } + } + _ => {} + }, + Ok(Event::Empty(e)) => match e.name().local_name().as_ref() { + // with no children still counts, so the indexes stay aligned. + "item" => out.push(Vec::new()), + "enclosure" => { + if let (Some(list), Some(enc)) = (current.as_mut(), read_enclosure(&e)) { + list.push(enc); + } + } + _ => {} + }, + Ok(Event::End(e)) => { + if e.name().local_name().as_ref() == "item" + && let Some(list) = current.take() + { + out.push(list); + } + } + Ok(Event::Eof) | Err(_) => break, + _ => {} + } + buf.clear(); + } + if let Some(list) = current.take() { + out.push(list); + } + out +} + +fn from_rss(ch: rss::Channel, bytes: &[u8]) -> ParsedFeed { + let per_item = enclosures_by_item(bytes); let explicit = ch .itunes_ext() .and_then(|it| it.explicit()) @@ -137,17 +208,21 @@ fn from_rss(ch: rss::Channel) -> ParsedFeed { let entries = ch .items() .iter() - .filter_map(|item| { - let enclosures: Vec = item - .enclosure() - .into_iter() - .map(|e| Enclosure { - url: e.url().trim().to_owned(), - mime: non_empty(Some(e.mime_type())), - length: e.length().parse().ok(), - }) - .filter(|e| !e.url.is_empty()) - .collect(); + .enumerate() + .filter_map(|(idx, item)| { + // Straight from the XML, so an item with several keeps all of them. Falls + // back to the parsed one if the scan and the parser disagree on item count. + let enclosures: Vec = per_item.get(idx).cloned().unwrap_or_else(|| { + item.enclosure() + .into_iter() + .map(|e| Enclosure { + url: e.url().trim().to_owned(), + mime: non_empty(Some(e.mime_type())), + length: e.length().parse().ok(), + }) + .filter(|e| !e.url.is_empty()) + .collect() + }); let guid = pick_guid( item.guid().map(|g| g.value()), @@ -442,6 +517,36 @@ mod tests { assert!(!is_opml(include_bytes!("../tests/data/atom.xml"))); } + #[test] + fn an_item_may_carry_several_enclosures() { + // The rss crate keeps only one per item -- the last -- so these come from the XML. + let xml = br#" + Mhttps://xd + Two filesm1 + + + + One filem2 + + Nonem3 + "#; + let f = parse(xml).unwrap(); + assert_eq!(f.entries.len(), 3); + + let two = &f.entries[0].enclosures; + assert_eq!(two.len(), 2, "both enclosures survive"); + assert_eq!( + two[0].url, "https://x/a.mp3?v=1&t=2", + "document order, and the escaped ampersand is decoded" + ); + assert_eq!(two[0].length, Some(111)); + assert_eq!(two[1].url, "https://x/b.mp4"); + assert_eq!(two[1].mime.as_deref(), Some("video/mp4")); + + assert_eq!(f.entries[1].enclosures.len(), 1); + assert_eq!(f.entries[2].enclosures.len(), 0, "an item may have none"); + } + #[test] fn durations_parse_from_seconds_or_a_clock() { assert_eq!(parse_duration("5649"), Some(5649)); @@ -464,3 +569,4 @@ mod tests { assert_eq!(pick_guid(None, None, None, None), None); } } + diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index d3e7e8e..b650f08 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -11,7 +11,7 @@ test('the page loads and lists the configured feeds', async ({ page }) => { // Regression: a ReferenceError in the script left the shell rendered and the sidebar // empty, with every handler below the error dead. Server-side checks all passed. // Three top-level feeds in the fixture config; the OPML's child is inside a closed folder. - await expect(page.locator('.feed')).toHaveCount(3, { timeout: 15_000 }); + await expect(page.locator('.feed')).toHaveCount(4, { timeout: 15_000 }); await expect(page.getByText('Test Show')).toBeVisible(); const errors = []; page.on('pageerror', e => errors.push(e.message)); @@ -99,8 +99,25 @@ test('a downloaded file that is not audio gets no player', async ({ page }) => { await expect(page.locator('#detail audio')).toHaveCount(0); await expect(page.locator('#detail .encbox')).toContainText('image'); await expect(page.locator('#detail .encbox')).toContainText('downloaded'); - // Still offered as a file, just not as an episode. + // Still offered as a file, just not as an episode: viewable and keepable. await expect(page.locator('#detail .btn', { hasText: 'Save' })).toBeVisible(); + const view = page.locator('#detail a', { hasText: 'View' }); + await expect(view).toHaveAttribute('target', '_blank'); + await expect(view).toHaveAttribute('rel', /noopener/); + await expect(view).toHaveAttribute('href', /\/media\/\d+/); +}); + +test('an item with several enclosures lists them all', async ({ page }) => { + await page.locator('.feed', { hasText: 'Multi Show' }).click(); + const row = page.locator('.ep', { hasText: 'Two Files' }); + await expect(row).toBeVisible({ timeout: 20_000 }); + // The row says there is more than one without listing them. + await expect(row).toContainText('+1 more file'); + + await row.click(); + // The pane below lists every one: the audio and the image. + await expect(page.locator('#detail .encbox')).toHaveCount(2); + await expect(page.locator('#detail .encbox').nth(1)).toContainText('image'); }); test('the filter tabs change what is listed', async ({ page }) => { diff --git a/tests/ui/fixtures/art2.jpg b/tests/ui/fixtures/art2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ad3c9efba69cae79a7c23337c2e337ae32e3d5ba GIT binary patch literal 2022 zcmV~7(UD#nqV!5PCmPfM|+5z+tBC(Li?;hgpU1f-wBh<_X!$%Fbp z6fup{$HF*1G-_HL4^)vjt0_w~`vFGns#Z9IJ0t7lNlWj@wYH+O=K*$E zZaj7(4MX<=VNEvzXadp{w^~e5Y$erlPM7=3&O3wgTbzZ=Z|iqa5rh;WNUyPdmL@+W zrf>^>kv0{8`T#CSR9m40c##4D92g6^76}waRynV8)Al=hT4bYsos1Jf0KI<0xm%%nd02;4H$TUfp+=l zopO;R^z_tbCC&n%XQVw4wqk$r89}cK&wt=%W7lL?N=&AaI3b}QH%Ol^OZ9Mq>IzK; z9=;tJfgDzTUwp$&-?isIzm`Z=K%Cy>`aUs`dETwuTZJreZz){zK51`xqW5VBV)jnZ z-rEQ;2;_PuUZKpah_O24<3<)8;!LaOR>c~en=yFEx|nYXM7Rm9|2IUlLp8$B9;TXi zJ2oirO8f0|Ne1s+tRPRD`%iwwzZ^BugotlbIr1D)&rf@#(>^ zYicujD)-D-RJej=$K+Gt0nu5Y1ShGH@wb22JG-g{TFIJpgb^ZJh-E*Ve$y&MYcsLq zA_%iLwjt#&7$Plcch%W{^Z-*G{R;QAN?t;!y^!o>=ANc9pDHct%T74{$uneyp=x1l z-78VoQ)HrMEiKd8VDk)}Lt28|A~zE~T@con4U~sc6&7M+gW=Y3C5|tn?~!Tw+}9{~ zq!i0A1V~wcOCOTZ_2R6)*|kJ|APx^KbLzur5y!nVkUbX-jzkRSMCFZi0%!tVbHk|w zyZR5=m~r7rn(7{~p;gb8JZ)-KYGSSM2G)*xT$NBnU9-Rbxf_Z8EwqV65D&ed-G2~= znAzSnYFuo{X~lGF52l)wGztLcBBPDn_{h`irjk}dVoU(1Iwg9RTen9Vu3e^4-Wobi zwlk%*2$d6_tvu`^hfis@C{F7TC(k$U#R8Up9#N8YNE6HDb|&fXO_Dcpwr`+1gbHoL z2gZT2UMKQD;-rt&CgP(nGHO$Qm8cD+RRG+-5f^AaAQ({(E9KXOu8eYY?`IIw1Mz~* zuJCTrFOIQQlr?o|rOlz}IdkR9$-*U~>b3eVD96r9ryk;IAzrZSxr=CAPIarU`f7S+ zbw}D;)`9WO?udlNx?#qfS)@w{VwZCJMORyF+uyBESfz;vp`ax==g?~_RE8~(-(JY>)Pu(<)r9+Yb@J2*QqnhGM$F6E&M!bUv{I)s#Jm!bRX)<)7ltWE z1UaA7W5~%z7&$@!6M9gZ)^(UOf=nSrEKeB#M?l8Zzq6Ey?s=Ekd=oZ-7n1E+n$!r_LwC>8?K zH0B7ESutouO9>s=?9+ZKf)-9?T`1GE@G$zlu5q4#$v`i$eb(;^S%u1$p6 z6mTuY6HGa7%lqiQEjI5RHSveo4PSkob&M9{!ICDAR2hb3TQtv3jT61#n}eourRQDR zjU!H?N11~IEipC`;v_}bL^{0FFk2ip^#$fF=;U{U7PQM}bxoU)VF70$8sjf#0fjoM zU{iL}<@!OYHW?I0@ru*#xu_hkFRFYxxd)?ZGy3-v@cSHi_TB1FNB-}q=tq+=>w5w~ zHOuG$&aizaz2sHdI$|7k@P;09K{3(;=;d$1=vhC#93I-UL{%N+3*`^(IzV~dQnvVA zEEDfKLWGFlHNZ8PVr*P(ALqp%HNopsjC?b6I?S?Ru=s2sMs7o4a)WRMI^PZ*t@*$M zS=`umOb|d`@7~2ySuDKV5{3%j6dHV%;u3*}SXY)$v^_l7SZ_SQ7^0aaR6W<99cYC~5 zmXm)YJa*~|c_x(lH~Ut}kC^hxOcTs#H=>VgcV6i=VjlXm{0bDdBZ{YWhq?QV)_lu8 z^R9h-$t-(HxJM>Sg-brhKTXFUKu5)%Hz^qxM(*8m<>|`;av^W51Rf|y;Ue8I1$Phs E*?7ItX#fBK literal 0 HcmV?d00001 diff --git a/tests/ui/fixtures/ep2.mp3 b/tests/ui/fixtures/ep2.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..3365e266095f1d80bf0d122c85b4c77eea16c19e GIT binary patch literal 40000 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

|B$dsT??#x*QT30Je}n)h7zEo3Dcl*TN5K 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 +Multi Showhttp://127.0.0.1:8792/ +An item with more than one file. +Two Filesmu-1 + Mon, 01 Sep 2026 10:00:00 +0000 + Audio and a picture. + + + \ No newline at end of file diff --git a/tests/ui/global-setup.js b/tests/ui/global-setup.js index d8048b0..c09e795 100644 --- a/tests/ui/global-setup.js +++ b/tests/ui/global-setup.js @@ -40,6 +40,10 @@ url = "http://127.0.0.1:8792/pics.xml" auto_download = true media_types = ["image"] +[feeds.multi-show] +url = "http://127.0.0.1:8792/multi.xml" +auto_download = true + [feeds.test-subscriptions] url = "http://127.0.0.1:8792/subs.opml" auto_download = false diff --git a/web/index.html b/web/index.html index 0681971..d2e5884 100644 --- a/web/index.html +++ b/web/index.html @@ -617,9 +617,13 @@ function renderEntries(){ } } function epEl(e){ - const enc=e.enclosures[0]; + // An item may carry several files. The row summarises the one you would act on -- + // the playable one, else anything already downloaded, else the first -- and says how + // many others there are; the pane below lists them all. + const enc=e.enclosures.find(isPlayable) || e.enclosures.find(x=>x.path) || e.enclosures[0]; const has=!!(enc&&enc.path); const playable=isPlayable(enc); + const others=e.enclosures.length-1; const el=document.createElement('div'); el.className='ep'+(e.read?' read':'')+(S.sel===e.guid?' sel':'')+ (player.guid===e.guid?' playing':''); @@ -638,6 +642,7 @@ function epEl(e){ ${enc?`${ has?'downloaded':(enc.state==='skipped'?kindOf(enc):esc(enc.state))}`:''} ${enc&&enc.length?`${mb(enc.length)}`:''} + ${others>0?`+${others} more file${others===1?'':'s'}`:''} ${e.flagged?'★ kept':''} ${enc&&!has?`

`:''} @@ -751,10 +756,11 @@ function showDetail(e){ function encBox(x){ const size=x.length?mb(x.length):''; if(x.path && !isPlayable(x)){ - // On disk, but not audio or video: offer the file, not a player. + // On disk, but not audio or video: view it, keep it, or remove it -- no player. return `
${esc(kindOf(x))} downloaded${size?' \u00b7 '+size:''} + View Save
`; @@ -767,10 +773,15 @@ function encBox(x){ `; } + // Nothing on disk. For an image or a PDF you usually just want to look at it, so link + // straight to the publisher's copy in a new tab -- no download, and nothing proxied + // through here, which would make ipx a fetch-anything relay. + const viewable = !isPlayable(x) && x.state !== 'pending'; return `
${x.state==='skipped'?kindOf(x):esc(x.state)} ${esc(kindOf(x))}${size?' \u00b7 '+size:''} ${x.last_error?`${esc(x.last_error)}`:''} + ${viewable?`View`:''}
`; }