use mediatype::{MediaTypeBuf, names};
use std::collections::HashSet;
use std::io::BufRead;
use crate::model::{Category, Content, Entry, Feed, FeedType, Generator, Image, Link, LinkTarget, MediaContent, MediaObject, MediaObjectSource, Person};
use crate::parser::itunes::{handle_itunes_channel_element, handle_itunes_item_element};
use crate::parser::podcast::{handle_podcast_channel_element, handle_podcast_item_element};
use crate::parser::util::{if_ok_then_some, if_some_then};
use crate::parser::{ParseErrorKind, ParseFeedError, ParseFeedResult};
use crate::parser::{Parser, atom};
use crate::parser::{common, util};
use crate::xml::{Element, NS};
#[cfg(test)]
mod tests;
pub(crate) fn parse<R: BufRead>(parser: &Parser, root: Element<R>) -> ParseFeedResult<Feed> {
let found_channel = root.children().find(|result| match result {
Ok(element) => &element.name == "channel",
Err(_) => true,
});
if let Some(channel) = found_channel {
handle_channel(parser, channel?)
} else {
Err(ParseFeedError::ParseError(ParseErrorKind::NoFeedRoot))
}
}
fn handle_channel<R: BufRead>(parser: &Parser, channel: Element<R>) -> ParseFeedResult<Feed> {
let mut feed = Feed::new(FeedType::RSS2);
for child in channel.children() {
let child = child?;
match child.ns_and_tag() {
(NS::RSS, "title") => feed.title = common::handle_text(child),
(NS::RSS, "link") => if_some_then(common::handle_link(None, child), |link| feed.links.push(link)),
(NS::Atom, "link") => if_some_then(atom::handle_link(child), |link| feed.links.push(link)),
(NS::RSS, "description") => feed.description = common::handle_text(child),
(NS::RSS, "language") => feed.language = child.child_as_text().map(|text| text.to_lowercase()),
(NS::RSS, "copyright") => feed.rights = common::handle_text(child),
(NS::RSS, "managingEditor") => if_some_then(handle_contact("managingEditor", child), |person| feed.contributors.push(person)),
(NS::RSS, "webMaster") => if_some_then(handle_contact("webMaster", child), |person| feed.contributors.push(person)),
(NS::RSS, "pubDate") => feed.published = common::handle_timestamp(parser, child),
(NS::RSS, "lastBuildDate") | (NS::RSS, "updated") => feed.updated = common::handle_timestamp(parser, child),
(NS::RSS, "category") => if_some_then(handle_category(child), |category| feed.categories.push(category)),
(NS::RSS, "generator") => feed.generator = handle_generator(child),
(NS::RSS, "ttl") => if_some_then(child.child_as_text(), |text| if_ok_then_some(text.parse::<u32>(), |ttl| feed.ttl = ttl)),
(NS::RSS, "image") => feed.logo = handle_image(child)?,
(NS::RSS, "item") => if_some_then(handle_item(parser, child)?, |item| feed.entries.push(item)),
(NS::Itunes, _) => handle_itunes_channel_element(child, &mut feed)?,
(NS::Podcast, _) => handle_podcast_channel_element(child, &mut feed)?,
_ => {}
}
}
Ok(feed)
}
fn handle_category<R: BufRead>(element: Element<R>) -> Option<Category> {
element.child_as_text().map(|text| {
let mut category = Category::new(&text);
category.scheme = element.attr_value("domain");
category
})
}
fn handle_contact<R: BufRead>(role: &str, element: Element<R>) -> Option<Person> {
element.child_as_text().map(|text| Person::parse(&text).role(role))
}
fn handle_generator<R: BufRead>(element: Element<R>) -> Option<Generator> {
element.child_as_text().map(|c| {
let mut generator = Generator::new(&c);
for attr in element.attributes {
let tag_name = attr.name.as_str();
if tag_name == "uri" {
generator.uri = Some(attr.value.clone());
}
}
generator
})
}
fn handle_enclosure<R: BufRead>(element: Element<R>, media_obj: &mut MediaObject) {
let mut content = MediaContent::new();
for attr in &element.attributes {
let tag_name = attr.name.as_str();
match tag_name {
"url" => content.url = util::parse_uri(&attr.value, element.xml_base.as_ref()),
"length" => content.size = attr.value.parse::<u64>().ok(),
"type" => if_ok_then_some(attr.value.parse::<MediaTypeBuf>(), |mime| content.content_type = mime),
_ => {}
}
}
if content.url.is_some() {
media_obj.content.push(content);
}
}
fn handle_image<R: BufRead>(element: Element<R>) -> ParseFeedResult<Option<Image>> {
let mut image = Image::new("".to_owned());
for child in element.children() {
let child = child?;
match child.ns_and_tag() {
(NS::RSS, "url") => if_some_then(child.child_as_text(), |url| image.uri = url),
(NS::RSS, "title") => image.title = child.child_as_text(),
(NS::RSS, "link") => if_some_then(child.child_as_text(), |uri| image.link = Some(Link::new(uri, element.xml_base.as_ref()))),
(NS::RSS, "width") => if_some_then(child.child_as_text(), |width| {
if let Ok(width) = width.parse::<u32>() {
if width > 0 && width <= 144 {
image.width = Some(width)
}
}
}),
(NS::RSS, "height") => if_some_then(child.child_as_text(), |height| {
if let Ok(height) = height.parse::<u32>() {
if height > 0 && height <= 400 {
image.height = Some(height)
}
}
}),
(NS::RSS, "description") => image.description = child.child_as_text(),
_ => {}
}
}
Ok(if !image.uri.is_empty() { Some(image) } else { None })
}
fn handle_content_encoded<R: BufRead>(element: Element<R>) -> ParseFeedResult<Option<Content>> {
let src = element.xml_base.as_ref().map(|xml_base| Link::new(xml_base, element.xml_base.as_ref()));
Ok(element.child_as_text().and_then(|string| {
if string.is_empty() {
None
} else {
Some(Content {
body: Some(string),
content_type: MediaTypeBuf::new(names::TEXT, names::HTML),
src,
..Default::default()
})
}
}))
}
fn handle_item<R: BufRead>(parser: &Parser, element: Element<R>) -> ParseFeedResult<Option<Entry>> {
let mut entry = Entry::default();
let mut enclosure_media_obj = MediaObject::new(MediaObjectSource::RSS);
let mut itunes_media_obj = MediaObject::new(MediaObjectSource::ITunes);
let mut podcast_media_obj = MediaObject::new(MediaObjectSource::Podcast);
parser.mediarss.entry_begin();
for child in element.children() {
let child = child?;
match child.ns_and_tag() {
(NS::RSS, "title") => entry.title = common::handle_text(child),
(NS::RSS, "link") => if_some_then(common::handle_link(None, child), |link| entry.links.push(link)),
(NS::RSS, "description") => entry.summary = common::handle_encoded(child)?,
(NS::RSS, "author") => if_some_then(handle_contact("author", child), |person| entry.authors.push(person)),
(NS::RSS, "category") => if_some_then(handle_category(child), |category| entry.categories.push(category)),
(NS::RSS, "guid") => if_some_then(child.child_as_text(), |guid| entry.id = guid.trim().to_string()),
(NS::RSS, "enclosure") => handle_enclosure(child, &mut enclosure_media_obj),
(NS::RSS, "pubDate") | (NS::DublinCore, "date") => entry.published = common::handle_timestamp(parser, child),
(NS::RSS, "updated") | (NS::Atom, "updated") => entry.updated = common::handle_timestamp(parser, child),
(NS::Content, "encoded") => entry.content = handle_content_encoded(child)?,
(NS::DublinCore, "creator") => if_some_then(child.child_as_text(), |name| entry.authors.push(Person::parse(&name).role("creator"))),
(NS::Itunes, _) => handle_itunes_item_element(child, &mut itunes_media_obj)?,
(NS::MediaRSS, _) => parser.mediarss.handle_entry_mediarss_element(child)?,
(NS::Podcast, _) => handle_podcast_item_element(child, &mut podcast_media_obj)?,
(NS::WellFormedWebComments, "commentRss") | (NS::WellFormedWebComments, "commentRSS") => {
if_some_then(common::handle_link(Some(LinkTarget::CommentsFeed), child), |link| entry.links.push(link))
}
(NS::RSS, "comments") => if_some_then(common::handle_link(Some(LinkTarget::Comments), child), |link| entry.links.push(link)),
_ => {}
}
}
parser.mediarss.entry_end(&mut entry);
for media_obj in [itunes_media_obj, podcast_media_obj, enclosure_media_obj] {
if media_obj.is_not_empty() {
entry.media.push(media_obj);
}
}
postprocess_media(&mut entry.media);
if entry.updated.is_none() && entry.published.is_some() {
entry.updated = entry.published;
}
clean_links(&mut entry.links);
Ok(Some(entry))
}
fn clean_links(links: &mut Vec<Link>) {
let non_comment_targets: HashSet<String> = links
.iter()
.filter(|link| link.target != Some(LinkTarget::Comments))
.map(|link| link.href.clone())
.collect::<HashSet<_>>();
links.retain(|link| link.target != Some(LinkTarget::Comments) || !non_comment_targets.contains(link.href.as_str()));
}
fn postprocess_media(objects: &mut Vec<MediaObject>) {
if let Some(mediarss_idx) = objects.iter().position(|mo| mo.source == Some(MediaObjectSource::MediaRSS)) {
let mut mediarss_obj = objects.swap_remove(mediarss_idx);
let len = objects.len();
for idx in (0..len).rev() {
if objects[idx].source == Some(MediaObjectSource::RSS) {
let rss_obj = objects.swap_remove(idx);
postprocess_merge_media(rss_obj, &mut mediarss_obj);
} else if objects[idx].source == Some(MediaObjectSource::ITunes) {
let itunes_obj = &objects[idx];
if itunes_obj.season.is_none() && itunes_obj.episode.is_none() {
let itunes_obj = objects.swap_remove(idx);
postprocess_merge_media(itunes_obj, &mut mediarss_obj);
}
}
}
objects.push(mediarss_obj);
} else if let Some(podcast_idx) = objects.iter().position(|mo| mo.source == Some(MediaObjectSource::Podcast)) {
let mut postcast_obj = objects.swap_remove(podcast_idx);
let len = objects.len();
for idx in (0..len).rev() {
let other = objects.swap_remove(idx);
postprocess_merge_media(other, &mut postcast_obj);
}
objects.push(postcast_obj);
}
}
fn postprocess_merge_media(source: MediaObject, target: &mut MediaObject) {
if target.title.is_none() {
target.title = source.title;
}
if target.content.is_empty() {
target.content = source.content;
}
if target.duration.is_none() {
target.duration = source.duration;
}
if target.thumbnails.is_empty() {
target.thumbnails = source.thumbnails;
}
if target.texts.is_empty() {
target.texts = source.texts;
}
if target.description.is_none() {
target.description = source.description;
}
if target.community.is_none() {
target.community = source.community;
}
if target.credits.is_empty() {
target.credits = source.credits;
}
if target.people.is_empty() {
target.people = source.people;
}
if target.season.is_none() {
target.season = source.season;
}
if target.episode.is_none() {
target.episode = source.episode;
}
if target.transcripts.is_empty() {
target.transcripts = source.transcripts;
}
}