summaryrefslogtreecommitdiff
path: root/src/feed.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/feed.rs')
-rw-r--r--src/feed.rs148
1 files changed, 148 insertions, 0 deletions
diff --git a/src/feed.rs b/src/feed.rs
new file mode 100644
index 0000000..3926fd4
--- /dev/null
+++ b/src/feed.rs
@@ -0,0 +1,148 @@
1use std::fmt;
2
3use crate::{
4 error::{EntryError, PullError},
5 status::PullStatus,
6};
7
8use chrono::prelude::*;
9use feed_rs::{
10 model::{Entry as ChannelEntry, Feed as Channel},
11 parser,
12};
13use serde::{Deserialize, Serialize};
14use url::Url;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Feed {
18 // channel url
19 pub link: Url,
20
21 // channel data
22 entries: Vec<Entry>,
23
24 // channel meta
25 title: String,
26 html_link: String,
27}
28
29impl Feed {
30 pub fn new(link: Url) -> Self {
31 Self {
32 link,
33 entries: Vec::new(),
34 title: String::new(),
35 html_link: String::new(),
36 }
37 }
38
39 pub fn total_count(&self) -> usize {
40 self.entries.len()
41 }
42
43 pub fn unread_count(&self) -> usize {
44 self.entries.iter().filter(|e| e.unread).count()
45 }
46
47 fn update_title(&mut self, channel: &Channel) -> bool {
48 if let Some(t) = channel.title.as_ref() {
49 self.title = t.content.clone();
50 return true;
51 }
52 false
53 }
54
55 fn update_html_link(&mut self, channel: &Channel) -> bool {
56 // update html link
57 if let Some(l) = channel.links.first() {
58 self.html_link = l.href.clone();
59 return true;
60 }
61 false
62 }
63
64 pub fn entries(&self) -> &[Entry] {
65 self.entries.as_slice()
66 }
67
68 pub async fn pull(&mut self) -> Result<PullStatus, PullError> {
69 let content = reqwest::get(self.link.clone()).await?.bytes().await?;
70 let channel = parser::parse(&content[..])?;
71
72 // update title
73 if !self.update_title(&channel) {
74 return Err(PullError::TitleUpdate);
75 }
76
77 // update html link
78 if !self.update_html_link(&channel) {
79 return Err(PullError::LinkUpdate);
80 };
81
82 // fetch new entries
83 let (entries, errors): (Vec<_>, Vec<_>) = channel
84 .entries
85 .iter()
86 .map(Entry::try_from)
87 .partition(Result::is_ok);
88
89 // pull status
90 let count = entries.len().saturating_sub(self.total_count());
91 let errors = errors.into_iter().map(Result::unwrap_err).collect();
92
93 let pull_status = PullStatus::new(count, errors);
94
95 // update entries
96 self.entries = entries.into_iter().map(Result::unwrap).collect();
97
98 Ok(pull_status)
99 }
100}
101
102impl fmt::Display for Feed {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 write!(f, "{}", self.title)
105 }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct Entry {
110 pub title: String,
111 pub link: Url,
112 pub published: DateTime<Utc>,
113 pub unread: bool,
114}
115
116impl TryFrom<&ChannelEntry> for Entry {
117 type Error = EntryError;
118 fn try_from(e: &ChannelEntry) -> Result<Self, Self::Error> {
119 let title = e
120 .title
121 .as_ref()
122 .map(|t| t.content.clone())
123 .ok_or(EntryError::MissingTitle)?;
124 let raw_link = e
125 .links
126 .first()
127 .map(|l| l.href.clone())
128 .ok_or(EntryError::MissingLink)?;
129 let link = Url::parse(&raw_link).map_err(|_| EntryError::InvalidLink)?;
130 let published = e
131 .published
132 .or(e.updated)
133 .ok_or(EntryError::MissingPubDate)?;
134
135 Ok(Self {
136 title,
137 link,
138 published,
139 unread: true,
140 })
141 }
142}
143
144impl fmt::Display for Entry {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 write!(f, "{} {} {}", self.link, self.title, self.published)
147 }
148}