feed/article_store/
mod.rs1mod fetch;
2
3use std::path::{Path, PathBuf};
4use std::time::Duration;
5
6use chrono::{DateTime, Utc};
7use reqwest::Client;
8
9use crate::article::Article;
10use crate::cache::CacheStore;
11use crate::config::{Config, FeedEntry};
12
13pub struct ArticleStore {
14 articles: Vec<Article>,
15 feeds: Vec<FeedEntry>,
16 config: Config,
17 cache: CacheStore,
18 client: Client,
19}
20
21#[derive(Debug, Clone, Default)]
22pub struct FilterParams {
23 pub show_read: bool,
24 pub from: Option<DateTime<Utc>>,
25 pub limit: Option<usize>,
26}
27
28impl ArticleStore {
29 pub fn new(feeds: Vec<FeedEntry>, config: Config, data_dir: PathBuf) -> Self {
30 let client = Client::builder()
31 .timeout(Duration::from_secs(10))
32 .connect_timeout(Duration::from_secs(5))
33 .gzip(true)
34 .brotli(true)
35 .build()
36 .unwrap_or_else(|_| Client::new());
37 Self::with_client(feeds, config, data_dir, client)
38 }
39
40 pub fn with_client(
41 feeds: Vec<FeedEntry>,
42 config: Config,
43 data_dir: PathBuf,
44 client: Client,
45 ) -> Self {
46 Self {
47 articles: Vec::new(),
48 feeds,
49 config,
50 cache: CacheStore::new(data_dir),
51 client,
52 }
53 }
54
55 pub fn feeds(&self) -> &[FeedEntry] {
56 &self.feeds
57 }
58
59 pub fn config(&self) -> &Config {
60 &self.config
61 }
62
63 pub fn cache(&self) -> &CacheStore {
64 &self.cache
65 }
66
67 pub fn data_dir(&self) -> &Path {
68 self.cache.data_dir()
69 }
70
71 pub fn client(&self) -> &Client {
72 &self.client
73 }
74
75 pub fn articles(&self) -> &[Article] {
77 &self.articles
78 }
79
80 pub fn set_articles(&mut self, articles: Vec<Article>) {
82 self.articles = articles;
83 }
84
85 pub fn take_articles(&mut self) -> Vec<Article> {
86 std::mem::take(&mut self.articles)
87 }
88
89 pub fn get(&self, index: usize) -> Option<&Article> {
91 self.articles.get(index)
92 }
93
94 pub fn len(&self) -> usize {
96 self.articles.len()
97 }
98
99 pub fn is_empty(&self) -> bool {
101 self.articles.is_empty()
102 }
103
104 pub fn mark_read(&mut self, index: usize) {
106 if let Some(a) = self.articles.get_mut(index) {
107 a.read = true;
108 let cache = self.cache.clone();
109 let feed_url = a.feed_url.clone();
110 let url = a.url.clone();
111 tokio::task::spawn_blocking(move || {
112 let _ = cache.set_read_status(&feed_url, &url, true);
113 });
114 }
115 }
116
117 pub fn toggle_read(&mut self, index: usize) -> bool {
119 if let Some(a) = self.articles.get_mut(index) {
120 a.read = !a.read;
121 let new_read = a.read;
122 let cache = self.cache.clone();
123 let feed_url = a.feed_url.clone();
124 let url = a.url.clone();
125 tokio::task::spawn_blocking(move || {
126 let _ = cache.set_read_status(&feed_url, &url, new_read);
127 });
128 new_read
129 } else {
130 false
131 }
132 }
133
134 pub fn query(&self, params: &FilterParams) -> Vec<usize> {
136 let mut result: Vec<usize> = self
137 .articles
138 .iter()
139 .enumerate()
140 .filter(|(_, a)| params.show_read || !a.read)
141 .filter(|(_, a)| match params.from {
142 Some(from) => a.published.is_none_or(|dt| dt >= from),
143 None => true,
144 })
145 .map(|(i, _)| i)
146 .collect();
147
148 if let Some(limit) = params.limit {
149 result.truncate(limit);
150 }
151
152 result
153 }
154
155 pub fn query_articles(&self, params: &FilterParams) -> Vec<Article> {
157 self.query(params)
158 .into_iter()
159 .filter_map(|i| self.articles.get(i).cloned())
160 .collect()
161 }
162}