termusiclib/new_database/
album_ops.rs

1use anyhow::{Result, bail};
2use indoc::{formatdoc, indoc};
3use rusqlite::{Connection, OptionalExtension, Row, named_params};
4
5use crate::new_database::{
6    Integer,
7    artist_ops::{ArtistRead, common_row_to_artistread},
8};
9
10/// Count all rows currently in the `albums` database
11pub fn count_all_albums(conn: &Connection) -> Result<Integer> {
12    let count = conn.query_row("SELECT COUNT(id) FROM albums;", [], |v| v.get(0))?;
13
14    Ok(count)
15}
16
17/// Count all rows currently in the `albums_artists` database
18#[cfg(test)]
19pub(super) fn count_all_albums_artist_mapping(conn: &Connection) -> Result<Integer> {
20    let count = conn.query_row("SELECT COUNT(album) FROM albums_artists;", [], |v| v.get(0))?;
21
22    Ok(count)
23}
24
25#[derive(Debug, Clone, PartialEq)]
26pub struct AlbumRead {
27    pub id: Integer,
28
29    pub title: String,
30    pub artist_display: String,
31
32    pub artists: Vec<ArtistRead>,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum RowOrdering {
37    IdAsc,
38    IdDesc,
39}
40
41impl RowOrdering {
42    /// Represent it as the data for a `ORDER BY` clause.
43    fn as_sql(self) -> &'static str {
44        match self {
45            RowOrdering::IdAsc => "albums.id ASC",
46            RowOrdering::IdDesc => "albums.id DESC",
47        }
48    }
49}
50
51/// Get all the Albums currently stored in the database with all the important data.
52///
53/// # Panics
54///
55/// If the database schema does not match what is expected.
56pub fn get_all_albums(conn: &Connection, order: RowOrdering) -> Result<Vec<AlbumRead>> {
57    let stmt = formatdoc! {"
58        SELECT albums.id as album_id, albums.title, albums.artist_display
59        FROM albums
60        ORDER BY {};
61        ",
62        order.as_sql()
63    };
64    let mut stmt = conn.prepare(&stmt)?;
65
66    let result: Vec<AlbumRead> = stmt
67        .query_map(named_params! {}, |row| {
68            let album_read = common_row_to_album(conn, row);
69
70            Ok(album_read)
71        })?
72        .collect::<Result<Vec<_>, rusqlite::Error>>()?;
73
74    Ok(result)
75}
76
77/// Get all the Albums that match `like`.
78///
79/// # Panics
80///
81/// If the database schema does not match what is expected.
82pub fn get_all_albums_like(
83    conn: &Connection,
84    like: &str,
85    order: RowOrdering,
86) -> Result<Vec<AlbumRead>> {
87    let stmt = formatdoc! {"
88        SELECT albums.id as album_id, albums.title, albums.artist_display
89        FROM albums
90        WHERE albums.title LIKE :like
91        ORDER BY {};
92        ",
93        order.as_sql()
94    };
95    let mut stmt = conn.prepare(&stmt)?;
96
97    let result: Vec<AlbumRead> = stmt
98        .query_map(named_params! {":like": like}, |row| {
99            let album_read = common_row_to_album(conn, row);
100
101            Ok(album_read)
102        })?
103        .collect::<Result<Vec<_>, rusqlite::Error>>()?;
104
105    Ok(result)
106}
107
108/// Get all the artists for a given album.
109///
110/// # Panics
111///
112/// If the database schema does not match what is expected.
113// maybe this should be in "artist_ops" instead?
114pub fn get_all_artists_for_album(conn: &Connection, album_id: Integer) -> Result<Vec<ArtistRead>> {
115    let mut stmt = conn.prepare(indoc! {"
116        SELECT artists.id AS artist_id, artists.artist FROM artists
117        INNER JOIN albums_artists ON albums_artists.album=(:album_id)
118        WHERE artists.id=albums_artists.artist;
119    "})?;
120
121    let result: Vec<ArtistRead> = stmt
122        .query_map(named_params! {":album_id": album_id}, |row| {
123            let artist_read = common_row_to_artistread(row);
124
125            Ok(artist_read)
126        })?
127        .collect::<Result<Vec<_>, rusqlite::Error>>()?;
128
129    Ok(result)
130}
131
132/// Check if a entry for the given `album` exists.
133///
134/// # Panics
135///
136/// If sqlite somehow does not return what is expected.
137pub fn album_exists(conn: &Connection, album: &str) -> Result<bool> {
138    if album.trim().is_empty() {
139        bail!("Given album is empty!");
140    }
141
142    let mut stmt = conn.prepare(indoc! {"
143        SELECT albums.id
144        FROM albums
145        WHERE albums.title=:album_name;
146    "})?;
147
148    let exists = stmt.exists(named_params! {":album_name": album})?;
149
150    Ok(exists)
151}
152
153/// Common function that converts a well-known named row to a [`AlbumRead`].
154///
155/// For row names look at [`get_all_albums`].
156fn common_row_to_album(conn: &Connection, row: &Row<'_>) -> AlbumRead {
157    let id = row.get("album_id").unwrap();
158    let title = row.get("title").unwrap();
159    let artist_display = row.get("artist_display").unwrap();
160
161    let artists = match get_all_artists_for_album(conn, id) {
162        Ok(v) => v,
163        Err(err) => {
164            warn!("Error resolving artists for a album: {err:#?}");
165            Vec::new()
166        }
167    };
168
169    AlbumRead {
170        id,
171        title,
172        artist_display,
173        artists,
174    }
175}
176
177/// Remove all albums-artists mappings for the given album.
178///
179/// Returns the number of deleted rows. Will return `Ok(0)` if the query did not do anything.
180///
181/// # Panics
182///
183/// If the database schema does not match what is expected.
184pub fn delete_albums_artist_mapping_for(conn: &Connection, album_id: Integer) -> Result<usize> {
185    let mut stmt = conn.prepare_cached(indoc! {"
186        DELETE FROM albums_artists
187        WHERE albums_artists.album=:album_id;
188    "})?;
189
190    let affected = stmt
191        .execute(named_params! {":album_id": album_id})
192        .optional()?
193        .unwrap_or_default();
194
195    Ok(affected)
196}
197
198/// Remove all albums that are unreferenced from `tracks`.
199///
200/// Returns the number of deleted rows. Will return `Ok(0)` if the query did not do anything.
201///
202/// # Panics
203///
204/// If the database schema does not match what is expected.
205pub fn delete_all_unreferenced_albums(conn: &Connection) -> Result<usize> {
206    // the following query is likely very slow compared to other queries
207    let mut stmt = conn.prepare_cached(indoc! {"
208        DELETE FROM albums
209        WHERE albums.id NOT IN (
210            SELECT tracks.album FROM tracks
211            WHERE tracks.album IS NOT NULL
212        );
213    "})?;
214
215    let affected = stmt
216        .execute(named_params! {})
217        .optional()?
218        .unwrap_or_default();
219
220    Ok(affected)
221}
222
223#[cfg(test)]
224mod tests {
225    use std::{path::Path, time::Duration};
226
227    use either::Either;
228
229    use crate::{
230        new_database::{
231            album_insert::AlbumInsertable,
232            album_ops::{
233                AlbumRead, RowOrdering, album_exists, count_all_albums,
234                count_all_albums_artist_mapping, delete_albums_artist_mapping_for,
235                delete_all_unreferenced_albums, get_all_albums, get_all_albums_like,
236                get_all_artists_for_album,
237            },
238            artist_insert::ArtistInsertable,
239            artist_ops::ArtistRead,
240            test_utils::{gen_database, test_path},
241            track_insert::TrackInsertable,
242        },
243        track::TrackMetadata,
244    };
245
246    #[test]
247    fn artists_for_album() {
248        let db = gen_database();
249
250        let album = AlbumInsertable {
251            title: "AlbumA",
252            artist_display: "ArtistA",
253            artists: vec![
254                Either::Left(ArtistInsertable { artist: "ArtistA" }.into()),
255                Either::Left(ArtistInsertable { artist: "ArtistB" }.into()),
256            ],
257        };
258        let album_id = album.try_insert_or_update(&db.get_connection()).unwrap();
259
260        let mut all_artists: Vec<String> =
261            get_all_artists_for_album(&db.get_connection(), album_id)
262                .unwrap()
263                .into_iter()
264                .map(|v| v.name)
265                .collect();
266        // just making sure they are consistently ordered
267        all_artists.sort();
268
269        assert_eq!(all_artists, &["ArtistA", "ArtistB"]);
270    }
271
272    #[test]
273    fn all_albums() {
274        let db = gen_database();
275
276        let album = AlbumInsertable {
277            title: "AlbumA",
278            artist_display: "ArtistA",
279            artists: vec![Either::Left(ArtistInsertable { artist: "ArtistA" }.into())],
280        };
281        let _album_id = album.try_insert_or_update(&db.get_connection()).unwrap();
282
283        let album = AlbumInsertable {
284            title: "AlbumB",
285            artist_display: "ArtistB",
286            artists: vec![Either::Left(ArtistInsertable { artist: "ArtistB" }.into())],
287        };
288        let _album_id = album.try_insert_or_update(&db.get_connection()).unwrap();
289
290        let all_albums = get_all_albums(&db.get_connection(), RowOrdering::IdAsc).unwrap();
291
292        assert_eq!(
293            all_albums,
294            &[
295                AlbumRead {
296                    id: 1,
297                    title: "AlbumA".to_string(),
298                    artist_display: "ArtistA".to_string(),
299                    artists: vec![ArtistRead {
300                        id: 1,
301                        name: "ArtistA".to_string()
302                    }]
303                },
304                AlbumRead {
305                    id: 2,
306                    title: "AlbumB".to_string(),
307                    artist_display: "ArtistB".to_string(),
308                    artists: vec![ArtistRead {
309                        id: 2,
310                        name: "ArtistB".to_string()
311                    }]
312                },
313            ]
314        );
315    }
316
317    #[test]
318    fn all_albums_like() {
319        let db = gen_database();
320
321        let album = AlbumInsertable {
322            title: "AlbumA",
323            artist_display: "ArtistA",
324            artists: vec![Either::Left(ArtistInsertable { artist: "ArtistA" }.into())],
325        };
326        let _album_id = album.try_insert_or_update(&db.get_connection()).unwrap();
327
328        let album = AlbumInsertable {
329            title: "AlbumB",
330            artist_display: "ArtistB",
331            artists: vec![Either::Left(ArtistInsertable { artist: "ArtistB" }.into())],
332        };
333        let _album_id = album.try_insert_or_update(&db.get_connection()).unwrap();
334
335        let all_albums =
336            get_all_albums_like(&db.get_connection(), "%albuma%", RowOrdering::IdAsc).unwrap();
337
338        assert_eq!(
339            all_albums,
340            &[AlbumRead {
341                id: 1,
342                title: "AlbumA".to_string(),
343                artist_display: "ArtistA".to_string(),
344                artists: vec![ArtistRead {
345                    id: 1,
346                    name: "ArtistA".to_string()
347                }]
348            },]
349        );
350    }
351
352    #[test]
353    fn exists() {
354        let db = gen_database();
355
356        let album = AlbumInsertable {
357            title: "AlbumA",
358            artist_display: "ArtistA",
359            artists: vec![Either::Left(ArtistInsertable { artist: "ArtistA" }.into())],
360        };
361        let _album_id = album.try_insert_or_update(&db.get_connection()).unwrap();
362
363        let res = album_exists(&db.get_connection(), "AlbumA").unwrap();
364
365        assert!(res);
366
367        let res = album_exists(&db.get_connection(), "AlbumB").unwrap();
368
369        assert!(!res);
370    }
371
372    #[test]
373    fn delete_albums_artists_mapping() {
374        let db = gen_database();
375
376        let album = AlbumInsertable {
377            title: "AlbumA",
378            artist_display: "ArtistA feat. ArtistB",
379            artists: vec![
380                Either::Left(ArtistInsertable { artist: "ArtistA" }.into()),
381                Either::Left(ArtistInsertable { artist: "ArtistB" }.into()),
382            ],
383        };
384        let album_id_a = album.try_insert_or_update(&db.get_connection()).unwrap();
385
386        let album = AlbumInsertable {
387            title: "AlbumB",
388            artist_display: "ArtistA feat. ArtistB",
389            artists: vec![
390                Either::Left(ArtistInsertable { artist: "ArtistA" }.into()),
391                Either::Left(ArtistInsertable { artist: "ArtistB" }.into()),
392            ],
393        };
394        let _album_id = album.try_insert_or_update(&db.get_connection()).unwrap();
395
396        let mapping_counts = count_all_albums_artist_mapping(&db.get_connection()).unwrap();
397
398        assert_eq!(mapping_counts, 4);
399
400        let affected = delete_albums_artist_mapping_for(&db.get_connection(), album_id_a).unwrap();
401
402        assert_eq!(affected, 2);
403
404        let mapping_counts = count_all_albums_artist_mapping(&db.get_connection()).unwrap();
405
406        assert_eq!(mapping_counts, 2);
407    }
408
409    #[test]
410    fn delete_unreferenced_albums() {
411        let db = gen_database();
412
413        let metadata = TrackMetadata {
414            album: Some("AlbumA".to_string()),
415            album_artist: Some("ArtistA".to_string()),
416            album_artists: Some(vec!["ArtistA".to_string()]),
417            artist: Some("ArtistA".to_string()),
418            artists: Some(vec!["ArtistA".to_string()]),
419            title: Some("FileA1".to_string()),
420            duration: Some(Duration::from_secs(10)),
421            ..Default::default()
422        };
423        let path = &test_path(Path::new("/somewhere/fileA1.ext"));
424        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
425        let _ = insertable
426            .try_insert_or_update(&db.get_connection())
427            .unwrap();
428
429        let metadata = TrackMetadata {
430            album: Some("AlbumB".to_string()),
431            album_artist: Some("ArtistA".to_string()),
432            album_artists: Some(vec!["ArtistA".to_string()]),
433            artist: Some("ArtistA".to_string()),
434            artists: Some(vec!["ArtistA".to_string()]),
435            title: Some("FileB1".to_string()),
436            duration: Some(Duration::from_secs(10)),
437            ..Default::default()
438        };
439        let path = &test_path(Path::new("/somewhere/fileB1.ext"));
440        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
441        let _ = insertable
442            .try_insert_or_update(&db.get_connection())
443            .unwrap();
444
445        let albums = count_all_albums(&db.get_connection()).unwrap();
446
447        assert_eq!(albums, 2);
448
449        let affected = delete_all_unreferenced_albums(&db.get_connection()).unwrap();
450
451        assert_eq!(affected, 0);
452
453        let metadata = TrackMetadata {
454            album: Some("AlbumA".to_string()),
455            album_artist: Some("ArtistA".to_string()),
456            album_artists: Some(vec!["ArtistA".to_string()]),
457            artist: Some("ArtistA".to_string()),
458            artists: Some(vec!["ArtistA".to_string()]),
459            title: Some("FileB1".to_string()),
460            duration: Some(Duration::from_secs(10)),
461            ..Default::default()
462        };
463        let path = &test_path(Path::new("/somewhere/fileB1.ext"));
464        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
465        let _ = insertable
466            .try_insert_or_update(&db.get_connection())
467            .unwrap();
468
469        let albums = count_all_albums(&db.get_connection()).unwrap();
470
471        assert_eq!(albums, 2);
472
473        let affected = delete_all_unreferenced_albums(&db.get_connection()).unwrap();
474
475        assert_eq!(affected, 1);
476
477        let albums = count_all_albums(&db.get_connection()).unwrap();
478
479        assert_eq!(albums, 1);
480    }
481}