fix(fetch): map media:thumbnail so YouTube items get thumbnails
All checks were successful
deploy / build-web (push) Successful in 1m31s
deploy / deploy-web (push) Successful in 3s
deploy / build-api (push) Successful in 6m39s
deploy / deploy-api (push) Successful in 12s

media:content-only mapping dropped media:thumbnail elements, but YouTube
(and podcast) feeds carry their only image there — media:content is the
video/audio itself. Map thumbnails as image/thumbnail media so the SPA's
existing image pick renders them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuVbgSbyfRBkd4XkEDHV4B
This commit is contained in:
2026-07-15 19:09:07 +03:00
parent 243b17e9b3
commit 1193c9aefc

View File

@@ -93,11 +93,9 @@ fn entry_to_feed_entry(entry: feed_rs::model::Entry) -> FeedEntry {
let tags = entry.categories.into_iter().map(|c| c.term).collect();
let published_at = entry.published.or(entry.updated);
let media = entry
.media
.into_iter()
.flat_map(|m| m.content)
.filter_map(|c| {
let mut media = Vec::new();
for m in entry.media {
media.extend(m.content.into_iter().filter_map(|c| {
c.url.map(|u| Media {
kind: c
.content_type
@@ -107,8 +105,16 @@ fn entry_to_feed_entry(entry: feed_rs::model::Entry) -> FeedEntry {
width: c.width,
height: c.height,
})
})
.collect();
}));
// media:thumbnail is the only image YouTube (and many podcast) feeds carry —
// their media:content is the video/audio itself.
media.extend(m.thumbnails.into_iter().map(|t| Media {
kind: "image/thumbnail".to_string(),
url: t.image.uri,
width: t.image.width,
height: t.image.height,
}));
}
FeedEntry {
external_id,
@@ -363,6 +369,40 @@ mod tests {
assert!(find_feed_link(html).is_none());
}
#[test]
fn youtube_media_thumbnail_becomes_image_media() {
// Shape of a YouTube channel feed entry: the media:group's media:content is the
// video itself; the only image is the media:thumbnail.
let atom = r#"<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:media="http://search.yahoo.com/mrss/">
<title>channel</title>
<entry>
<id>yt:video:abc123</id>
<title>a video</title>
<link rel="alternate" href="https://www.youtube.com/watch?v=abc123"/>
<media:group>
<media:title>a video</media:title>
<media:content url="https://www.youtube.com/v/abc123?version=3"
type="application/x-shockwave-flash" width="640" height="390"/>
<media:thumbnail url="https://i2.ytimg.com/vi/abc123/hqdefault.jpg"
width="480" height="360"/>
</media:group>
</entry>
</feed>"#;
let feed = feed_rs::parser::parse(atom.as_bytes()).unwrap();
let entry = feed.entries.into_iter().next().unwrap();
let fe = entry_to_feed_entry(entry);
let thumb = fe
.media
.iter()
.find(|m| m.kind.starts_with("image"))
.expect("thumbnail mapped as image media");
assert_eq!(thumb.url, "https://i2.ytimg.com/vi/abc123/hqdefault.jpg");
assert_eq!(thumb.width, Some(480));
assert_eq!(thumb.height, Some(360));
}
#[test]
fn parses_opml_with_nested_folders() {
let opml = r#"<?xml version="1.0"?>