diff --git a/src/collection/mod.rs b/src/collection/mod.rs new file mode 100644 index 0000000..25e6876 --- /dev/null +++ b/src/collection/mod.rs @@ -0,0 +1,673 @@ +use std::{ + cmp::Ordering, + fmt::{self, Debug, Display}, + iter::Peekable, + mem, +}; + +use paste::paste; +use serde::{Deserialize, Serialize}; +use url::Url; +use uuid::Uuid; + +// FIXME: these imports are not acceptable +use crate::Error; + +/// An object with the [`IMbid`] trait contains a [MusicBrainz +/// Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). +pub trait IMbid { + fn mbid(&self) -> &str; +} + +/// The different URL types supported by MusicHoard. +#[derive(Debug)] +enum UrlType { + MusicBrainz, + MusicButler, + Bandcamp, + Qobuz, +} + +/// Invalid URL error. +// FIXME: should not be public (or at least not in this form) +pub struct InvalidUrlError { + url_type: UrlType, + url: String, +} + +impl Display for InvalidUrlError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "invalid url of type {:?}: {}", self.url_type, self.url) + } +} + +/// MusicBrainz reference. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct MusicBrainz(Url); + +impl MusicBrainz { + /// Validate and wrap a MusicBrainz URL. + pub fn new>(url: S) -> Result { + let url = Url::parse(url.as_ref())?; + + if !url + .domain() + .map(|u| u.ends_with("musicbrainz.org")) + .unwrap_or(false) + { + return Err(Self::invalid_url_error(url).into()); + } + + match url.path_segments().and_then(|mut ps| ps.nth(1)) { + Some(segment) => Uuid::try_parse(segment)?, + None => return Err(Self::invalid_url_error(url).into()), + }; + + Ok(MusicBrainz(url)) + } + + fn invalid_url_error>(url: S) -> InvalidUrlError { + InvalidUrlError { + url_type: UrlType::MusicBrainz, + url: url.into(), + } + } +} + +impl AsRef for MusicBrainz { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl TryFrom<&str> for MusicBrainz { + type Error = Error; + + fn try_from(value: &str) -> Result { + MusicBrainz::new(value) + } +} + +impl Display for MusicBrainz { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl IMbid for MusicBrainz { + fn mbid(&self) -> &str { + // The URL is assumed to have been validated. + self.0.path_segments().and_then(|mut ps| ps.nth(1)).unwrap() + } +} + +/// MusicButler reference. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct MusicButler(Url); + +impl MusicButler { + /// Validate and wrap a MusicButler URL. + pub fn new>(url: S) -> Result { + let url = Url::parse(url.as_ref())?; + + if !url + .domain() + .map(|u| u.ends_with("musicbutler.io")) + .unwrap_or(false) + { + return Err(Self::invalid_url_error(url).into()); + } + + Ok(MusicButler(url)) + } + + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + fn invalid_url_error>(url: S) -> InvalidUrlError { + InvalidUrlError { + url_type: UrlType::MusicButler, + url: url.into(), + } + } +} + +impl AsRef for MusicButler { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl TryFrom<&str> for MusicButler { + type Error = Error; + + fn try_from(value: &str) -> Result { + MusicButler::new(value) + } +} + +/// Bandcamp reference. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct Bandcamp(Url); + +impl Bandcamp { + /// Validate and wrap a Bandcamp URL. + pub fn new>(url: S) -> Result { + let url = Url::parse(url.as_ref())?; + + if !url + .domain() + .map(|u| u.ends_with("bandcamp.com")) + .unwrap_or(false) + { + return Err(Self::invalid_url_error(url).into()); + } + + Ok(Bandcamp(url)) + } + + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + fn invalid_url_error>(url: S) -> InvalidUrlError { + InvalidUrlError { + url_type: UrlType::Bandcamp, + url: url.into(), + } + } +} + +impl AsRef for Bandcamp { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl TryFrom<&str> for Bandcamp { + type Error = Error; + + fn try_from(value: &str) -> Result { + Bandcamp::new(value) + } +} + +/// Qobuz reference. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct Qobuz(Url); + +impl Qobuz { + /// Validate and wrap a Qobuz URL. + pub fn new>(url: S) -> Result { + let url = Url::parse(url.as_ref())?; + + if !url + .domain() + .map(|u| u.ends_with("qobuz.com")) + .unwrap_or(false) + { + return Err(Self::invalid_url_error(url).into()); + } + + Ok(Qobuz(url)) + } + + fn invalid_url_error>(url: S) -> InvalidUrlError { + InvalidUrlError { + url_type: UrlType::Qobuz, + url: url.into(), + } + } +} + +impl AsRef for Qobuz { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl TryFrom<&str> for Qobuz { + type Error = Error; + + fn try_from(value: &str) -> Result { + Qobuz::new(value) + } +} + +impl Display for Qobuz { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// The track file format. +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] +pub enum Format { + Flac, + Mp3, +} + +/// The track quality. Combines format and bitrate information. +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct Quality { + pub format: Format, + pub bitrate: u32, +} + +/// The track identifier. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TrackId { + pub number: u32, + pub title: String, +} + +/// A single track on an album. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct Track { + pub id: TrackId, + pub artist: Vec, + pub quality: Quality, +} + +impl PartialOrd for Track { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Track { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.cmp(&other.id) + } +} + +impl Merge for Track { + fn merge_in_place(&mut self, other: Self) { + assert_eq!(self.id, other.id); + } +} + +/// The album identifier. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Hash)] +pub struct AlbumId { + pub year: u32, + pub title: String, +} + +/// An album is a collection of tracks that were released together. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct Album { + pub id: AlbumId, + pub tracks: Vec, +} + +impl PartialOrd for Album { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Album { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.cmp(&other.id) + } +} + +impl Merge for Album { + fn merge_in_place(&mut self, other: Self) { + assert_eq!(self.id, other.id); + let tracks = mem::take(&mut self.tracks); + self.tracks = MergeSorted::new(tracks.into_iter(), other.tracks.into_iter()).collect(); + } +} + +/// The artist identifier. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ArtistId { + pub name: String, +} + +impl AsRef for ArtistId { + fn as_ref(&self) -> &ArtistId { + self + } +} + +impl ArtistId { + pub fn new>(name: S) -> ArtistId { + ArtistId { name: name.into() } + } +} + +impl Display for ArtistId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +/// The artist properties. +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +pub struct ArtistProperties { + pub musicbrainz: Option, + pub musicbutler: Vec, + pub bandcamp: Vec, + pub qobuz: Option, +} + +impl Merge for ArtistProperties { + fn merge_in_place(&mut self, other: Self) { + self.musicbrainz = self.musicbrainz.take().or(other.musicbrainz); + Self::merge_vecs(&mut self.musicbutler, other.musicbutler); + Self::merge_vecs(&mut self.bandcamp, other.bandcamp); + self.qobuz = self.qobuz.take().or(other.qobuz); + } +} + +/// An artist. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct Artist { + pub id: ArtistId, + pub sort: Option, + pub properties: ArtistProperties, + pub albums: Vec, +} + +macro_rules! artist_unique_url_dispatch { + ($field:ident) => { + paste! { + pub fn []>(&mut self, url: S) -> Result<(), Error> { + Self::add_unique_url(&mut self.properties.$field, url) + } + + pub fn []>(&mut self, url: S) -> Result<(), Error> { + Self::remove_unique_url(&mut self.properties.$field, url) + } + + pub fn []>(&mut self, url: S) -> Result<(), Error> { + Self::set_unique_url(&mut self.properties.$field, url) + } + + pub fn [](&mut self) { + Self::clear_unique_url(&mut self.properties.$field); + } + } + }; +} + +macro_rules! artist_multi_url_dispatch { + ($field:ident) => { + paste! { + pub fn []>(&mut self, urls: Vec) -> Result<(), Error> { + Self::add_multi_urls(&mut self.properties.$field, urls) + } + + pub fn []>(&mut self, urls: Vec) -> Result<(), Error> { + Self::remove_multi_urls(&mut self.properties.$field, urls) + } + + pub fn []>(&mut self, urls: Vec) -> Result<(), Error> { + Self::set_multi_urls(&mut self.properties.$field, urls) + } + + pub fn [](&mut self) { + Self::clear_multi_urls(&mut self.properties.$field); + } + } + }; +} + +impl Artist { + /// Create new [`Artist`] with the given [`ArtistId`]. + pub fn new>(id: ID) -> Self { + Artist { + id: id.into(), + sort: None, + properties: ArtistProperties::default(), + albums: vec![], + } + } + + fn get_sort_key(&self) -> &ArtistId { + self.sort.as_ref().unwrap_or(&self.id) + } + + pub fn set_sort_key>(&mut self, sort: SORT) { + self.sort = Some(sort.into()); + } + + pub fn clear_sort_key(&mut self) { + _ = self.sort.take(); + } + + fn add_unique_url, T: for<'a> TryFrom<&'a str, Error = Error> + Eq + Display>( + container: &mut Option, + url: S, + ) -> Result<(), Error> { + let url: T = url.as_ref().try_into()?; + + match container { + Some(current) => { + if current != &url { + return Err(Error::CollectionError(format!( + "artist already has a different URL: {}", + current + ))); + } + } + None => { + _ = container.insert(url); + } + } + + Ok(()) + } + + fn remove_unique_url, T: for<'a> TryFrom<&'a str, Error = Error> + Eq>( + container: &mut Option, + url: S, + ) -> Result<(), Error> { + let url: T = url.as_ref().try_into()?; + + if container == &Some(url) { + _ = container.take(); + } + + Ok(()) + } + + fn set_unique_url, T: for<'a> TryFrom<&'a str, Error = Error>>( + container: &mut Option, + url: S, + ) -> Result<(), Error> { + _ = container.insert(url.as_ref().try_into()?); + Ok(()) + } + + fn clear_unique_url(container: &mut Option) { + _ = container.take(); + } + + fn add_multi_urls, T: for<'a> TryFrom<&'a str, Error = Error> + Eq>( + container: &mut Vec, + urls: Vec, + ) -> Result<(), Error> { + let mut new_urls = urls + .iter() + .map(|url| url.as_ref().try_into()) + .filter(|res| { + res.as_ref() + .map(|url| !container.contains(url)) + .unwrap_or(true) // Propagate errors. + }) + .collect::, Error>>()?; + + container.append(&mut new_urls); + Ok(()) + } + + fn remove_multi_urls, T: for<'a> TryFrom<&'a str, Error = Error> + Eq>( + container: &mut Vec, + urls: Vec, + ) -> Result<(), Error> { + let urls = urls + .iter() + .map(|url| url.as_ref().try_into()) + .collect::, Error>>()?; + + container.retain(|url| !urls.contains(url)); + Ok(()) + } + + fn set_multi_urls, T: for<'a> TryFrom<&'a str, Error = Error>>( + container: &mut Vec, + urls: Vec, + ) -> Result<(), Error> { + let mut urls = urls + .iter() + .map(|url| url.as_ref().try_into()) + .collect::, Error>>()?; + + container.clear(); + container.append(&mut urls); + Ok(()) + } + + fn clear_multi_urls(container: &mut Vec) { + container.clear(); + } + + artist_unique_url_dispatch!(musicbrainz); + + artist_multi_url_dispatch!(musicbutler); + + artist_multi_url_dispatch!(bandcamp); + + artist_unique_url_dispatch!(qobuz); +} + +impl PartialOrd for Artist { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Artist { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.get_sort_key().cmp(other.get_sort_key()) + } +} + +impl Merge for Artist { + fn merge_in_place(&mut self, other: Self) { + assert_eq!(self.id, other.id); + self.sort = self.sort.take().or(other.sort); + self.properties.merge_in_place(other.properties); + let albums = mem::take(&mut self.albums); + self.albums = MergeSorted::new(albums.into_iter(), other.albums.into_iter()).collect(); + } +} + +// FIXME: should not be public +pub trait Merge { + fn merge_in_place(&mut self, other: Self); + + fn merge(mut self, other: Self) -> Self + where + Self: Sized, + { + self.merge_in_place(other); + self + } + + fn merge_vecs(this: &mut Vec, mut other: Vec) { + this.append(&mut other); + this.sort_unstable(); + this.dedup(); + } +} + +struct MergeSorted +where + L: Iterator, + R: Iterator, +{ + left: Peekable, + right: Peekable, +} + +impl MergeSorted +where + L: Iterator, + R: Iterator, +{ + fn new(left: L, right: R) -> MergeSorted { + MergeSorted { + left: left.peekable(), + right: right.peekable(), + } + } +} + +impl Iterator for MergeSorted +where + L: Iterator, + R: Iterator, + L::Item: Ord + Merge, +{ + type Item = L::Item; + + fn next(&mut self) -> Option { + let which = match (self.left.peek(), self.right.peek()) { + (Some(l), Some(r)) => l.cmp(r), + (Some(_), None) => Ordering::Less, + (None, Some(_)) => Ordering::Greater, + (None, None) => return None, + }; + + match which { + Ordering::Less => self.left.next(), + Ordering::Equal => Some(self.left.next().unwrap().merge(self.right.next().unwrap())), + Ordering::Greater => self.right.next(), + } + } +} + +/// The collection type. Currently, a collection is a list of artists. +pub type Collection = Vec; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn musicbrainz() { + let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8"; + let url = format!("https://musicbrainz.org/artist/{uuid}"); + let mb = MusicBrainz::new(&url).unwrap(); + assert_eq!(url, mb.as_ref()); + assert_eq!(uuid, mb.mbid()); + + let url = "not a url at all".to_string(); + let expected_error: Error = url::ParseError::RelativeUrlWithoutBase.into(); + let actual_error = MusicBrainz::new(url).unwrap_err(); + assert_eq!(actual_error, expected_error); + assert_eq!(actual_error.to_string(), expected_error.to_string()); + + let url = "https://musicbrainz.org/artist/i-am-not-a-uuid".to_string(); + let expected_error: Error = Uuid::try_parse("i-am-not-a-uuid").unwrap_err().into(); + let actual_error = MusicBrainz::new(url).unwrap_err(); + assert_eq!(actual_error, expected_error); + assert_eq!(actual_error.to_string(), expected_error.to_string()); + + let url = "https://musicbrainz.org/artist".to_string(); + let expected_error: Error = InvalidUrlError { + url_type: UrlType::MusicBrainz, + url: url.clone(), + } + .into(); + let actual_error = MusicBrainz::new(&url).unwrap_err(); + assert_eq!(actual_error, expected_error); + assert_eq!(actual_error.to_string(), expected_error.to_string()); + } +} diff --git a/src/lib.rs b/src/lib.rs index 1efded8..aeee122 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,643 +1,25 @@ //! MusicHoard - a music collection manager. +mod collection; pub mod database; pub mod library; use std::{ - cmp::Ordering, collections::HashMap, - fmt::{self, Debug, Display}, - iter::Peekable, + fmt::{self, Display}, mem, }; +use collection::{InvalidUrlError, Merge}; use database::IDatabase; use library::{ILibrary, Item, Query}; use paste::paste; -use serde::{Deserialize, Serialize}; -use url::Url; -use uuid::Uuid; -/// An object with the [`IMbid`] trait contains a [MusicBrainz -/// Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). -pub trait IMbid { - fn mbid(&self) -> &str; -} - -/// The different URL types supported by MusicHoard. -#[derive(Debug)] -enum UrlType { - MusicBrainz, - MusicButler, - Bandcamp, - Qobuz, -} - -/// Invalid URL error. -struct InvalidUrlError { - url_type: UrlType, - url: String, -} - -impl Display for InvalidUrlError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "invalid url of type {:?}: {}", self.url_type, self.url) - } -} - -/// MusicBrainz reference. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] -pub struct MusicBrainz(Url); - -impl MusicBrainz { - /// Validate and wrap a MusicBrainz URL. - pub fn new>(url: S) -> Result { - let url = Url::parse(url.as_ref())?; - - if !url - .domain() - .map(|u| u.ends_with("musicbrainz.org")) - .unwrap_or(false) - { - return Err(Self::invalid_url_error(url).into()); - } - - match url.path_segments().and_then(|mut ps| ps.nth(1)) { - Some(segment) => Uuid::try_parse(segment)?, - None => return Err(Self::invalid_url_error(url).into()), - }; - - Ok(MusicBrainz(url)) - } - - fn invalid_url_error>(url: S) -> InvalidUrlError { - InvalidUrlError { - url_type: UrlType::MusicBrainz, - url: url.into(), - } - } -} - -impl AsRef for MusicBrainz { - fn as_ref(&self) -> &str { - self.0.as_ref() - } -} - -impl TryFrom<&str> for MusicBrainz { - type Error = Error; - - fn try_from(value: &str) -> Result { - MusicBrainz::new(value) - } -} - -impl Display for MusicBrainz { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl IMbid for MusicBrainz { - fn mbid(&self) -> &str { - // The URL is assumed to have been validated. - self.0.path_segments().and_then(|mut ps| ps.nth(1)).unwrap() - } -} - -/// MusicButler reference. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] -pub struct MusicButler(Url); - -impl MusicButler { - /// Validate and wrap a MusicButler URL. - pub fn new>(url: S) -> Result { - let url = Url::parse(url.as_ref())?; - - if !url - .domain() - .map(|u| u.ends_with("musicbutler.io")) - .unwrap_or(false) - { - return Err(Self::invalid_url_error(url).into()); - } - - Ok(MusicButler(url)) - } - - pub fn as_str(&self) -> &str { - self.0.as_str() - } - - fn invalid_url_error>(url: S) -> InvalidUrlError { - InvalidUrlError { - url_type: UrlType::MusicButler, - url: url.into(), - } - } -} - -impl AsRef for MusicButler { - fn as_ref(&self) -> &str { - self.0.as_ref() - } -} - -impl TryFrom<&str> for MusicButler { - type Error = Error; - - fn try_from(value: &str) -> Result { - MusicButler::new(value) - } -} - -/// Bandcamp reference. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] -pub struct Bandcamp(Url); - -impl Bandcamp { - /// Validate and wrap a Bandcamp URL. - pub fn new>(url: S) -> Result { - let url = Url::parse(url.as_ref())?; - - if !url - .domain() - .map(|u| u.ends_with("bandcamp.com")) - .unwrap_or(false) - { - return Err(Self::invalid_url_error(url).into()); - } - - Ok(Bandcamp(url)) - } - - pub fn as_str(&self) -> &str { - self.0.as_str() - } - - fn invalid_url_error>(url: S) -> InvalidUrlError { - InvalidUrlError { - url_type: UrlType::Bandcamp, - url: url.into(), - } - } -} - -impl AsRef for Bandcamp { - fn as_ref(&self) -> &str { - self.0.as_ref() - } -} - -impl TryFrom<&str> for Bandcamp { - type Error = Error; - - fn try_from(value: &str) -> Result { - Bandcamp::new(value) - } -} - -/// Qobuz reference. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] -pub struct Qobuz(Url); - -impl Qobuz { - /// Validate and wrap a Qobuz URL. - pub fn new>(url: S) -> Result { - let url = Url::parse(url.as_ref())?; - - if !url - .domain() - .map(|u| u.ends_with("qobuz.com")) - .unwrap_or(false) - { - return Err(Self::invalid_url_error(url).into()); - } - - Ok(Qobuz(url)) - } - - fn invalid_url_error>(url: S) -> InvalidUrlError { - InvalidUrlError { - url_type: UrlType::Qobuz, - url: url.into(), - } - } -} - -impl AsRef for Qobuz { - fn as_ref(&self) -> &str { - self.0.as_ref() - } -} - -impl TryFrom<&str> for Qobuz { - type Error = Error; - - fn try_from(value: &str) -> Result { - Qobuz::new(value) - } -} - -impl Display for Qobuz { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -/// The track file format. -#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] -pub enum Format { - Flac, - Mp3, -} - -/// The track quality. Combines format and bitrate information. -#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct Quality { - pub format: Format, - pub bitrate: u32, -} - -/// The track identifier. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct TrackId { - pub number: u32, - pub title: String, -} - -/// A single track on an album. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct Track { - pub id: TrackId, - pub artist: Vec, - pub quality: Quality, -} - -impl PartialOrd for Track { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Track { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.id.cmp(&other.id) - } -} - -impl Merge for Track { - fn merge_in_place(&mut self, other: Self) { - assert_eq!(self.id, other.id); - } -} - -/// The album identifier. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Hash)] -pub struct AlbumId { - pub year: u32, - pub title: String, -} - -/// An album is a collection of tracks that were released together. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct Album { - pub id: AlbumId, - pub tracks: Vec, -} - -impl PartialOrd for Album { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Album { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.id.cmp(&other.id) - } -} - -impl Merge for Album { - fn merge_in_place(&mut self, other: Self) { - assert_eq!(self.id, other.id); - let tracks = mem::take(&mut self.tracks); - self.tracks = MergeSorted::new(tracks.into_iter(), other.tracks.into_iter()).collect(); - } -} - -/// The artist identifier. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ArtistId { - pub name: String, -} - -impl AsRef for ArtistId { - fn as_ref(&self) -> &ArtistId { - self - } -} - -impl ArtistId { - pub fn new>(name: S) -> ArtistId { - ArtistId { name: name.into() } - } -} - -impl Display for ArtistId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.name) - } -} - -/// The artist properties. -#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] -pub struct ArtistProperties { - pub musicbrainz: Option, - pub musicbutler: Vec, - pub bandcamp: Vec, - pub qobuz: Option, -} - -impl Merge for ArtistProperties { - fn merge_in_place(&mut self, other: Self) { - self.musicbrainz = self.musicbrainz.take().or(other.musicbrainz); - Self::merge_vecs(&mut self.musicbutler, other.musicbutler); - Self::merge_vecs(&mut self.bandcamp, other.bandcamp); - self.qobuz = self.qobuz.take().or(other.qobuz); - } -} - -/// An artist. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct Artist { - pub id: ArtistId, - pub sort: Option, - pub properties: ArtistProperties, - pub albums: Vec, -} - -macro_rules! artist_unique_url_dispatch { - ($field:ident) => { - paste! { - fn []>(&mut self, url: S) -> Result<(), Error> { - Self::add_unique_url(&mut self.properties.$field, url) - } - - fn []>(&mut self, url: S) -> Result<(), Error> { - Self::remove_unique_url(&mut self.properties.$field, url) - } - - fn []>(&mut self, url: S) -> Result<(), Error> { - Self::set_unique_url(&mut self.properties.$field, url) - } - - fn [](&mut self) { - Self::clear_unique_url(&mut self.properties.$field); - } - } - }; -} - -macro_rules! artist_multi_url_dispatch { - ($field:ident) => { - paste! { - fn []>(&mut self, urls: Vec) -> Result<(), Error> { - Self::add_multi_urls(&mut self.properties.$field, urls) - } - - fn []>(&mut self, urls: Vec) -> Result<(), Error> { - Self::remove_multi_urls(&mut self.properties.$field, urls) - } - - fn []>(&mut self, urls: Vec) -> Result<(), Error> { - Self::set_multi_urls(&mut self.properties.$field, urls) - } - - fn [](&mut self) { - Self::clear_multi_urls(&mut self.properties.$field); - } - } - }; -} - -impl Artist { - /// Create new [`Artist`] with the given [`ArtistId`]. - pub fn new>(id: ID) -> Self { - Artist { - id: id.into(), - sort: None, - properties: ArtistProperties::default(), - albums: vec![], - } - } - - fn get_sort_key(&self) -> &ArtistId { - self.sort.as_ref().unwrap_or(&self.id) - } - - fn set_sort_key>(&mut self, sort: SORT) { - self.sort = Some(sort.into()); - } - - fn clear_sort_key(&mut self) { - _ = self.sort.take(); - } - - fn add_unique_url, T: for<'a> TryFrom<&'a str, Error = Error> + Eq + Display>( - container: &mut Option, - url: S, - ) -> Result<(), Error> { - let url: T = url.as_ref().try_into()?; - - match container { - Some(current) => { - if current != &url { - return Err(Error::CollectionError(format!( - "artist already has a different URL: {}", - current - ))); - } - } - None => { - _ = container.insert(url); - } - } - - Ok(()) - } - - fn remove_unique_url, T: for<'a> TryFrom<&'a str, Error = Error> + Eq>( - container: &mut Option, - url: S, - ) -> Result<(), Error> { - let url: T = url.as_ref().try_into()?; - - if container == &Some(url) { - _ = container.take(); - } - - Ok(()) - } - - fn set_unique_url, T: for<'a> TryFrom<&'a str, Error = Error>>( - container: &mut Option, - url: S, - ) -> Result<(), Error> { - _ = container.insert(url.as_ref().try_into()?); - Ok(()) - } - - fn clear_unique_url(container: &mut Option) { - _ = container.take(); - } - - fn add_multi_urls, T: for<'a> TryFrom<&'a str, Error = Error> + Eq>( - container: &mut Vec, - urls: Vec, - ) -> Result<(), Error> { - let mut new_urls = urls - .iter() - .map(|url| url.as_ref().try_into()) - .filter(|res| { - res.as_ref() - .map(|url| !container.contains(url)) - .unwrap_or(true) // Propagate errors. - }) - .collect::, Error>>()?; - - container.append(&mut new_urls); - Ok(()) - } - - fn remove_multi_urls, T: for<'a> TryFrom<&'a str, Error = Error> + Eq>( - container: &mut Vec, - urls: Vec, - ) -> Result<(), Error> { - let urls = urls - .iter() - .map(|url| url.as_ref().try_into()) - .collect::, Error>>()?; - - container.retain(|url| !urls.contains(url)); - Ok(()) - } - - fn set_multi_urls, T: for<'a> TryFrom<&'a str, Error = Error>>( - container: &mut Vec, - urls: Vec, - ) -> Result<(), Error> { - let mut urls = urls - .iter() - .map(|url| url.as_ref().try_into()) - .collect::, Error>>()?; - - container.clear(); - container.append(&mut urls); - Ok(()) - } - - fn clear_multi_urls(container: &mut Vec) { - container.clear(); - } - - artist_unique_url_dispatch!(musicbrainz); - - artist_multi_url_dispatch!(musicbutler); - - artist_multi_url_dispatch!(bandcamp); - - artist_unique_url_dispatch!(qobuz); -} - -impl PartialOrd for Artist { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Artist { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.get_sort_key().cmp(other.get_sort_key()) - } -} - -impl Merge for Artist { - fn merge_in_place(&mut self, other: Self) { - assert_eq!(self.id, other.id); - self.sort = self.sort.take().or(other.sort); - self.properties.merge_in_place(other.properties); - let albums = mem::take(&mut self.albums); - self.albums = MergeSorted::new(albums.into_iter(), other.albums.into_iter()).collect(); - } -} - -/// The collection type. Currently, a collection is a list of artists. -pub type Collection = Vec; - -trait Merge { - fn merge_in_place(&mut self, other: Self); - - fn merge(mut self, other: Self) -> Self - where - Self: Sized, - { - self.merge_in_place(other); - self - } - - fn merge_vecs(this: &mut Vec, mut other: Vec) { - this.append(&mut other); - this.sort_unstable(); - this.dedup(); - } -} - -struct MergeSorted -where - L: Iterator, - R: Iterator, -{ - left: Peekable, - right: Peekable, -} - -impl MergeSorted -where - L: Iterator, - R: Iterator, -{ - fn new(left: L, right: R) -> MergeSorted { - MergeSorted { - left: left.peekable(), - right: right.peekable(), - } - } -} - -impl Iterator for MergeSorted -where - L: Iterator, - R: Iterator, - L::Item: Ord + Merge, -{ - type Item = L::Item; - - fn next(&mut self) -> Option { - let which = match (self.left.peek(), self.right.peek()) { - (Some(l), Some(r)) => l.cmp(r), - (Some(_), None) => Ordering::Less, - (None, Some(_)) => Ordering::Greater, - (None, None) => return None, - }; - - match which { - Ordering::Less => self.left.next(), - Ordering::Equal => Some(self.left.next().unwrap().merge(self.right.next().unwrap())), - Ordering::Greater => self.right.next(), - } - } -} +// TODO: validate the re-exports. +pub use collection::{ + Album, AlbumId, Artist, ArtistId, ArtistProperties, Bandcamp, Collection, Format, MusicBrainz, + MusicButler, Qobuz, Quality, Track, TrackId, +}; /// Error type for `musichoard`. #[derive(Debug, PartialEq, Eq)] @@ -1070,1224 +452,4 @@ mod testmacros; mod testlib; #[cfg(test)] -mod tests { - use mockall::predicate; - - use super::*; - use database::MockIDatabase; - use library::{testmod::LIBRARY_ITEMS, MockILibrary}; - use testlib::{FULL_COLLECTION, LIBRARY_COLLECTION}; - - 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"; - - #[test] - fn musicbrainz() { - let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8"; - let url = format!("https://musicbrainz.org/artist/{uuid}"); - let mb = MusicBrainz::new(&url).unwrap(); - assert_eq!(url, mb.as_ref()); - assert_eq!(uuid, mb.mbid()); - - let url = "not a url at all".to_string(); - let expected_error: Error = url::ParseError::RelativeUrlWithoutBase.into(); - let actual_error = MusicBrainz::new(url).unwrap_err(); - assert_eq!(actual_error, expected_error); - assert_eq!(actual_error.to_string(), expected_error.to_string()); - - let url = "https://musicbrainz.org/artist/i-am-not-a-uuid".to_string(); - let expected_error: Error = Uuid::try_parse("i-am-not-a-uuid").unwrap_err().into(); - let actual_error = MusicBrainz::new(url).unwrap_err(); - assert_eq!(actual_error, expected_error); - assert_eq!(actual_error.to_string(), expected_error.to_string()); - - let url = "https://musicbrainz.org/artist".to_string(); - let expected_error: Error = InvalidUrlError { - url_type: UrlType::MusicBrainz, - url: url.clone(), - } - .into(); - let actual_error = MusicBrainz::new(&url).unwrap_err(); - assert_eq!(actual_error, expected_error); - assert_eq!(actual_error.to_string(), expected_error.to_string()); - } - - #[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()); - } -} +mod tests; diff --git a/src/library/beets/mod.rs b/src/library/beets/mod.rs index 202722b..ffddae5 100644 --- a/src/library/beets/mod.rs +++ b/src/library/beets/mod.rs @@ -4,7 +4,7 @@ #[cfg(test)] use mockall::automock; -use crate::Format; +use crate::collection::Format; use super::{Error, Field, ILibrary, Item, Query}; diff --git a/src/library/mod.rs b/src/library/mod.rs index a76f3e6..3d2ae87 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -5,7 +5,7 @@ use std::{collections::HashSet, fmt, num::ParseIntError, str::Utf8Error}; #[cfg(test)] use mockall::automock; -use crate::Format; +use crate::collection::Format; #[cfg(feature = "library-beets")] pub mod beets; diff --git a/src/library/testmod.rs b/src/library/testmod.rs index 87b5dc2..284ec54 100644 --- a/src/library/testmod.rs +++ b/src/library/testmod.rs @@ -1,6 +1,6 @@ use once_cell::sync::Lazy; -use crate::{library::Item, Format}; +use crate::{collection::Format, library::Item}; pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { vec![ diff --git a/src/testlib.rs b/src/testlib.rs index 377b724..4015bef 100644 --- a/src/testlib.rs +++ b/src/testlib.rs @@ -1,4 +1,4 @@ -use crate::{ +use crate::collection::{ Album, AlbumId, Artist, ArtistId, ArtistProperties, Bandcamp, Format, MusicBrainz, MusicButler, Qobuz, Quality, Track, TrackId, }; diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..14e31b0 --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,1185 @@ +use mockall::predicate; + +use super::*; +use collection::{Bandcamp, Format, Merge, MusicBrainz, MusicButler, Qobuz}; +use database::MockIDatabase; +use library::{testmod::LIBRARY_ITEMS, MockILibrary}; +use testlib::{FULL_COLLECTION, LIBRARY_COLLECTION}; + +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"; + +#[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()); +}