termusiclib/podcast/db/
podcast_db.rs

1use chrono::{DateTime, Utc};
2use indoc::indoc;
3use rusqlite::{Connection, Row, named_params, params};
4
5use super::{PodcastDBId, convert_date};
6use crate::podcast::PodcastNoId;
7
8/// A struct representing a podcast feed in the database
9#[derive(Debug, Clone)]
10pub struct PodcastDB {
11    pub id: PodcastDBId,
12    pub title: String,
13    pub url: String,
14    pub description: Option<String>,
15    pub author: Option<String>,
16    pub explicit: Option<bool>,
17    pub last_checked: DateTime<Utc>,
18    pub image_url: Option<String>,
19}
20
21impl PodcastDB {
22    /// Try to convert a given row to a [`PodcastDB`] instance, using column names to resolve the values
23    pub fn try_from_row_named(row: &Row<'_>) -> Result<Self, rusqlite::Error> {
24        // NOTE: all the names in "get" below are the *column names* as defined in migrations/001.sql#table_podcasts (pseudo link)
25        let last_checked =
26            convert_date(&row.get("last_checked")).ok_or(rusqlite::Error::InvalidQuery)?;
27        Ok(PodcastDB {
28            id: row.get("id")?,
29            title: row.get("title")?,
30            url: row.get("url")?,
31            description: row.get("description")?,
32            author: row.get("author")?,
33            explicit: row.get("explicit")?,
34            last_checked,
35            image_url: row.get("image_url")?,
36        })
37    }
38}
39
40/// A struct representing a podcast feed in the database to be inserted
41///
42/// This is required as some fields are auto-generated by the database compared to [`PodcastDB`]
43#[derive(Debug, Clone)]
44pub struct PodcastDBInsertable<'a> {
45    // generated by the database
46    // pub id: PodcastDBId,
47    pub title: &'a str,
48    pub url: &'a str,
49    pub description: Option<&'a str>,
50    pub author: Option<&'a str>,
51    pub explicit: Option<bool>,
52    pub last_checked: DateTime<Utc>,
53    pub image_url: Option<&'a str>,
54}
55
56impl<'a> From<&'a PodcastNoId> for PodcastDBInsertable<'a> {
57    fn from(value: &'a PodcastNoId) -> Self {
58        Self {
59            title: &value.title,
60            url: &value.url,
61            description: value.description.as_deref(),
62            author: value.author.as_deref(),
63            explicit: value.explicit,
64            last_checked: value.last_checked,
65            image_url: value.image_url.as_deref(),
66        }
67    }
68}
69
70impl PodcastDBInsertable<'_> {
71    /// Insert the current [`PodcastDBInsertable`] into the `podcasts` table
72    #[inline]
73    pub fn insert_podcast(&self, con: &Connection) -> Result<usize, rusqlite::Error> {
74        let mut stmt = con.prepare_cached(indoc! {"
75            INSERT INTO podcasts (title, url, description, author, explicit, last_checked, image_url)
76            VALUES (:title, :url, :description, :author, :explicit, :last_checked, :image_url);
77        "})?;
78        stmt.execute(named_params![
79            ":title": self.title,
80            ":url": self.url,
81            ":description": self.description,
82            ":author": self.author,
83            ":explicit": self.explicit,
84            ":last_checked": self.last_checked.timestamp(),
85            ":image_url": self.image_url
86        ])
87    }
88
89    /// Update a given id with the current [`PodcastDBInsertable`] in the `podcasts` table
90    #[inline]
91    pub fn update_podcast(
92        &self,
93        id: PodcastDBId,
94        con: &Connection,
95    ) -> Result<usize, rusqlite::Error> {
96        let mut stmt = con.prepare_cached(indoc! {"
97            UPDATE podcasts SET title = :title, url = :url, description = :description,
98                author = :author, explicit = :explicit, last_checked = :last_checked
99            WHERE id = :id;
100        "})?;
101        stmt.execute(named_params![
102            ":title": self.title,
103            ":url": self.url,
104            ":description": self.description,
105            ":author": self.author,
106            ":explicit": self.explicit,
107            ":last_checked": self.last_checked.timestamp(),
108            ":id": id,
109        ])
110    }
111}
112
113/// Delete a podcast by id
114///
115/// This also deletes all associated episodes and files (not removing the actual files)!
116pub fn delete_podcast(id: PodcastDBId, con: &Connection) -> Result<usize, rusqlite::Error> {
117    // Note: Because of the foreign key constraints on `episodes`
118    // and `files` tables, all associated episodes for this podcast
119    // will also be deleted, and all associated file entries for
120    // those episodes as well.
121    let mut stmt = con.prepare_cached("DELETE FROM podcasts WHERE id = ?;")?;
122    stmt.execute(params![id])
123}