diff --git a/crates/newsfeed-fetch/src/lib.rs b/crates/newsfeed-fetch/src/lib.rs index 857d49f..f140a8f 100644 --- a/crates/newsfeed-fetch/src/lib.rs +++ b/crates/newsfeed-fetch/src/lib.rs @@ -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#" + + channel + + yt:video:abc123 + a video + + + a video + + + + + "#; + 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#"