fix(fetch): map media:thumbnail so YouTube items get thumbnails
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:
@@ -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 tags = entry.categories.into_iter().map(|c| c.term).collect();
|
||||||
let published_at = entry.published.or(entry.updated);
|
let published_at = entry.published.or(entry.updated);
|
||||||
|
|
||||||
let media = entry
|
let mut media = Vec::new();
|
||||||
.media
|
for m in entry.media {
|
||||||
.into_iter()
|
media.extend(m.content.into_iter().filter_map(|c| {
|
||||||
.flat_map(|m| m.content)
|
|
||||||
.filter_map(|c| {
|
|
||||||
c.url.map(|u| Media {
|
c.url.map(|u| Media {
|
||||||
kind: c
|
kind: c
|
||||||
.content_type
|
.content_type
|
||||||
@@ -107,8 +105,16 @@ fn entry_to_feed_entry(entry: feed_rs::model::Entry) -> FeedEntry {
|
|||||||
width: c.width,
|
width: c.width,
|
||||||
height: c.height,
|
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 {
|
FeedEntry {
|
||||||
external_id,
|
external_id,
|
||||||
@@ -363,6 +369,40 @@ mod tests {
|
|||||||
assert!(find_feed_link(html).is_none());
|
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]
|
#[test]
|
||||||
fn parses_opml_with_nested_folders() {
|
fn parses_opml_with_nested_folders() {
|
||||||
let opml = r#"<?xml version="1.0"?>
|
let opml = r#"<?xml version="1.0"?>
|
||||||
|
|||||||
Reference in New Issue
Block a user