1 // dbqueries.rs
2 //
3 // Copyright 2017 Jordan Petridis <jpetridis@gnome.org>
4 //
5 // This program is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with this program. If not, see <http://www.gnu.org/licenses/>.
17 //
18 // SPDX-License-Identifier: GPL-3.0-or-later
19
20 //! Random CRUD helper functions.
21
22 use chrono::prelude::*;
23 use diesel::prelude::*;
24
25 use diesel::dsl::exists;
26 use diesel::select;
27
28 use crate::database::connection;
29 use crate::errors::DataError;
30 use crate::models::*;
31
get_sources() -> Result<Vec<Source>, DataError>32 pub fn get_sources() -> Result<Vec<Source>, DataError> {
33 use crate::schema::source::dsl::*;
34 let db = connection();
35 let con = db.get()?;
36
37 source
38 .order((http_etag.asc(), last_modified.asc()))
39 .load::<Source>(&con)
40 .map_err(From::from)
41 }
42
get_podcasts() -> Result<Vec<Show>, DataError>43 pub fn get_podcasts() -> Result<Vec<Show>, DataError> {
44 use crate::schema::shows::dsl::*;
45 let db = connection();
46 let con = db.get()?;
47
48 shows
49 .order(title.asc())
50 .load::<Show>(&con)
51 .map_err(From::from)
52 }
53
get_podcasts_filter(filter_ids: &[i32]) -> Result<Vec<Show>, DataError>54 pub fn get_podcasts_filter(filter_ids: &[i32]) -> Result<Vec<Show>, DataError> {
55 use crate::schema::shows::dsl::*;
56 let db = connection();
57 let con = db.get()?;
58
59 shows
60 .order(title.asc())
61 .filter(id.ne_all(filter_ids))
62 .load::<Show>(&con)
63 .map_err(From::from)
64 }
65
get_episodes() -> Result<Vec<Episode>, DataError>66 pub fn get_episodes() -> Result<Vec<Episode>, DataError> {
67 use crate::schema::episodes::dsl::*;
68 let db = connection();
69 let con = db.get()?;
70
71 episodes
72 .order(epoch.desc())
73 .load::<Episode>(&con)
74 .map_err(From::from)
75 }
76
get_downloaded_episodes() -> Result<Vec<EpisodeCleanerModel>, DataError>77 pub(crate) fn get_downloaded_episodes() -> Result<Vec<EpisodeCleanerModel>, DataError> {
78 use crate::schema::episodes::dsl::*;
79 let db = connection();
80 let con = db.get()?;
81
82 episodes
83 .select((rowid, local_uri, played))
84 .filter(local_uri.is_not_null())
85 .load::<EpisodeCleanerModel>(&con)
86 .map_err(From::from)
87 }
88
89 // pub(crate) fn get_played_episodes() -> Result<Vec<Episode>, DataError> {
90 // use schema::episodes::dsl::*;
91
92 // let db = connection();
93 // let con = db.get()?;
94 // episodes
95 // .filter(played.is_not_null())
96 // .load::<Episode>(&con)
97 // .map_err(From::from)
98 // }
99
get_played_cleaner_episodes() -> Result<Vec<EpisodeCleanerModel>, DataError>100 pub(crate) fn get_played_cleaner_episodes() -> Result<Vec<EpisodeCleanerModel>, DataError> {
101 use crate::schema::episodes::dsl::*;
102 let db = connection();
103 let con = db.get()?;
104
105 episodes
106 .select((rowid, local_uri, played))
107 .filter(played.is_not_null())
108 .load::<EpisodeCleanerModel>(&con)
109 .map_err(From::from)
110 }
111
get_episode_from_rowid(ep_id: i32) -> Result<Episode, DataError>112 pub fn get_episode_from_rowid(ep_id: i32) -> Result<Episode, DataError> {
113 use crate::schema::episodes::dsl::*;
114 let db = connection();
115 let con = db.get()?;
116
117 episodes
118 .filter(rowid.eq(ep_id))
119 .get_result::<Episode>(&con)
120 .map_err(From::from)
121 }
122
get_episode_widget_from_rowid(ep_id: i32) -> Result<EpisodeWidgetModel, DataError>123 pub fn get_episode_widget_from_rowid(ep_id: i32) -> Result<EpisodeWidgetModel, DataError> {
124 use crate::schema::episodes::dsl::*;
125 let db = connection();
126 let con = db.get()?;
127
128 episodes
129 .select((
130 rowid,
131 title,
132 uri,
133 local_uri,
134 epoch,
135 length,
136 duration,
137 played,
138 play_position,
139 show_id,
140 ))
141 .filter(rowid.eq(ep_id))
142 .get_result::<EpisodeWidgetModel>(&con)
143 .map_err(From::from)
144 }
145
get_episode_local_uri_from_id(ep_id: i32) -> Result<Option<String>, DataError>146 pub fn get_episode_local_uri_from_id(ep_id: i32) -> Result<Option<String>, DataError> {
147 use crate::schema::episodes::dsl::*;
148 let db = connection();
149 let con = db.get()?;
150
151 episodes
152 .filter(rowid.eq(ep_id))
153 .select(local_uri)
154 .get_result::<Option<String>>(&con)
155 .map_err(From::from)
156 }
157
get_episodes_widgets_filter_limit( filter_ids: &[i32], limit: u32, ) -> Result<Vec<EpisodeWidgetModel>, DataError>158 pub fn get_episodes_widgets_filter_limit(
159 filter_ids: &[i32],
160 limit: u32,
161 ) -> Result<Vec<EpisodeWidgetModel>, DataError> {
162 use crate::schema::episodes::dsl::*;
163 let db = connection();
164 let con = db.get()?;
165 let columns = (
166 rowid,
167 title,
168 uri,
169 local_uri,
170 epoch,
171 length,
172 duration,
173 played,
174 play_position,
175 show_id,
176 );
177
178 episodes
179 .select(columns)
180 .order(epoch.desc())
181 .filter(show_id.ne_all(filter_ids))
182 .limit(i64::from(limit))
183 .load::<EpisodeWidgetModel>(&con)
184 .map_err(From::from)
185 }
186
get_podcast_from_id(pid: i32) -> Result<Show, DataError>187 pub fn get_podcast_from_id(pid: i32) -> Result<Show, DataError> {
188 use crate::schema::shows::dsl::*;
189 let db = connection();
190 let con = db.get()?;
191
192 shows
193 .filter(id.eq(pid))
194 .get_result::<Show>(&con)
195 .map_err(From::from)
196 }
197
get_podcast_cover_from_id(pid: i32) -> Result<ShowCoverModel, DataError>198 pub fn get_podcast_cover_from_id(pid: i32) -> Result<ShowCoverModel, DataError> {
199 use crate::schema::shows::dsl::*;
200 let db = connection();
201 let con = db.get()?;
202
203 shows
204 .select((id, title, image_uri, image_uri_hash, image_cached))
205 .filter(id.eq(pid))
206 .get_result::<ShowCoverModel>(&con)
207 .map_err(From::from)
208 }
209
get_pd_episodes(parent: &Show) -> Result<Vec<Episode>, DataError>210 pub fn get_pd_episodes(parent: &Show) -> Result<Vec<Episode>, DataError> {
211 use crate::schema::episodes::dsl::*;
212 let db = connection();
213 let con = db.get()?;
214
215 Episode::belonging_to(parent)
216 .order(epoch.desc())
217 .load::<Episode>(&con)
218 .map_err(From::from)
219 }
220
get_pd_episodes_count(parent: &Show) -> Result<i64, DataError>221 pub fn get_pd_episodes_count(parent: &Show) -> Result<i64, DataError> {
222 let db = connection();
223 let con = db.get()?;
224
225 Episode::belonging_to(parent)
226 .count()
227 .get_result(&con)
228 .map_err(From::from)
229 }
230
get_pd_episodeswidgets(parent: &Show) -> Result<Vec<EpisodeWidgetModel>, DataError>231 pub fn get_pd_episodeswidgets(parent: &Show) -> Result<Vec<EpisodeWidgetModel>, DataError> {
232 use crate::schema::episodes::dsl::*;
233 let db = connection();
234 let con = db.get()?;
235 let columns = (
236 rowid,
237 title,
238 uri,
239 local_uri,
240 epoch,
241 length,
242 duration,
243 played,
244 play_position,
245 show_id,
246 );
247
248 episodes
249 .select(columns)
250 .filter(show_id.eq(parent.id()))
251 .order(epoch.desc())
252 .load::<EpisodeWidgetModel>(&con)
253 .map_err(From::from)
254 }
255
get_pd_unplayed_episodes(parent: &Show) -> Result<Vec<Episode>, DataError>256 pub fn get_pd_unplayed_episodes(parent: &Show) -> Result<Vec<Episode>, DataError> {
257 use crate::schema::episodes::dsl::*;
258 let db = connection();
259 let con = db.get()?;
260
261 Episode::belonging_to(parent)
262 .filter(played.is_null())
263 .order(epoch.desc())
264 .load::<Episode>(&con)
265 .map_err(From::from)
266 }
267
268 // pub(crate) fn get_pd_episodes_limit(parent: &Show, limit: u32) ->
269 // Result<Vec<Episode>, DataError> { use schema::episodes::dsl::*;
270
271 // let db = connection();
272 // let con = db.get()?;
273
274 // Episode::belonging_to(parent)
275 // .order(epoch.desc())
276 // .limit(i64::from(limit))
277 // .load::<Episode>(&con)
278 // .map_err(From::from)
279 // }
280
get_source_from_uri(uri_: &str) -> Result<Source, DataError>281 pub fn get_source_from_uri(uri_: &str) -> Result<Source, DataError> {
282 use crate::schema::source::dsl::*;
283 let db = connection();
284 let con = db.get()?;
285
286 source
287 .filter(uri.eq(uri_))
288 .get_result::<Source>(&con)
289 .map_err(From::from)
290 }
291
get_source_from_id(id_: i32) -> Result<Source, DataError>292 pub fn get_source_from_id(id_: i32) -> Result<Source, DataError> {
293 use crate::schema::source::dsl::*;
294 let db = connection();
295 let con = db.get()?;
296
297 source
298 .filter(id.eq(id_))
299 .get_result::<Source>(&con)
300 .map_err(From::from)
301 }
302
get_podcast_from_source_id(sid: i32) -> Result<Show, DataError>303 pub fn get_podcast_from_source_id(sid: i32) -> Result<Show, DataError> {
304 use crate::schema::shows::dsl::*;
305 let db = connection();
306 let con = db.get()?;
307
308 shows
309 .filter(source_id.eq(sid))
310 .get_result::<Show>(&con)
311 .map_err(From::from)
312 }
313
get_episode_from_pk(title_: &str, pid: i32) -> Result<Episode, DataError>314 pub fn get_episode_from_pk(title_: &str, pid: i32) -> Result<Episode, DataError> {
315 use crate::schema::episodes::dsl::*;
316 let db = connection();
317 let con = db.get()?;
318
319 episodes
320 .filter(title.eq(title_))
321 .filter(show_id.eq(pid))
322 .get_result::<Episode>(&con)
323 .map_err(From::from)
324 }
325
get_episode_minimal_from_pk( title_: &str, pid: i32, ) -> Result<EpisodeMinimal, DataError>326 pub(crate) fn get_episode_minimal_from_pk(
327 title_: &str,
328 pid: i32,
329 ) -> Result<EpisodeMinimal, DataError> {
330 use crate::schema::episodes::dsl::*;
331 let db = connection();
332 let con = db.get()?;
333
334 episodes
335 .select((
336 rowid,
337 title,
338 uri,
339 epoch,
340 length,
341 duration,
342 play_position,
343 guid,
344 show_id,
345 ))
346 .filter(title.eq(title_))
347 .filter(show_id.eq(pid))
348 .get_result::<EpisodeMinimal>(&con)
349 .map_err(From::from)
350 }
351
352 #[cfg(test)]
get_episode_cleaner_from_pk( title_: &str, pid: i32, ) -> Result<EpisodeCleanerModel, DataError>353 pub(crate) fn get_episode_cleaner_from_pk(
354 title_: &str,
355 pid: i32,
356 ) -> Result<EpisodeCleanerModel, DataError> {
357 use crate::schema::episodes::dsl::*;
358 let db = connection();
359 let con = db.get()?;
360
361 episodes
362 .select((rowid, local_uri, played))
363 .filter(title.eq(title_))
364 .filter(show_id.eq(pid))
365 .get_result::<EpisodeCleanerModel>(&con)
366 .map_err(From::from)
367 }
368
remove_feed(pd: &Show) -> Result<(), DataError>369 pub(crate) fn remove_feed(pd: &Show) -> Result<(), DataError> {
370 let db = connection();
371 let con = db.get()?;
372
373 con.transaction(|| {
374 delete_source(&con, pd.source_id())?;
375 delete_podcast(&con, pd.id())?;
376 delete_podcast_episodes(&con, pd.id())?;
377 info!("Feed removed from the Database.");
378 Ok(())
379 })
380 }
381
delete_source(con: &SqliteConnection, source_id: i32) -> QueryResult<usize>382 fn delete_source(con: &SqliteConnection, source_id: i32) -> QueryResult<usize> {
383 use crate::schema::source::dsl::*;
384
385 diesel::delete(source.filter(id.eq(source_id))).execute(con)
386 }
387
delete_podcast(con: &SqliteConnection, show_id: i32) -> QueryResult<usize>388 fn delete_podcast(con: &SqliteConnection, show_id: i32) -> QueryResult<usize> {
389 use crate::schema::shows::dsl::*;
390
391 diesel::delete(shows.filter(id.eq(show_id))).execute(con)
392 }
393
delete_podcast_episodes(con: &SqliteConnection, parent_id: i32) -> QueryResult<usize>394 fn delete_podcast_episodes(con: &SqliteConnection, parent_id: i32) -> QueryResult<usize> {
395 use crate::schema::episodes::dsl::*;
396
397 diesel::delete(episodes.filter(show_id.eq(parent_id))).execute(con)
398 }
399
source_exists(url: &str) -> Result<bool, DataError>400 pub fn source_exists(url: &str) -> Result<bool, DataError> {
401 use crate::schema::source::dsl::*;
402
403 let db = connection();
404 let con = db.get()?;
405
406 select(exists(source.filter(uri.eq(url))))
407 .get_result(&con)
408 .map_err(From::from)
409 }
410
podcast_exists(source_id_: i32) -> Result<bool, DataError>411 pub(crate) fn podcast_exists(source_id_: i32) -> Result<bool, DataError> {
412 use crate::schema::shows::dsl::*;
413
414 let db = connection();
415 let con = db.get()?;
416
417 select(exists(shows.filter(source_id.eq(source_id_))))
418 .get_result(&con)
419 .map_err(From::from)
420 }
421
episode_exists(title_: &str, show_id_: i32) -> Result<bool, DataError>422 pub(crate) fn episode_exists(title_: &str, show_id_: i32) -> Result<bool, DataError> {
423 use crate::schema::episodes::dsl::*;
424
425 let db = connection();
426 let con = db.get()?;
427
428 select(exists(
429 episodes
430 .filter(show_id.eq(show_id_))
431 .filter(title.eq(title_)),
432 ))
433 .get_result(&con)
434 .map_err(From::from)
435 }
436
437 /// Check if the `episodes table contains any rows
438 ///
439 /// Return true if `episodes` table is populated.
is_episodes_populated(filter_show_ids: &[i32]) -> Result<bool, DataError>440 pub fn is_episodes_populated(filter_show_ids: &[i32]) -> Result<bool, DataError> {
441 use crate::schema::episodes::dsl::*;
442
443 let db = connection();
444 let con = db.get()?;
445
446 select(exists(episodes.filter(show_id.ne_all(filter_show_ids))))
447 .get_result(&con)
448 .map_err(From::from)
449 }
450
451 /// Check if the `shows` table contains any rows
452 ///
453 /// Return true if `shows` table is populated.
is_podcasts_populated(filter_ids: &[i32]) -> Result<bool, DataError>454 pub fn is_podcasts_populated(filter_ids: &[i32]) -> Result<bool, DataError> {
455 use crate::schema::shows::dsl::*;
456
457 let db = connection();
458 let con = db.get()?;
459
460 select(exists(shows.filter(id.ne_all(filter_ids))))
461 .get_result(&con)
462 .map_err(From::from)
463 }
464
465 /// Check if the `source` table contains any rows
466 ///
467 /// Return true if `source` table is populated.
is_source_populated(filter_ids: &[i32]) -> Result<bool, DataError>468 pub fn is_source_populated(filter_ids: &[i32]) -> Result<bool, DataError> {
469 use crate::schema::source::dsl::*;
470
471 let db = connection();
472 let con = db.get()?;
473
474 select(exists(source.filter(id.ne_all(filter_ids))))
475 .get_result(&con)
476 .map_err(From::from)
477 }
478
index_new_episodes(eps: &[NewEpisode]) -> Result<(), DataError>479 pub(crate) fn index_new_episodes(eps: &[NewEpisode]) -> Result<(), DataError> {
480 use crate::schema::episodes::dsl::*;
481 let db = connection();
482 let con = db.get()?;
483
484 diesel::insert_into(episodes)
485 .values(eps)
486 .execute(&*con)
487 .map_err(From::from)
488 .map(|_| ())
489 }
490
update_none_to_played_now(parent: &Show) -> Result<usize, DataError>491 pub fn update_none_to_played_now(parent: &Show) -> Result<usize, DataError> {
492 use crate::schema::episodes::dsl::*;
493 let db = connection();
494 let con = db.get()?;
495
496 let epoch_now = Utc::now().timestamp() as i32;
497 con.transaction(|| {
498 diesel::update(Episode::belonging_to(parent).filter(played.is_null()))
499 .set(played.eq(Some(epoch_now)))
500 .execute(&con)
501 .map_err(From::from)
502 })
503 }
504
505 #[cfg(test)]
506 mod tests {
507 use super::*;
508 use crate::database::*;
509 use crate::pipeline::pipeline;
510 use anyhow::Result;
511
512 #[test]
test_update_none_to_played_now() -> Result<()>513 fn test_update_none_to_played_now() -> Result<()> {
514 truncate_db()?;
515
516 let url = "https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
517 com/InterceptedWithJeremyScahill";
518 let source = Source::from_url(url)?;
519 let id = source.id();
520 let rt = tokio::runtime::Runtime::new()?;
521 rt.block_on(pipeline(vec![source]));
522 let pd = get_podcast_from_source_id(id)?;
523
524 let eps_num = get_pd_unplayed_episodes(&pd)?.len();
525 assert_ne!(eps_num, 0);
526
527 update_none_to_played_now(&pd)?;
528 let eps_num2 = get_pd_unplayed_episodes(&pd)?.len();
529 assert_eq!(eps_num2, 0);
530 Ok(())
531 }
532 }
533