termusiclib/new_database/
track_ops.rs

1use std::{
2    ffi::OsString,
3    path::{Path, PathBuf},
4    time::Duration,
5};
6
7use anyhow::{Result, bail};
8use either::Either;
9use indoc::{formatdoc, indoc};
10use rusqlite::{Connection, OptionalExtension, Row, ToSql, named_params};
11
12use crate::new_database::{
13    artist_ops::{ArtistRead, common_row_to_artistread},
14    track_insert::{path_to_db_comp, validate_path},
15};
16
17use super::Integer;
18
19/// Count all rows currently in the `tracks` database
20pub fn count_all_tracks(conn: &Connection) -> Result<Integer> {
21    let count = conn.query_row("SELECT COUNT(id) FROM tracks;", [], |v| v.get(0))?;
22
23    Ok(count)
24}
25
26/// Count all rows currently in the `tracks_metadata` database
27pub fn count_all_track_metadata(conn: &Connection) -> Result<Integer> {
28    let count = conn.query_row("SELECT COUNT(track) FROM tracks_metadata;", [], |v| {
29        v.get(0)
30    })?;
31
32    Ok(count)
33}
34
35/// Count all rows currently in the `tracks_artists` database
36#[cfg(test)]
37pub(super) fn count_all_track_artist_mapping(conn: &Connection) -> Result<Integer> {
38    let count = conn.query_row("SELECT COUNT(track) FROM tracks_artists;", [], |v| v.get(0))?;
39
40    Ok(count)
41}
42
43/// The lowest information required for a [`TrackRead`] to identify a Album.
44#[derive(Debug, Clone, PartialEq)]
45pub struct AlbumRead {
46    pub id: Integer,
47
48    pub title: String,
49}
50
51#[derive(Debug, Clone, PartialEq)]
52pub struct TrackRead {
53    pub id: Integer,
54
55    // Track identifier
56    pub file_dir: PathBuf,
57    pub file_stem: OsString,
58    pub file_ext: OsString,
59
60    // Direct data on `tracks`
61    pub duration: Option<Duration>,
62    pub last_position: Option<Duration>,
63    /// Either a reference to a insertable to look-up or a direct integer to use as reference into `albums`.
64    pub album: Option<AlbumRead>,
65
66    // Data on `tracks_metadata`
67    pub title: Option<String>,
68    pub genre: Option<String>,
69    pub artist_display: Option<String>,
70
71    // mapped metadata
72    pub artists: Vec<ArtistRead>,
73}
74
75impl TrackRead {
76    /// Convert all the `file_` values to a single path.
77    #[must_use]
78    pub fn as_pathbuf(&self) -> PathBuf {
79        let mut path = self.file_dir.clone();
80        let mut file_name = self.file_stem.clone();
81        file_name.reserve_exact(self.file_ext.len() + 1);
82        file_name.push(".");
83        file_name.push(&self.file_ext);
84        path.push(file_name);
85
86        path
87    }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum RowOrdering {
92    IdAsc,
93    IdDesc,
94    AddedAsc,
95    AddedDesc,
96}
97
98impl RowOrdering {
99    /// Represent it as the data for a `ORDER BY` clause.
100    fn as_sql(self) -> &'static str {
101        match self {
102            RowOrdering::IdAsc => "tracks.id ASC",
103            RowOrdering::IdDesc => "tracks.id DESC",
104            RowOrdering::AddedAsc => "tracks.added_at ASC",
105            RowOrdering::AddedDesc => "tracks.added_at DESC",
106        }
107    }
108}
109
110/// Get all the Tracks currently stored in the database with all the important data.
111///
112/// # Panics
113///
114/// If the database schema does not match what is expected.
115pub fn get_all_tracks(conn: &Connection, order: RowOrdering) -> Result<Vec<TrackRead>> {
116    let stmt = formatdoc! {"
117        SELECT 
118            tracks.id AS track_id, tracks.file_dir, tracks.file_stem, tracks.file_ext, tracks.duration, tracks.last_position,
119            tracks_metadata.title AS track_title, tracks_metadata.artist_display, tracks_metadata.genre,
120            albums.id AS album_id, albums.title AS album_title
121        FROM tracks
122        LEFT JOIN tracks_metadata ON tracks.id = tracks_metadata.track
123        LEFT JOIN albums ON tracks.album = albums.id
124        ORDER BY {};
125        ",
126        order.as_sql()
127    };
128    let mut stmt = conn.prepare(&stmt)?;
129
130    let result: Vec<TrackRead> = stmt
131        .query_map(named_params! {}, |row| {
132            let trackread = common_row_to_trackread(conn, row);
133
134            Ok(trackread)
135        })?
136        .collect::<Result<Vec<_>, rusqlite::Error>>()?;
137
138    Ok(result)
139}
140
141/// Get all the artists for a given track.
142///
143/// # Panics
144///
145/// If the database schema does not match what is expected.
146// maybe this should be in "artist_ops" instead?
147pub fn get_all_artists_for_track(conn: &Connection, track_id: Integer) -> Result<Vec<ArtistRead>> {
148    let mut stmt = conn.prepare(indoc! {"
149        SELECT artists.id AS artist_id, artists.artist FROM artists
150        INNER JOIN tracks_artists ON tracks_artists.track=:track_id
151        WHERE artists.id=tracks_artists.artist;
152    "})?;
153
154    let result: Vec<ArtistRead> = stmt
155        .query_map(named_params! {":track_id": track_id}, |row| {
156            let artist_read = common_row_to_artistread(row);
157
158            Ok(artist_read)
159        })?
160        .collect::<Result<Vec<_>, rusqlite::Error>>()?;
161
162    Ok(result)
163}
164
165/// Get the `last_position` for the given `track`.
166///
167/// # Panics
168///
169/// If the database schema does not match what is expected.
170pub fn get_last_position(conn: &Connection, track: &Path) -> Result<Option<Duration>> {
171    let (file_dir, file_stem, file_ext) = path_to_db_comp(track)?;
172    let file_dir = file_dir.to_string_lossy();
173    let file_stem = file_stem.to_string_lossy();
174    let file_ext = file_ext.to_string_lossy();
175
176    let mut stmt = conn.prepare_cached(indoc!{"
177        SELECT last_position FROM tracks
178        WHERE tracks.file_dir=:file_dir AND tracks.file_stem=:file_stem AND tracks.file_ext=:file_ext;
179    "})?;
180
181    let result: Option<Integer> = stmt.query_row(
182        named_params! {":file_dir": file_dir, ":file_stem": file_stem, ":file_ext": file_ext},
183        |row| row.get(0),
184    )?;
185
186    let last_position = result.map(|v: Integer| {
187        let int = u64::try_from(v.max(0)).unwrap();
188        Duration::from_secs(int)
189    });
190
191    Ok(last_position)
192}
193
194/// Set the `last_positon` for the given `track`.
195pub fn set_last_position(conn: &Connection, track: &Path, to: Option<Duration>) -> Result<()> {
196    let (file_dir, file_stem, file_ext) = path_to_db_comp(track)?;
197    let file_dir = file_dir.to_string_lossy();
198    let file_stem = file_stem.to_string_lossy();
199    let file_ext = file_ext.to_string_lossy();
200
201    let last_position = to.map(|v| v.as_secs());
202
203    let mut stmt = conn.prepare_cached(indoc!{"
204        UPDATE tracks SET last_position=:last_position
205        WHERE tracks.file_dir=:file_dir AND tracks.file_stem=:file_stem AND tracks.file_ext=:file_ext;
206    "})?;
207
208    let affected = stmt.execute(named_params! {":file_dir": file_dir, ":file_stem": file_stem, ":file_ext": file_ext, ":last_position": last_position})?;
209
210    // update would otherwise fail silently
211    if affected == 0 {
212        bail!("Track not found");
213    }
214
215    Ok(())
216}
217
218/// Get all tracks associated with the given album.
219///
220/// # Panics
221///
222/// If the database schema does not match what is expected.
223pub fn get_tracks_from_album(
224    conn: &Connection,
225    album_title: &str,
226    album_artist: &str,
227    order: RowOrdering,
228) -> Result<Vec<TrackRead>> {
229    let stmt = formatdoc! {"
230        SELECT 
231            tracks.id AS track_id, tracks.file_dir, tracks.file_stem, tracks.file_ext, tracks.duration, tracks.last_position,
232            tracks_metadata.title AS track_title, tracks_metadata.artist_display, tracks_metadata.genre,
233            albums.id AS album_id, albums.title AS album_title
234        FROM tracks
235        LEFT JOIN tracks_metadata ON tracks.id=tracks_metadata.track
236        LEFT JOIN albums ON tracks.album = albums.id
237        WHERE albums.title=:album_title AND albums.artist_display=:album_artist
238        ORDER BY {};
239        ",
240        order.as_sql()
241    };
242    let mut stmt = conn.prepare(&stmt)?;
243
244    let result: Vec<TrackRead> = stmt
245        .query_map(
246            named_params! {":album_title": album_title, ":album_artist": album_artist},
247            |row| {
248                let trackread = common_row_to_trackread(conn, row);
249
250                Ok(trackread)
251            },
252        )?
253        .collect::<Result<Vec<_>, rusqlite::Error>>()?;
254
255    Ok(result)
256}
257
258/// Get all tracks associated with the given artist.
259///
260/// # Panics
261///
262/// If the database schema does not match what is expected.
263pub fn get_tracks_from_artist(
264    conn: &Connection,
265    artist: &str,
266    order: RowOrdering,
267) -> Result<Vec<TrackRead>> {
268    let stmt = formatdoc! {"
269        SELECT 
270            tracks.id AS track_id, tracks.file_dir, tracks.file_stem, tracks.file_ext, tracks.duration, tracks.last_position,
271            tracks_metadata.title AS track_title, tracks_metadata.artist_display, tracks_metadata.genre,
272            albums.id AS album_id, albums.title AS album_title
273        FROM tracks
274        LEFT JOIN tracks_metadata ON tracks.id=tracks_metadata.track
275        LEFT JOIN albums ON tracks.album = albums.id
276        INNER JOIN tracks_artists ON tracks.id = tracks_artists.track
277        INNER JOIN artists ON artists.id=tracks_artists.artist
278        WHERE artists.artist=:artist
279        ORDER BY {};
280        ",
281        order.as_sql()
282    };
283    let mut stmt = conn.prepare(&stmt)?;
284
285    let result: Vec<TrackRead> = stmt
286        .query_map(named_params! {":artist": artist}, |row| {
287            let trackread = common_row_to_trackread(conn, row);
288
289            Ok(trackread)
290        })?
291        .collect::<Result<Vec<_>, rusqlite::Error>>()?;
292
293    Ok(result)
294}
295
296/// Get all tracks associated with a genre.
297///
298/// Note `None` will use `IS NULL` to find all tracks without a genre.
299///
300/// # Panics
301///
302/// If the database schema does not match what is expected.
303pub fn get_tracks_from_genre(
304    conn: &Connection,
305    genre: Option<&str>,
306    order: RowOrdering,
307) -> Result<Vec<TrackRead>> {
308    let (where_clause, params): (&str, &[(&str, &str)]) = if let Some(genre) = genre {
309        ("tracks_metadata.genre=:genre", &[(":genre", genre)])
310    } else {
311        ("tracks_metadata.genre IS NULL", &[])
312    };
313
314    let stmt = formatdoc! {"
315        SELECT
316            tracks.id AS track_id, tracks.file_dir, tracks.file_stem, tracks.file_ext, tracks.duration, tracks.last_position,
317            tracks_metadata.title AS track_title, tracks_metadata.artist_display, tracks_metadata.genre,
318            albums.id AS album_id, albums.title AS album_title
319        FROM tracks
320        INNER JOIN tracks_metadata ON tracks.id=tracks_metadata.track
321        LEFT JOIN albums ON tracks.album = albums.id
322        WHERE {where_clause}
323        ORDER BY {};
324        ",
325        order.as_sql()
326    };
327    let mut stmt = conn.prepare(&stmt)?;
328
329    let result: Vec<TrackRead> = stmt
330        .query_map(params, |row| {
331            let trackread = common_row_to_trackread(conn, row);
332
333            Ok(trackread)
334        })?
335        .collect::<Result<Vec<_>, rusqlite::Error>>()?;
336
337    Ok(result)
338}
339
340/// Get all tracks associated with the given directory.
341///
342/// # Panics
343///
344/// If the database schema does not match what is expected.
345pub fn get_tracks_from_directory(
346    conn: &Connection,
347    dir: &Path,
348    order: RowOrdering,
349) -> Result<Vec<TrackRead>> {
350    validate_path(dir)?;
351    let dir = dir.to_string_lossy();
352
353    let stmt = formatdoc! {"
354        SELECT 
355            tracks.id AS track_id, tracks.file_dir, tracks.file_stem, tracks.file_ext, tracks.duration, tracks.last_position,
356            tracks_metadata.title AS track_title, tracks_metadata.artist_display, tracks_metadata.genre,
357            albums.id AS album_id, albums.title AS album_title
358        FROM tracks
359        LEFT JOIN tracks_metadata ON tracks.id=tracks_metadata.track
360        LEFT JOIN albums ON tracks.album = albums.id
361        WHERE tracks.file_dir=:dir
362        ORDER BY {};
363        ",
364        order.as_sql()
365    };
366    let mut stmt = conn.prepare(&stmt)?;
367
368    let result: Vec<TrackRead> = stmt
369        .query_map(named_params! {":dir": dir}, |row| {
370            let trackread = common_row_to_trackread(conn, row);
371
372            Ok(trackread)
373        })?
374        .collect::<Result<Vec<_>, rusqlite::Error>>()?;
375
376    Ok(result)
377}
378
379/// Get all tracks that match a genre `like`.
380///
381/// # Panics
382///
383/// If the database schema does not match what is expected.
384pub fn get_tracks_from_genre_like(
385    conn: &Connection,
386    genre_like: &str,
387    order: RowOrdering,
388) -> Result<Vec<TrackRead>> {
389    let stmt = formatdoc! {"
390        SELECT
391            tracks.id AS track_id, tracks.file_dir, tracks.file_stem, tracks.file_ext, tracks.duration, tracks.last_position,
392            tracks_metadata.title AS track_title, tracks_metadata.artist_display, tracks_metadata.genre,
393            albums.id AS album_id, albums.title AS album_title
394        FROM tracks
395        INNER JOIN tracks_metadata ON tracks.id = tracks_metadata.track
396        LEFT JOIN albums ON tracks.album = albums.id
397        WHERE tracks_metadata.genre LIKE :genre_like
398        ORDER BY {};
399        ",
400        order.as_sql()
401    };
402    let mut stmt = conn.prepare(&stmt)?;
403
404    let result: Vec<TrackRead> = stmt
405        .query_map(named_params! {":genre_like": genre_like}, |row| {
406            let trackread = common_row_to_trackread(conn, row);
407
408            Ok(trackread)
409        })?
410        .collect::<Result<Vec<_>, rusqlite::Error>>()?;
411
412    Ok(result)
413}
414
415/// Get all tracks associated with a genre.
416///
417/// # Panics
418///
419/// If the database schema does not match what is expected.
420pub fn get_track_from_path(conn: &Connection, path: &Path) -> Result<TrackRead> {
421    let (file_dir, file_stem, file_ext) = path_to_db_comp(path)?;
422    let file_dir = file_dir.to_string_lossy();
423    let file_stem = file_stem.to_string_lossy();
424    let file_ext = file_ext.to_string_lossy();
425
426    let mut stmt = conn.prepare(indoc! {"
427        SELECT
428            tracks.id AS track_id, tracks.file_dir, tracks.file_stem, tracks.file_ext, tracks.duration, tracks.last_position,
429            tracks_metadata.title AS track_title, tracks_metadata.artist_display, tracks_metadata.genre,
430            albums.id AS album_id, albums.title AS album_title
431        FROM tracks
432        INNER JOIN tracks_metadata ON tracks.id=tracks_metadata.track
433        LEFT JOIN albums ON tracks.album = albums.id
434        WHERE tracks.file_dir=:file_dir AND tracks.file_stem=:file_stem AND tracks.file_ext=:file_ext;
435        ",
436    })?;
437
438    let result: TrackRead = stmt.query_row(
439        named_params! {":file_dir": file_dir, ":file_stem": file_stem, ":file_ext": file_ext},
440        |row| {
441            let trackread = common_row_to_trackread(conn, row);
442
443            Ok(trackread)
444        },
445    )?;
446
447    Ok(result)
448}
449
450/// Common function that converts a well-known named row to a [`TrackRead`].
451///
452/// For row names look at [`get_all_tracks`].
453fn common_row_to_trackread(conn: &Connection, row: &Row<'_>) -> TrackRead {
454    let file_dir = row
455        .get("file_dir")
456        .map(|v: String| PathBuf::from(v))
457        .unwrap();
458    let file_stem = row
459        .get("file_stem")
460        .map(|v: String| OsString::from(v))
461        .unwrap();
462    let file_ext = row
463        .get("file_ext")
464        .map(|v: String| OsString::from(v))
465        .unwrap();
466    let id = row.get("track_id").unwrap();
467
468    let duration = row.get("duration").ok().map(|v: Integer| {
469        let int = u64::try_from(v.max(0)).unwrap();
470        Duration::from_secs(int)
471    });
472    let last_position = row.get("last_position").ok().map(|v: Integer| {
473        let int = u64::try_from(v.max(0)).unwrap();
474        Duration::from_secs(int)
475    });
476    let title = row.get("track_title").unwrap_or_default();
477    let genre = row.get("genre").unwrap_or_default();
478    let artist_display = row.get("artist_display").unwrap_or_default();
479
480    let album_id = row.get("album_id").ok();
481    let album_title = row.get("album_title").ok();
482
483    let album = if let (Some(album_id), Some(album_title)) = (album_id, album_title) {
484        Some(AlbumRead {
485            id: album_id,
486            title: album_title,
487        })
488    } else {
489        None
490    };
491
492    let artists = match get_all_artists_for_track(conn, id) {
493        Ok(v) => v,
494        Err(err) => {
495            warn!("Error resolving artists for a track: {err:#?}");
496            Vec::new()
497        }
498    };
499
500    TrackRead {
501        id,
502        file_dir,
503        file_stem,
504        file_ext,
505        duration,
506        last_position,
507        album,
508        title,
509        genre,
510        artist_display,
511        artists,
512    }
513}
514
515/// Check if a entry for the given `track` exists.
516///
517/// # Panics
518///
519/// If sqlite somehow does not return what is expected.
520pub fn track_exists(conn: &Connection, track: &Path) -> Result<bool> {
521    let (file_dir, file_stem, file_ext) = path_to_db_comp(track)?;
522    let file_dir = file_dir.to_string_lossy();
523    let file_stem = file_stem.to_string_lossy();
524    let file_ext = file_ext.to_string_lossy();
525
526    let mut stmt = conn.prepare(indoc!{"
527        SELECT tracks.id
528        FROM tracks
529        WHERE tracks.file_dir=:file_dir AND tracks.file_stem=:file_stem AND tracks.file_ext=:file_ext;
530    "})?;
531
532    let exists = stmt.exists(
533        named_params! {":file_dir": file_dir, ":file_stem": file_stem, ":file_ext": file_ext},
534    )?;
535
536    Ok(exists)
537}
538
539/// Get all genres that are currently in the database.
540/// Note that `NULL` will be mapped to `[unknown]`
541///
542/// # Panics
543///
544/// If sqlite somehow does not return what is expected.
545pub fn all_distinct_genres(conn: &Connection) -> Result<Vec<String>> {
546    let mut stmt = conn.prepare(indoc! {"
547        SELECT DISTINCT tracks_metadata.genre
548        FROM tracks_metadata
549        ",
550    })?;
551
552    let result: Vec<String> = stmt
553        .query_map(named_params! {}, |row| {
554            let res = row.get(0).unwrap_or_else(|_| "[unknown]".to_string());
555            Ok(res)
556        })?
557        .collect::<Result<Vec<_>, rusqlite::Error>>()?;
558
559    Ok(result)
560}
561
562/// Get all distinct directories.
563///
564/// # Panics
565///
566/// If sqlite somehow does not return what is expected.
567pub fn all_distinct_directories(conn: &Connection) -> Result<Vec<String>> {
568    let mut stmt = conn.prepare(indoc! {"
569        SELECT DISTINCT tracks.file_dir
570        FROM tracks
571        ",
572    })?;
573
574    let result: Vec<String> = stmt
575        .query_map(named_params! {}, |row| {
576            let res = row.get(0).unwrap();
577            Ok(res)
578        })?
579        .collect::<Result<Vec<_>, rusqlite::Error>>()?;
580
581    Ok(result)
582}
583
584/// Remove all tracks-artists mappings for the given path or track id.
585///
586/// Returns the number of deleted rows. Will return `Ok(0)` if the query did not do anything.
587///
588/// # Panics
589///
590/// If the database schema does not match what is expected.
591pub fn delete_tracks_artists_mapping_for(
592    conn: &Connection,
593    track: Either<&Path, Integer>,
594) -> Result<usize> {
595    let (where_clause, params): (&str, &[(&str, &dyn ToSql)]) = match track {
596        Either::Left(path) => {
597            let (file_dir, file_stem, file_ext) = path_to_db_comp(path)?;
598
599            let where_c = indoc! {"
600                (
601                    SELECT tracks.id FROM tracks
602                    WHERE tracks.file_dir=:file_dir AND tracks.file_stem=:file_stem AND tracks.file_ext=:file_ext
603                )
604            "};
605
606            // for some reason rust does not like the following "to_str().unwrap()" to be their own binding
607            (
608                where_c,
609                &[
610                    (":file_dir", &file_dir.to_str().unwrap()),
611                    (":file_stem", &file_stem.to_str().unwrap()),
612                    (":file_ext", &file_ext.to_str().unwrap()),
613                ],
614            )
615        }
616        Either::Right(ref id) => (":track_id", &[(":track_id", id)]),
617    };
618
619    let stmt = formatdoc! {"
620        DELETE FROM tracks_artists
621        WHERE tracks_artists.track = {where_clause};
622    "};
623
624    let mut stmt = conn.prepare_cached(&stmt)?;
625
626    let affected = stmt.execute(params).optional()?.unwrap_or_default();
627
628    Ok(affected)
629}
630
631#[cfg(test)]
632mod tests {
633    use std::{
634        ffi::{OsStr, OsString},
635        path::{Path, PathBuf},
636        time::Duration,
637    };
638
639    use either::Either;
640    use pretty_assertions::assert_eq;
641
642    use crate::{
643        new_database::{
644            album_insert::AlbumInsertable,
645            artist_insert::ArtistInsertable,
646            test_utils::{gen_database, test_path},
647            track_insert::TrackInsertable,
648            track_ops::{
649                AlbumRead, ArtistRead, RowOrdering, TrackRead, all_distinct_directories,
650                all_distinct_genres, count_all_track_artist_mapping,
651                delete_tracks_artists_mapping_for, get_all_tracks, get_last_position,
652                get_track_from_path, get_tracks_from_album, get_tracks_from_artist,
653                get_tracks_from_directory, get_tracks_from_genre, get_tracks_from_genre_like,
654                set_last_position, track_exists,
655            },
656        },
657        track::TrackMetadata,
658    };
659
660    use super::get_all_artists_for_track;
661
662    #[test]
663    fn artists_for_track() {
664        let db = gen_database();
665
666        let track = TrackInsertable {
667            file_dir: Path::new("/somewhere"),
668            file_stem: OsStr::new("file"),
669            file_ext: OsStr::new("ext"),
670            duration: Some(Duration::from_secs(10)),
671            last_position: None,
672            album: Some(Either::Left(
673                AlbumInsertable {
674                    title: "AlbumA",
675                    artist_display: "ArtistA",
676                    artists: vec![Either::Left(ArtistInsertable { artist: "ArtistA" }.into())],
677                }
678                .into(),
679            )),
680            title: Some("file test"),
681            genre: None,
682            artist_display: Some("ArtistA feat. ArtistB"),
683            artists: vec![
684                Either::Left(ArtistInsertable { artist: "ArtistA" }.into()),
685                Either::Left(ArtistInsertable { artist: "ArtistB" }.into()),
686            ],
687        };
688        let track_id = track.try_insert_or_update(&db.get_connection()).unwrap();
689
690        let mut all_artists: Vec<String> =
691            get_all_artists_for_track(&db.get_connection(), track_id)
692                .unwrap()
693                .into_iter()
694                .map(|v| v.name)
695                .collect();
696        // just making sure they are consistently ordered
697        all_artists.sort();
698
699        assert_eq!(all_artists, &["ArtistA", "ArtistB"]);
700    }
701
702    #[test]
703    fn all_tracks() {
704        let db = gen_database();
705
706        let track = TrackInsertable {
707            file_dir: Path::new("/somewhere"),
708            file_stem: OsStr::new("file"),
709            file_ext: OsStr::new("ext"),
710            duration: Some(Duration::from_secs(10)),
711            last_position: None,
712            album: Some(Either::Left(
713                AlbumInsertable {
714                    title: "AlbumA",
715                    artist_display: "ArtistA",
716                    artists: vec![Either::Left(ArtistInsertable { artist: "ArtistA" }.into())],
717                }
718                .into(),
719            )),
720            title: Some("file test"),
721            genre: None,
722            artist_display: Some("ArtistA"),
723            artists: vec![Either::Left(ArtistInsertable { artist: "ArtistA" }.into())],
724        };
725        let _track_id = track.try_insert_or_update(&db.get_connection()).unwrap();
726
727        let all_tracks = get_all_tracks(&db.get_connection(), RowOrdering::IdAsc).unwrap();
728
729        assert_eq!(
730            all_tracks,
731            &[TrackRead {
732                id: 1,
733                file_dir: PathBuf::from("/somewhere"),
734                file_stem: OsString::from("file"),
735                file_ext: OsString::from("ext"),
736                duration: Some(Duration::from_secs(10)),
737                last_position: None,
738                album: Some(AlbumRead {
739                    id: 1,
740                    title: "AlbumA".to_string()
741                }),
742                title: Some("file test".to_string()),
743                genre: None,
744                artist_display: Some("ArtistA".to_string()),
745                artists: vec![ArtistRead {
746                    id: 1,
747                    name: "ArtistA".to_string()
748                }]
749            }]
750        );
751    }
752
753    #[test]
754    fn last_position_some() {
755        let db = gen_database();
756
757        let track = TrackInsertable {
758            file_dir: &test_path(Path::new("/somewhere")),
759            file_stem: OsStr::new("file"),
760            file_ext: OsStr::new("ext"),
761            duration: Some(Duration::from_secs(10)),
762            last_position: Some(Duration::from_secs(5)),
763            album: Some(Either::Left(
764                AlbumInsertable {
765                    title: "AlbumA",
766                    artist_display: "ArtistA",
767                    artists: vec![Either::Left(ArtistInsertable { artist: "ArtistA" }.into())],
768                }
769                .into(),
770            )),
771            title: Some("file test"),
772            genre: None,
773            artist_display: Some("ArtistA"),
774            artists: vec![Either::Left(ArtistInsertable { artist: "ArtistA" }.into())],
775        };
776        let path = &test_path(Path::new("/somewhere/file.ext"));
777        let _track_id = track.try_insert_or_update(&db.get_connection()).unwrap();
778
779        let last_position = get_last_position(&db.get_connection(), path).unwrap();
780
781        assert_eq!(last_position, Some(Duration::from_secs(5)));
782
783        set_last_position(&db.get_connection(), path, None).unwrap();
784
785        let last_position = get_last_position(&db.get_connection(), path).unwrap();
786
787        assert_eq!(last_position, None);
788    }
789
790    #[test]
791    fn last_position_none() {
792        let db = gen_database();
793
794        let track = TrackInsertable {
795            file_dir: &test_path(Path::new("/somewhere")),
796            file_stem: OsStr::new("file"),
797            file_ext: OsStr::new("ext"),
798            duration: Some(Duration::from_secs(10)),
799            last_position: None,
800            album: Some(Either::Left(
801                AlbumInsertable {
802                    title: "AlbumA",
803                    artist_display: "ArtistA",
804                    artists: vec![Either::Left(ArtistInsertable { artist: "ArtistA" }.into())],
805                }
806                .into(),
807            )),
808            title: Some("file test"),
809            genre: None,
810            artist_display: Some("ArtistA"),
811            artists: vec![Either::Left(ArtistInsertable { artist: "ArtistA" }.into())],
812        };
813        let path = &test_path(Path::new("/somewhere/file.ext"));
814        let _track_id = track.try_insert_or_update(&db.get_connection()).unwrap();
815
816        let last_position = get_last_position(&db.get_connection(), path).unwrap();
817
818        assert_eq!(last_position, None);
819
820        set_last_position(&db.get_connection(), path, Some(Duration::from_secs(5))).unwrap();
821
822        let last_position = get_last_position(&db.get_connection(), path).unwrap();
823
824        assert_eq!(last_position, Some(Duration::from_secs(5)));
825    }
826
827    #[test]
828    fn last_position_not_found() {
829        let db = gen_database();
830
831        let path = &test_path(Path::new("/somewhere/file.ext"));
832
833        // get
834        let err = get_last_position(&db.get_connection(), path).unwrap_err();
835        let err = err.downcast::<rusqlite::Error>().unwrap();
836
837        assert_eq!(err, rusqlite::Error::QueryReturnedNoRows);
838
839        // set
840        let err = set_last_position(&db.get_connection(), path, None).unwrap_err();
841
842        assert!(err.to_string().contains("Track not found"));
843    }
844
845    #[test]
846    fn tracks_by_album() {
847        let db = gen_database();
848
849        let metadata = TrackMetadata {
850            album: Some("AlbumA".to_string()),
851            album_artist: Some("ArtistA".to_string()),
852            album_artists: Some(vec!["ArtistA".to_string()]),
853            artist: Some("ArtistA".to_string()),
854            artists: Some(vec!["ArtistA".to_string()]),
855            title: Some("FileA1".to_string()),
856            duration: Some(Duration::from_secs(10)),
857            ..Default::default()
858        };
859        let path = &test_path(Path::new("/somewhere/fileA1.ext"));
860        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
861        let _ = insertable
862            .try_insert_or_update(&db.get_connection())
863            .unwrap();
864
865        let metadata = TrackMetadata {
866            album: Some("AlbumA".to_string()),
867            album_artist: Some("ArtistA".to_string()),
868            album_artists: Some(vec!["ArtistA".to_string()]),
869            artist: Some("ArtistA".to_string()),
870            artists: Some(vec!["ArtistA".to_string()]),
871            title: Some("FileA2".to_string()),
872            duration: Some(Duration::from_secs(10)),
873            ..Default::default()
874        };
875        let path = &test_path(Path::new("/somewhere/fileA2.ext"));
876        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
877        let _ = insertable
878            .try_insert_or_update(&db.get_connection())
879            .unwrap();
880
881        let metadata = TrackMetadata {
882            album: Some("AlbumB".to_string()),
883            album_artist: Some("ArtistA".to_string()),
884            album_artists: Some(vec!["ArtistA".to_string()]),
885            artist: Some("ArtistA".to_string()),
886            artists: Some(vec!["ArtistA".to_string()]),
887            title: Some("FileB1".to_string()),
888            duration: Some(Duration::from_secs(10)),
889            ..Default::default()
890        };
891        let path = &test_path(Path::new("/somewhere/fileB1.ext"));
892        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
893        let _ = insertable
894            .try_insert_or_update(&db.get_connection())
895            .unwrap();
896
897        let res = get_tracks_from_album(
898            &db.get_connection(),
899            "AlbumA",
900            "ArtistA",
901            RowOrdering::IdAsc,
902        )
903        .unwrap();
904        let res: Vec<String> = res.into_iter().map(|v| v.title.unwrap()).collect();
905
906        assert_eq!(&res, &["FileA1", "FileA2"]);
907    }
908
909    #[test]
910    fn tracks_by_artist() {
911        let db = gen_database();
912
913        let metadata = TrackMetadata {
914            album: Some("AlbumA".to_string()),
915            album_artist: Some("ArtistA".to_string()),
916            album_artists: Some(vec!["ArtistA".to_string()]),
917            artist: Some("ArtistA".to_string()),
918            artists: Some(vec!["ArtistA".to_string()]),
919            title: Some("FileA1".to_string()),
920            duration: Some(Duration::from_secs(10)),
921            ..Default::default()
922        };
923        let path = &test_path(Path::new("/somewhere/fileA1.ext"));
924        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
925        let _ = insertable
926            .try_insert_or_update(&db.get_connection())
927            .unwrap();
928
929        let metadata = TrackMetadata {
930            album: Some("AlbumA".to_string()),
931            album_artist: Some("ArtistB".to_string()),
932            album_artists: Some(vec!["ArtistB".to_string()]),
933            artist: Some("ArtistB".to_string()),
934            artists: Some(vec!["ArtistB".to_string()]),
935            title: Some("FileA2".to_string()),
936            duration: Some(Duration::from_secs(10)),
937            ..Default::default()
938        };
939        let path = &test_path(Path::new("/somewhere/fileA2.ext"));
940        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
941        let _ = insertable
942            .try_insert_or_update(&db.get_connection())
943            .unwrap();
944
945        let metadata = TrackMetadata {
946            album: Some("AlbumB".to_string()),
947            album_artist: Some("ArtistB".to_string()),
948            album_artists: Some(vec!["ArtistB".to_string()]),
949            artist: Some("ArtistB".to_string()),
950            artists: Some(vec!["ArtistB".to_string()]),
951            title: Some("FileB1".to_string()),
952            duration: Some(Duration::from_secs(10)),
953            ..Default::default()
954        };
955        let path = &test_path(Path::new("/somewhere/fileB1.ext"));
956        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
957        let _ = insertable
958            .try_insert_or_update(&db.get_connection())
959            .unwrap();
960
961        let res =
962            get_tracks_from_artist(&db.get_connection(), "ArtistB", RowOrdering::IdAsc).unwrap();
963        let res: Vec<String> = res.into_iter().map(|v| v.title.unwrap()).collect();
964
965        assert_eq!(&res, &["FileA2", "FileB1"]);
966    }
967
968    #[test]
969    fn tracks_by_genre() {
970        let db = gen_database();
971
972        let metadata = TrackMetadata {
973            artist: Some("ArtistA".to_string()),
974            artists: Some(vec!["ArtistA".to_string()]),
975            title: Some("FileA1".to_string()),
976            duration: Some(Duration::from_secs(10)),
977            genre: Some("Rock".to_string()),
978            ..Default::default()
979        };
980        let path = &test_path(Path::new("/somewhere/fileA1.ext"));
981        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
982        let _ = insertable
983            .try_insert_or_update(&db.get_connection())
984            .unwrap();
985
986        let metadata = TrackMetadata {
987            artist: Some("ArtistA".to_string()),
988            artists: Some(vec!["ArtistA".to_string()]),
989            title: Some("FileA2".to_string()),
990            duration: Some(Duration::from_secs(10)),
991            genre: Some("Pop".to_string()),
992            ..Default::default()
993        };
994        let path = &test_path(Path::new("/somewhere/fileA2.ext"));
995        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
996        let _ = insertable
997            .try_insert_or_update(&db.get_connection())
998            .unwrap();
999
1000        let metadata = TrackMetadata {
1001            artist: Some("ArtistA".to_string()),
1002            artists: Some(vec!["ArtistA".to_string()]),
1003            title: Some("FileB1".to_string()),
1004            duration: Some(Duration::from_secs(10)),
1005            genre: None,
1006            ..Default::default()
1007        };
1008        let path = &test_path(Path::new("/somewhere/fileB1.ext"));
1009        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1010        let _ = insertable
1011            .try_insert_or_update(&db.get_connection())
1012            .unwrap();
1013
1014        let res =
1015            get_tracks_from_genre(&db.get_connection(), Some("Rock"), RowOrdering::IdAsc).unwrap();
1016        let res: Vec<String> = res.into_iter().map(|v| v.title.unwrap()).collect();
1017
1018        assert_eq!(&res, &["FileA1"]);
1019    }
1020
1021    #[test]
1022    fn tracks_by_genre_like() {
1023        let db = gen_database();
1024
1025        let metadata = TrackMetadata {
1026            artist: Some("ArtistA".to_string()),
1027            artists: Some(vec!["ArtistA".to_string()]),
1028            title: Some("FileA1".to_string()),
1029            duration: Some(Duration::from_secs(10)),
1030            genre: Some("Rock".to_string()),
1031            ..Default::default()
1032        };
1033        let path = &test_path(Path::new("/somewhere/fileA1.ext"));
1034        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1035        let _ = insertable
1036            .try_insert_or_update(&db.get_connection())
1037            .unwrap();
1038
1039        let metadata = TrackMetadata {
1040            artist: Some("ArtistA".to_string()),
1041            artists: Some(vec!["ArtistA".to_string()]),
1042            title: Some("FileA2".to_string()),
1043            duration: Some(Duration::from_secs(10)),
1044            genre: Some("Pop".to_string()),
1045            ..Default::default()
1046        };
1047        let path = &test_path(Path::new("/somewhere/fileA2.ext"));
1048        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1049        let _ = insertable
1050            .try_insert_or_update(&db.get_connection())
1051            .unwrap();
1052
1053        let metadata = TrackMetadata {
1054            artist: Some("ArtistA".to_string()),
1055            artists: Some(vec!["ArtistA".to_string()]),
1056            title: Some("FileB1".to_string()),
1057            duration: Some(Duration::from_secs(10)),
1058            genre: None,
1059            ..Default::default()
1060        };
1061        let path = &test_path(Path::new("/somewhere/fileB1.ext"));
1062        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1063        let _ = insertable
1064            .try_insert_or_update(&db.get_connection())
1065            .unwrap();
1066
1067        let res =
1068            get_tracks_from_genre_like(&db.get_connection(), "%pop%", RowOrdering::IdAsc).unwrap();
1069        let res: Vec<String> = res.into_iter().map(|v| v.title.unwrap()).collect();
1070
1071        assert_eq!(&res, &["FileA2"]);
1072    }
1073
1074    #[test]
1075    fn tracks_by_genre_null() {
1076        let db = gen_database();
1077
1078        let metadata = TrackMetadata {
1079            artist: Some("ArtistA".to_string()),
1080            artists: Some(vec!["ArtistA".to_string()]),
1081            title: Some("FileA1".to_string()),
1082            duration: Some(Duration::from_secs(10)),
1083            genre: Some("Rock".to_string()),
1084            ..Default::default()
1085        };
1086        let path = &test_path(Path::new("/somewhere/fileA1.ext"));
1087        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1088        let _ = insertable
1089            .try_insert_or_update(&db.get_connection())
1090            .unwrap();
1091
1092        let metadata = TrackMetadata {
1093            artist: Some("ArtistA".to_string()),
1094            artists: Some(vec!["ArtistA".to_string()]),
1095            title: Some("FileA2".to_string()),
1096            duration: Some(Duration::from_secs(10)),
1097            genre: Some("Pop".to_string()),
1098            ..Default::default()
1099        };
1100        let path = &test_path(Path::new("/somewhere/fileA2.ext"));
1101        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1102        let _ = insertable
1103            .try_insert_or_update(&db.get_connection())
1104            .unwrap();
1105
1106        let metadata = TrackMetadata {
1107            artist: Some("ArtistA".to_string()),
1108            artists: Some(vec!["ArtistA".to_string()]),
1109            title: Some("FileB1".to_string()),
1110            duration: Some(Duration::from_secs(10)),
1111            genre: None,
1112            ..Default::default()
1113        };
1114        let path = &test_path(Path::new("/somewhere/fileB1.ext"));
1115        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1116        let _ = insertable
1117            .try_insert_or_update(&db.get_connection())
1118            .unwrap();
1119
1120        let res = get_tracks_from_genre(&db.get_connection(), None, RowOrdering::IdAsc).unwrap();
1121        let res: Vec<String> = res.into_iter().map(|v| v.title.unwrap()).collect();
1122
1123        assert_eq!(&res, &["FileB1"]);
1124    }
1125
1126    #[test]
1127    fn genre_distinct() {
1128        let db = gen_database();
1129
1130        let metadata = TrackMetadata {
1131            artist: Some("ArtistA".to_string()),
1132            artists: Some(vec!["ArtistA".to_string()]),
1133            title: Some("FileA1".to_string()),
1134            duration: Some(Duration::from_secs(10)),
1135            genre: Some("Rock".to_string()),
1136            ..Default::default()
1137        };
1138        let path = &test_path(Path::new("/somewhere/fileA1.ext"));
1139        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1140        let _ = insertable
1141            .try_insert_or_update(&db.get_connection())
1142            .unwrap();
1143
1144        let metadata = TrackMetadata {
1145            artist: Some("ArtistA".to_string()),
1146            artists: Some(vec!["ArtistA".to_string()]),
1147            title: Some("FileA2".to_string()),
1148            duration: Some(Duration::from_secs(10)),
1149            genre: Some("Pop".to_string()),
1150            ..Default::default()
1151        };
1152        let path = &test_path(Path::new("/somewhere/fileA2.ext"));
1153        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1154        let _ = insertable
1155            .try_insert_or_update(&db.get_connection())
1156            .unwrap();
1157
1158        let metadata = TrackMetadata {
1159            artist: Some("ArtistA".to_string()),
1160            artists: Some(vec!["ArtistA".to_string()]),
1161            title: Some("FileB1".to_string()),
1162            duration: Some(Duration::from_secs(10)),
1163            genre: None,
1164            ..Default::default()
1165        };
1166        let path = &test_path(Path::new("/somewhere/fileB1.ext"));
1167        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1168        let _ = insertable
1169            .try_insert_or_update(&db.get_connection())
1170            .unwrap();
1171
1172        let metadata = TrackMetadata {
1173            artist: Some("ArtistA".to_string()),
1174            artists: Some(vec!["ArtistA".to_string()]),
1175            title: Some("FileB2".to_string()),
1176            duration: Some(Duration::from_secs(10)),
1177            genre: Some("Rock".to_string()),
1178            ..Default::default()
1179        };
1180        let path = &test_path(Path::new("/somewhere/fileB2.ext"));
1181        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1182        let _ = insertable
1183            .try_insert_or_update(&db.get_connection())
1184            .unwrap();
1185
1186        let res = all_distinct_genres(&db.get_connection()).unwrap();
1187
1188        assert_eq!(&res, &["Rock", "Pop", "[unknown]"]);
1189    }
1190
1191    #[test]
1192    fn exists() {
1193        let db = gen_database();
1194
1195        let metadata = TrackMetadata {
1196            title: Some("FileA1".to_string()),
1197            duration: Some(Duration::from_secs(10)),
1198            ..Default::default()
1199        };
1200        let path = &test_path(Path::new("/somewhere/fileA1.ext"));
1201        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1202        let _ = insertable
1203            .try_insert_or_update(&db.get_connection())
1204            .unwrap();
1205
1206        let res = track_exists(&db.get_connection(), path).unwrap();
1207
1208        assert!(res);
1209
1210        let res = track_exists(
1211            &db.get_connection(),
1212            &test_path(Path::new("/somewhere/else.ext")),
1213        )
1214        .unwrap();
1215
1216        assert!(!res);
1217    }
1218
1219    #[test]
1220    fn single_track() {
1221        let db = gen_database();
1222
1223        let metadata = TrackMetadata {
1224            title: Some("FileA1".to_string()),
1225            duration: Some(Duration::from_secs(10)),
1226            ..Default::default()
1227        };
1228        let path = &test_path(Path::new("/somewhere/fileA1.ext"));
1229        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1230        let _ = insertable
1231            .try_insert_or_update(&db.get_connection())
1232            .unwrap();
1233
1234        let res = get_track_from_path(&db.get_connection(), path).unwrap();
1235
1236        assert_eq!(res.title, Some("FileA1".to_string()));
1237
1238        let err = get_track_from_path(
1239            &db.get_connection(),
1240            &test_path(Path::new("/somewhere/else.ext")),
1241        )
1242        .unwrap_err();
1243        let err = err.downcast::<rusqlite::Error>().unwrap();
1244
1245        assert_eq!(err, rusqlite::Error::QueryReturnedNoRows);
1246    }
1247
1248    #[test]
1249    fn track_read_to_path() {
1250        let read = TrackRead {
1251            id: 0,
1252            file_dir: PathBuf::from("/path/to/somewhere"),
1253            file_stem: OsString::from("filename"),
1254            file_ext: OsString::from("ext"),
1255            duration: None,
1256            last_position: None,
1257            album: None,
1258            title: None,
1259            genre: None,
1260            artist_display: None,
1261            artists: Vec::new(),
1262        };
1263
1264        assert_eq!(
1265            read.as_pathbuf(),
1266            PathBuf::from("/path/to/somewhere/filename.ext")
1267        );
1268    }
1269
1270    #[test]
1271    fn distinct_directories() {
1272        let db = gen_database();
1273
1274        let metadata = TrackMetadata {
1275            album: Some("AlbumA".to_string()),
1276            album_artist: Some("ArtistA".to_string()),
1277            album_artists: Some(vec!["ArtistA".to_string()]),
1278            artist: Some("ArtistA".to_string()),
1279            artists: Some(vec!["ArtistA".to_string()]),
1280            title: Some("FileA1".to_string()),
1281            duration: Some(Duration::from_secs(10)),
1282            ..Default::default()
1283        };
1284        let path = &test_path(Path::new("/somewhere/dirA/fileA1.ext"));
1285        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1286        let _ = insertable
1287            .try_insert_or_update(&db.get_connection())
1288            .unwrap();
1289
1290        let metadata = TrackMetadata {
1291            album: Some("AlbumA".to_string()),
1292            album_artist: Some("ArtistA".to_string()),
1293            album_artists: Some(vec!["ArtistA".to_string()]),
1294            artist: Some("ArtistA".to_string()),
1295            artists: Some(vec!["ArtistA".to_string()]),
1296            title: Some("FileA2".to_string()),
1297            duration: Some(Duration::from_secs(10)),
1298            ..Default::default()
1299        };
1300        let path = &test_path(Path::new("/somewhere/dirA/fileA2.ext"));
1301        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1302        let _ = insertable
1303            .try_insert_or_update(&db.get_connection())
1304            .unwrap();
1305
1306        let metadata = TrackMetadata {
1307            album: Some("AlbumB".to_string()),
1308            album_artist: Some("ArtistA".to_string()),
1309            album_artists: Some(vec!["ArtistA".to_string()]),
1310            artist: Some("ArtistA".to_string()),
1311            artists: Some(vec!["ArtistA".to_string()]),
1312            title: Some("FileB1".to_string()),
1313            duration: Some(Duration::from_secs(10)),
1314            ..Default::default()
1315        };
1316        let path = &test_path(Path::new("/somewhere/dirB/fileB1.ext"));
1317        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1318        let _ = insertable
1319            .try_insert_or_update(&db.get_connection())
1320            .unwrap();
1321
1322        let res = all_distinct_directories(&db.get_connection()).unwrap();
1323
1324        assert_eq!(
1325            &res,
1326            &[
1327                test_path(Path::new("/somewhere/dirA")).to_string_lossy(),
1328                test_path(Path::new("/somewhere/dirB")).to_string_lossy()
1329            ]
1330        );
1331    }
1332
1333    #[test]
1334    fn tracks_by_directory() {
1335        let db = gen_database();
1336
1337        let metadata = TrackMetadata {
1338            album: Some("AlbumA".to_string()),
1339            album_artist: Some("ArtistA".to_string()),
1340            album_artists: Some(vec!["ArtistA".to_string()]),
1341            artist: Some("ArtistA".to_string()),
1342            artists: Some(vec!["ArtistA".to_string()]),
1343            title: Some("FileA1".to_string()),
1344            duration: Some(Duration::from_secs(10)),
1345            ..Default::default()
1346        };
1347        let path = &test_path(Path::new("/somewhere/dirA/fileA1.ext"));
1348        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1349        let _ = insertable
1350            .try_insert_or_update(&db.get_connection())
1351            .unwrap();
1352
1353        let metadata = TrackMetadata {
1354            album: Some("AlbumA".to_string()),
1355            album_artist: Some("ArtistA".to_string()),
1356            album_artists: Some(vec!["ArtistA".to_string()]),
1357            artist: Some("ArtistA".to_string()),
1358            artists: Some(vec!["ArtistA".to_string()]),
1359            title: Some("FileA2".to_string()),
1360            duration: Some(Duration::from_secs(10)),
1361            ..Default::default()
1362        };
1363        let path = &test_path(Path::new("/somewhere/dirA/fileA2.ext"));
1364        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1365        let _ = insertable
1366            .try_insert_or_update(&db.get_connection())
1367            .unwrap();
1368
1369        let metadata = TrackMetadata {
1370            album: Some("AlbumB".to_string()),
1371            album_artist: Some("ArtistA".to_string()),
1372            album_artists: Some(vec!["ArtistA".to_string()]),
1373            artist: Some("ArtistA".to_string()),
1374            artists: Some(vec!["ArtistA".to_string()]),
1375            title: Some("FileB1".to_string()),
1376            duration: Some(Duration::from_secs(10)),
1377            ..Default::default()
1378        };
1379        let path = &test_path(Path::new("/somewhere/dirB/fileB1.ext"));
1380        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1381        let _ = insertable
1382            .try_insert_or_update(&db.get_connection())
1383            .unwrap();
1384
1385        let res = get_tracks_from_directory(
1386            &db.get_connection(),
1387            &test_path(Path::new("/somewhere/dirA")),
1388            RowOrdering::IdAsc,
1389        )
1390        .unwrap();
1391        let res: Vec<String> = res.into_iter().map(|v| v.title.unwrap()).collect();
1392
1393        assert_eq!(&res, &["FileA1", "FileA2"]);
1394    }
1395
1396    #[test]
1397    fn delete_tracks_artists_mapping() {
1398        let db = gen_database();
1399
1400        let metadata = TrackMetadata {
1401            artist: Some("ArtistA feat. ArtistB".to_string()),
1402            artists: Some(vec!["ArtistA".to_string(), "ArtistB".to_string()]),
1403            title: Some("FileA1".to_string()),
1404            duration: Some(Duration::from_secs(10)),
1405            ..Default::default()
1406        };
1407        let path_a1 = &test_path(Path::new("/somewhere/fileA1.ext"));
1408        let insertable = TrackInsertable::try_from_track(path_a1, &metadata).unwrap();
1409        let _ = insertable
1410            .try_insert_or_update(&db.get_connection())
1411            .unwrap();
1412
1413        let metadata = TrackMetadata {
1414            artist: Some("ArtistA feat. ArtistB".to_string()),
1415            artists: Some(vec!["ArtistA".to_string(), "ArtistB".to_string()]),
1416            title: Some("FileB1".to_string()),
1417            duration: Some(Duration::from_secs(10)),
1418            ..Default::default()
1419        };
1420        let path = &test_path(Path::new("/somewhere/fileB1.ext"));
1421        let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
1422        let track2_id = insertable
1423            .try_insert_or_update(&db.get_connection())
1424            .unwrap();
1425
1426        let mapping_counts = count_all_track_artist_mapping(&db.get_connection()).unwrap();
1427
1428        assert_eq!(mapping_counts, 4);
1429
1430        let affected =
1431            delete_tracks_artists_mapping_for(&db.get_connection(), Either::Left(path_a1)).unwrap();
1432
1433        assert_eq!(affected, 2);
1434
1435        let mapping_counts = count_all_track_artist_mapping(&db.get_connection()).unwrap();
1436
1437        assert_eq!(mapping_counts, 2);
1438
1439        let affected =
1440            delete_tracks_artists_mapping_for(&db.get_connection(), Either::Right(track2_id))
1441                .unwrap();
1442
1443        assert_eq!(affected, 2);
1444
1445        let mapping_counts = count_all_track_artist_mapping(&db.get_connection()).unwrap();
1446
1447        assert_eq!(mapping_counts, 0);
1448    }
1449}