diff --git a/src/core/mod.rs b/src/core/mod.rs index 870545f..af427e7 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,3 +1,4 @@ +// FIXME: visibility? pub mod collection; pub mod database; pub mod library; diff --git a/src/core/musichoard/mod.rs b/src/core/musichoard/mod.rs index 4408b8d..10473d5 100644 --- a/src/core/musichoard/mod.rs +++ b/src/core/musichoard/mod.rs @@ -1,375 +1,12 @@ +#![allow(clippy::module_inception)] +pub mod musichoard; +pub mod musichoard_builder; + use std::fmt::{self, Display}; -use std::{collections::HashMap, mem}; -use paste::paste; - -use super::collection::{ - self, - album::{Album, AlbumId}, - artist::{Artist, ArtistId}, - track::{Quality, Track, TrackId}, - Collection, Merge, -}; -use super::database::{self, IDatabase}; -use super::library::{self, ILibrary, Item, Query}; - -/// The Music Hoard. It is responsible for pulling information from both the library and the -/// database, ensuring its consistent and writing back any changes. -pub struct MusicHoard { - collection: Collection, - library: LIB, - database: DB, -} - -macro_rules! music_hoard_unique_url_dispatch { - ($field:ident) => { - paste! { - pub fn [], S: AsRef>( - &mut self, - artist_id: ID, - url: S, - ) -> Result<(), Error> { - Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](url)?) - } - - pub fn [], S: AsRef>( - &mut self, - artist_id: ID, - url: S, - ) -> Result<(), Error> { - Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](url)?) - } - - pub fn [], S: AsRef>( - &mut self, - artist_id: ID, - url: S, - ) -> Result<(), Error> { - Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](url)?) - } - - pub fn []>( - &mut self, - artist_id: ID, - ) -> Result<(), Error> { - self.get_artist_mut_or_err(artist_id.as_ref())?.[](); - Ok(()) - } - } - }; -} - -macro_rules! music_hoard_multi_url_dispatch { - ($field:ident) => { - paste! { - pub fn [], S: AsRef>( - &mut self, - artist_id: ID, - urls: Vec, - ) -> Result<(), Error> { - Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](urls)?) - } - - pub fn [], S: AsRef>( - &mut self, - artist_id: ID, - urls: Vec, - ) -> Result<(), Error> { - Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](urls)?) - } - - pub fn [], S: AsRef>( - &mut self, - artist_id: ID, - urls: Vec, - ) -> Result<(), Error> { - Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](urls)?) - } - - pub fn []>( - &mut self, artist_id: ID, - ) -> Result<(), Error> { - self.get_artist_mut_or_err(artist_id.as_ref())?.[](); - Ok(()) - } - } - }; -} - -impl MusicHoard { - /// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`]. - pub fn new(library: LIB, database: DB) -> Self { - MusicHoard { - collection: vec![], - library, - database, - } - } - - /// Retrieve the [`Collection`]. - pub fn get_collection(&self) -> &Collection { - &self.collection - } - - pub fn add_artist>(&mut self, artist_id: ID) { - let artist_id: ArtistId = artist_id.into(); - - if self.get_artist(&artist_id).is_none() { - self.collection.push(Artist::new(artist_id)); - Self::sort_artists(&mut self.collection); - } - } - - pub fn remove_artist>(&mut self, artist_id: ID) { - let index_opt = self - .collection - .iter() - .position(|a| &a.id == artist_id.as_ref()); - - if let Some(index) = index_opt { - self.collection.remove(index); - } - } - - pub fn set_artist_sort, SORT: Into>( - &mut self, - artist_id: ID, - artist_sort: SORT, - ) -> Result<(), Error> { - self.get_artist_mut_or_err(artist_id.as_ref())? - .set_sort_key(artist_sort); - Self::sort(&mut self.collection); - Ok(()) - } - - pub fn clear_artist_sort>(&mut self, artist_id: ID) -> Result<(), Error> { - self.get_artist_mut_or_err(artist_id.as_ref())? - .clear_sort_key(); - Self::sort(&mut self.collection); - Ok(()) - } - - music_hoard_unique_url_dispatch!(musicbrainz); - - music_hoard_multi_url_dispatch!(musicbutler); - - music_hoard_multi_url_dispatch!(bandcamp); - - music_hoard_unique_url_dispatch!(qobuz); - - fn sort(collection: &mut [Artist]) { - Self::sort_artists(collection); - Self::sort_albums_and_tracks(collection.iter_mut()); - } - - fn sort_artists(collection: &mut [Artist]) { - collection.sort_unstable(); - } - - fn sort_albums_and_tracks<'a, COL: Iterator>(collection: COL) { - for artist in collection { - artist.albums.sort_unstable(); - for album in artist.albums.iter_mut() { - album.tracks.sort_unstable(); - } - } - } - - fn merge_with_primary(&mut self, primary: HashMap) { - let collection = mem::take(&mut self.collection); - self.collection = Self::merge_collections(primary, collection); - } - - fn merge_with_secondary>(&mut self, secondary: SEC) { - let primary_map: HashMap = self - .collection - .drain(..) - .map(|a| (a.id.clone(), a)) - .collect(); - self.collection = Self::merge_collections(primary_map, secondary); - } - - fn merge_collections>( - mut primary: HashMap, - secondary: SEC, - ) -> Collection { - for secondary_artist in secondary.into_iter() { - if let Some(ref mut primary_artist) = primary.get_mut(&secondary_artist.id) { - primary_artist.merge_in_place(secondary_artist); - } else { - primary.insert(secondary_artist.id.clone(), secondary_artist); - } - } - let mut collection: Collection = primary.into_values().collect(); - Self::sort_artists(&mut collection); - collection - } - - fn items_to_artists(items: Vec) -> Result, Error> { - let mut collection = HashMap::::new(); - - for item in items.into_iter() { - let artist_id = ArtistId { - name: item.album_artist, - }; - - let artist_sort = item.album_artist_sort.map(|s| ArtistId { name: s }); - - let album_id = AlbumId { - year: item.album_year, - title: item.album_title, - }; - - let track = Track { - id: TrackId { - number: item.track_number, - title: item.track_title, - }, - artist: item.track_artist, - quality: Quality { - format: item.track_format, - bitrate: item.track_bitrate, - }, - }; - - // There are usually many entries per artist. Therefore, we avoid simply calling - // .entry(artist_id.clone()).or_insert_with(..), because of the clone. The flipside is - // that insertions will thus do an additional lookup. - let artist = match collection.get_mut(&artist_id) { - Some(artist) => artist, - None => collection - .entry(artist_id.clone()) - .or_insert_with(|| Artist::new(artist_id)), - }; - - if artist.sort.is_some() { - if artist_sort.is_some() && (artist.sort != artist_sort) { - return Err(Error::CollectionError(format!( - "multiple album_artist_sort found for artist '{}': '{}' != '{}'", - artist.id, - artist.sort.as_ref().unwrap(), - artist_sort.as_ref().unwrap() - ))); - } - } else if artist_sort.is_some() { - artist.sort = artist_sort; - } - - // Do a linear search as few artists have more than a handful of albums. Search from the - // back as the original items vector is usually already sorted. - match artist - .albums - .iter_mut() - .rev() - .find(|album| album.id == album_id) - { - Some(album) => album.tracks.push(track), - None => artist.albums.push(Album { - id: album_id, - tracks: vec![track], - }), - } - } - - Ok(collection) - } - - fn get_artist(&self, artist_id: &ArtistId) -> Option<&Artist> { - self.collection.iter().find(|a| &a.id == artist_id) - } - - fn get_artist_mut(&mut self, artist_id: &ArtistId) -> Option<&mut Artist> { - self.collection.iter_mut().find(|a| &a.id == artist_id) - } - - fn get_artist_mut_or_err(&mut self, artist_id: &ArtistId) -> Result<&mut Artist, Error> { - self.get_artist_mut(artist_id).ok_or_else(|| { - Error::CollectionError(format!("artist '{}' is not in the collection", artist_id)) - }) - } -} - -impl MusicHoard { - /// Rescan the library and merge with the in-memory collection. - pub fn rescan_library(&mut self) -> Result<(), Error> { - let items = self.library.list(&Query::new())?; - let mut library_collection = Self::items_to_artists(items)?; - Self::sort_albums_and_tracks(library_collection.values_mut()); - - self.merge_with_primary(library_collection); - Ok(()) - } -} - -impl MusicHoard { - /// Load the database and merge with the in-memory collection. - pub fn load_from_database(&mut self) -> Result<(), Error> { - let mut database_collection = self.database.load()?; - Self::sort_albums_and_tracks(database_collection.iter_mut()); - - self.merge_with_secondary(database_collection); - Ok(()) - } - - /// Save the in-memory collection to the database. - pub fn save_to_database(&mut self) -> Result<(), Error> { - self.database.save(&self.collection)?; - Ok(()) - } -} - -/// Builder for [`MusicHoard`]. Its purpose is to make it easier to set various combinations of -/// library/database or their absence. -pub struct MusicHoardBuilder { - library: LIB, - database: DB, -} - -/// Phantom type for when a library implementation is not needed. -pub struct NoLibrary; - -/// Phantom type for when a database implementation is not needed. -pub struct NoDatabase; - -impl Default for MusicHoardBuilder { - /// Create a [`MusicHoardBuilder`]. - fn default() -> Self { - Self::new() - } -} - -impl MusicHoardBuilder { - /// Create a [`MusicHoardBuilder`]. - pub fn new() -> Self { - MusicHoardBuilder { - library: NoLibrary, - database: NoDatabase, - } - } -} - -impl MusicHoardBuilder { - /// Set a library for [`MusicHoard`]. - pub fn set_library(self, library: NEWLIB) -> MusicHoardBuilder { - MusicHoardBuilder { - library, - database: self.database, - } - } - - /// Set a database for [`MusicHoard`]. - pub fn set_database(self, database: NEWDB) -> MusicHoardBuilder { - MusicHoardBuilder { - library: self.library, - database, - } - } - - /// Build [`MusicHoard`] with the currently set library and database. - pub fn build(self) -> MusicHoard { - MusicHoard::new(self.library, self.database) - } -} +use super::collection; +use super::database; +use super::library; /// Error type for `musichoard`. #[derive(Debug, PartialEq, Eq)] @@ -420,6 +57,3 @@ impl From for Error { #[cfg(test)] pub mod testmod; - -#[cfg(test)] -mod tests; diff --git a/src/core/musichoard/musichoard.rs b/src/core/musichoard/musichoard.rs new file mode 100644 index 0000000..c94e9c6 --- /dev/null +++ b/src/core/musichoard/musichoard.rs @@ -0,0 +1,1525 @@ +use std::{collections::HashMap, mem}; + +use paste::paste; + +use super::collection::{ + album::{Album, AlbumId}, + artist::{Artist, ArtistId}, + track::{Quality, Track, TrackId}, + Collection, Merge, +}; +use super::database::IDatabase; +use super::library::{ILibrary, Item, Query}; +use super::Error; + +/// The Music Hoard. It is responsible for pulling information from both the library and the +/// database, ensuring its consistent and writing back any changes. +pub struct MusicHoard { + collection: Collection, + library: LIB, + database: DB, +} + +/// Phantom type for when a library implementation is not needed. +pub struct NoLibrary; + +/// Phantom type for when a database implementation is not needed. +pub struct NoDatabase; + +macro_rules! music_hoard_unique_url_dispatch { + ($field:ident) => { + paste! { + pub fn [], S: AsRef>( + &mut self, + artist_id: ID, + url: S, + ) -> Result<(), Error> { + Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](url)?) + } + + pub fn [], S: AsRef>( + &mut self, + artist_id: ID, + url: S, + ) -> Result<(), Error> { + Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](url)?) + } + + pub fn [], S: AsRef>( + &mut self, + artist_id: ID, + url: S, + ) -> Result<(), Error> { + Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](url)?) + } + + pub fn []>( + &mut self, + artist_id: ID, + ) -> Result<(), Error> { + self.get_artist_mut_or_err(artist_id.as_ref())?.[](); + Ok(()) + } + } + }; +} + +macro_rules! music_hoard_multi_url_dispatch { + ($field:ident) => { + paste! { + pub fn [], S: AsRef>( + &mut self, + artist_id: ID, + urls: Vec, + ) -> Result<(), Error> { + Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](urls)?) + } + + pub fn [], S: AsRef>( + &mut self, + artist_id: ID, + urls: Vec, + ) -> Result<(), Error> { + Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](urls)?) + } + + pub fn [], S: AsRef>( + &mut self, + artist_id: ID, + urls: Vec, + ) -> Result<(), Error> { + Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](urls)?) + } + + pub fn []>( + &mut self, artist_id: ID, + ) -> Result<(), Error> { + self.get_artist_mut_or_err(artist_id.as_ref())?.[](); + Ok(()) + } + } + }; +} + +impl MusicHoard { + /// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`]. + pub fn new(library: LIB, database: DB) -> Self { + MusicHoard { + collection: vec![], + library, + database, + } + } + + /// Retrieve the [`Collection`]. + pub fn get_collection(&self) -> &Collection { + &self.collection + } + + pub fn add_artist>(&mut self, artist_id: ID) { + let artist_id: ArtistId = artist_id.into(); + + if self.get_artist(&artist_id).is_none() { + self.collection.push(Artist::new(artist_id)); + Self::sort_artists(&mut self.collection); + } + } + + pub fn remove_artist>(&mut self, artist_id: ID) { + let index_opt = self + .collection + .iter() + .position(|a| &a.id == artist_id.as_ref()); + + if let Some(index) = index_opt { + self.collection.remove(index); + } + } + + pub fn set_artist_sort, SORT: Into>( + &mut self, + artist_id: ID, + artist_sort: SORT, + ) -> Result<(), Error> { + self.get_artist_mut_or_err(artist_id.as_ref())? + .set_sort_key(artist_sort); + Self::sort(&mut self.collection); + Ok(()) + } + + pub fn clear_artist_sort>(&mut self, artist_id: ID) -> Result<(), Error> { + self.get_artist_mut_or_err(artist_id.as_ref())? + .clear_sort_key(); + Self::sort(&mut self.collection); + Ok(()) + } + + music_hoard_unique_url_dispatch!(musicbrainz); + + music_hoard_multi_url_dispatch!(musicbutler); + + music_hoard_multi_url_dispatch!(bandcamp); + + music_hoard_unique_url_dispatch!(qobuz); + + fn sort(collection: &mut [Artist]) { + Self::sort_artists(collection); + Self::sort_albums_and_tracks(collection.iter_mut()); + } + + fn sort_artists(collection: &mut [Artist]) { + collection.sort_unstable(); + } + + fn sort_albums_and_tracks<'a, COL: Iterator>(collection: COL) { + for artist in collection { + artist.albums.sort_unstable(); + for album in artist.albums.iter_mut() { + album.tracks.sort_unstable(); + } + } + } + + fn merge_with_primary(&mut self, primary: HashMap) { + let collection = mem::take(&mut self.collection); + self.collection = Self::merge_collections(primary, collection); + } + + fn merge_with_secondary>(&mut self, secondary: SEC) { + let primary_map: HashMap = self + .collection + .drain(..) + .map(|a| (a.id.clone(), a)) + .collect(); + self.collection = Self::merge_collections(primary_map, secondary); + } + + fn merge_collections>( + mut primary: HashMap, + secondary: SEC, + ) -> Collection { + for secondary_artist in secondary.into_iter() { + if let Some(ref mut primary_artist) = primary.get_mut(&secondary_artist.id) { + primary_artist.merge_in_place(secondary_artist); + } else { + primary.insert(secondary_artist.id.clone(), secondary_artist); + } + } + let mut collection: Collection = primary.into_values().collect(); + Self::sort_artists(&mut collection); + collection + } + + fn items_to_artists(items: Vec) -> Result, Error> { + let mut collection = HashMap::::new(); + + for item in items.into_iter() { + let artist_id = ArtistId { + name: item.album_artist, + }; + + let artist_sort = item.album_artist_sort.map(|s| ArtistId { name: s }); + + let album_id = AlbumId { + year: item.album_year, + title: item.album_title, + }; + + let track = Track { + id: TrackId { + number: item.track_number, + title: item.track_title, + }, + artist: item.track_artist, + quality: Quality { + format: item.track_format, + bitrate: item.track_bitrate, + }, + }; + + // There are usually many entries per artist. Therefore, we avoid simply calling + // .entry(artist_id.clone()).or_insert_with(..), because of the clone. The flipside is + // that insertions will thus do an additional lookup. + let artist = match collection.get_mut(&artist_id) { + Some(artist) => artist, + None => collection + .entry(artist_id.clone()) + .or_insert_with(|| Artist::new(artist_id)), + }; + + if artist.sort.is_some() { + if artist_sort.is_some() && (artist.sort != artist_sort) { + return Err(Error::CollectionError(format!( + "multiple album_artist_sort found for artist '{}': '{}' != '{}'", + artist.id, + artist.sort.as_ref().unwrap(), + artist_sort.as_ref().unwrap() + ))); + } + } else if artist_sort.is_some() { + artist.sort = artist_sort; + } + + // Do a linear search as few artists have more than a handful of albums. Search from the + // back as the original items vector is usually already sorted. + match artist + .albums + .iter_mut() + .rev() + .find(|album| album.id == album_id) + { + Some(album) => album.tracks.push(track), + None => artist.albums.push(Album { + id: album_id, + tracks: vec![track], + }), + } + } + + Ok(collection) + } + + fn get_artist(&self, artist_id: &ArtistId) -> Option<&Artist> { + self.collection.iter().find(|a| &a.id == artist_id) + } + + fn get_artist_mut(&mut self, artist_id: &ArtistId) -> Option<&mut Artist> { + self.collection.iter_mut().find(|a| &a.id == artist_id) + } + + fn get_artist_mut_or_err(&mut self, artist_id: &ArtistId) -> Result<&mut Artist, Error> { + self.get_artist_mut(artist_id).ok_or_else(|| { + Error::CollectionError(format!("artist '{}' is not in the collection", artist_id)) + }) + } +} + +impl MusicHoard { + /// Rescan the library and merge with the in-memory collection. + pub fn rescan_library(&mut self) -> Result<(), Error> { + let items = self.library.list(&Query::new())?; + let mut library_collection = Self::items_to_artists(items)?; + Self::sort_albums_and_tracks(library_collection.values_mut()); + + self.merge_with_primary(library_collection); + Ok(()) + } +} + +impl MusicHoard { + /// Load the database and merge with the in-memory collection. + pub fn load_from_database(&mut self) -> Result<(), Error> { + let mut database_collection = self.database.load()?; + Self::sort_albums_and_tracks(database_collection.iter_mut()); + + self.merge_with_secondary(database_collection); + Ok(()) + } + + /// Save the in-memory collection to the database. + pub fn save_to_database(&mut self) -> Result<(), Error> { + self.database.save(&self.collection)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use mockall::predicate; + + use super::*; + + // FIXME: check all crate::* imports - are the tests where they should be? + use crate::core::collection::{ + artist::{ArtistId, ArtistProperties, Bandcamp, MusicBrainz, MusicButler, Qobuz}, + track::Format, + Merge, + }; + use crate::core::musichoard::musichoard_builder::MusicHoardBuilder; + use crate::core::musichoard::testmod::{FULL_COLLECTION, LIBRARY_COLLECTION}; + use crate::database::{self, MockIDatabase}; + use crate::library::{self, testmod::LIBRARY_ITEMS, MockILibrary}; + + static MUSICBRAINZ: &str = + "https://musicbrainz.org/artist/d368baa8-21ca-4759-9731-0b2753071ad8"; + static MUSICBRAINZ_2: &str = + "https://musicbrainz.org/artist/823869a5-5ded-4f6b-9fb7-2a9344d83c6b"; + static MUSICBUTLER: &str = "https://www.musicbutler.io/artist-page/483340948"; + static MUSICBUTLER_2: &str = "https://www.musicbutler.io/artist-page/658903042/"; + static BANDCAMP: &str = "https://thelasthangmen.bandcamp.com/"; + static BANDCAMP_2: &str = "https://viciouscrusade.bandcamp.com/"; + static QOBUZ: &str = "https://www.qobuz.com/nl-nl/interpreter/the-last-hangmen/1244413"; + static QOBUZ_2: &str = "https://www.qobuz.com/nl-nl/interpreter/vicious-crusade/7522386"; + + // FIXME: all tests with URLs should go to collection::artist + #[test] + fn urls() { + assert!(MusicBrainz::new(MUSICBRAINZ).is_ok()); + assert!(MusicBrainz::new(MUSICBUTLER).is_err()); + assert!(MusicBrainz::new(BANDCAMP).is_err()); + assert!(MusicBrainz::new(QOBUZ).is_err()); + + assert!(MusicButler::new(MUSICBRAINZ).is_err()); + assert!(MusicButler::new(MUSICBUTLER).is_ok()); + assert!(MusicButler::new(BANDCAMP).is_err()); + assert!(MusicButler::new(QOBUZ).is_err()); + + assert!(Bandcamp::new(MUSICBRAINZ).is_err()); + assert!(Bandcamp::new(MUSICBUTLER).is_err()); + assert!(Bandcamp::new(BANDCAMP).is_ok()); + assert!(Bandcamp::new(QOBUZ).is_err()); + + assert!(Qobuz::new(MUSICBRAINZ).is_err()); + assert!(Qobuz::new(MUSICBUTLER).is_err()); + assert!(Qobuz::new(BANDCAMP).is_err()); + assert!(Qobuz::new(QOBUZ).is_ok()); + } + + #[test] + fn artist_new_delete() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoardBuilder::default().build(); + + let mut expected: Vec = vec![]; + + music_hoard.add_artist(artist_id.clone()); + expected.push(Artist::new(artist_id.clone())); + assert_eq!(music_hoard.collection, expected); + + music_hoard.add_artist(artist_id.clone()); + assert_eq!(music_hoard.collection, expected); + + music_hoard.remove_artist(&artist_id_2); + assert_eq!(music_hoard.collection, expected); + + music_hoard.remove_artist(&artist_id); + _ = expected.pop(); + assert_eq!(music_hoard.collection, expected); + } + + #[test] + fn artist_sort_set_clear() { + let mut music_hoard = MusicHoardBuilder::default().build(); + + let artist_1_id = ArtistId::new("the artist"); + let artist_1_sort = ArtistId::new("artist, the"); + + // Must be after "artist, the", but before "the artist" + let artist_2_id = ArtistId::new("b-artist"); + + assert!(artist_1_sort < artist_2_id); + assert!(artist_2_id < artist_1_id); + + music_hoard.add_artist(artist_1_id.clone()); + music_hoard.add_artist(artist_2_id.clone()); + + let artist_1: &Artist = music_hoard.get_artist(&artist_1_id).unwrap(); + let artist_2: &Artist = music_hoard.get_artist(&artist_2_id).unwrap(); + + assert!(artist_2 < artist_1); + + assert_eq!(artist_1, &music_hoard.collection[1]); + assert_eq!(artist_2, &music_hoard.collection[0]); + + music_hoard + .set_artist_sort(artist_1_id.as_ref(), artist_1_sort.clone()) + .unwrap(); + + let artist_1: &Artist = music_hoard.get_artist(&artist_1_id).unwrap(); + let artist_2: &Artist = music_hoard.get_artist(&artist_2_id).unwrap(); + + assert!(artist_1 < artist_2); + + assert_eq!(artist_1, &music_hoard.collection[0]); + assert_eq!(artist_2, &music_hoard.collection[1]); + + music_hoard.clear_artist_sort(artist_1_id.as_ref()).unwrap(); + + let artist_1: &Artist = music_hoard.get_artist(&artist_1_id).unwrap(); + let artist_2: &Artist = music_hoard.get_artist(&artist_2_id).unwrap(); + + assert!(artist_2 < artist_1); + + assert_eq!(artist_1, &music_hoard.collection[1]); + assert_eq!(artist_2, &music_hoard.collection[0]); + } + + #[test] + fn collection_error() { + let artist_id = ArtistId::new("an artist"); + let mut music_hoard = MusicHoardBuilder::default().build(); + + let actual_err = music_hoard + .add_musicbrainz_url(&artist_id, QOBUZ) + .unwrap_err(); + let expected_err = + Error::CollectionError(String::from("artist 'an artist' is not in the collection")); + assert_eq!(actual_err, expected_err); + assert_eq!(actual_err.to_string(), expected_err.to_string()); + } + + #[test] + fn add_remove_musicbrainz_url() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoardBuilder::default().build(); + + music_hoard.add_artist(artist_id.clone()); + + let mut expected: Option = None; + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Adding incorect URL is an error. + assert!(music_hoard + .add_musicbrainz_url(&artist_id, MUSICBUTLER) + .is_err()); + assert!(music_hoard + .add_musicbrainz_url(&artist_id, BANDCAMP) + .is_err()); + assert!(music_hoard.add_musicbrainz_url(&artist_id, QOBUZ).is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Adding URL to an artist not in the collection is an error. + assert!(music_hoard + .add_musicbrainz_url(&artist_id_2, MUSICBRAINZ) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Adding URL to artist. + assert!(music_hoard + .add_musicbrainz_url(&artist_id, MUSICBRAINZ) + .is_ok()); + _ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Adding the same URL again is ok, but does not do anything. + assert!(music_hoard + .add_musicbrainz_url(&artist_id, MUSICBRAINZ) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Adding further URLs is an error. + assert!(music_hoard + .add_musicbrainz_url(&artist_id, MUSICBRAINZ_2) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Removing a URL from an artist not in the collection is an error. + assert!(music_hoard + .remove_musicbrainz_url(&artist_id_2, MUSICBRAINZ) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Removing a URL not in the collection is okay, but does not do anything. + assert!(music_hoard + .remove_musicbrainz_url(&artist_id, MUSICBRAINZ_2) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Removing a URL in the collection removes it. + assert!(music_hoard + .remove_musicbrainz_url(&artist_id, MUSICBRAINZ) + .is_ok()); + _ = expected.take(); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + assert!(music_hoard + .remove_musicbrainz_url(&artist_id, MUSICBRAINZ) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + } + + #[test] + fn set_clear_musicbrainz_url() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoardBuilder::default().build(); + + music_hoard.add_artist(artist_id.clone()); + + let mut expected: Option = None; + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Setting an incorrect URL is an error. + assert!(music_hoard + .set_musicbrainz_url(&artist_id, MUSICBUTLER) + .is_err()); + assert!(music_hoard + .set_musicbrainz_url(&artist_id, BANDCAMP) + .is_err()); + assert!(music_hoard.set_musicbrainz_url(&artist_id, QOBUZ).is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Setting a URL on an artist not in the collection is an error. + assert!(music_hoard + .set_musicbrainz_url(&artist_id_2, MUSICBRAINZ) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Setting a URL on an artist. + assert!(music_hoard + .set_musicbrainz_url(&artist_id, MUSICBRAINZ) + .is_ok()); + _ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + assert!(music_hoard + .set_musicbrainz_url(&artist_id, MUSICBRAINZ) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + assert!(music_hoard + .set_musicbrainz_url(&artist_id, MUSICBRAINZ_2) + .is_ok()); + _ = expected.insert(MusicBrainz::new(MUSICBRAINZ_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Clearing URLs on an artist that does not exist is an error. + assert!(music_hoard.clear_musicbrainz_url(&artist_id_2).is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Clearing URLs. + assert!(music_hoard.clear_musicbrainz_url(&artist_id).is_ok()); + _ = expected.take(); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + } + + #[test] + fn add_remove_musicbutler_urls() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoardBuilder::default().build(); + + music_hoard.add_artist(artist_id.clone()); + + let mut expected: Vec = vec![]; + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // If any URL is incorrect adding URLs is an error. + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBRAINZ, MUSICBRAINZ_2]) + .is_err()); + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) + .is_err()); + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![QOBUZ, QOBUZ_2]) + .is_err()); + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBRAINZ, MUSICBUTLER, BANDCAMP, QOBUZ]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Adding URLs to an artist not in the collection is an error. + assert!(music_hoard + .add_musicbutler_urls(&artist_id_2, vec![MUSICBUTLER]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Adding a single URL. + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) + .is_ok()); + expected.push(MusicButler::new(MUSICBUTLER).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Adding a URL that already exists is ok, but does not do anything. + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Adding another single URL. + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER_2]) + .is_ok()); + expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER_2]) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Removing URLs from an artist not in the collection is an error. + assert!(music_hoard + .remove_musicbutler_urls(&artist_id_2, vec![MUSICBUTLER]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Removing a URL. + assert!(music_hoard + .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) + .is_ok()); + expected.retain(|url| url.as_str() != MUSICBUTLER); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Removing URls that do not exist is okay, they will be ignored. + assert!(music_hoard + .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Removing a URL. + assert!(music_hoard + .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER_2]) + .is_ok()); + expected.retain(|url| url.as_str() != MUSICBUTLER_2); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + assert!(music_hoard + .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER_2]) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Adding URLs if some exist is okay, they will be ignored. + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) + .is_ok()); + expected.push(MusicButler::new(MUSICBUTLER).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .is_ok()); + expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Removing URLs if some do not exist is okay, they will be ignored. + assert!(music_hoard + .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) + .is_ok()); + expected.retain(|url| url.as_str() != MUSICBUTLER); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + assert!(music_hoard + .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .is_ok()); + expected.retain(|url| url.as_str() != MUSICBUTLER_2); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Adding mutliple URLs without clashes. + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .is_ok()); + expected.push(MusicButler::new(MUSICBUTLER).unwrap()); + expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Removing multiple URLs without clashes. + assert!(music_hoard + .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .is_ok()); + expected.clear(); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + } + + #[test] + fn set_clear_musicbutler_urls() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoardBuilder::default().build(); + + music_hoard.add_artist(artist_id.clone()); + + let mut expected: Vec = vec![]; + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // If any URL is incorrect setting URLs is an error. + assert!(music_hoard + .set_musicbutler_urls(&artist_id, vec![MUSICBRAINZ, MUSICBRAINZ_2]) + .is_err()); + assert!(music_hoard + .set_musicbutler_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) + .is_err()); + assert!(music_hoard + .set_musicbutler_urls(&artist_id, vec![QOBUZ, QOBUZ_2]) + .is_err()); + assert!(music_hoard + .set_musicbutler_urls(&artist_id, vec![MUSICBRAINZ, MUSICBUTLER, BANDCAMP, QOBUZ]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Seting URL on an artist not in the collection is an error. + assert!(music_hoard + .set_musicbutler_urls(&artist_id_2, vec![MUSICBUTLER]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Set URLs. + assert!(music_hoard + .set_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) + .is_ok()); + expected.push(MusicButler::new(MUSICBUTLER).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + assert!(music_hoard + .set_musicbutler_urls(&artist_id, vec![MUSICBUTLER_2]) + .is_ok()); + expected.clear(); + expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + assert!(music_hoard + .set_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .is_ok()); + expected.clear(); + expected.push(MusicButler::new(MUSICBUTLER).unwrap()); + expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Clearing URLs on an artist that does not exist is an error. + assert!(music_hoard.clear_musicbutler_urls(&artist_id_2).is_err()); + + // Clear URLs. + assert!(music_hoard.clear_musicbutler_urls(&artist_id).is_ok()); + expected.clear(); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + } + + #[test] + fn add_remove_bandcamp_urls() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoardBuilder::default().build(); + + music_hoard.add_artist(artist_id.clone()); + + let mut expected: Vec = vec![]; + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // If any URL is incorrect adding URLs is an error. + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![MUSICBRAINZ, MUSICBRAINZ_2]) + .is_err()); + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .is_err()); + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![QOBUZ, QOBUZ_2]) + .is_err()); + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![MUSICBRAINZ, MUSICBUTLER, BANDCAMP, QOBUZ]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Adding URLs to an artist not in the collection is an error. + assert!(music_hoard + .add_bandcamp_urls(&artist_id_2, vec![BANDCAMP]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Adding a single URL. + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![BANDCAMP]) + .is_ok()); + expected.push(Bandcamp::new(BANDCAMP).unwrap()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Adding a URL that already exists is ok, but does not do anything. + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![BANDCAMP]) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Adding another single URL. + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![BANDCAMP_2]) + .is_ok()); + expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![BANDCAMP_2]) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Removing URLs from an artist not in the collection is an error. + assert!(music_hoard + .remove_bandcamp_urls(&artist_id_2, vec![BANDCAMP]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Removing a URL. + assert!(music_hoard + .remove_bandcamp_urls(&artist_id, vec![BANDCAMP]) + .is_ok()); + expected.retain(|url| url.as_str() != BANDCAMP); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Removing URls that do not exist is okay, they will be ignored. + assert!(music_hoard + .remove_bandcamp_urls(&artist_id, vec![BANDCAMP]) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Removing a URL. + assert!(music_hoard + .remove_bandcamp_urls(&artist_id, vec![BANDCAMP_2]) + .is_ok()); + expected.retain(|url| url.as_str() != BANDCAMP_2); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + assert!(music_hoard + .remove_bandcamp_urls(&artist_id, vec![BANDCAMP_2]) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Adding URLs if some exist is okay, they will be ignored. + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![BANDCAMP]) + .is_ok()); + expected.push(Bandcamp::new(BANDCAMP).unwrap()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) + .is_ok()); + expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Removing URLs if some do not exist is okay, they will be ignored. + assert!(music_hoard + .remove_bandcamp_urls(&artist_id, vec![BANDCAMP]) + .is_ok()); + expected.retain(|url| url.as_str() != BANDCAMP); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + assert!(music_hoard + .remove_bandcamp_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) + .is_ok()); + expected.retain(|url| url.as_str() != BANDCAMP_2); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Adding mutliple URLs without clashes. + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) + .is_ok()); + expected.push(Bandcamp::new(BANDCAMP).unwrap()); + expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Removing multiple URLs without clashes. + assert!(music_hoard + .remove_bandcamp_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) + .is_ok()); + expected.clear(); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + } + + #[test] + fn set_clear_bandcamp_urls() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoardBuilder::default().build(); + + music_hoard.add_artist(artist_id.clone()); + + let mut expected: Vec = vec![]; + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // If any URL is incorrect setting URLs is an error. + assert!(music_hoard + .set_bandcamp_urls(&artist_id, vec![MUSICBRAINZ, MUSICBRAINZ_2]) + .is_err()); + assert!(music_hoard + .set_bandcamp_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .is_err()); + assert!(music_hoard + .set_bandcamp_urls(&artist_id, vec![QOBUZ, QOBUZ_2]) + .is_err()); + assert!(music_hoard + .set_bandcamp_urls(&artist_id, vec![MUSICBRAINZ, MUSICBUTLER, BANDCAMP, QOBUZ]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Seting URL on an artist not in the collection is an error. + assert!(music_hoard + .set_bandcamp_urls(&artist_id_2, vec![BANDCAMP]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Set URLs. + assert!(music_hoard + .set_bandcamp_urls(&artist_id, vec![BANDCAMP]) + .is_ok()); + expected.push(Bandcamp::new(BANDCAMP).unwrap()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + assert!(music_hoard + .set_bandcamp_urls(&artist_id, vec![BANDCAMP_2]) + .is_ok()); + expected.clear(); + expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + assert!(music_hoard + .set_bandcamp_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) + .is_ok()); + expected.clear(); + expected.push(Bandcamp::new(BANDCAMP).unwrap()); + expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Clearing URLs on an artist that does not exist is an error. + assert!(music_hoard.clear_bandcamp_urls(&artist_id_2).is_err()); + + // Clear URLs. + assert!(music_hoard.clear_bandcamp_urls(&artist_id).is_ok()); + expected.clear(); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + } + + #[test] + fn add_remove_qobuz_url() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoardBuilder::default().build(); + + music_hoard.add_artist(artist_id.clone()); + + let mut expected: Option = None; + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Adding incorect URL is an error. + assert!(music_hoard.add_qobuz_url(&artist_id, MUSICBRAINZ).is_err()); + assert!(music_hoard.add_qobuz_url(&artist_id, MUSICBUTLER).is_err()); + assert!(music_hoard.add_qobuz_url(&artist_id, BANDCAMP).is_err()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Adding URL to an artist not in the collection is an error. + assert!(music_hoard.add_qobuz_url(&artist_id_2, QOBUZ).is_err()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Adding URL to artist. + assert!(music_hoard.add_qobuz_url(&artist_id, QOBUZ).is_ok()); + _ = expected.insert(Qobuz::new(QOBUZ).unwrap()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Adding the same URL again is ok, but does not do anything. + assert!(music_hoard.add_qobuz_url(&artist_id, QOBUZ).is_ok()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Adding further URLs is an error. + assert!(music_hoard.add_qobuz_url(&artist_id, QOBUZ_2).is_err()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Removing a URL from an artist not in the collection is an error. + assert!(music_hoard.remove_qobuz_url(&artist_id_2, QOBUZ).is_err()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Removing a URL not in the collection is okay, but does not do anything. + assert!(music_hoard.remove_qobuz_url(&artist_id, QOBUZ_2).is_ok()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Removing a URL in the collection removes it. + assert!(music_hoard.remove_qobuz_url(&artist_id, QOBUZ).is_ok()); + _ = expected.take(); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + assert!(music_hoard.remove_qobuz_url(&artist_id, QOBUZ).is_ok()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + } + + #[test] + fn set_clear_qobuz_url() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoardBuilder::default().build(); + + music_hoard.add_artist(artist_id.clone()); + + let mut expected: Option = None; + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Setting an incorrect URL is an error. + assert!(music_hoard.set_qobuz_url(&artist_id, MUSICBUTLER).is_err()); + assert!(music_hoard.set_qobuz_url(&artist_id, BANDCAMP).is_err()); + assert!(music_hoard.set_qobuz_url(&artist_id, MUSICBRAINZ).is_err()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Setting a URL on an artist not in the collection is an error. + assert!(music_hoard.set_qobuz_url(&artist_id_2, QOBUZ).is_err()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Setting a URL on an artist. + assert!(music_hoard.set_qobuz_url(&artist_id, QOBUZ).is_ok()); + _ = expected.insert(Qobuz::new(QOBUZ).unwrap()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + assert!(music_hoard.set_qobuz_url(&artist_id, QOBUZ).is_ok()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + assert!(music_hoard.set_qobuz_url(&artist_id, QOBUZ_2).is_ok()); + _ = expected.insert(Qobuz::new(QOBUZ_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Clearing URLs on an artist that does not exist is an error. + assert!(music_hoard.clear_qobuz_url(&artist_id_2).is_err()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Clearing URLs. + assert!(music_hoard.clear_qobuz_url(&artist_id).is_ok()); + _ = expected.take(); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + } + + #[test] + fn merge_track() { + let left = Track { + id: TrackId { + number: 4, + title: String::from("a title"), + }, + artist: vec![String::from("left artist")], + quality: Quality { + format: Format::Flac, + bitrate: 1411, + }, + }; + let right = Track { + id: left.id.clone(), + artist: vec![String::from("right artist")], + quality: Quality { + format: Format::Mp3, + bitrate: 320, + }, + }; + + let merged = left.clone().merge(right); + assert_eq!(left, merged); + } + + #[test] + fn merge_album_no_overlap() { + let left = FULL_COLLECTION[0].albums[0].to_owned(); + let mut right = FULL_COLLECTION[0].albums[1].to_owned(); + right.id = left.id.clone(); + + let mut expected = left.clone(); + expected.tracks.append(&mut right.tracks.clone()); + expected.tracks.sort_unstable(); + + let merged = left.clone().merge(right.clone()); + assert_eq!(expected, merged); + + // Non-overlapping merge should be commutative. + let merged = right.clone().merge(left.clone()); + assert_eq!(expected, merged); + } + + #[test] + fn merge_album_overlap() { + let mut left = FULL_COLLECTION[0].albums[0].to_owned(); + let mut right = FULL_COLLECTION[0].albums[1].to_owned(); + right.id = left.id.clone(); + left.tracks.push(right.tracks[0].clone()); + left.tracks.sort_unstable(); + + let mut expected = left.clone(); + expected.tracks.append(&mut right.tracks.clone()); + expected.tracks.sort_unstable(); + expected.tracks.dedup(); + + let merged = left.clone().merge(right); + assert_eq!(expected, merged); + } + + #[test] + fn merge_artist_no_overlap() { + let left = FULL_COLLECTION[0].to_owned(); + let mut right = FULL_COLLECTION[1].to_owned(); + right.id = left.id.clone(); + right.properties = ArtistProperties::default(); + + let mut expected = left.clone(); + expected.properties = expected.properties.merge(right.clone().properties); + expected.albums.append(&mut right.albums.clone()); + expected.albums.sort_unstable(); + + let merged = left.clone().merge(right.clone()); + assert_eq!(expected, merged); + + // Non-overlapping merge should be commutative. + let merged = right.clone().merge(left.clone()); + assert_eq!(expected, merged); + } + + #[test] + fn merge_artist_overlap() { + let mut left = FULL_COLLECTION[0].to_owned(); + let mut right = FULL_COLLECTION[1].to_owned(); + right.id = left.id.clone(); + left.albums.push(right.albums[0].clone()); + left.albums.sort_unstable(); + + let mut expected = left.clone(); + expected.properties = expected.properties.merge(right.clone().properties); + expected.albums.append(&mut right.albums.clone()); + expected.albums.sort_unstable(); + expected.albums.dedup(); + + let merged = left.clone().merge(right); + assert_eq!(expected, merged); + } + + #[test] + fn merge_collection_no_overlap() { + let half: usize = FULL_COLLECTION.len() / 2; + + let left = FULL_COLLECTION[..half].to_owned(); + let right = FULL_COLLECTION[half..].to_owned(); + + let mut expected = FULL_COLLECTION.to_owned(); + expected.sort_unstable(); + + let merged = MusicHoard::::merge_collections( + left.clone() + .into_iter() + .map(|a| (a.id.clone(), a)) + .collect(), + right.clone(), + ); + assert_eq!(expected, merged); + + // The merge is completely non-overlapping so it should be commutative. + let merged = MusicHoard::::merge_collections( + right + .clone() + .into_iter() + .map(|a| (a.id.clone(), a)) + .collect(), + left.clone(), + ); + assert_eq!(expected, merged); + } + + #[test] + fn merge_collection_overlap() { + let half: usize = FULL_COLLECTION.len() / 2; + + let left = FULL_COLLECTION[..(half + 1)].to_owned(); + let right = FULL_COLLECTION[half..].to_owned(); + + let mut expected = FULL_COLLECTION.to_owned(); + expected.sort_unstable(); + + let merged = MusicHoard::::merge_collections( + left.clone() + .into_iter() + .map(|a| (a.id.clone(), a)) + .collect(), + right.clone(), + ); + assert_eq!(expected, merged); + + // The merge does not overwrite any data so it should be commutative. + let merged = MusicHoard::::merge_collections( + right + .clone() + .into_iter() + .map(|a| (a.id.clone(), a)) + .collect(), + left.clone(), + ); + assert_eq!(expected, merged); + } + + #[test] + fn merge_collection_incompatible_sorting() { + // It may be that the same artist in one collection has a "sort" field defined while the + // same artist in the other collection does not. This means that the two collections are not + // sorted consistently. If the merge assumes they are sorted consistently this will lead to + // the same artist appearing twice in the final list. This should not be the case. + + // We will mimic this situation by taking the last artist from FULL_COLLECTION and giving it a + // sorting name that would place it in the beginning. + let left = FULL_COLLECTION.to_owned(); + let mut right: Vec = vec![left.last().unwrap().clone()]; + + assert!(right.first().unwrap() > left.first().unwrap()); + let artist_sort = Some(ArtistId::new("album_artist 0")); + right[0].sort = artist_sort.clone(); + assert!(right.first().unwrap() < left.first().unwrap()); + + // The result of the merge should be the same list of artists, but with the last artist now + // in first place. + let mut expected = left.to_owned(); + expected.last_mut().as_mut().unwrap().sort = artist_sort.clone(); + expected.rotate_right(1); + + let merged = MusicHoard::::merge_collections( + left.clone() + .into_iter() + .map(|a| (a.id.clone(), a)) + .collect(), + right.clone(), + ); + assert_eq!(expected.len(), merged.len()); + assert_eq!(expected, merged); + + // The merge overwrites the sort data, but no data is erased so it should be commutative. + let merged = MusicHoard::::merge_collections( + right + .clone() + .into_iter() + .map(|a| (a.id.clone(), a)) + .collect(), + left.clone(), + ); + assert_eq!(expected.len(), merged.len()); + assert_eq!(expected, merged); + } + + #[test] + fn rescan_library_ordered() { + let mut library = MockILibrary::new(); + let database = MockIDatabase::new(); + + let library_input = Query::new(); + let library_result = Ok(LIBRARY_ITEMS.to_owned()); + + library + .expect_list() + .with(predicate::eq(library_input)) + .times(1) + .return_once(|_| library_result); + + let mut music_hoard = MusicHoardBuilder::default() + .set_library(library) + .set_database(database) + .build(); + + music_hoard.rescan_library().unwrap(); + assert_eq!(music_hoard.get_collection(), &*LIBRARY_COLLECTION); + } + + #[test] + fn rescan_library_unordered() { + let mut library = MockILibrary::new(); + let database = MockIDatabase::new(); + + let library_input = Query::new(); + let mut library_result = Ok(LIBRARY_ITEMS.to_owned()); + + // Swap the last item with the first. + let last = library_result.as_ref().unwrap().len() - 1; + library_result.as_mut().unwrap().swap(0, last); + + library + .expect_list() + .with(predicate::eq(library_input)) + .times(1) + .return_once(|_| library_result); + + let mut music_hoard = MusicHoardBuilder::default() + .set_library(library) + .set_database(database) + .build(); + + music_hoard.rescan_library().unwrap(); + assert_eq!(music_hoard.get_collection(), &*LIBRARY_COLLECTION); + } + + #[test] + fn rescan_library_album_title_year_clash() { + let mut library = MockILibrary::new(); + let database = MockIDatabase::new(); + + let mut expected = LIBRARY_COLLECTION.to_owned(); + let removed_album_id = expected[0].albums[0].id.clone(); + let clashed_album_id = &expected[1].albums[0].id; + + let mut items = LIBRARY_ITEMS.to_owned(); + for item in items.iter_mut().filter(|it| { + (it.album_year == removed_album_id.year) && (it.album_title == removed_album_id.title) + }) { + item.album_year = clashed_album_id.year; + item.album_title = clashed_album_id.title.clone(); + } + + expected[0].albums[0].id = clashed_album_id.clone(); + + let library_input = Query::new(); + let library_result = Ok(items); + + library + .expect_list() + .with(predicate::eq(library_input)) + .times(1) + .return_once(|_| library_result); + + let mut music_hoard = MusicHoardBuilder::default() + .set_library(library) + .set_database(database) + .build(); + + music_hoard.rescan_library().unwrap(); + assert_eq!(music_hoard.get_collection(), &expected); + } + + #[test] + fn rescan_library_album_artist_sort_clash() { + let mut library = MockILibrary::new(); + let database = MockIDatabase::new(); + + let library_input = Query::new(); + let mut library_items = LIBRARY_ITEMS.to_owned(); + + assert_eq!(library_items[0].album_artist, library_items[1].album_artist); + library_items[0].album_artist_sort = Some(library_items[0].album_artist.clone()); + library_items[1].album_artist_sort = Some( + library_items[1] + .album_artist + .clone() + .chars() + .rev() + .collect(), + ); + + let library_result = Ok(library_items); + + library + .expect_list() + .with(predicate::eq(library_input)) + .times(1) + .return_once(|_| library_result); + + let mut music_hoard = MusicHoardBuilder::default() + .set_library(library) + .set_database(database) + .build(); + + assert!(music_hoard.rescan_library().is_err()); + } + + #[test] + fn load_database() { + let library = MockILibrary::new(); + let mut database = MockIDatabase::new(); + + database + .expect_load() + .times(1) + .return_once(|| Ok(FULL_COLLECTION.to_owned())); + + let mut music_hoard = MusicHoardBuilder::default() + .set_library(library) + .set_database(database) + .build(); + + music_hoard.load_from_database().unwrap(); + assert_eq!(music_hoard.get_collection(), &*FULL_COLLECTION); + } + + #[test] + fn rescan_get_save() { + let mut library = MockILibrary::new(); + let mut database = MockIDatabase::new(); + + let library_input = Query::new(); + let library_result = Ok(LIBRARY_ITEMS.to_owned()); + + let database_input = LIBRARY_COLLECTION.to_owned(); + let database_result = Ok(()); + + library + .expect_list() + .with(predicate::eq(library_input)) + .times(1) + .return_once(|_| library_result); + + database + .expect_save() + .with(predicate::eq(database_input)) + .times(1) + .return_once(|_: &Collection| database_result); + + let mut music_hoard = MusicHoardBuilder::default() + .set_library(library) + .set_database(database) + .build(); + + music_hoard.rescan_library().unwrap(); + assert_eq!(music_hoard.get_collection(), &*LIBRARY_COLLECTION); + music_hoard.save_to_database().unwrap(); + } + + #[test] + fn library_error() { + let mut library = MockILibrary::new(); + let database = MockIDatabase::new(); + + let library_result = Err(library::Error::Invalid(String::from("invalid data"))); + + library + .expect_list() + .times(1) + .return_once(|_| library_result); + + let mut music_hoard = MusicHoardBuilder::default() + .set_library(library) + .set_database(database) + .build(); + + let actual_err = music_hoard.rescan_library().unwrap_err(); + let expected_err = + Error::LibraryError(library::Error::Invalid(String::from("invalid data")).to_string()); + + assert_eq!(actual_err, expected_err); + assert_eq!(actual_err.to_string(), expected_err.to_string()); + } + + #[test] + fn database_load_error() { + let library = MockILibrary::new(); + let mut database = MockIDatabase::new(); + + let database_result = Err(database::LoadError::IoError(String::from("I/O error"))); + + database + .expect_load() + .times(1) + .return_once(|| database_result); + + let mut music_hoard = MusicHoardBuilder::default() + .set_library(library) + .set_database(database) + .build(); + + let actual_err = music_hoard.load_from_database().unwrap_err(); + let expected_err = Error::DatabaseError( + database::LoadError::IoError(String::from("I/O error")).to_string(), + ); + + assert_eq!(actual_err, expected_err); + assert_eq!(actual_err.to_string(), expected_err.to_string()); + } + + #[test] + fn database_save_error() { + let library = MockILibrary::new(); + let mut database = MockIDatabase::new(); + + let database_result = Err(database::SaveError::IoError(String::from("I/O error"))); + + database + .expect_save() + .times(1) + .return_once(|_: &Collection| database_result); + + let mut music_hoard = MusicHoardBuilder::default() + .set_library(library) + .set_database(database) + .build(); + + let actual_err = music_hoard.save_to_database().unwrap_err(); + let expected_err = Error::DatabaseError( + database::SaveError::IoError(String::from("I/O error")).to_string(), + ); + + assert_eq!(actual_err, expected_err); + assert_eq!(actual_err.to_string(), expected_err.to_string()); + } +} diff --git a/src/core/musichoard/musichoard_builder.rs b/src/core/musichoard/musichoard_builder.rs new file mode 100644 index 0000000..c531c3b --- /dev/null +++ b/src/core/musichoard/musichoard_builder.rs @@ -0,0 +1,50 @@ +use super::database::IDatabase; +use super::library::ILibrary; +use super::musichoard::{MusicHoard, NoDatabase, NoLibrary}; + +/// Builder for [`MusicHoard`]. Its purpose is to make it easier to set various combinations of +/// library/database or their absence. +pub struct MusicHoardBuilder { + library: LIB, + database: DB, +} + +impl Default for MusicHoardBuilder { + /// Create a [`MusicHoardBuilder`]. + fn default() -> Self { + Self::new() + } +} + +impl MusicHoardBuilder { + /// Create a [`MusicHoardBuilder`]. + pub fn new() -> Self { + MusicHoardBuilder { + library: NoLibrary, + database: NoDatabase, + } + } +} + +impl MusicHoardBuilder { + /// Set a library for [`MusicHoard`]. + pub fn set_library(self, library: NEWLIB) -> MusicHoardBuilder { + MusicHoardBuilder { + library, + database: self.database, + } + } + + /// Set a database for [`MusicHoard`]. + pub fn set_database(self, database: NEWDB) -> MusicHoardBuilder { + MusicHoardBuilder { + library: self.library, + database, + } + } + + /// Build [`MusicHoard`] with the currently set library and database. + pub fn build(self) -> MusicHoard { + MusicHoard::new(self.library, self.database) + } +} diff --git a/src/core/musichoard/tests.rs b/src/core/musichoard/tests.rs deleted file mode 100644 index ec6cd15..0000000 --- a/src/core/musichoard/tests.rs +++ /dev/null @@ -1,1193 +0,0 @@ -use mockall::predicate; - -use super::*; - -use super::testmod::{FULL_COLLECTION, LIBRARY_COLLECTION}; - -// FIXME: check all crate::* imports - are the tests where they should be? -use crate::core::collection::{ - artist::{ArtistProperties, Bandcamp, MusicBrainz, MusicButler, Qobuz}, - track::Format, - Merge, -}; -use crate::database::{self, MockIDatabase}; -use crate::library::{self, testmod::LIBRARY_ITEMS, MockILibrary}; - -static MUSICBRAINZ: &str = "https://musicbrainz.org/artist/d368baa8-21ca-4759-9731-0b2753071ad8"; -static MUSICBRAINZ_2: &str = "https://musicbrainz.org/artist/823869a5-5ded-4f6b-9fb7-2a9344d83c6b"; -static MUSICBUTLER: &str = "https://www.musicbutler.io/artist-page/483340948"; -static MUSICBUTLER_2: &str = "https://www.musicbutler.io/artist-page/658903042/"; -static BANDCAMP: &str = "https://thelasthangmen.bandcamp.com/"; -static BANDCAMP_2: &str = "https://viciouscrusade.bandcamp.com/"; -static QOBUZ: &str = "https://www.qobuz.com/nl-nl/interpreter/the-last-hangmen/1244413"; -static QOBUZ_2: &str = "https://www.qobuz.com/nl-nl/interpreter/vicious-crusade/7522386"; - -// FIXME: all tests with URLs should go to collection::artist -#[test] -fn urls() { - assert!(MusicBrainz::new(MUSICBRAINZ).is_ok()); - assert!(MusicBrainz::new(MUSICBUTLER).is_err()); - assert!(MusicBrainz::new(BANDCAMP).is_err()); - assert!(MusicBrainz::new(QOBUZ).is_err()); - - assert!(MusicButler::new(MUSICBRAINZ).is_err()); - assert!(MusicButler::new(MUSICBUTLER).is_ok()); - assert!(MusicButler::new(BANDCAMP).is_err()); - assert!(MusicButler::new(QOBUZ).is_err()); - - assert!(Bandcamp::new(MUSICBRAINZ).is_err()); - assert!(Bandcamp::new(MUSICBUTLER).is_err()); - assert!(Bandcamp::new(BANDCAMP).is_ok()); - assert!(Bandcamp::new(QOBUZ).is_err()); - - assert!(Qobuz::new(MUSICBRAINZ).is_err()); - assert!(Qobuz::new(MUSICBUTLER).is_err()); - assert!(Qobuz::new(BANDCAMP).is_err()); - assert!(Qobuz::new(QOBUZ).is_ok()); -} - -#[test] -fn artist_new_delete() { - let artist_id = ArtistId::new("an artist"); - let artist_id_2 = ArtistId::new("another artist"); - let mut music_hoard = MusicHoardBuilder::default().build(); - - let mut expected: Vec = vec![]; - - music_hoard.add_artist(artist_id.clone()); - expected.push(Artist::new(artist_id.clone())); - assert_eq!(music_hoard.collection, expected); - - music_hoard.add_artist(artist_id.clone()); - assert_eq!(music_hoard.collection, expected); - - music_hoard.remove_artist(&artist_id_2); - assert_eq!(music_hoard.collection, expected); - - music_hoard.remove_artist(&artist_id); - _ = expected.pop(); - assert_eq!(music_hoard.collection, expected); -} - -#[test] -fn artist_sort_set_clear() { - let mut music_hoard = MusicHoardBuilder::default().build(); - - let artist_1_id = ArtistId::new("the artist"); - let artist_1_sort = ArtistId::new("artist, the"); - - // Must be after "artist, the", but before "the artist" - let artist_2_id = ArtistId::new("b-artist"); - - assert!(artist_1_sort < artist_2_id); - assert!(artist_2_id < artist_1_id); - - music_hoard.add_artist(artist_1_id.clone()); - music_hoard.add_artist(artist_2_id.clone()); - - let artist_1: &Artist = music_hoard.get_artist(&artist_1_id).unwrap(); - let artist_2: &Artist = music_hoard.get_artist(&artist_2_id).unwrap(); - - assert!(artist_2 < artist_1); - - assert_eq!(artist_1, &music_hoard.collection[1]); - assert_eq!(artist_2, &music_hoard.collection[0]); - - music_hoard - .set_artist_sort(artist_1_id.as_ref(), artist_1_sort.clone()) - .unwrap(); - - let artist_1: &Artist = music_hoard.get_artist(&artist_1_id).unwrap(); - let artist_2: &Artist = music_hoard.get_artist(&artist_2_id).unwrap(); - - assert!(artist_1 < artist_2); - - assert_eq!(artist_1, &music_hoard.collection[0]); - assert_eq!(artist_2, &music_hoard.collection[1]); - - music_hoard.clear_artist_sort(artist_1_id.as_ref()).unwrap(); - - let artist_1: &Artist = music_hoard.get_artist(&artist_1_id).unwrap(); - let artist_2: &Artist = music_hoard.get_artist(&artist_2_id).unwrap(); - - assert!(artist_2 < artist_1); - - assert_eq!(artist_1, &music_hoard.collection[1]); - assert_eq!(artist_2, &music_hoard.collection[0]); -} - -#[test] -fn collection_error() { - let artist_id = ArtistId::new("an artist"); - let mut music_hoard = MusicHoardBuilder::default().build(); - - let actual_err = music_hoard - .add_musicbrainz_url(&artist_id, QOBUZ) - .unwrap_err(); - let expected_err = - Error::CollectionError(String::from("artist 'an artist' is not in the collection")); - assert_eq!(actual_err, expected_err); - assert_eq!(actual_err.to_string(), expected_err.to_string()); -} - -#[test] -fn add_remove_musicbrainz_url() { - let artist_id = ArtistId::new("an artist"); - let artist_id_2 = ArtistId::new("another artist"); - let mut music_hoard = MusicHoardBuilder::default().build(); - - music_hoard.add_artist(artist_id.clone()); - - let mut expected: Option = None; - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); - - // Adding incorect URL is an error. - assert!(music_hoard - .add_musicbrainz_url(&artist_id, MUSICBUTLER) - .is_err()); - assert!(music_hoard - .add_musicbrainz_url(&artist_id, BANDCAMP) - .is_err()); - assert!(music_hoard.add_musicbrainz_url(&artist_id, QOBUZ).is_err()); - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); - - // Adding URL to an artist not in the collection is an error. - assert!(music_hoard - .add_musicbrainz_url(&artist_id_2, MUSICBRAINZ) - .is_err()); - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); - - // Adding URL to artist. - assert!(music_hoard - .add_musicbrainz_url(&artist_id, MUSICBRAINZ) - .is_ok()); - _ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap()); - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); - - // Adding the same URL again is ok, but does not do anything. - assert!(music_hoard - .add_musicbrainz_url(&artist_id, MUSICBRAINZ) - .is_ok()); - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); - - // Adding further URLs is an error. - assert!(music_hoard - .add_musicbrainz_url(&artist_id, MUSICBRAINZ_2) - .is_err()); - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); - - // Removing a URL from an artist not in the collection is an error. - assert!(music_hoard - .remove_musicbrainz_url(&artist_id_2, MUSICBRAINZ) - .is_err()); - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); - - // Removing a URL not in the collection is okay, but does not do anything. - assert!(music_hoard - .remove_musicbrainz_url(&artist_id, MUSICBRAINZ_2) - .is_ok()); - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); - - // Removing a URL in the collection removes it. - assert!(music_hoard - .remove_musicbrainz_url(&artist_id, MUSICBRAINZ) - .is_ok()); - _ = expected.take(); - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); - - assert!(music_hoard - .remove_musicbrainz_url(&artist_id, MUSICBRAINZ) - .is_ok()); - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); -} - -#[test] -fn set_clear_musicbrainz_url() { - let artist_id = ArtistId::new("an artist"); - let artist_id_2 = ArtistId::new("another artist"); - let mut music_hoard = MusicHoardBuilder::default().build(); - - music_hoard.add_artist(artist_id.clone()); - - let mut expected: Option = None; - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); - - // Setting an incorrect URL is an error. - assert!(music_hoard - .set_musicbrainz_url(&artist_id, MUSICBUTLER) - .is_err()); - assert!(music_hoard - .set_musicbrainz_url(&artist_id, BANDCAMP) - .is_err()); - assert!(music_hoard.set_musicbrainz_url(&artist_id, QOBUZ).is_err()); - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); - - // Setting a URL on an artist not in the collection is an error. - assert!(music_hoard - .set_musicbrainz_url(&artist_id_2, MUSICBRAINZ) - .is_err()); - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); - - // Setting a URL on an artist. - assert!(music_hoard - .set_musicbrainz_url(&artist_id, MUSICBRAINZ) - .is_ok()); - _ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap()); - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); - - assert!(music_hoard - .set_musicbrainz_url(&artist_id, MUSICBRAINZ) - .is_ok()); - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); - - assert!(music_hoard - .set_musicbrainz_url(&artist_id, MUSICBRAINZ_2) - .is_ok()); - _ = expected.insert(MusicBrainz::new(MUSICBRAINZ_2).unwrap()); - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); - - // Clearing URLs on an artist that does not exist is an error. - assert!(music_hoard.clear_musicbrainz_url(&artist_id_2).is_err()); - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); - - // Clearing URLs. - assert!(music_hoard.clear_musicbrainz_url(&artist_id).is_ok()); - _ = expected.take(); - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); -} - -#[test] -fn add_remove_musicbutler_urls() { - let artist_id = ArtistId::new("an artist"); - let artist_id_2 = ArtistId::new("another artist"); - let mut music_hoard = MusicHoardBuilder::default().build(); - - music_hoard.add_artist(artist_id.clone()); - - let mut expected: Vec = vec![]; - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // If any URL is incorrect adding URLs is an error. - assert!(music_hoard - .add_musicbutler_urls(&artist_id, vec![MUSICBRAINZ, MUSICBRAINZ_2]) - .is_err()); - assert!(music_hoard - .add_musicbutler_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) - .is_err()); - assert!(music_hoard - .add_musicbutler_urls(&artist_id, vec![QOBUZ, QOBUZ_2]) - .is_err()); - assert!(music_hoard - .add_musicbutler_urls(&artist_id, vec![MUSICBRAINZ, MUSICBUTLER, BANDCAMP, QOBUZ]) - .is_err()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // Adding URLs to an artist not in the collection is an error. - assert!(music_hoard - .add_musicbutler_urls(&artist_id_2, vec![MUSICBUTLER]) - .is_err()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // Adding a single URL. - assert!(music_hoard - .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) - .is_ok()); - expected.push(MusicButler::new(MUSICBUTLER).unwrap()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // Adding a URL that already exists is ok, but does not do anything. - assert!(music_hoard - .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) - .is_ok()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // Adding another single URL. - assert!(music_hoard - .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER_2]) - .is_ok()); - expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - assert!(music_hoard - .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER_2]) - .is_ok()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // Removing URLs from an artist not in the collection is an error. - assert!(music_hoard - .remove_musicbutler_urls(&artist_id_2, vec![MUSICBUTLER]) - .is_err()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // Removing a URL. - assert!(music_hoard - .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) - .is_ok()); - expected.retain(|url| url.as_str() != MUSICBUTLER); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // Removing URls that do not exist is okay, they will be ignored. - assert!(music_hoard - .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) - .is_ok()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // Removing a URL. - assert!(music_hoard - .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER_2]) - .is_ok()); - expected.retain(|url| url.as_str() != MUSICBUTLER_2); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - assert!(music_hoard - .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER_2]) - .is_ok()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // Adding URLs if some exist is okay, they will be ignored. - assert!(music_hoard - .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) - .is_ok()); - expected.push(MusicButler::new(MUSICBUTLER).unwrap()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - assert!(music_hoard - .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) - .is_ok()); - expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // Removing URLs if some do not exist is okay, they will be ignored. - assert!(music_hoard - .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) - .is_ok()); - expected.retain(|url| url.as_str() != MUSICBUTLER); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - assert!(music_hoard - .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) - .is_ok()); - expected.retain(|url| url.as_str() != MUSICBUTLER_2); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // Adding mutliple URLs without clashes. - assert!(music_hoard - .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) - .is_ok()); - expected.push(MusicButler::new(MUSICBUTLER).unwrap()); - expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // Removing multiple URLs without clashes. - assert!(music_hoard - .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) - .is_ok()); - expected.clear(); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); -} - -#[test] -fn set_clear_musicbutler_urls() { - let artist_id = ArtistId::new("an artist"); - let artist_id_2 = ArtistId::new("another artist"); - let mut music_hoard = MusicHoardBuilder::default().build(); - - music_hoard.add_artist(artist_id.clone()); - - let mut expected: Vec = vec![]; - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // If any URL is incorrect setting URLs is an error. - assert!(music_hoard - .set_musicbutler_urls(&artist_id, vec![MUSICBRAINZ, MUSICBRAINZ_2]) - .is_err()); - assert!(music_hoard - .set_musicbutler_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) - .is_err()); - assert!(music_hoard - .set_musicbutler_urls(&artist_id, vec![QOBUZ, QOBUZ_2]) - .is_err()); - assert!(music_hoard - .set_musicbutler_urls(&artist_id, vec![MUSICBRAINZ, MUSICBUTLER, BANDCAMP, QOBUZ]) - .is_err()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // Seting URL on an artist not in the collection is an error. - assert!(music_hoard - .set_musicbutler_urls(&artist_id_2, vec![MUSICBUTLER]) - .is_err()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // Set URLs. - assert!(music_hoard - .set_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) - .is_ok()); - expected.push(MusicButler::new(MUSICBUTLER).unwrap()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - assert!(music_hoard - .set_musicbutler_urls(&artist_id, vec![MUSICBUTLER_2]) - .is_ok()); - expected.clear(); - expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - assert!(music_hoard - .set_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) - .is_ok()); - expected.clear(); - expected.push(MusicButler::new(MUSICBUTLER).unwrap()); - expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); - - // Clearing URLs on an artist that does not exist is an error. - assert!(music_hoard.clear_musicbutler_urls(&artist_id_2).is_err()); - - // Clear URLs. - assert!(music_hoard.clear_musicbutler_urls(&artist_id).is_ok()); - expected.clear(); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); -} - -#[test] -fn add_remove_bandcamp_urls() { - let artist_id = ArtistId::new("an artist"); - let artist_id_2 = ArtistId::new("another artist"); - let mut music_hoard = MusicHoardBuilder::default().build(); - - music_hoard.add_artist(artist_id.clone()); - - let mut expected: Vec = vec![]; - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // If any URL is incorrect adding URLs is an error. - assert!(music_hoard - .add_bandcamp_urls(&artist_id, vec![MUSICBRAINZ, MUSICBRAINZ_2]) - .is_err()); - assert!(music_hoard - .add_bandcamp_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) - .is_err()); - assert!(music_hoard - .add_bandcamp_urls(&artist_id, vec![QOBUZ, QOBUZ_2]) - .is_err()); - assert!(music_hoard - .add_bandcamp_urls(&artist_id, vec![MUSICBRAINZ, MUSICBUTLER, BANDCAMP, QOBUZ]) - .is_err()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // Adding URLs to an artist not in the collection is an error. - assert!(music_hoard - .add_bandcamp_urls(&artist_id_2, vec![BANDCAMP]) - .is_err()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // Adding a single URL. - assert!(music_hoard - .add_bandcamp_urls(&artist_id, vec![BANDCAMP]) - .is_ok()); - expected.push(Bandcamp::new(BANDCAMP).unwrap()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // Adding a URL that already exists is ok, but does not do anything. - assert!(music_hoard - .add_bandcamp_urls(&artist_id, vec![BANDCAMP]) - .is_ok()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // Adding another single URL. - assert!(music_hoard - .add_bandcamp_urls(&artist_id, vec![BANDCAMP_2]) - .is_ok()); - expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - assert!(music_hoard - .add_bandcamp_urls(&artist_id, vec![BANDCAMP_2]) - .is_ok()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // Removing URLs from an artist not in the collection is an error. - assert!(music_hoard - .remove_bandcamp_urls(&artist_id_2, vec![BANDCAMP]) - .is_err()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // Removing a URL. - assert!(music_hoard - .remove_bandcamp_urls(&artist_id, vec![BANDCAMP]) - .is_ok()); - expected.retain(|url| url.as_str() != BANDCAMP); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // Removing URls that do not exist is okay, they will be ignored. - assert!(music_hoard - .remove_bandcamp_urls(&artist_id, vec![BANDCAMP]) - .is_ok()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // Removing a URL. - assert!(music_hoard - .remove_bandcamp_urls(&artist_id, vec![BANDCAMP_2]) - .is_ok()); - expected.retain(|url| url.as_str() != BANDCAMP_2); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - assert!(music_hoard - .remove_bandcamp_urls(&artist_id, vec![BANDCAMP_2]) - .is_ok()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // Adding URLs if some exist is okay, they will be ignored. - assert!(music_hoard - .add_bandcamp_urls(&artist_id, vec![BANDCAMP]) - .is_ok()); - expected.push(Bandcamp::new(BANDCAMP).unwrap()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - assert!(music_hoard - .add_bandcamp_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) - .is_ok()); - expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // Removing URLs if some do not exist is okay, they will be ignored. - assert!(music_hoard - .remove_bandcamp_urls(&artist_id, vec![BANDCAMP]) - .is_ok()); - expected.retain(|url| url.as_str() != BANDCAMP); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - assert!(music_hoard - .remove_bandcamp_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) - .is_ok()); - expected.retain(|url| url.as_str() != BANDCAMP_2); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // Adding mutliple URLs without clashes. - assert!(music_hoard - .add_bandcamp_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) - .is_ok()); - expected.push(Bandcamp::new(BANDCAMP).unwrap()); - expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // Removing multiple URLs without clashes. - assert!(music_hoard - .remove_bandcamp_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) - .is_ok()); - expected.clear(); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); -} - -#[test] -fn set_clear_bandcamp_urls() { - let artist_id = ArtistId::new("an artist"); - let artist_id_2 = ArtistId::new("another artist"); - let mut music_hoard = MusicHoardBuilder::default().build(); - - music_hoard.add_artist(artist_id.clone()); - - let mut expected: Vec = vec![]; - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // If any URL is incorrect setting URLs is an error. - assert!(music_hoard - .set_bandcamp_urls(&artist_id, vec![MUSICBRAINZ, MUSICBRAINZ_2]) - .is_err()); - assert!(music_hoard - .set_bandcamp_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) - .is_err()); - assert!(music_hoard - .set_bandcamp_urls(&artist_id, vec![QOBUZ, QOBUZ_2]) - .is_err()); - assert!(music_hoard - .set_bandcamp_urls(&artist_id, vec![MUSICBRAINZ, MUSICBUTLER, BANDCAMP, QOBUZ]) - .is_err()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // Seting URL on an artist not in the collection is an error. - assert!(music_hoard - .set_bandcamp_urls(&artist_id_2, vec![BANDCAMP]) - .is_err()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // Set URLs. - assert!(music_hoard - .set_bandcamp_urls(&artist_id, vec![BANDCAMP]) - .is_ok()); - expected.push(Bandcamp::new(BANDCAMP).unwrap()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - assert!(music_hoard - .set_bandcamp_urls(&artist_id, vec![BANDCAMP_2]) - .is_ok()); - expected.clear(); - expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - assert!(music_hoard - .set_bandcamp_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) - .is_ok()); - expected.clear(); - expected.push(Bandcamp::new(BANDCAMP).unwrap()); - expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); - - // Clearing URLs on an artist that does not exist is an error. - assert!(music_hoard.clear_bandcamp_urls(&artist_id_2).is_err()); - - // Clear URLs. - assert!(music_hoard.clear_bandcamp_urls(&artist_id).is_ok()); - expected.clear(); - assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); -} - -#[test] -fn add_remove_qobuz_url() { - let artist_id = ArtistId::new("an artist"); - let artist_id_2 = ArtistId::new("another artist"); - let mut music_hoard = MusicHoardBuilder::default().build(); - - music_hoard.add_artist(artist_id.clone()); - - let mut expected: Option = None; - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); - - // Adding incorect URL is an error. - assert!(music_hoard.add_qobuz_url(&artist_id, MUSICBRAINZ).is_err()); - assert!(music_hoard.add_qobuz_url(&artist_id, MUSICBUTLER).is_err()); - assert!(music_hoard.add_qobuz_url(&artist_id, BANDCAMP).is_err()); - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); - - // Adding URL to an artist not in the collection is an error. - assert!(music_hoard.add_qobuz_url(&artist_id_2, QOBUZ).is_err()); - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); - - // Adding URL to artist. - assert!(music_hoard.add_qobuz_url(&artist_id, QOBUZ).is_ok()); - _ = expected.insert(Qobuz::new(QOBUZ).unwrap()); - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); - - // Adding the same URL again is ok, but does not do anything. - assert!(music_hoard.add_qobuz_url(&artist_id, QOBUZ).is_ok()); - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); - - // Adding further URLs is an error. - assert!(music_hoard.add_qobuz_url(&artist_id, QOBUZ_2).is_err()); - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); - - // Removing a URL from an artist not in the collection is an error. - assert!(music_hoard.remove_qobuz_url(&artist_id_2, QOBUZ).is_err()); - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); - - // Removing a URL not in the collection is okay, but does not do anything. - assert!(music_hoard.remove_qobuz_url(&artist_id, QOBUZ_2).is_ok()); - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); - - // Removing a URL in the collection removes it. - assert!(music_hoard.remove_qobuz_url(&artist_id, QOBUZ).is_ok()); - _ = expected.take(); - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); - - assert!(music_hoard.remove_qobuz_url(&artist_id, QOBUZ).is_ok()); - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); -} - -#[test] -fn set_clear_qobuz_url() { - let artist_id = ArtistId::new("an artist"); - let artist_id_2 = ArtistId::new("another artist"); - let mut music_hoard = MusicHoardBuilder::default().build(); - - music_hoard.add_artist(artist_id.clone()); - - let mut expected: Option = None; - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); - - // Setting an incorrect URL is an error. - assert!(music_hoard.set_qobuz_url(&artist_id, MUSICBUTLER).is_err()); - assert!(music_hoard.set_qobuz_url(&artist_id, BANDCAMP).is_err()); - assert!(music_hoard.set_qobuz_url(&artist_id, MUSICBRAINZ).is_err()); - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); - - // Setting a URL on an artist not in the collection is an error. - assert!(music_hoard.set_qobuz_url(&artist_id_2, QOBUZ).is_err()); - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); - - // Setting a URL on an artist. - assert!(music_hoard.set_qobuz_url(&artist_id, QOBUZ).is_ok()); - _ = expected.insert(Qobuz::new(QOBUZ).unwrap()); - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); - - assert!(music_hoard.set_qobuz_url(&artist_id, QOBUZ).is_ok()); - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); - - assert!(music_hoard.set_qobuz_url(&artist_id, QOBUZ_2).is_ok()); - _ = expected.insert(Qobuz::new(QOBUZ_2).unwrap()); - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); - - // Clearing URLs on an artist that does not exist is an error. - assert!(music_hoard.clear_qobuz_url(&artist_id_2).is_err()); - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); - - // Clearing URLs. - assert!(music_hoard.clear_qobuz_url(&artist_id).is_ok()); - _ = expected.take(); - assert_eq!(music_hoard.collection[0].properties.qobuz, expected); -} - -#[test] -fn merge_track() { - let left = Track { - id: TrackId { - number: 4, - title: String::from("a title"), - }, - artist: vec![String::from("left artist")], - quality: Quality { - format: Format::Flac, - bitrate: 1411, - }, - }; - let right = Track { - id: left.id.clone(), - artist: vec![String::from("right artist")], - quality: Quality { - format: Format::Mp3, - bitrate: 320, - }, - }; - - let merged = left.clone().merge(right); - assert_eq!(left, merged); -} - -#[test] -fn merge_album_no_overlap() { - let left = FULL_COLLECTION[0].albums[0].to_owned(); - let mut right = FULL_COLLECTION[0].albums[1].to_owned(); - right.id = left.id.clone(); - - let mut expected = left.clone(); - expected.tracks.append(&mut right.tracks.clone()); - expected.tracks.sort_unstable(); - - let merged = left.clone().merge(right.clone()); - assert_eq!(expected, merged); - - // Non-overlapping merge should be commutative. - let merged = right.clone().merge(left.clone()); - assert_eq!(expected, merged); -} - -#[test] -fn merge_album_overlap() { - let mut left = FULL_COLLECTION[0].albums[0].to_owned(); - let mut right = FULL_COLLECTION[0].albums[1].to_owned(); - right.id = left.id.clone(); - left.tracks.push(right.tracks[0].clone()); - left.tracks.sort_unstable(); - - let mut expected = left.clone(); - expected.tracks.append(&mut right.tracks.clone()); - expected.tracks.sort_unstable(); - expected.tracks.dedup(); - - let merged = left.clone().merge(right); - assert_eq!(expected, merged); -} - -#[test] -fn merge_artist_no_overlap() { - let left = FULL_COLLECTION[0].to_owned(); - let mut right = FULL_COLLECTION[1].to_owned(); - right.id = left.id.clone(); - right.properties = ArtistProperties::default(); - - let mut expected = left.clone(); - expected.properties = expected.properties.merge(right.clone().properties); - expected.albums.append(&mut right.albums.clone()); - expected.albums.sort_unstable(); - - let merged = left.clone().merge(right.clone()); - assert_eq!(expected, merged); - - // Non-overlapping merge should be commutative. - let merged = right.clone().merge(left.clone()); - assert_eq!(expected, merged); -} - -#[test] -fn merge_artist_overlap() { - let mut left = FULL_COLLECTION[0].to_owned(); - let mut right = FULL_COLLECTION[1].to_owned(); - right.id = left.id.clone(); - left.albums.push(right.albums[0].clone()); - left.albums.sort_unstable(); - - let mut expected = left.clone(); - expected.properties = expected.properties.merge(right.clone().properties); - expected.albums.append(&mut right.albums.clone()); - expected.albums.sort_unstable(); - expected.albums.dedup(); - - let merged = left.clone().merge(right); - assert_eq!(expected, merged); -} - -#[test] -fn merge_collection_no_overlap() { - let half: usize = FULL_COLLECTION.len() / 2; - - let left = FULL_COLLECTION[..half].to_owned(); - let right = FULL_COLLECTION[half..].to_owned(); - - let mut expected = FULL_COLLECTION.to_owned(); - expected.sort_unstable(); - - let merged = MusicHoard::::merge_collections( - left.clone() - .into_iter() - .map(|a| (a.id.clone(), a)) - .collect(), - right.clone(), - ); - assert_eq!(expected, merged); - - // The merge is completely non-overlapping so it should be commutative. - let merged = MusicHoard::::merge_collections( - right - .clone() - .into_iter() - .map(|a| (a.id.clone(), a)) - .collect(), - left.clone(), - ); - assert_eq!(expected, merged); -} - -#[test] -fn merge_collection_overlap() { - let half: usize = FULL_COLLECTION.len() / 2; - - let left = FULL_COLLECTION[..(half + 1)].to_owned(); - let right = FULL_COLLECTION[half..].to_owned(); - - let mut expected = FULL_COLLECTION.to_owned(); - expected.sort_unstable(); - - let merged = MusicHoard::::merge_collections( - left.clone() - .into_iter() - .map(|a| (a.id.clone(), a)) - .collect(), - right.clone(), - ); - assert_eq!(expected, merged); - - // The merge does not overwrite any data so it should be commutative. - let merged = MusicHoard::::merge_collections( - right - .clone() - .into_iter() - .map(|a| (a.id.clone(), a)) - .collect(), - left.clone(), - ); - assert_eq!(expected, merged); -} - -#[test] -fn merge_collection_incompatible_sorting() { - // It may be that the same artist in one collection has a "sort" field defined while the - // same artist in the other collection does not. This means that the two collections are not - // sorted consistently. If the merge assumes they are sorted consistently this will lead to - // the same artist appearing twice in the final list. This should not be the case. - - // We will mimic this situation by taking the last artist from FULL_COLLECTION and giving it a - // sorting name that would place it in the beginning. - let left = FULL_COLLECTION.to_owned(); - let mut right: Vec = vec![left.last().unwrap().clone()]; - - assert!(right.first().unwrap() > left.first().unwrap()); - let artist_sort = Some(ArtistId::new("album_artist 0")); - right[0].sort = artist_sort.clone(); - assert!(right.first().unwrap() < left.first().unwrap()); - - // The result of the merge should be the same list of artists, but with the last artist now - // in first place. - let mut expected = left.to_owned(); - expected.last_mut().as_mut().unwrap().sort = artist_sort.clone(); - expected.rotate_right(1); - - let merged = MusicHoard::::merge_collections( - left.clone() - .into_iter() - .map(|a| (a.id.clone(), a)) - .collect(), - right.clone(), - ); - assert_eq!(expected.len(), merged.len()); - assert_eq!(expected, merged); - - // The merge overwrites the sort data, but no data is erased so it should be commutative. - let merged = MusicHoard::::merge_collections( - right - .clone() - .into_iter() - .map(|a| (a.id.clone(), a)) - .collect(), - left.clone(), - ); - assert_eq!(expected.len(), merged.len()); - assert_eq!(expected, merged); -} - -#[test] -fn rescan_library_ordered() { - let mut library = MockILibrary::new(); - let database = MockIDatabase::new(); - - let library_input = Query::new(); - let library_result = Ok(LIBRARY_ITEMS.to_owned()); - - library - .expect_list() - .with(predicate::eq(library_input)) - .times(1) - .return_once(|_| library_result); - - let mut music_hoard = MusicHoardBuilder::default() - .set_library(library) - .set_database(database) - .build(); - - music_hoard.rescan_library().unwrap(); - assert_eq!(music_hoard.get_collection(), &*LIBRARY_COLLECTION); -} - -#[test] -fn rescan_library_unordered() { - let mut library = MockILibrary::new(); - let database = MockIDatabase::new(); - - let library_input = Query::new(); - let mut library_result = Ok(LIBRARY_ITEMS.to_owned()); - - // Swap the last item with the first. - let last = library_result.as_ref().unwrap().len() - 1; - library_result.as_mut().unwrap().swap(0, last); - - library - .expect_list() - .with(predicate::eq(library_input)) - .times(1) - .return_once(|_| library_result); - - let mut music_hoard = MusicHoardBuilder::default() - .set_library(library) - .set_database(database) - .build(); - - music_hoard.rescan_library().unwrap(); - assert_eq!(music_hoard.get_collection(), &*LIBRARY_COLLECTION); -} - -#[test] -fn rescan_library_album_title_year_clash() { - let mut library = MockILibrary::new(); - let database = MockIDatabase::new(); - - let mut expected = LIBRARY_COLLECTION.to_owned(); - let removed_album_id = expected[0].albums[0].id.clone(); - let clashed_album_id = &expected[1].albums[0].id; - - let mut items = LIBRARY_ITEMS.to_owned(); - for item in items.iter_mut().filter(|it| { - (it.album_year == removed_album_id.year) && (it.album_title == removed_album_id.title) - }) { - item.album_year = clashed_album_id.year; - item.album_title = clashed_album_id.title.clone(); - } - - expected[0].albums[0].id = clashed_album_id.clone(); - - let library_input = Query::new(); - let library_result = Ok(items); - - library - .expect_list() - .with(predicate::eq(library_input)) - .times(1) - .return_once(|_| library_result); - - let mut music_hoard = MusicHoardBuilder::default() - .set_library(library) - .set_database(database) - .build(); - - music_hoard.rescan_library().unwrap(); - assert_eq!(music_hoard.get_collection(), &expected); -} - -#[test] -fn rescan_library_album_artist_sort_clash() { - let mut library = MockILibrary::new(); - let database = MockIDatabase::new(); - - let library_input = Query::new(); - let mut library_items = LIBRARY_ITEMS.to_owned(); - - assert_eq!(library_items[0].album_artist, library_items[1].album_artist); - library_items[0].album_artist_sort = Some(library_items[0].album_artist.clone()); - library_items[1].album_artist_sort = Some( - library_items[1] - .album_artist - .clone() - .chars() - .rev() - .collect(), - ); - - let library_result = Ok(library_items); - - library - .expect_list() - .with(predicate::eq(library_input)) - .times(1) - .return_once(|_| library_result); - - let mut music_hoard = MusicHoardBuilder::default() - .set_library(library) - .set_database(database) - .build(); - - assert!(music_hoard.rescan_library().is_err()); -} - -#[test] -fn load_database() { - let library = MockILibrary::new(); - let mut database = MockIDatabase::new(); - - database - .expect_load() - .times(1) - .return_once(|| Ok(FULL_COLLECTION.to_owned())); - - let mut music_hoard = MusicHoardBuilder::default() - .set_library(library) - .set_database(database) - .build(); - - music_hoard.load_from_database().unwrap(); - assert_eq!(music_hoard.get_collection(), &*FULL_COLLECTION); -} - -#[test] -fn rescan_get_save() { - let mut library = MockILibrary::new(); - let mut database = MockIDatabase::new(); - - let library_input = Query::new(); - let library_result = Ok(LIBRARY_ITEMS.to_owned()); - - let database_input = LIBRARY_COLLECTION.to_owned(); - let database_result = Ok(()); - - library - .expect_list() - .with(predicate::eq(library_input)) - .times(1) - .return_once(|_| library_result); - - database - .expect_save() - .with(predicate::eq(database_input)) - .times(1) - .return_once(|_: &Collection| database_result); - - let mut music_hoard = MusicHoardBuilder::default() - .set_library(library) - .set_database(database) - .build(); - - music_hoard.rescan_library().unwrap(); - assert_eq!(music_hoard.get_collection(), &*LIBRARY_COLLECTION); - music_hoard.save_to_database().unwrap(); -} - -#[test] -fn library_error() { - let mut library = MockILibrary::new(); - let database = MockIDatabase::new(); - - let library_result = Err(library::Error::Invalid(String::from("invalid data"))); - - library - .expect_list() - .times(1) - .return_once(|_| library_result); - - let mut music_hoard = MusicHoardBuilder::default() - .set_library(library) - .set_database(database) - .build(); - - let actual_err = music_hoard.rescan_library().unwrap_err(); - let expected_err = - Error::LibraryError(library::Error::Invalid(String::from("invalid data")).to_string()); - - assert_eq!(actual_err, expected_err); - assert_eq!(actual_err.to_string(), expected_err.to_string()); -} - -#[test] -fn database_load_error() { - let library = MockILibrary::new(); - let mut database = MockIDatabase::new(); - - let database_result = Err(database::LoadError::IoError(String::from("I/O error"))); - - database - .expect_load() - .times(1) - .return_once(|| database_result); - - let mut music_hoard = MusicHoardBuilder::default() - .set_library(library) - .set_database(database) - .build(); - - let actual_err = music_hoard.load_from_database().unwrap_err(); - let expected_err = - Error::DatabaseError(database::LoadError::IoError(String::from("I/O error")).to_string()); - - assert_eq!(actual_err, expected_err); - assert_eq!(actual_err.to_string(), expected_err.to_string()); -} - -#[test] -fn database_save_error() { - let library = MockILibrary::new(); - let mut database = MockIDatabase::new(); - - let database_result = Err(database::SaveError::IoError(String::from("I/O error"))); - - database - .expect_save() - .times(1) - .return_once(|_: &Collection| database_result); - - let mut music_hoard = MusicHoardBuilder::default() - .set_library(library) - .set_database(database) - .build(); - - let actual_err = music_hoard.save_to_database().unwrap_err(); - let expected_err = - Error::DatabaseError(database::SaveError::IoError(String::from("I/O error")).to_string()); - - assert_eq!(actual_err, expected_err); - assert_eq!(actual_err.to_string(), expected_err.to_string()); -} diff --git a/src/lib.rs b/src/lib.rs index d9329b3..bf6be8f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,11 @@ pub use core::collection::{ }; // FIXME: validate the re-exports -pub use core::musichoard::{Error, MusicHoard, MusicHoardBuilder, NoDatabase, NoLibrary}; +pub use core::musichoard::{ + musichoard::{MusicHoard, NoDatabase, NoLibrary}, + musichoard_builder::MusicHoardBuilder, + Error, +}; #[cfg(test)] #[macro_use]