1use crate::models::{ArticleID, FeedID, Url};
2use crate::schema::articles;
3use chrono::{DateTime, Utc};
4use diesel::backend::Backend;
5use diesel::deserialize;
6use diesel::deserialize::FromSql;
7use diesel::serialize::{self, IsNull};
8use diesel::serialize::{Output, ToSql};
9use diesel::sql_types::Integer;
10use diesel::sqlite::Sqlite;
11use serde::{Deserialize, Serialize};
12use std::io::{Error, ErrorKind, Write};
13use std::path::PathBuf;
14
15#[derive(Identifiable, Insertable, Queryable, Clone, Eq, PartialEq, Debug)]
16#[diesel(primary_key(article_id))]
17#[diesel(belongs_to(Feed))]
18#[diesel(treat_none_as_default_value = false)]
19#[diesel(table_name = articles)]
20#[diesel(check_for_backend(SQLite))]
21pub struct Article {
22 pub article_id: ArticleID,
23 pub title: Option<String>,
24 pub author: Option<String>,
25 pub feed_id: FeedID,
26 pub url: Option<Url>,
27 pub date: DateTime<Utc>,
28 pub synced: DateTime<Utc>,
29 pub summary: Option<String>,
30 pub direction: Option<Direction>,
31 pub unread: Read,
32 pub marked: Marked,
33 pub thumbnail_url: Option<String>,
34 pub updated: Option<DateTime<Utc>>,
35}
36
37#[derive(Identifiable, Insertable, Queryable, Clone, Eq, PartialEq, Debug)]
40#[diesel(primary_key(article_id))]
41#[diesel(belongs_to(Feed))]
42#[diesel(treat_none_as_default_value = false)]
43#[diesel(table_name = articles)]
44#[diesel(check_for_backend(SQLite))]
45pub struct FatArticle {
46 pub article_id: ArticleID,
47 pub title: Option<String>,
48 pub author: Option<String>,
49 pub feed_id: FeedID,
50 pub url: Option<Url>,
51 pub date: DateTime<Utc>,
52 pub synced: DateTime<Utc>,
53 pub html: Option<String>,
54 pub summary: Option<String>,
55 pub direction: Option<Direction>,
56 pub unread: Read,
57 pub marked: Marked,
58 pub scraped_content: Option<String>,
59 pub plain_text: Option<String>,
60 pub thumbnail_url: Option<String>,
61 pub updated: Option<DateTime<Utc>>,
62}
63
64impl FatArticle {
65 pub fn save_html(&self, path: &PathBuf) -> Result<(), Error> {
66 if let Some(ref html) = self.html
67 && let Ok(()) = std::fs::create_dir_all(path)
68 {
69 let mut file_name = match self.title.clone() {
70 Some(file_name) => file_name,
71 None => "Unknown Title".to_owned(),
72 };
73 file_name.push_str(".html");
74 let path = path.join(file_name);
75 let mut html_file = std::fs::File::create(path)?;
76 html_file.write_all(html.as_bytes())?;
77 }
78
79 Err(Error::new(ErrorKind::NotFound, "No HTML for article"))
80 }
81}
82
83#[derive(Eq, PartialEq, Copy, Clone, Debug, Default, AsExpression, FromSqlRow, Serialize, Deserialize)]
86#[diesel(sql_type = Integer)]
87pub enum Direction {
88 #[default]
89 LeftToRight,
90 RightToLeft,
91}
92
93impl Direction {
94 pub fn to_int(self) -> i32 {
95 self as i32
96 }
97
98 pub fn from_int(int: i32) -> Self {
99 match int {
100 0 => Direction::LeftToRight,
101 1 => Direction::RightToLeft,
102
103 _ => Direction::LeftToRight,
104 }
105 }
106}
107
108impl FromSql<Integer, Sqlite> for Direction {
109 fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
110 let int = i32::from_sql(bytes)?;
111 Ok(Direction::from_int(int))
112 }
113}
114
115impl ToSql<Integer, Sqlite> for Direction {
116 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
117 out.set_value(*self as i32);
118 Ok(IsNull::No)
119 }
120}
121
122#[derive(Eq, PartialEq, Copy, Clone, Debug, Default, AsExpression, FromSqlRow, Serialize, Deserialize)]
125#[diesel(sql_type = Integer)]
126pub enum Read {
127 Read,
128 #[default]
129 Unread,
130}
131
132impl Read {
133 pub fn to_int(self) -> i32 {
134 self as i32
135 }
136
137 pub fn from_int(int: i32) -> Self {
138 match int {
139 0 => Read::Read,
140 1 => Read::Unread,
141
142 _ => Read::Read,
143 }
144 }
145
146 pub fn invert(&self) -> Read {
147 match self {
148 Read::Read => Read::Unread,
149 Read::Unread => Read::Read,
150 }
151 }
152}
153
154impl FromSql<Integer, Sqlite> for Read {
155 fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
156 let int = i32::from_sql(bytes)?;
157 Ok(Read::from_int(int))
158 }
159}
160
161impl ToSql<Integer, Sqlite> for Read {
162 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
163 out.set_value(*self as i32);
164 Ok(IsNull::No)
165 }
166}
167
168#[derive(Eq, PartialEq, Copy, Clone, Debug, Default, AsExpression, FromSqlRow, Serialize, Deserialize)]
171#[diesel(sql_type = Integer)]
172pub enum Marked {
173 Marked,
174 #[default]
175 Unmarked,
176}
177
178impl Marked {
179 pub fn to_int(self) -> i32 {
180 self as i32
181 }
182
183 pub fn from_int(int: i32) -> Self {
184 match int {
185 0 => Marked::Marked,
186 1 => Marked::Unmarked,
187
188 _ => Marked::Unmarked,
189 }
190 }
191
192 pub fn invert(&self) -> Marked {
193 match self {
194 Marked::Marked => Marked::Unmarked,
195 Marked::Unmarked => Marked::Marked,
196 }
197 }
198}
199
200impl FromSql<Integer, Sqlite> for Marked {
201 fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
202 let int = i32::from_sql(bytes)?;
203 Ok(Marked::from_int(int))
204 }
205}
206
207impl ToSql<Integer, Sqlite> for Marked {
208 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
209 out.set_value(*self as i32);
210 Ok(IsNull::No)
211 }
212}
213
214#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)]
217pub enum ArticleOrder {
218 #[default]
219 NewestFirst,
220 OldestFirst,
221}
222
223impl ArticleOrder {
224 pub fn invert(&self) -> ArticleOrder {
225 match self {
226 ArticleOrder::NewestFirst => ArticleOrder::OldestFirst,
227 ArticleOrder::OldestFirst => ArticleOrder::NewestFirst,
228 }
229 }
230}
231
232#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)]
235pub enum OrderBy {
236 #[default]
237 Updated,
238 Published,
239}