termusiclib/library_db/
track_db.rs

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