use std::fmt; use crate::{ error::{EntryError, PullError}, status::PullStatus, }; use chrono::prelude::*; use feed_rs::{ model::{Entry as ChannelEntry, Feed as Channel}, parser, }; use serde::{Deserialize, Serialize}; use url::Url; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Feed { // channel url pub link: Url, // channel data entries: Vec, // channel meta title: String, html_link: String, } impl Feed { pub fn new(link: Url) -> Self { Self { link, entries: Vec::new(), title: String::new(), html_link: String::new(), } } pub fn total_count(&self) -> usize { self.entries.len() } pub fn unread_count(&self) -> usize { self.entries.iter().filter(|e| e.unread).count() } fn update_title(&mut self, channel: &Channel) -> bool { if let Some(t) = channel.title.as_ref() { self.title = t.content.clone(); return true; } false } fn update_html_link(&mut self, channel: &Channel) -> bool { // update html link if let Some(l) = channel.links.first() { self.html_link = l.href.clone(); return true; } false } pub fn entries(&self) -> &[Entry] { self.entries.as_slice() } pub async fn pull(&mut self) -> Result { let content = reqwest::get(self.link.clone()).await?.bytes().await?; let channel = parser::parse(&content[..])?; // update title if !self.update_title(&channel) { return Err(PullError::TitleUpdate); } // update html link if !self.update_html_link(&channel) { return Err(PullError::LinkUpdate); }; // fetch new entries let (entries, errors): (Vec<_>, Vec<_>) = channel .entries .iter() .map(Entry::try_from) .partition(Result::is_ok); // pull status let title = self.title.clone(); let count = entries.len().saturating_sub(self.total_count()); let errors = errors.into_iter().map(Result::unwrap_err).collect(); let pull_status = PullStatus::new(title, count, errors); // update entries self.entries = entries.into_iter().map(Result::unwrap).collect(); Ok(pull_status) } } impl fmt::Display for Feed { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.title) } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Entry { pub title: String, pub link: Url, pub published: DateTime, pub unread: bool, } impl TryFrom<&ChannelEntry> for Entry { type Error = EntryError; fn try_from(e: &ChannelEntry) -> Result { let title = e .title .as_ref() .map(|t| t.content.clone()) .ok_or(EntryError::MissingTitle)?; let raw_link = e .links .first() .map(|l| l.href.clone()) .ok_or(EntryError::MissingLink)?; let link = Url::parse(&raw_link).map_err(|_| EntryError::InvalidLink)?; let published = e .published .or(e.updated) .ok_or(EntryError::MissingPubDate)?; Ok(Self { title, link, published, unread: true, }) } } impl fmt::Display for Entry { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} {} {}", self.link, self.title, self.published) } }