diff options
Diffstat (limited to 'src/feed.rs')
-rw-r--r-- | src/feed.rs | 148 |
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 @@ | |||
1 | use std::fmt; | ||
2 | |||
3 | use crate::{ | ||
4 | error::{EntryError, PullError}, | ||
5 | status::PullStatus, | ||
6 | }; | ||
7 | |||
8 | use chrono::prelude::*; | ||
9 | use feed_rs::{ | ||
10 | model::{Entry as ChannelEntry, Feed as Channel}, | ||
11 | parser, | ||
12 | }; | ||
13 | use serde::{Deserialize, Serialize}; | ||
14 | use url::Url; | ||
15 | |||
16 | #[derive(Debug, Clone, Serialize, Deserialize)] | ||
17 | pub 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 | |||
29 | impl 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 | |||
102 | impl 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)] | ||
109 | pub struct Entry { | ||
110 | pub title: String, | ||
111 | pub link: Url, | ||
112 | pub published: DateTime<Utc>, | ||
113 | pub unread: bool, | ||
114 | } | ||
115 | |||
116 | impl 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 | |||
144 | impl 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 | } | ||