termusiclib/library_db/
track_db.rs

1use std::time::{Duration, UNIX_EPOCH};
2
3use rusqlite::{named_params, Connection, Row};
4
5use crate::track::Track;
6
7/// A struct representing a [`Track`](Track) in the database
8#[derive(Clone, Debug)]
9pub struct TrackDB {
10    pub id: u64,
11    pub artist: String,
12    pub title: String,
13    pub album: String,
14    pub genre: String,
15    pub file: String,
16    pub duration: Duration,
17    pub name: String,
18    pub ext: String,
19    pub directory: String,
20    pub last_modified: String,
21    pub last_position: Duration,
22}
23
24impl TrackDB {
25    /// Try to convert a given row to a [`TrackDB`] instance, expecting correct row order.
26    ///
27    /// Use [`Self::try_from_row_named`] if possible.
28    pub fn try_from_row_id(row: &Row<'_>) -> Result<Self, rusqlite::Error> {
29        let d_u64: u64 = row.get(6)?;
30        let last_position_u64: u64 = row.get(11)?;
31        Ok(TrackDB {
32            id: row.get(0)?,
33            artist: row.get(1)?,
34            title: row.get(2)?,
35            album: row.get(3)?,
36            genre: row.get(4)?,
37            file: row.get(5)?,
38            duration: Duration::from_secs(d_u64),
39            name: row.get(7)?,
40            ext: row.get(8)?,
41            directory: row.get(9)?,
42            last_modified: row.get(10)?,
43            last_position: Duration::from_secs(last_position_u64),
44        })
45    }
46
47    /// Try to convert a given row to a [`TrackDB`] instance, using column names to resolve the values
48    pub fn try_from_row_named(row: &Row<'_>) -> Result<Self, rusqlite::Error> {
49        // NOTE: all the names in "get" below are the *column names* as defined in migrations/002.sql#table_tracks (pseudo link)
50        let d_u64: u64 = row.get("duration")?;
51        let last_position_u64: u64 = row.get("last_position")?;
52        Ok(TrackDB {
53            id: row.get("id")?,
54            artist: row.get("artist")?,
55            title: row.get("title")?,
56            album: row.get("album")?,
57            genre: row.get("genre")?,
58            file: row.get("file")?,
59            duration: Duration::from_secs(d_u64),
60            name: row.get("name")?,
61            ext: row.get("ext")?,
62            directory: row.get("directory")?,
63            last_modified: row.get("last_modified")?,
64            last_position: Duration::from_secs(last_position_u64),
65        })
66    }
67}
68
69/// A struct representing a [`Track`](Track) in the database to be inserted
70///
71/// This is required as some fields are auto-generated by the database compared to [`TrackDB`]
72#[derive(Clone, Debug)]
73pub struct TrackDBInsertable<'a> {
74    // generated by the database
75    // pub id: u64,
76    pub artist: &'a str,
77    pub title: &'a str,
78    pub album: &'a str,
79    pub genre: &'a str,
80    pub file: &'a str,
81    pub duration: Duration,
82    pub name: &'a str,
83    pub ext: &'a str,
84    pub directory: &'a str,
85    pub last_modified: String,
86    pub last_position: Duration,
87}
88
89/// Constant strings for Unknown values
90pub mod const_unknown {
91    use crate::const_str;
92
93    const_str! {
94        UNKNOWN_ARTIST "Unknown Artist",
95        UNKNOWN_TITLE "Unknown Title",
96        UNKNOWN_ALBUM "empty",
97        UNKNOWN_GENRE "no type",
98        UNKNOWN_FILE "Unknown File",
99    }
100
101    // TODO: use this for database migration
102    /// This is the old string that was used as default for "artist" & "album"
103    ///
104    /// this value is currently unused, but this will stay here as a reminder until the database is migrated to use NULL values
105    #[allow(unused)]
106    pub const OLD_UNSUPPORTED: &str = "Unsupported?";
107
108    // NOTE: previously artist & album were `OLD_UNSUPPORTED`, but now they are `UNKNOWN_ARTIST` or `UNKNOWN_ALBUM`, these values will stay until the database is migrated to use NULL instead
109    // even after, it is likely the `UNKNOWN_` values will continue to exist for display purposes
110}
111use const_unknown::{UNKNOWN_ALBUM, UNKNOWN_ARTIST, UNKNOWN_FILE, UNKNOWN_GENRE, UNKNOWN_TITLE};
112
113impl<'a> From<&'a Track> for TrackDBInsertable<'a> {
114    fn from(value: &'a Track) -> Self {
115        Self {
116            artist: value.artist().unwrap_or(UNKNOWN_ARTIST),
117            title: value.title().unwrap_or(UNKNOWN_TITLE),
118            album: value.album().unwrap_or(UNKNOWN_ALBUM),
119            genre: value.genre().unwrap_or(UNKNOWN_GENRE),
120            file: value.file().unwrap_or(UNKNOWN_FILE),
121            duration: value.duration(),
122            name: value.name().unwrap_or_default(),
123            ext: value.ext().unwrap_or_default(),
124            directory: value.directory().unwrap_or_default(),
125            last_modified: value
126                .last_modified
127                .duration_since(UNIX_EPOCH)
128                .unwrap_or_default()
129                .as_secs()
130                .to_string(),
131            last_position: Duration::default(),
132        }
133    }
134}
135
136impl TrackDBInsertable<'_> {
137    /// Insert the current [`TrackDBInsertable`] into the `tracks` table
138    #[inline]
139    pub fn insert_track(&self, con: &Connection) -> Result<usize, rusqlite::Error> {
140        con.execute(
141            "INSERT INTO tracks (artist, title, album, genre, file, duration, name, ext, directory, last_modified, last_position) 
142            values (:artist, :title, :album, :genre, :file, :duration, :name, :ext, :directory, :last_modified, :last_position)",
143            named_params![
144                ":artist": &self.artist,
145                ":title": &self.title,
146                ":album": &self.album,
147                ":genre": &self.genre,
148                ":file": &self.file,
149                ":duration": &self.duration.as_secs(),
150                ":name": &self.name,
151                ":ext": &self.ext,
152                ":directory": &self.directory,
153                ":last_modified": &self.last_modified,
154                ":last_position": &self.last_position.as_secs().to_string(),
155            ],
156        )
157    }
158}
159
160/// Defined for types which could be indexed.
161/// Was made to allow generalization of indexing/search functions.
162///
163/// the required functions are generally the metadata you would find in an mp3 file.
164pub trait Indexable {
165    fn meta_file(&self) -> Option<&str>;
166    fn meta_title(&self) -> Option<&str>;
167    fn meta_album(&self) -> Option<&str>;
168    fn meta_artist(&self) -> Option<&str>;
169    fn meta_genre(&self) -> Option<&str>;
170    fn meta_duration(&self) -> Duration;
171}
172
173impl Indexable for Track {
174    fn meta_file(&self) -> Option<&str> {
175        self.file()
176    }
177    fn meta_title(&self) -> Option<&str> {
178        self.title()
179    }
180    fn meta_album(&self) -> Option<&str> {
181        self.album()
182    }
183    fn meta_artist(&self) -> Option<&str> {
184        self.artist()
185    }
186    fn meta_genre(&self) -> Option<&str> {
187        self.genre()
188    }
189    fn meta_duration(&self) -> Duration {
190        self.duration()
191    }
192}
193
194impl Indexable for TrackDB {
195    fn meta_file(&self) -> Option<&str> {
196        if self.file == UNKNOWN_FILE {
197            return None;
198        }
199        Some(&self.file)
200    }
201    fn meta_title(&self) -> Option<&str> {
202        if self.title == UNKNOWN_TITLE {
203            return None;
204        }
205        Some(&self.title)
206    }
207    fn meta_album(&self) -> Option<&str> {
208        if self.album == UNKNOWN_ALBUM {
209            return None;
210        }
211        Some(&self.album)
212    }
213    fn meta_artist(&self) -> Option<&str> {
214        if self.artist == UNKNOWN_ARTIST {
215            return None;
216        }
217        Some(&self.artist)
218    }
219    fn meta_genre(&self) -> Option<&str> {
220        if self.genre == UNKNOWN_GENRE {
221            return None;
222        }
223        Some(&self.genre)
224    }
225
226    fn meta_duration(&self) -> Duration {
227        self.duration
228    }
229}
230
231impl Indexable for &Track {
232    fn meta_file(&self) -> Option<&str> {
233        self.file()
234    }
235    fn meta_title(&self) -> Option<&str> {
236        self.title()
237    }
238    fn meta_album(&self) -> Option<&str> {
239        self.album()
240    }
241    fn meta_artist(&self) -> Option<&str> {
242        self.artist()
243    }
244    fn meta_genre(&self) -> Option<&str> {
245        self.genre()
246    }
247    fn meta_duration(&self) -> Duration {
248        self.duration()
249    }
250}
251
252impl Indexable for &TrackDB {
253    fn meta_file(&self) -> Option<&str> {
254        if self.file == UNKNOWN_FILE {
255            return None;
256        }
257        Some(&self.file)
258    }
259    fn meta_title(&self) -> Option<&str> {
260        if self.title == UNKNOWN_TITLE {
261            return None;
262        }
263        Some(&self.title)
264    }
265    fn meta_album(&self) -> Option<&str> {
266        if self.album == UNKNOWN_ALBUM {
267            return None;
268        }
269        Some(&self.album)
270    }
271    fn meta_artist(&self) -> Option<&str> {
272        if self.artist == UNKNOWN_ARTIST {
273            return None;
274        }
275        Some(&self.artist)
276    }
277    fn meta_genre(&self) -> Option<&str> {
278        if self.genre == UNKNOWN_GENRE {
279            return None;
280        }
281        Some(&self.genre)
282    }
283
284    fn meta_duration(&self) -> Duration {
285        self.duration
286    }
287}