diff --git a/Cargo.lock b/Cargo.lock index 4ad4190..491d365 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,7 +369,6 @@ dependencies = [ "mockall", "once_cell", "openssh", - "paste", "ratatui", "serde", "serde_json", @@ -457,12 +456,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "paste" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" - [[package]] name = "percent-encoding" version = "2.2.0" diff --git a/Cargo.toml b/Cargo.toml index 9c02ace..08f4ddf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ edition = "2021" [dependencies] crossterm = { version = "0.26.1", optional = true} openssh = { version = "0.9.9", features = ["native-mux"], default-features = false, optional = true} -paste = { version = "1.0.14" } ratatui = { version = "0.20.1", optional = true} serde = { version = "1.0.159", features = ["derive"] } serde_json = { version = "1.0.95", optional = true} diff --git a/src/bin/musichoard-edit.rs b/src/bin/musichoard-edit.rs index 636c8a8..6b86875 100644 --- a/src/bin/musichoard-edit.rs +++ b/src/bin/musichoard-edit.rs @@ -1,6 +1,5 @@ use std::path::PathBuf; -use paste::paste; use structopt::{clap::AppSettings, StructOpt}; use musichoard::{ @@ -49,16 +48,9 @@ enum ArtistCommand { #[structopt(about = "Edit the artist's sort name")] Sort(SortCommand), #[structopt(name = "musicbrainz", about = "Edit the MusicBrainz URL of an artist")] - MusicBrainz(UrlCommand), - #[structopt( - name = "musicbutler", - about = "Edit the MusicButler URL(s) of an artist" - )] - MusicButler(UrlCommand), - #[structopt(about = "Edit the Bandcamp URL(s) of an artist")] - Bandcamp(UrlCommand), - #[structopt(about = "Edit the Qobuz URL of an artist")] - Qobuz(UrlCommand), + MusicBrainz(MusicBrainzCommand), + #[structopt(name = "property", about = "Edit a property of an artist")] + Property(PropertyCommand), } #[derive(StructOpt, Debug)] @@ -84,68 +76,53 @@ struct ArtistSortValue { } #[derive(StructOpt, Debug)] -enum UrlCommand { - #[structopt(about = "Add the provided URL(s) without overwriting existing values")] - Add(T), - #[structopt(about = "Remove the provided URL(s)")] - Remove(T), - #[structopt(about = "Set the provided URL(s) overwriting any existing values")] - Set(T), - #[structopt(about = "Clear all URL(s)")] +enum MusicBrainzCommand { + #[structopt(about = "Add a MusicBrainz URL without overwriting the existing value")] + Add(MusicBrainzValue), + #[structopt(about = "Remove the MusicBrainz URL")] + Remove(MusicBrainzValue), + #[structopt(about = "Set the MusicBrainz URL overwriting any existing value")] + Set(MusicBrainzValue), + #[structopt(about = "Clear the MusicBrainz URL)")] Clear(ArtistValue), } #[derive(StructOpt, Debug)] -struct SingleUrlValue { +struct MusicBrainzValue { #[structopt(help = "The name of the artist")] artist: String, - #[structopt(help = "The URL")] + #[structopt(help = "The MusicBrainz URL")] url: String, } #[derive(StructOpt, Debug)] -struct MultiUrlValue { +enum PropertyCommand { + #[structopt(about = "Add values to the property without overwriting existing values")] + Add(PropertyValue), + #[structopt(about = "Remove values from the property")] + Remove(PropertyValue), + #[structopt(about = "Set the property's values overwriting any existing values")] + Set(PropertyValue), + #[structopt(about = "Clear all values of a property")] + Clear(PropertyName), +} + +#[derive(StructOpt, Debug)] +struct PropertyValue { #[structopt(help = "The name of the artist")] artist: String, - #[structopt(help = "The list of URLs")] - urls: Vec, + #[structopt(help = "The name of the property")] + property: String, + #[structopt(help = "The list of values")] + values: Vec, } -macro_rules! url_command_dispatch { - ($cmd:ident, $mh:ident, $field:ident, $url:ident) => { - paste! { - match $cmd { - UrlCommand::Add(url_value) => { - $mh.[](ArtistId::new(url_value.artist), url_value.$url) - .expect("failed to add URL(s)"); - } - UrlCommand::Remove(url_value) => { - $mh.[](ArtistId::new(url_value.artist), url_value.$url) - .expect("failed to remove URL(s)"); - } - UrlCommand::Set(url_value) => { - $mh.[](ArtistId::new(url_value.artist), url_value.$url) - .expect("failed to set URL(s)"); - } - UrlCommand::Clear(artist_value) => { - $mh.[](ArtistId::new(artist_value.artist)) - .expect("failed to clear URL(s)"); - } - } - } - }; -} - -macro_rules! single_url_command_dispatch { - ($cmd:ident, $mh:ident, $field:ident) => { - url_command_dispatch!($cmd, $mh, $field, url) - }; -} - -macro_rules! multi_url_command_dispatch { - ($cmd:ident, $mh:ident, $field:ident) => { - url_command_dispatch!($cmd, $mh, $field, urls) - }; +#[derive(StructOpt, Debug)] +struct PropertyName { + #[structopt(help = "The name of the artist")] + artist: String, + #[structopt(help = "The name of the property")] + property: String, } impl ArtistCommand { @@ -160,17 +137,11 @@ impl ArtistCommand { ArtistCommand::Sort(sort_command) => { sort_command.handle(music_hoard); } - ArtistCommand::MusicBrainz(url_command) => { - single_url_command_dispatch!(url_command, music_hoard, musicbrainz) + ArtistCommand::MusicBrainz(musicbrainz_command) => { + musicbrainz_command.handle(music_hoard) } - ArtistCommand::MusicButler(url_command) => { - multi_url_command_dispatch!(url_command, music_hoard, musicbutler) - } - ArtistCommand::Bandcamp(url_command) => { - multi_url_command_dispatch!(url_command, music_hoard, bandcamp) - } - ArtistCommand::Qobuz(url_command) => { - single_url_command_dispatch!(url_command, music_hoard, qobuz) + ArtistCommand::Property(property_command) => { + property_command.handle(music_hoard); } } } @@ -192,6 +163,65 @@ impl SortCommand { } } +impl MusicBrainzCommand { + fn handle(self, music_hoard: &mut MH) { + match self { + MusicBrainzCommand::Add(musicbrainz_value) => music_hoard + .add_musicbrainz_url( + ArtistId::new(musicbrainz_value.artist), + musicbrainz_value.url, + ) + .expect("failed to add MusicBrainz URL"), + MusicBrainzCommand::Remove(musicbrainz_value) => music_hoard + .remove_musicbrainz_url( + ArtistId::new(musicbrainz_value.artist), + musicbrainz_value.url, + ) + .expect("failed to remove MusicBrainz URL"), + MusicBrainzCommand::Set(musicbrainz_value) => music_hoard + .set_musicbrainz_url( + ArtistId::new(musicbrainz_value.artist), + musicbrainz_value.url, + ) + .expect("failed to set MusicBrainz URL"), + MusicBrainzCommand::Clear(artist_value) => music_hoard + .clear_musicbrainz_url(ArtistId::new(artist_value.artist)) + .expect("failed to clear MusicBrainz URL"), + } + } +} + +impl PropertyCommand { + fn handle(self, music_hoard: &mut MH) { + match self { + PropertyCommand::Add(property_value) => music_hoard + .add_to_property( + ArtistId::new(property_value.artist), + property_value.property, + property_value.values, + ) + .expect("failed to add values to property"), + PropertyCommand::Remove(property_value) => music_hoard + .remove_from_property( + ArtistId::new(property_value.artist), + property_value.property, + property_value.values, + ) + .expect("failed to remove values from property"), + PropertyCommand::Set(property_value) => music_hoard + .set_property( + ArtistId::new(property_value.artist), + property_value.property, + property_value.values, + ) + .expect("failed to set property"), + PropertyCommand::Clear(property_name) => music_hoard + .clear_property(ArtistId::new(property_name.artist), property_name.property) + .expect("failed to clear property"), + } + } +} + fn main() { let opt = Opt::from_args(); diff --git a/src/core/collection/artist.rs b/src/core/collection/artist.rs index 35d25dd..ee58257 100644 --- a/src/core/collection/artist.rs +++ b/src/core/collection/artist.rs @@ -1,9 +1,9 @@ use std::{ + collections::HashMap, fmt::{self, Debug, Display}, mem, }; -use paste::paste; use serde::{Deserialize, Serialize}; use url::Url; use uuid::Uuid; @@ -19,7 +19,8 @@ use crate::core::collection::{ pub struct Artist { pub id: ArtistId, pub sort: Option, - pub properties: ArtistProperties, + pub musicbrainz: Option, + pub properties: HashMap>, pub albums: Vec, } @@ -29,75 +30,14 @@ pub struct ArtistId { pub name: String, } -/// 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, -} - -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(), + musicbrainz: None, + properties: HashMap::new(), albums: vec![], } } @@ -114,13 +54,10 @@ impl Artist { _ = 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()?; + pub fn add_musicbrainz_url>(&mut self, url: S) -> Result<(), Error> { + let url: MusicBrainz = url.as_ref().try_into()?; - match container { + match &self.musicbrainz { Some(current) => { if current != &url { return Err(Error::UrlError(format!( @@ -129,94 +66,75 @@ impl Artist { } } None => { - _ = container.insert(url); + _ = self.musicbrainz.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()?; + pub fn remove_musicbrainz_url>(&mut self, url: S) -> Result<(), Error> { + let url = url.as_ref().try_into()?; - if container == &Some(url) { - _ = container.take(); + if self.musicbrainz == Some(url) { + _ = self.musicbrainz.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()?); + pub fn set_musicbrainz_url>(&mut self, url: S) -> Result<(), Error> { + _ = self.musicbrainz.insert(url.as_ref().try_into()?); Ok(()) } - fn clear_unique_url(container: &mut Option) { - _ = container.take(); + pub fn clear_musicbrainz_url(&mut self) { + _ = self.musicbrainz.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>>()?; + // In the functions below, it would be better to use `contains` instead of `iter().any`, but for + // type reasons that does not work: + // https://stackoverflow.com/questions/48985924/why-does-a-str-not-coerce-to-a-string-when-using-veccontains - container.append(&mut new_urls); - Ok(()) + pub fn add_to_property + Into>(&mut self, property: S, values: Vec) { + match self.properties.get_mut(property.as_ref()) { + Some(container) => { + container.append( + &mut values + .into_iter() + .filter(|val| !container.iter().any(|x| x == val.as_ref())) + .map(|val| val.into()) + .collect(), + ); + } + None => { + self.properties.insert( + property.into(), + values.into_iter().map(|s| s.into()).collect(), + ); + } + } } - 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(()) + pub fn remove_from_property>(&mut self, property: S, values: Vec) { + if let Some(container) = self.properties.get_mut(property.as_ref()) { + container.retain(|val| !values.iter().any(|x| x.as_ref() == val)); + if container.is_empty() { + self.properties.remove(property.as_ref()); + } + } } - 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(()) + pub fn set_property + Into>(&mut self, property: S, values: Vec) { + self.properties.insert( + property.into(), + values.into_iter().map(|s| s.into()).collect(), + ); } - fn clear_multi_urls(container: &mut Vec) { - container.clear(); + pub fn clear_property>(&mut self, property: S) { + self.properties.remove(property.as_ref()); } - - artist_unique_url_dispatch!(musicbrainz); - - artist_multi_url_dispatch!(musicbutler); - - artist_multi_url_dispatch!(bandcamp); - - artist_unique_url_dispatch!(qobuz); } impl PartialOrd for Artist { @@ -235,6 +153,7 @@ 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.musicbrainz = self.musicbrainz.take().or(other.musicbrainz); 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(); @@ -259,15 +178,6 @@ impl Display for ArtistId { } } -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 object with the [`IMbid`] trait contains a [MusicBrainz /// Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). pub trait IMbid { @@ -331,137 +241,6 @@ impl IMbid for MusicBrainz { } } -/// 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)); - } - - Ok(MusicButler(url)) - } - - pub fn as_str(&self) -> &str { - self.0.as_str() - } - - fn invalid_url_error(url: U) -> Error { - Error::UrlError(format!("invalid MusicButler URL: {url}")) - } -} - -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)); - } - - Ok(Bandcamp(url)) - } - - pub fn as_str(&self) -> &str { - self.0.as_str() - } - - fn invalid_url_error(url: U) -> Error { - Error::UrlError(format!("invalid Bandcamp URL: {url}")) - } -} - -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)); - } - - Ok(Qobuz(url)) - } - - fn invalid_url_error(url: U) -> Error { - Error::UrlError(format!("invalid Qobuz URL: {url}")) - } -} - -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) - } -} - #[cfg(test)] mod tests { use crate::core::testmod::FULL_COLLECTION; @@ -474,10 +253,6 @@ mod tests { "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() { @@ -510,23 +285,6 @@ mod tests { 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] @@ -573,38 +331,36 @@ mod tests { let mut artist = Artist::new(ArtistId::new("an artist")); let mut expected: Option = None; - assert_eq!(artist.properties.musicbrainz, expected); + assert_eq!(artist.musicbrainz, expected); // Adding incorect URL is an error. assert!(artist.add_musicbrainz_url(MUSICBUTLER).is_err()); - assert!(artist.add_musicbrainz_url(BANDCAMP).is_err()); - assert!(artist.add_musicbrainz_url(QOBUZ).is_err()); - assert_eq!(artist.properties.musicbrainz, expected); + assert_eq!(artist.musicbrainz, expected); // Adding URL to artist. assert!(artist.add_musicbrainz_url(MUSICBRAINZ).is_ok()); _ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap()); - assert_eq!(artist.properties.musicbrainz, expected); + assert_eq!(artist.musicbrainz, expected); // Adding the same URL again is ok, but does not do anything. assert!(artist.add_musicbrainz_url(MUSICBRAINZ).is_ok()); - assert_eq!(artist.properties.musicbrainz, expected); + assert_eq!(artist.musicbrainz, expected); // Adding further URLs is an error. assert!(artist.add_musicbrainz_url(MUSICBRAINZ_2).is_err()); - assert_eq!(artist.properties.musicbrainz, expected); + assert_eq!(artist.musicbrainz, expected); // Removing a URL not in the collection is okay, but does not do anything. assert!(artist.remove_musicbrainz_url(MUSICBRAINZ_2).is_ok()); - assert_eq!(artist.properties.musicbrainz, expected); + assert_eq!(artist.musicbrainz, expected); // Removing a URL in the collection removes it. assert!(artist.remove_musicbrainz_url(MUSICBRAINZ).is_ok()); _ = expected.take(); - assert_eq!(artist.properties.musicbrainz, expected); + assert_eq!(artist.musicbrainz, expected); assert!(artist.remove_musicbrainz_url(MUSICBRAINZ).is_ok()); - assert_eq!(artist.properties.musicbrainz, expected); + assert_eq!(artist.musicbrainz, expected); } #[test] @@ -612,366 +368,128 @@ mod tests { let mut artist = Artist::new(ArtistId::new("an artist")); let mut expected: Option = None; - assert_eq!(artist.properties.musicbrainz, expected); + assert_eq!(artist.musicbrainz, expected); // Setting an incorrect URL is an error. assert!(artist.set_musicbrainz_url(MUSICBUTLER).is_err()); - assert!(artist.set_musicbrainz_url(BANDCAMP).is_err()); - assert!(artist.set_musicbrainz_url(QOBUZ).is_err()); - assert_eq!(artist.properties.musicbrainz, expected); + assert_eq!(artist.musicbrainz, expected); // Setting a URL on an artist. assert!(artist.set_musicbrainz_url(MUSICBRAINZ).is_ok()); _ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap()); - assert_eq!(artist.properties.musicbrainz, expected); + assert_eq!(artist.musicbrainz, expected); assert!(artist.set_musicbrainz_url(MUSICBRAINZ).is_ok()); - assert_eq!(artist.properties.musicbrainz, expected); + assert_eq!(artist.musicbrainz, expected); assert!(artist.set_musicbrainz_url(MUSICBRAINZ_2).is_ok()); _ = expected.insert(MusicBrainz::new(MUSICBRAINZ_2).unwrap()); - assert_eq!(artist.properties.musicbrainz, expected); + assert_eq!(artist.musicbrainz, expected); // Clearing URLs. artist.clear_musicbrainz_url(); _ = expected.take(); - assert_eq!(artist.properties.musicbrainz, expected); + assert_eq!(artist.musicbrainz, expected); } #[test] - fn add_remove_musicbutler_urls() { + fn add_to_remove_from_property() { let mut artist = Artist::new(ArtistId::new("an artist")); - let mut expected: Vec = vec![]; - assert_eq!(artist.properties.musicbutler, expected); - - // If any URL is incorrect adding URLs is an error. - assert!(artist - .add_musicbutler_urls(vec![MUSICBRAINZ, MUSICBRAINZ_2]) - .is_err()); - assert!(artist - .add_musicbutler_urls(vec![BANDCAMP, BANDCAMP_2]) - .is_err()); - assert!(artist.add_musicbutler_urls(vec![QOBUZ, QOBUZ_2]).is_err()); - assert!(artist - .add_musicbutler_urls(vec![MUSICBRAINZ, MUSICBUTLER, BANDCAMP, QOBUZ]) - .is_err()); - assert_eq!(artist.properties.musicbutler, expected); + let mut expected: Vec = vec![]; + assert!(artist.properties.is_empty()); // Adding a single URL. - assert!(artist.add_musicbutler_urls(vec![MUSICBUTLER]).is_ok()); - expected.push(MusicButler::new(MUSICBUTLER).unwrap()); - assert_eq!(artist.properties.musicbutler, expected); + artist.add_to_property("MusicButler", vec![MUSICBUTLER]); + expected.push(MUSICBUTLER.to_owned()); + assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); // Adding a URL that already exists is ok, but does not do anything. - assert!(artist.add_musicbutler_urls(vec![MUSICBUTLER]).is_ok()); - assert_eq!(artist.properties.musicbutler, expected); + artist.add_to_property("MusicButler", vec![MUSICBUTLER]); + assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); // Adding another single URL. - assert!(artist.add_musicbutler_urls(vec![MUSICBUTLER_2]).is_ok()); - expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); - assert_eq!(artist.properties.musicbutler, expected); + artist.add_to_property("MusicButler", vec![MUSICBUTLER_2]); + expected.push(MUSICBUTLER_2.to_owned()); + assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); - assert!(artist.add_musicbutler_urls(vec![MUSICBUTLER_2]).is_ok()); - assert_eq!(artist.properties.musicbutler, expected); + artist.add_to_property("MusicButler", vec![MUSICBUTLER_2]); + assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); // Removing a URL. - assert!(artist.remove_musicbutler_urls(vec![MUSICBUTLER]).is_ok()); - expected.retain(|url| url.as_str() != MUSICBUTLER); - assert_eq!(artist.properties.musicbutler, expected); + artist.remove_from_property("MusicButler", vec![MUSICBUTLER]); + expected.retain(|url| url != MUSICBUTLER); + assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); // Removing URls that do not exist is okay, they will be ignored. - assert!(artist.remove_musicbutler_urls(vec![MUSICBUTLER]).is_ok()); - assert_eq!(artist.properties.musicbutler, expected); + artist.remove_from_property("MusicButler", vec![MUSICBUTLER]); + assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); // Removing a URL. - assert!(artist.remove_musicbutler_urls(vec![MUSICBUTLER_2]).is_ok()); + artist.remove_from_property("MusicButler", vec![MUSICBUTLER_2]); expected.retain(|url| url.as_str() != MUSICBUTLER_2); - assert_eq!(artist.properties.musicbutler, expected); + assert!(artist.properties.is_empty()); - assert!(artist.remove_musicbutler_urls(vec![MUSICBUTLER_2]).is_ok()); - assert_eq!(artist.properties.musicbutler, expected); + artist.remove_from_property("MusicButler", vec![MUSICBUTLER_2]); + assert!(artist.properties.is_empty()); // Adding URLs if some exist is okay, they will be ignored. - assert!(artist.add_musicbutler_urls(vec![MUSICBUTLER]).is_ok()); - expected.push(MusicButler::new(MUSICBUTLER).unwrap()); - assert_eq!(artist.properties.musicbutler, expected); + artist.add_to_property("MusicButler", vec![MUSICBUTLER]); + expected.push(MUSICBUTLER.to_owned()); + assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); - assert!(artist - .add_musicbutler_urls(vec![MUSICBUTLER, MUSICBUTLER_2]) - .is_ok()); - expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); - assert_eq!(artist.properties.musicbutler, expected); + artist.add_to_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); + expected.push(MUSICBUTLER_2.to_owned()); + assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); // Removing URLs if some do not exist is okay, they will be ignored. - assert!(artist.remove_musicbutler_urls(vec![MUSICBUTLER]).is_ok()); + artist.remove_from_property("MusicButler", vec![MUSICBUTLER]); expected.retain(|url| url.as_str() != MUSICBUTLER); - assert_eq!(artist.properties.musicbutler, expected); + assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); - assert!(artist - .remove_musicbutler_urls(vec![MUSICBUTLER, MUSICBUTLER_2]) - .is_ok()); + artist.remove_from_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); expected.retain(|url| url.as_str() != MUSICBUTLER_2); - assert_eq!(artist.properties.musicbutler, expected); + assert!(artist.properties.is_empty()); // Adding mutliple URLs without clashes. - assert!(artist - .add_musicbutler_urls(vec![MUSICBUTLER, MUSICBUTLER_2]) - .is_ok()); - expected.push(MusicButler::new(MUSICBUTLER).unwrap()); - expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); - assert_eq!(artist.properties.musicbutler, expected); + artist.add_to_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); + expected.push(MUSICBUTLER.to_owned()); + expected.push(MUSICBUTLER_2.to_owned()); + assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); // Removing multiple URLs without clashes. - assert!(artist - .remove_musicbutler_urls(vec![MUSICBUTLER, MUSICBUTLER_2]) - .is_ok()); + artist.remove_from_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); expected.clear(); - assert_eq!(artist.properties.musicbutler, expected); + assert!(artist.properties.is_empty()); } #[test] fn set_clear_musicbutler_urls() { let mut artist = Artist::new(ArtistId::new("an artist")); - let mut expected: Vec = vec![]; - assert_eq!(artist.properties.musicbutler, expected); - - // If any URL is incorrect setting URLs is an error. - assert!(artist - .set_musicbutler_urls(vec![MUSICBRAINZ, MUSICBRAINZ_2]) - .is_err()); - assert!(artist - .set_musicbutler_urls(vec![BANDCAMP, BANDCAMP_2]) - .is_err()); - assert!(artist.set_musicbutler_urls(vec![QOBUZ, QOBUZ_2]).is_err()); - assert!(artist - .set_musicbutler_urls(vec![MUSICBRAINZ, MUSICBUTLER, BANDCAMP, QOBUZ]) - .is_err()); - assert_eq!(artist.properties.musicbutler, expected); + let mut expected: Vec = vec![]; + assert!(artist.properties.is_empty()); // Set URLs. - assert!(artist.set_musicbutler_urls(vec![MUSICBUTLER]).is_ok()); - expected.push(MusicButler::new(MUSICBUTLER).unwrap()); - assert_eq!(artist.properties.musicbutler, expected); + artist.set_property("MusicButler", vec![MUSICBUTLER]); + expected.push(MUSICBUTLER.to_owned()); + assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); - assert!(artist.set_musicbutler_urls(vec![MUSICBUTLER_2]).is_ok()); + artist.set_property("MusicButler", vec![MUSICBUTLER_2]); expected.clear(); - expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); - assert_eq!(artist.properties.musicbutler, expected); + expected.push(MUSICBUTLER_2.to_owned()); + assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); - assert!(artist - .set_musicbutler_urls(vec![MUSICBUTLER, MUSICBUTLER_2]) - .is_ok()); + artist.set_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); expected.clear(); - expected.push(MusicButler::new(MUSICBUTLER).unwrap()); - expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); - assert_eq!(artist.properties.musicbutler, expected); + expected.push(MUSICBUTLER.to_owned()); + expected.push(MUSICBUTLER_2.to_owned()); + assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); // Clear URLs. - artist.clear_musicbutler_urls(); + artist.clear_property("MusicButler"); expected.clear(); - assert_eq!(artist.properties.musicbutler, expected); - } - - #[test] - fn add_remove_bandcamp_urls() { - let mut artist = Artist::new(ArtistId::new("an artist")); - - let mut expected: Vec = vec![]; - assert_eq!(artist.properties.bandcamp, expected); - - // If any URL is incorrect adding URLs is an error. - assert!(artist - .add_bandcamp_urls(vec![MUSICBRAINZ, MUSICBRAINZ_2]) - .is_err()); - assert!(artist - .add_bandcamp_urls(vec![MUSICBUTLER, MUSICBUTLER_2]) - .is_err()); - assert!(artist.add_bandcamp_urls(vec![QOBUZ, QOBUZ_2]).is_err()); - assert!(artist - .add_bandcamp_urls(vec![MUSICBRAINZ, MUSICBUTLER, BANDCAMP, QOBUZ]) - .is_err()); - assert_eq!(artist.properties.bandcamp, expected); - - // Adding a single URL. - assert!(artist.add_bandcamp_urls(vec![BANDCAMP]).is_ok()); - expected.push(Bandcamp::new(BANDCAMP).unwrap()); - assert_eq!(artist.properties.bandcamp, expected); - - // Adding a URL that already exists is ok, but does not do anything. - assert!(artist.add_bandcamp_urls(vec![BANDCAMP]).is_ok()); - assert_eq!(artist.properties.bandcamp, expected); - - // Adding another single URL. - assert!(artist.add_bandcamp_urls(vec![BANDCAMP_2]).is_ok()); - expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); - assert_eq!(artist.properties.bandcamp, expected); - - assert!(artist.add_bandcamp_urls(vec![BANDCAMP_2]).is_ok()); - assert_eq!(artist.properties.bandcamp, expected); - - // Removing a URL. - assert!(artist.remove_bandcamp_urls(vec![BANDCAMP]).is_ok()); - expected.retain(|url| url.as_str() != BANDCAMP); - assert_eq!(artist.properties.bandcamp, expected); - - // Removing URls that do not exist is okay, they will be ignored. - assert!(artist.remove_bandcamp_urls(vec![BANDCAMP]).is_ok()); - assert_eq!(artist.properties.bandcamp, expected); - - // Removing a URL. - assert!(artist.remove_bandcamp_urls(vec![BANDCAMP_2]).is_ok()); - expected.retain(|url| url.as_str() != BANDCAMP_2); - assert_eq!(artist.properties.bandcamp, expected); - - assert!(artist.remove_bandcamp_urls(vec![BANDCAMP_2]).is_ok()); - assert_eq!(artist.properties.bandcamp, expected); - - // Adding URLs if some exist is okay, they will be ignored. - assert!(artist.add_bandcamp_urls(vec![BANDCAMP]).is_ok()); - expected.push(Bandcamp::new(BANDCAMP).unwrap()); - assert_eq!(artist.properties.bandcamp, expected); - - assert!(artist.add_bandcamp_urls(vec![BANDCAMP, BANDCAMP_2]).is_ok()); - expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); - assert_eq!(artist.properties.bandcamp, expected); - - // Removing URLs if some do not exist is okay, they will be ignored. - assert!(artist.remove_bandcamp_urls(vec![BANDCAMP]).is_ok()); - expected.retain(|url| url.as_str() != BANDCAMP); - assert_eq!(artist.properties.bandcamp, expected); - - assert!(artist - .remove_bandcamp_urls(vec![BANDCAMP, BANDCAMP_2]) - .is_ok()); - expected.retain(|url| url.as_str() != BANDCAMP_2); - assert_eq!(artist.properties.bandcamp, expected); - - // Adding mutliple URLs without clashes. - assert!(artist.add_bandcamp_urls(vec![BANDCAMP, BANDCAMP_2]).is_ok()); - expected.push(Bandcamp::new(BANDCAMP).unwrap()); - expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); - assert_eq!(artist.properties.bandcamp, expected); - - // Removing multiple URLs without clashes. - assert!(artist - .remove_bandcamp_urls(vec![BANDCAMP, BANDCAMP_2]) - .is_ok()); - expected.clear(); - assert_eq!(artist.properties.bandcamp, expected); - } - - #[test] - fn set_clear_bandcamp_urls() { - let mut artist = Artist::new(ArtistId::new("an artist")); - - let mut expected: Vec = vec![]; - assert_eq!(artist.properties.bandcamp, expected); - - // If any URL is incorrect setting URLs is an error. - assert!(artist - .set_bandcamp_urls(vec![MUSICBRAINZ, MUSICBRAINZ_2]) - .is_err()); - assert!(artist - .set_bandcamp_urls(vec![MUSICBUTLER, MUSICBUTLER_2]) - .is_err()); - assert!(artist.set_bandcamp_urls(vec![QOBUZ, QOBUZ_2]).is_err()); - assert!(artist - .set_bandcamp_urls(vec![MUSICBRAINZ, MUSICBUTLER, BANDCAMP, QOBUZ]) - .is_err()); - assert_eq!(artist.properties.bandcamp, expected); - - // Set URLs. - assert!(artist.set_bandcamp_urls(vec![BANDCAMP]).is_ok()); - expected.push(Bandcamp::new(BANDCAMP).unwrap()); - assert_eq!(artist.properties.bandcamp, expected); - - assert!(artist.set_bandcamp_urls(vec![BANDCAMP_2]).is_ok()); - expected.clear(); - expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); - assert_eq!(artist.properties.bandcamp, expected); - - assert!(artist.set_bandcamp_urls(vec![BANDCAMP, BANDCAMP_2]).is_ok()); - expected.clear(); - expected.push(Bandcamp::new(BANDCAMP).unwrap()); - expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); - assert_eq!(artist.properties.bandcamp, expected); - - // Clear URLs. - artist.clear_bandcamp_urls(); - expected.clear(); - assert_eq!(artist.properties.bandcamp, expected); - } - - #[test] - fn add_remove_qobuz_url() { - let mut artist = Artist::new(ArtistId::new("an artist")); - - let mut expected: Option = None; - assert_eq!(artist.properties.qobuz, expected); - - // Adding incorect URL is an error. - assert!(artist.add_qobuz_url(MUSICBRAINZ).is_err()); - assert!(artist.add_qobuz_url(MUSICBUTLER).is_err()); - assert!(artist.add_qobuz_url(BANDCAMP).is_err()); - assert_eq!(artist.properties.qobuz, expected); - - // Adding URL to artist. - assert!(artist.add_qobuz_url(QOBUZ).is_ok()); - _ = expected.insert(Qobuz::new(QOBUZ).unwrap()); - assert_eq!(artist.properties.qobuz, expected); - - // Adding the same URL again is ok, but does not do anything. - assert!(artist.add_qobuz_url(QOBUZ).is_ok()); - assert_eq!(artist.properties.qobuz, expected); - - // Adding further URLs is an error. - assert!(artist.add_qobuz_url(QOBUZ_2).is_err()); - assert_eq!(artist.properties.qobuz, expected); - - // Removing a URL not in the collection is okay, but does not do anything. - assert!(artist.remove_qobuz_url(QOBUZ_2).is_ok()); - assert_eq!(artist.properties.qobuz, expected); - - // Removing a URL in the collection removes it. - assert!(artist.remove_qobuz_url(QOBUZ).is_ok()); - _ = expected.take(); - assert_eq!(artist.properties.qobuz, expected); - - assert!(artist.remove_qobuz_url(QOBUZ).is_ok()); - assert_eq!(artist.properties.qobuz, expected); - } - - #[test] - fn set_clear_qobuz_url() { - let mut artist = Artist::new(ArtistId::new("an artist")); - - let mut expected: Option = None; - assert_eq!(artist.properties.qobuz, expected); - - // Setting an incorrect URL is an error. - assert!(artist.set_qobuz_url(MUSICBUTLER).is_err()); - assert!(artist.set_qobuz_url(BANDCAMP).is_err()); - assert!(artist.set_qobuz_url(MUSICBRAINZ).is_err()); - assert_eq!(artist.properties.qobuz, expected); - - // Setting a URL on an artist. - assert!(artist.set_qobuz_url(QOBUZ).is_ok()); - _ = expected.insert(Qobuz::new(QOBUZ).unwrap()); - assert_eq!(artist.properties.qobuz, expected); - - assert!(artist.set_qobuz_url(QOBUZ).is_ok()); - assert_eq!(artist.properties.qobuz, expected); - - assert!(artist.set_qobuz_url(QOBUZ_2).is_ok()); - _ = expected.insert(Qobuz::new(QOBUZ_2).unwrap()); - assert_eq!(artist.properties.qobuz, expected); - - // Clearing URLs. - artist.clear_qobuz_url(); - _ = expected.take(); - assert_eq!(artist.properties.qobuz, expected); + assert!(artist.properties.is_empty()); } #[test] @@ -979,7 +497,8 @@ mod tests { let left = FULL_COLLECTION[0].to_owned(); let mut right = FULL_COLLECTION[1].to_owned(); right.id = left.id.clone(); - right.properties = ArtistProperties::default(); + right.musicbrainz = None; + right.properties = HashMap::new(); let mut expected = left.clone(); expected.properties = expected.properties.merge(right.clone().properties); diff --git a/src/core/collection/merge.rs b/src/core/collection/merge.rs index fcad1b9..e2a5fc5 100644 --- a/src/core/collection/merge.rs +++ b/src/core/collection/merge.rs @@ -1,4 +1,4 @@ -use std::{cmp::Ordering, iter::Peekable}; +use std::{cmp::Ordering, collections::HashMap, hash::Hash, iter::Peekable}; /// A trait for merging two objects. The merge is asymmetric with the left argument considered to be /// the primary whose properties are to be kept in case of collisions. @@ -12,11 +12,25 @@ pub trait Merge { self.merge_in_place(other); self } +} - fn merge_vecs(this: &mut Vec, mut other: Vec) { - this.append(&mut other); - this.sort_unstable(); - this.dedup(); +impl Merge for Vec { + fn merge_in_place(&mut self, mut other: Self) { + self.append(&mut other); + self.sort_unstable(); + self.dedup(); + } +} + +impl Merge for HashMap> { + fn merge_in_place(&mut self, mut other: Self) { + for (other_key, other_value) in other.drain() { + if let Some(ref mut value) = self.get_mut(&other_key) { + value.merge_in_place(other_value) + } else { + self.insert(other_key, other_value); + } + } } } diff --git a/src/core/database/json/mod.rs b/src/core/database/json/mod.rs index 2836ea2..2ece5a2 100644 --- a/src/core/database/json/mod.rs +++ b/src/core/database/json/mod.rs @@ -65,8 +65,6 @@ pub mod testmod; mod tests { use std::collections::HashMap; - use mockall::predicate; - use crate::core::{ collection::{ artist::{Artist, ArtistId}, @@ -78,21 +76,6 @@ mod tests { use super::*; use testmod::DATABASE_JSON; - #[test] - fn save() { - let write_data = FULL_COLLECTION.to_owned(); - let input = DATABASE_JSON.to_owned(); - - let mut backend = MockIJsonDatabaseBackend::new(); - backend - .expect_write() - .with(predicate::eq(input)) - .times(1) - .return_once(|_| Ok(())); - - JsonDatabase::new(backend).save(&write_data).unwrap(); - } - #[test] fn load() { let expected = FULL_COLLECTION.to_owned(); @@ -108,17 +91,24 @@ mod tests { #[test] fn reverse() { - let input = DATABASE_JSON.to_owned(); - let result = Ok(input.clone()); + // Saving is non-deterministic due to HashMap, but regardless of how the data ends up being + // saved, loading it again should always yield the exact same data as was input. + struct MockIJsonDatabaseBackend { + data: Option, + } - let mut backend = MockIJsonDatabaseBackend::new(); - backend - .expect_write() - .with(predicate::eq(input)) - .times(1) - .return_once(|_| Ok(())); - backend.expect_read().times(1).return_once(|| result); + impl IJsonDatabaseBackend for MockIJsonDatabaseBackend { + fn write(&mut self, json: &str) -> Result<(), std::io::Error> { + let _ = self.data.insert(json.to_owned()); + Ok(()) + } + fn read(&self) -> Result { + Ok(self.data.as_ref().unwrap().clone()) + } + } + + let backend = MockIJsonDatabaseBackend { data: None }; let mut database = JsonDatabase::new(backend); let write_data = FULL_COLLECTION.to_owned(); diff --git a/src/core/database/json/testmod.rs b/src/core/database/json/testmod.rs index ef5ea55..319ccd2 100644 --- a/src/core/database/json/testmod.rs +++ b/src/core/database/json/testmod.rs @@ -2,11 +2,10 @@ pub static DATABASE_JSON: &str = "[\ {\ \"id\":{\"name\":\"album_artist a\"},\ \"sort\":null,\ - \"properties\":{\ \"musicbrainz\":\"https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000\",\ - \"musicbutler\":[\"https://www.musicbutler.io/artist-page/000000000\"],\ - \"bandcamp\":[],\ - \"qobuz\":\"https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums\"\ + \"properties\":{\ + \"MusicButler\":[\"https://www.musicbutler.io/artist-page/000000000\"],\ + \"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums\"]\ },\ \"albums\":[\ {\ @@ -54,14 +53,14 @@ pub static DATABASE_JSON: &str = "[\ {\ \"id\":{\"name\":\"album_artist b\"},\ \"sort\":null,\ - \"properties\":{\ \"musicbrainz\":\"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111\",\ - \"musicbutler\":[\ + \"properties\":{\ + \"Bandcamp\":[\"https://artist-b.bandcamp.com/\"],\ + \"MusicButler\":[\ \"https://www.musicbutler.io/artist-page/111111111\",\ \"https://www.musicbutler.io/artist-page/111111112\"\ ],\ - \"bandcamp\":[\"https://artist-b.bandcamp.com/\"],\ - \"qobuz\":\"https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums\"\ + \"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums\"]\ },\ \"albums\":[\ {\ @@ -129,12 +128,8 @@ pub static DATABASE_JSON: &str = "[\ {\ \"id\":{\"name\":\"album_artist c\"},\ \"sort\":null,\ - \"properties\":{\ \"musicbrainz\":\"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111\",\ - \"musicbutler\":[],\ - \"bandcamp\":[],\ - \"qobuz\":null\ - },\ + \"properties\":{},\ \"albums\":[\ {\ \"id\":{\"year\":1985,\"title\":\"album_title c.a\"},\ @@ -171,12 +166,8 @@ pub static DATABASE_JSON: &str = "[\ {\ \"id\":{\"name\":\"album_artist d\"},\ \"sort\":null,\ - \"properties\":{\ \"musicbrainz\":null,\ - \"musicbutler\":[],\ - \"bandcamp\":[],\ - \"qobuz\":null\ - },\ + \"properties\":{},\ \"albums\":[\ {\ \"id\":{\"year\":1995,\"title\":\"album_title d.a\"},\ diff --git a/src/core/musichoard/musichoard.rs b/src/core/musichoard/musichoard.rs index 041569b..552f542 100644 --- a/src/core/musichoard/musichoard.rs +++ b/src/core/musichoard/musichoard.rs @@ -1,7 +1,5 @@ use std::collections::HashMap; -use paste::paste; - use crate::core::{ collection::{ album::{Album, AlbumId}, @@ -31,81 +29,6 @@ pub struct NoLibrary; /// Phantom type for when a database implementation is not needed. pub struct NoDatabase; -macro_rules! music_hoard_unique_url_dispatch { - ($field:ident) => { - paste! { - pub fn [], S: AsRef>( - &mut self, - artist_id: ID, - url: S, - ) -> Result<(), Error> { - Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](url)?) - } - - pub fn [], S: AsRef>( - &mut self, - artist_id: ID, - url: S, - ) -> Result<(), Error> { - Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](url)?) - } - - pub fn [], S: AsRef>( - &mut self, - artist_id: ID, - url: S, - ) -> Result<(), Error> { - Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](url)?) - } - - pub fn []>( - &mut self, - artist_id: ID, - ) -> Result<(), Error> { - self.get_artist_mut_or_err(artist_id.as_ref())?.[](); - Ok(()) - } - } - }; -} - -macro_rules! music_hoard_multi_url_dispatch { - ($field:ident) => { - paste! { - pub fn [], S: AsRef>( - &mut self, - artist_id: ID, - urls: Vec, - ) -> Result<(), Error> { - Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](urls)?) - } - - pub fn [], S: AsRef>( - &mut self, - artist_id: ID, - urls: Vec, - ) -> Result<(), Error> { - Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](urls)?) - } - - pub fn [], S: AsRef>( - &mut self, - artist_id: ID, - urls: Vec, - ) -> Result<(), Error> { - Ok(self.get_artist_mut_or_err(artist_id.as_ref())?.[](urls)?) - } - - pub fn []>( - &mut self, artist_id: ID, - ) -> Result<(), Error> { - self.get_artist_mut_or_err(artist_id.as_ref())?.[](); - Ok(()) - } - } - }; -} - impl Default for MusicHoard { /// Create a new [`MusicHoard`] without any library or database. fn default() -> Self { @@ -167,13 +90,87 @@ impl MusicHoard { Ok(()) } - music_hoard_unique_url_dispatch!(musicbrainz); + pub fn add_musicbrainz_url, S: AsRef>( + &mut self, + artist_id: ID, + url: S, + ) -> Result<(), Error> { + Ok(self + .get_artist_mut_or_err(artist_id.as_ref())? + .add_musicbrainz_url(url)?) + } - music_hoard_multi_url_dispatch!(musicbutler); + pub fn remove_musicbrainz_url, S: AsRef>( + &mut self, + artist_id: ID, + url: S, + ) -> Result<(), Error> { + Ok(self + .get_artist_mut_or_err(artist_id.as_ref())? + .remove_musicbrainz_url(url)?) + } - music_hoard_multi_url_dispatch!(bandcamp); + pub fn set_musicbrainz_url, S: AsRef>( + &mut self, + artist_id: ID, + url: S, + ) -> Result<(), Error> { + Ok(self + .get_artist_mut_or_err(artist_id.as_ref())? + .set_musicbrainz_url(url)?) + } - music_hoard_unique_url_dispatch!(qobuz); + pub fn clear_musicbrainz_url>( + &mut self, + artist_id: ID, + ) -> Result<(), Error> { + self.get_artist_mut_or_err(artist_id.as_ref())? + .clear_musicbrainz_url(); + Ok(()) + } + + pub fn add_to_property, S: AsRef + Into>( + &mut self, + artist_id: ID, + property: S, + values: Vec, + ) -> Result<(), Error> { + self.get_artist_mut_or_err(artist_id.as_ref())? + .add_to_property(property, values); + Ok(()) + } + + pub fn remove_from_property, S: AsRef>( + &mut self, + artist_id: ID, + property: S, + values: Vec, + ) -> Result<(), Error> { + self.get_artist_mut_or_err(artist_id.as_ref())? + .remove_from_property(property, values); + Ok(()) + } + + pub fn set_property, S: AsRef + Into>( + &mut self, + artist_id: ID, + property: S, + values: Vec, + ) -> Result<(), Error> { + self.get_artist_mut_or_err(artist_id.as_ref())? + .set_property(property, values); + Ok(()) + } + + pub fn clear_property, S: AsRef>( + &mut self, + artist_id: ID, + property: S, + ) -> Result<(), Error> { + self.get_artist_mut_or_err(artist_id.as_ref())? + .clear_property(property); + Ok(()) + } fn sort(collection: &mut [Artist]) { Self::sort_artists(collection); @@ -324,7 +321,7 @@ mod tests { use mockall::predicate; use crate::core::{ - collection::artist::{ArtistId, Bandcamp, MusicBrainz, MusicButler, Qobuz}, + collection::artist::{ArtistId, MusicBrainz}, database::{self, MockIDatabase}, library::{self, testmod::LIBRARY_ITEMS, MockILibrary}, testmod::{FULL_COLLECTION, LIBRARY_COLLECTION}, @@ -336,9 +333,6 @@ mod tests { "https://musicbrainz.org/artist/d368baa8-21ca-4759-9731-0b2753071ad8"; 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"; #[test] fn artist_new_delete() { @@ -417,10 +411,10 @@ mod tests { music_hoard.add_artist(artist_id.clone()); let actual_err = music_hoard - .add_musicbrainz_url(&artist_id, QOBUZ) + .add_musicbrainz_url(&artist_id, MUSICBUTLER) .unwrap_err(); let expected_err = Error::CollectionError(format!( - "an error occurred when processing a URL: invalid MusicBrainz URL: {QOBUZ}" + "an error occurred when processing a URL: invalid MusicBrainz URL: {MUSICBUTLER}" )); assert_eq!(actual_err, expected_err); assert_eq!(actual_err.to_string(), expected_err.to_string()); @@ -435,33 +429,33 @@ mod tests { music_hoard.add_artist(artist_id.clone()); let mut expected: Option = None; - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + assert_eq!(music_hoard.collection[0].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); + assert_eq!(music_hoard.collection[0].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); + assert_eq!(music_hoard.collection[0].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); + assert_eq!(music_hoard.collection[0].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_eq!(music_hoard.collection[0].musicbrainz, expected); } #[test] @@ -473,237 +467,116 @@ mod tests { music_hoard.add_artist(artist_id.clone()); let mut expected: Option = None; - assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + assert_eq!(music_hoard.collection[0].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); + assert_eq!(music_hoard.collection[0].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_eq!(music_hoard.collection[0].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); + assert_eq!(music_hoard.collection[0].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); + assert_eq!(music_hoard.collection[0].musicbrainz, expected); } #[test] - fn add_remove_musicbutler_urls() { + fn add_to_remove_from_property() { let artist_id = ArtistId::new("an artist"); let artist_id_2 = ArtistId::new("another artist"); let mut music_hoard = MusicHoard::default(); music_hoard.add_artist(artist_id.clone()); - let mut expected: Vec = vec![]; - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + let mut expected: Vec = vec![]; + assert!(music_hoard.collection[0].properties.is_empty()); // Adding URLs to an artist not in the collection is an error. assert!(music_hoard - .add_musicbutler_urls(&artist_id_2, vec![MUSICBUTLER]) + .add_to_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .is_err()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + assert!(music_hoard.collection[0].properties.is_empty()); // Adding mutliple URLs without clashes. assert!(music_hoard - .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .add_to_property(&artist_id, "MusicButler", 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); + expected.push(MUSICBUTLER.to_owned()); + expected.push(MUSICBUTLER_2.to_owned()); + assert_eq!( + music_hoard.collection[0].properties.get("MusicButler"), + Some(&expected) + ); // Removing URLs from an artist not in the collection is an error. assert!(music_hoard - .remove_musicbutler_urls(&artist_id_2, vec![MUSICBUTLER]) + .remove_from_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .is_err()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + assert_eq!( + music_hoard.collection[0].properties.get("MusicButler"), + Some(&expected) + ); // Removing multiple URLs without clashes. assert!(music_hoard - .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .remove_from_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]) .is_ok()); expected.clear(); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + assert!(music_hoard.collection[0].properties.is_empty()); } #[test] - fn set_clear_musicbutler_urls() { + fn set_clear_property() { let artist_id = ArtistId::new("an artist"); let artist_id_2 = ArtistId::new("another artist"); let mut music_hoard = MusicHoard::default(); music_hoard.add_artist(artist_id.clone()); - let mut expected: Vec = vec![]; - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + let mut expected: Vec = vec![]; + assert!(music_hoard.collection[0].properties.is_empty()); // Seting URL on an artist not in the collection is an error. assert!(music_hoard - .set_musicbutler_urls(&artist_id_2, vec![MUSICBUTLER]) + .set_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .is_err()); - assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + assert!(music_hoard.collection[0].properties.is_empty()); // Set URLs. assert!(music_hoard - .set_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .set_property(&artist_id, "MusicButler", 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); + expected.push(MUSICBUTLER.to_owned()); + expected.push(MUSICBUTLER_2.to_owned()); + assert_eq!( + music_hoard.collection[0].properties.get("MusicButler"), + Some(&expected) + ); // Clearing URLs on an artist that does not exist is an error. - assert!(music_hoard.clear_musicbutler_urls(&artist_id_2).is_err()); + assert!(music_hoard + .clear_property(&artist_id_2, "MusicButler") + .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 = MusicHoard::default(); - - music_hoard.add_artist(artist_id.clone()); - - let mut expected: Vec = vec![]; - 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 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 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 multiple URLs without clashes. - assert!(music_hoard - .remove_bandcamp_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) + .clear_property(&artist_id, "MusicButler") .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 = MusicHoard::default(); - - music_hoard.add_artist(artist_id.clone()); - - let mut expected: Vec = vec![]; - 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, 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 = MusicHoard::default(); - - music_hoard.add_artist(artist_id.clone()); - - let mut expected: Option = None; - 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); - - // 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 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); - } - - #[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 = MusicHoard::default(); - - music_hoard.add_artist(artist_id.clone()); - - let mut expected: Option = None; - 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); - - // 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); + assert!(music_hoard.collection[0].properties.is_empty()); } #[test] diff --git a/src/core/testmod.rs b/src/core/testmod.rs index 4527446..2f10f36 100644 --- a/src/core/testmod.rs +++ b/src/core/testmod.rs @@ -1,8 +1,9 @@ use once_cell::sync::Lazy; +use std::collections::HashMap; use crate::core::collection::{ album::{Album, AlbumId}, - artist::{Artist, ArtistId, ArtistProperties, Bandcamp, MusicBrainz, MusicButler, Qobuz}, + artist::{Artist, ArtistId, MusicBrainz}, track::{Format, Quality, Track, TrackId}, }; use crate::tests::*; diff --git a/src/tests.rs b/src/tests.rs index e952266..bfd4383 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -6,12 +6,8 @@ macro_rules! library_collection { name: "album_artist a".to_string(), }, sort: None, - properties: ArtistProperties { - musicbrainz: None, - musicbutler: vec![], - bandcamp: vec![], - qobuz: None, - }, + musicbrainz: None, + properties: HashMap::new(), albums: vec![ Album { id: AlbumId { @@ -105,12 +101,8 @@ macro_rules! library_collection { name: "album_artist b".to_string(), }, sort: None, - properties: ArtistProperties { - musicbrainz: None, - musicbutler: vec![], - bandcamp: vec![], - qobuz: None, - }, + musicbrainz: None, + properties: HashMap::new(), albums: vec![ Album { id: AlbumId { @@ -251,12 +243,8 @@ macro_rules! library_collection { name: "album_artist c".to_string(), }, sort: None, - properties: ArtistProperties { - musicbrainz: None, - musicbutler: vec![], - bandcamp: vec![], - qobuz: None, - }, + musicbrainz: None, + properties: HashMap::new(), albums: vec![ Album { id: AlbumId { @@ -331,12 +319,8 @@ macro_rules! library_collection { name: "album_artist d".to_string(), }, sort: None, - properties: ArtistProperties { - musicbrainz: None, - musicbutler: vec![], - bandcamp: vec![], - qobuz: None, - }, + musicbrainz: None, + properties: HashMap::new(), albums: vec![ Album { id: AlbumId { @@ -418,62 +402,54 @@ macro_rules! full_collection { let artist_a = iter.next().unwrap(); assert_eq!(artist_a.id.name, "album_artist a"); - artist_a.properties = ArtistProperties { - musicbrainz: Some( - MusicBrainz::new( - "https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000", - ) + artist_a.musicbrainz = Some( + MusicBrainz::new( + "https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000", + ) .unwrap(), - ), - musicbutler: vec![ - MusicButler::new("https://www.musicbutler.io/artist-page/000000000").unwrap(), - ], - bandcamp: vec![], - qobuz: Some( - Qobuz::new( + ); + + artist_a.properties = HashMap::from([ + (String::from("MusicButler"), vec![ + String::from("https://www.musicbutler.io/artist-page/000000000"), + ]), + (String::from("Qobuz"), vec![ + String::from( "https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums", ) - .unwrap(), - ), - }; + ]), + ]); let artist_b = iter.next().unwrap(); assert_eq!(artist_b.id.name, "album_artist b"); - artist_b.properties = ArtistProperties { - musicbrainz: Some( - MusicBrainz::new( - "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111", - ) - .unwrap(), - ), - musicbutler: vec![ - MusicButler::new("https://www.musicbutler.io/artist-page/111111111").unwrap(), - MusicButler::new("https://www.musicbutler.io/artist-page/111111112").unwrap(), - ], - bandcamp: vec![Bandcamp::new("https://artist-b.bandcamp.com/").unwrap()], - qobuz: Some( - Qobuz::new( + artist_b.musicbrainz = Some( + MusicBrainz::new( + "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111", + ).unwrap(), + ); + + artist_b.properties = HashMap::from([ + (String::from("MusicButler"), vec![ + String::from("https://www.musicbutler.io/artist-page/111111111"), + String::from("https://www.musicbutler.io/artist-page/111111112"), + ]), + (String::from("Bandcamp"), vec![String::from("https://artist-b.bandcamp.com/")]), + (String::from("Qobuz"), vec![ + String::from( "https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums", ) - .unwrap(), - ), - }; + ]), + ]); let artist_c = iter.next().unwrap(); assert_eq!(artist_c.id.name, "album_artist c"); - artist_c.properties = ArtistProperties { - musicbrainz: Some( - MusicBrainz::new( - "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111", - ) - .unwrap(), - ), - musicbutler: vec![], - bandcamp: vec![], - qobuz: None, - }; + artist_c.musicbrainz = Some( + MusicBrainz::new( + "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111", + ).unwrap(), + ); // Nothing for artist_d diff --git a/src/tui/testmod.rs b/src/tui/testmod.rs index f5d1bcf..7054f71 100644 --- a/src/tui/testmod.rs +++ b/src/tui/testmod.rs @@ -1,9 +1,10 @@ use musichoard::collection::{ album::{Album, AlbumId}, - artist::{Artist, ArtistId, ArtistProperties, Bandcamp, MusicBrainz, MusicButler, Qobuz}, + artist::{Artist, ArtistId, MusicBrainz}, track::{Format, Quality, Track, TrackId}, }; use once_cell::sync::Lazy; +use std::collections::HashMap; use crate::tests::*; diff --git a/src/tui/ui.rs b/src/tui/ui.rs index e9ba010..8d9d952 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use musichoard::collection::{ album::Album, artist::Artist, @@ -188,43 +190,70 @@ impl<'a> ArtistOverlay<'a> { opt.flatten().map(|item| item.as_ref()).unwrap_or("") } - fn opt_vec_to_string>(opt_vec: Option<&Vec>, indent: &str) -> String { - opt_vec - .map(|vec| { - if vec.len() < 2 { - vec.first() - .map(|item| item.as_ref()) - .unwrap_or("") - .to_string() - } else { - let indent = format!("\n{indent}"); - let list = vec - .iter() - .map(|item| item.as_ref()) - .collect::>() - .join(&indent); - format!("{indent}{list}") - } - }) + fn opt_hashmap_to_string, T: AsRef>( + opt_map: Option<&HashMap>>, + item_indent: &str, + list_indent: &str, + ) -> String { + opt_map + .map(|map| Self::hashmap_to_string(map, item_indent, list_indent)) .unwrap_or_else(|| String::from("")) } + fn hashmap_to_string, T: AsRef>( + map: &HashMap>, + item_indent: &str, + list_indent: &str, + ) -> String { + let mut vec: Vec<(&str, &Vec)> = map.iter().map(|(k, v)| (k.as_ref(), v)).collect(); + vec.sort_by(|x, y| x.0.cmp(y.0)); + + let indent = format!("\n{item_indent}"); + let list = vec + .iter() + .map(|(k, v)| format!("{k}: {}", Self::vec_to_string(v, list_indent))) + .collect::>() + .join(&indent); + format!("{indent}{list}") + } + + fn vec_to_string>(vec: &Vec, indent: &str) -> String { + if vec.len() < 2 { + vec.first() + .map(|item| item.as_ref()) + .unwrap_or("") + .to_string() + } else { + let indent = format!("\n{indent}"); + let list = vec + .iter() + .map(|item| item.as_ref()) + .collect::>() + .join(&indent); + format!("{indent}{list}") + } + } + fn new(artists: &'a [Artist], state: &ListState) -> ArtistOverlay<'a> { let artist = state.selected().map(|i| &artists[i]); let item_indent = " "; let list_indent = " - "; + + let double_item_indent = format!("{item_indent}{item_indent}"); + let double_list_indent = format!("{item_indent}{list_indent}"); + let properties = Paragraph::new(format!( "Artist: {}\n\n{item_indent}\ MusicBrainz: {}\n{item_indent}\ - MusicButler: {}\n{item_indent}\ - Bandcamp: {}\n{item_indent}\ - Qobuz: {}", + Properties: {}", artist.map(|a| a.id.name.as_str()).unwrap_or(""), - Self::opt_opt_to_str(artist.map(|a| a.properties.musicbrainz.as_ref())), - Self::opt_vec_to_string(artist.map(|a| &a.properties.musicbutler), list_indent), - Self::opt_vec_to_string(artist.map(|a| &a.properties.bandcamp), list_indent), - Self::opt_opt_to_str(artist.map(|a| a.properties.qobuz.as_ref())), + Self::opt_opt_to_str(artist.map(|a| a.musicbrainz.as_ref())), + Self::opt_hashmap_to_string( + artist.map(|a| &a.properties), + &double_item_indent, + &double_list_indent + ), )); ArtistOverlay { properties } diff --git a/tests/database/json.rs b/tests/database/json.rs index e7e18ac..95c8a99 100644 --- a/tests/database/json.rs +++ b/tests/database/json.rs @@ -16,22 +16,6 @@ use crate::testlib::COLLECTION; pub static DATABASE_TEST_FILE: Lazy = Lazy::new(|| fs::canonicalize("./tests/files/database/database.json").unwrap()); -#[test] -fn save() { - let file = NamedTempFile::new().unwrap(); - - let backend = JsonDatabaseFileBackend::new(file.path()); - let mut database = JsonDatabase::new(backend); - - let write_data = COLLECTION.to_owned(); - database.save(&write_data).unwrap(); - - let expected = fs::read_to_string(&*DATABASE_TEST_FILE).unwrap(); - let actual = fs::read_to_string(file.path()).unwrap(); - - assert_eq!(actual, expected); -} - #[test] fn load() { let backend = JsonDatabaseFileBackend::new(&*DATABASE_TEST_FILE); @@ -45,6 +29,8 @@ fn load() { #[test] fn reverse() { + // Saving is non-deterministic due to HashMap, but regardless of how the data ends up being + // saved, loading it again should always yield the exact same data as was input. let file = NamedTempFile::new().unwrap(); let backend = JsonDatabaseFileBackend::new(file.path()); @@ -52,7 +38,6 @@ fn reverse() { let write_data = COLLECTION.to_owned(); database.save(&write_data).unwrap(); - let read_data: Vec = database.load().unwrap(); assert_eq!(write_data, read_data); diff --git a/tests/files/database/database.json b/tests/files/database/database.json index 9c51a67..c5fe56c 100644 --- a/tests/files/database/database.json +++ b/tests/files/database/database.json @@ -1 +1 @@ -[{"id":{"name":"Аркона"},"sort":{"name":"Arkona"},"properties":{"musicbrainz":"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212","musicbutler":["https://www.musicbutler.io/artist-page/283448581"],"bandcamp":["https://arkonamoscow.bandcamp.com/"],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums"},"albums":[{"id":{"year":2011,"title":"Slovo"},"tracks":[{"id":{"number":1,"title":"Az’"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":992}},{"id":{"number":2,"title":"Arkaim"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1061}},{"id":{"number":3,"title":"Bol’no mne"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1004}},{"id":{"number":4,"title":"Leshiy"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1077}},{"id":{"number":5,"title":"Zakliatie"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1041}},{"id":{"number":6,"title":"Predok"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":756}},{"id":{"number":7,"title":"Nikogda"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1059}},{"id":{"number":8,"title":"Tam za tumanami"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1023}},{"id":{"number":9,"title":"Potomok"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":838}},{"id":{"number":10,"title":"Slovo"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1028}},{"id":{"number":11,"title":"Odna"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":991}},{"id":{"number":12,"title":"Vo moiom sadochke…"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":919}},{"id":{"number":13,"title":"Stenka na stenku"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1039}},{"id":{"number":14,"title":"Zimushka"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":974}}]}]},{"id":{"name":"Eluveitie"},"sort":null,"properties":{"musicbrainz":"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38","musicbutler":["https://www.musicbutler.io/artist-page/269358403"],"bandcamp":[],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums"},"albums":[{"id":{"year":2004,"title":"Vên [re‐recorded]"},"tracks":[{"id":{"number":1,"title":"Verja Urit an Bitus"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":961}},{"id":{"number":2,"title":"Uis Elveti"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1067}},{"id":{"number":3,"title":"Ôrô"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":933}},{"id":{"number":4,"title":"Lament"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1083}},{"id":{"number":5,"title":"Druid"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1073}},{"id":{"number":6,"title":"Jêzaïg"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1002}}]},{"id":{"year":2008,"title":"Slania"},"tracks":[{"id":{"number":1,"title":"Samon"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":953}},{"id":{"number":2,"title":"Primordial Breath"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1103}},{"id":{"number":3,"title":"Inis Mona"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1117}},{"id":{"number":4,"title":"Gray Sublime Archon"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1092}},{"id":{"number":5,"title":"Anagantios"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":923}},{"id":{"number":6,"title":"Bloodstained Ground"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1098}},{"id":{"number":7,"title":"The Somber Lay"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1068}},{"id":{"number":8,"title":"Slanias Song"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1098}},{"id":{"number":9,"title":"Giamonios"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":825}},{"id":{"number":10,"title":"Tarvos"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1115}},{"id":{"number":11,"title":"Calling the Rain"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1096}},{"id":{"number":12,"title":"Elembivos"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1059}}]}]},{"id":{"name":"Frontside"},"sort":null,"properties":{"musicbrainz":"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490","musicbutler":["https://www.musicbutler.io/artist-page/826588800"],"bandcamp":[],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums"},"albums":[{"id":{"year":2001,"title":"…nasze jest królestwo, potęga i chwała na wieki…"},"tracks":[{"id":{"number":1,"title":"Intro = Chaos"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1024}},{"id":{"number":2,"title":"Modlitwa"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1073}},{"id":{"number":3,"title":"Długa droga z piekła"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1058}},{"id":{"number":4,"title":"Synowie ognia"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1066}},{"id":{"number":5,"title":"1902"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1074}},{"id":{"number":6,"title":"Krew za krew"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1080}},{"id":{"number":7,"title":"Kulminacja"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":992}},{"id":{"number":8,"title":"Judasz"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1018}},{"id":{"number":9,"title":"Więzy"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1077}},{"id":{"number":10,"title":"Zagubione dusze"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1033}},{"id":{"number":11,"title":"Linia życia"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":987}}]}]},{"id":{"name":"Heaven’s Basement"},"sort":{"name":"Heaven’s Basement"},"properties":{"musicbrainz":"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc","musicbutler":["https://www.musicbutler.io/artist-page/291158685"],"bandcamp":[],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums"},"albums":[{"id":{"year":2011,"title":"Paper Plague"},"tracks":[{"id":{"number":0,"title":"Paper Plague"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":320}}]},{"id":{"year":2011,"title":"Unbreakable"},"tracks":[{"id":{"number":1,"title":"Unbreakable"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":208}},{"id":{"number":2,"title":"Guilt Trips and Sins"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":205}},{"id":{"number":3,"title":"The Long Goodbye"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":227}},{"id":{"number":4,"title":"Close Encounters"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":213}},{"id":{"number":5,"title":"Paranoia"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":218}},{"id":{"number":6,"title":"Let Me Out of Here"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":207}},{"id":{"number":7,"title":"Leeches"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":225}}]}]},{"id":{"name":"Metallica"},"sort":null,"properties":{"musicbrainz":"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab","musicbutler":["https://www.musicbutler.io/artist-page/3996865"],"bandcamp":[],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums"},"albums":[{"id":{"year":1984,"title":"Ride the Lightning"},"tracks":[{"id":{"number":1,"title":"Fight Fire with Fire"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":954}},{"id":{"number":2,"title":"Ride the Lightning"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":951}},{"id":{"number":3,"title":"For Whom the Bell Tolls"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":889}},{"id":{"number":4,"title":"Fade to Black"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":939}},{"id":{"number":5,"title":"Trapped under Ice"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":955}},{"id":{"number":6,"title":"Escape"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":941}},{"id":{"number":7,"title":"Creeping Death"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":958}},{"id":{"number":8,"title":"The Call of Ktulu"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":888}}]},{"id":{"year":1999,"title":"S&M"},"tracks":[{"id":{"number":1,"title":"The Ecstasy of Gold"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":875}},{"id":{"number":2,"title":"The Call of Ktulu"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1030}},{"id":{"number":3,"title":"Master of Puppets"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1082}},{"id":{"number":4,"title":"Of Wolf and Man"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1115}},{"id":{"number":5,"title":"The Thing That Should Not Be"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1029}},{"id":{"number":6,"title":"Fuel"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1057}},{"id":{"number":7,"title":"The Memory Remains"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1080}},{"id":{"number":8,"title":"No Leaf Clover"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1004}},{"id":{"number":9,"title":"Hero of the Day"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":962}},{"id":{"number":10,"title":"Devil’s Dance"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1076}},{"id":{"number":11,"title":"Bleeding Me"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":993}},{"id":{"number":12,"title":"Nothing Else Matters"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":875}},{"id":{"number":13,"title":"Until It Sleeps"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1038}},{"id":{"number":14,"title":"For Whom the Bell Tolls"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1072}},{"id":{"number":15,"title":"−Human"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1029}},{"id":{"number":16,"title":"Wherever I May Roam"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1035}},{"id":{"number":17,"title":"Outlaw Torn"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1042}},{"id":{"number":18,"title":"Sad but True"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1082}},{"id":{"number":19,"title":"One"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1017}},{"id":{"number":20,"title":"Enter Sandman"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":993}},{"id":{"number":21,"title":"Battery"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":967}}]}]}] \ No newline at end of file +[{"id":{"name":"Аркона"},"sort":{"name":"Arkona"},"musicbrainz":"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212","properties":{"Bandcamp":["https://arkonamoscow.bandcamp.com/"],"MusicButler":["https://www.musicbutler.io/artist-page/283448581"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums"]},"albums":[{"id":{"year":2011,"title":"Slovo"},"tracks":[{"id":{"number":1,"title":"Az’"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":992}},{"id":{"number":2,"title":"Arkaim"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1061}},{"id":{"number":3,"title":"Bol’no mne"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1004}},{"id":{"number":4,"title":"Leshiy"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1077}},{"id":{"number":5,"title":"Zakliatie"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1041}},{"id":{"number":6,"title":"Predok"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":756}},{"id":{"number":7,"title":"Nikogda"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1059}},{"id":{"number":8,"title":"Tam za tumanami"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1023}},{"id":{"number":9,"title":"Potomok"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":838}},{"id":{"number":10,"title":"Slovo"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1028}},{"id":{"number":11,"title":"Odna"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":991}},{"id":{"number":12,"title":"Vo moiom sadochke…"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":919}},{"id":{"number":13,"title":"Stenka na stenku"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1039}},{"id":{"number":14,"title":"Zimushka"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":974}}]}]},{"id":{"name":"Eluveitie"},"sort":null,"musicbrainz":"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/269358403"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums"]},"albums":[{"id":{"year":2004,"title":"Vên [re‐recorded]"},"tracks":[{"id":{"number":1,"title":"Verja Urit an Bitus"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":961}},{"id":{"number":2,"title":"Uis Elveti"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1067}},{"id":{"number":3,"title":"Ôrô"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":933}},{"id":{"number":4,"title":"Lament"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1083}},{"id":{"number":5,"title":"Druid"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1073}},{"id":{"number":6,"title":"Jêzaïg"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1002}}]},{"id":{"year":2008,"title":"Slania"},"tracks":[{"id":{"number":1,"title":"Samon"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":953}},{"id":{"number":2,"title":"Primordial Breath"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1103}},{"id":{"number":3,"title":"Inis Mona"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1117}},{"id":{"number":4,"title":"Gray Sublime Archon"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1092}},{"id":{"number":5,"title":"Anagantios"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":923}},{"id":{"number":6,"title":"Bloodstained Ground"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1098}},{"id":{"number":7,"title":"The Somber Lay"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1068}},{"id":{"number":8,"title":"Slanias Song"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1098}},{"id":{"number":9,"title":"Giamonios"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":825}},{"id":{"number":10,"title":"Tarvos"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1115}},{"id":{"number":11,"title":"Calling the Rain"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1096}},{"id":{"number":12,"title":"Elembivos"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1059}}]}]},{"id":{"name":"Frontside"},"sort":null,"musicbrainz":"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/826588800"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums"]},"albums":[{"id":{"year":2001,"title":"…nasze jest królestwo, potęga i chwała na wieki…"},"tracks":[{"id":{"number":1,"title":"Intro = Chaos"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1024}},{"id":{"number":2,"title":"Modlitwa"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1073}},{"id":{"number":3,"title":"Długa droga z piekła"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1058}},{"id":{"number":4,"title":"Synowie ognia"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1066}},{"id":{"number":5,"title":"1902"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1074}},{"id":{"number":6,"title":"Krew za krew"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1080}},{"id":{"number":7,"title":"Kulminacja"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":992}},{"id":{"number":8,"title":"Judasz"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1018}},{"id":{"number":9,"title":"Więzy"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1077}},{"id":{"number":10,"title":"Zagubione dusze"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1033}},{"id":{"number":11,"title":"Linia życia"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":987}}]}]},{"id":{"name":"Heaven’s Basement"},"sort":{"name":"Heaven’s Basement"},"musicbrainz":"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/291158685"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums"]},"albums":[{"id":{"year":2011,"title":"Paper Plague"},"tracks":[{"id":{"number":0,"title":"Paper Plague"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":320}}]},{"id":{"year":2011,"title":"Unbreakable"},"tracks":[{"id":{"number":1,"title":"Unbreakable"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":208}},{"id":{"number":2,"title":"Guilt Trips and Sins"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":205}},{"id":{"number":3,"title":"The Long Goodbye"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":227}},{"id":{"number":4,"title":"Close Encounters"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":213}},{"id":{"number":5,"title":"Paranoia"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":218}},{"id":{"number":6,"title":"Let Me Out of Here"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":207}},{"id":{"number":7,"title":"Leeches"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":225}}]}]},{"id":{"name":"Metallica"},"sort":null,"musicbrainz":"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/3996865"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums"]},"albums":[{"id":{"year":1984,"title":"Ride the Lightning"},"tracks":[{"id":{"number":1,"title":"Fight Fire with Fire"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":954}},{"id":{"number":2,"title":"Ride the Lightning"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":951}},{"id":{"number":3,"title":"For Whom the Bell Tolls"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":889}},{"id":{"number":4,"title":"Fade to Black"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":939}},{"id":{"number":5,"title":"Trapped under Ice"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":955}},{"id":{"number":6,"title":"Escape"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":941}},{"id":{"number":7,"title":"Creeping Death"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":958}},{"id":{"number":8,"title":"The Call of Ktulu"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":888}}]},{"id":{"year":1999,"title":"S&M"},"tracks":[{"id":{"number":1,"title":"The Ecstasy of Gold"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":875}},{"id":{"number":2,"title":"The Call of Ktulu"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1030}},{"id":{"number":3,"title":"Master of Puppets"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1082}},{"id":{"number":4,"title":"Of Wolf and Man"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1115}},{"id":{"number":5,"title":"The Thing That Should Not Be"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1029}},{"id":{"number":6,"title":"Fuel"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1057}},{"id":{"number":7,"title":"The Memory Remains"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1080}},{"id":{"number":8,"title":"No Leaf Clover"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1004}},{"id":{"number":9,"title":"Hero of the Day"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":962}},{"id":{"number":10,"title":"Devil’s Dance"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1076}},{"id":{"number":11,"title":"Bleeding Me"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":993}},{"id":{"number":12,"title":"Nothing Else Matters"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":875}},{"id":{"number":13,"title":"Until It Sleeps"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1038}},{"id":{"number":14,"title":"For Whom the Bell Tolls"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1072}},{"id":{"number":15,"title":"−Human"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1029}},{"id":{"number":16,"title":"Wherever I May Roam"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1035}},{"id":{"number":17,"title":"Outlaw Torn"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1042}},{"id":{"number":18,"title":"Sad but True"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1082}},{"id":{"number":19,"title":"One"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1017}},{"id":{"number":20,"title":"Enter Sandman"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":993}},{"id":{"number":21,"title":"Battery"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":967}}]}]}] \ No newline at end of file diff --git a/tests/testlib.rs b/tests/testlib.rs index 76bff31..ef4821e 100644 --- a/tests/testlib.rs +++ b/tests/testlib.rs @@ -1,8 +1,9 @@ use once_cell::sync::Lazy; +use std::collections::HashMap; use musichoard::collection::{ album::{Album, AlbumId}, - artist::{Artist, ArtistId, ArtistProperties, Bandcamp, MusicBrainz, MusicButler, Qobuz}, + artist::{Artist, ArtistId, MusicBrainz}, track::{Format, Quality, Track, TrackId}, Collection, }; @@ -16,20 +17,20 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { sort: Some(ArtistId{ name: String::from("Arkona") }), - properties: ArtistProperties { - musicbrainz: Some(MusicBrainz::new( - "https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212", - ).unwrap()), - musicbutler: vec![ - MusicButler::new("https://www.musicbutler.io/artist-page/283448581").unwrap(), - ], - bandcamp: vec![ - Bandcamp::new("https://arkonamoscow.bandcamp.com/").unwrap(), - ], - qobuz: Some(Qobuz::new( + musicbrainz: Some(MusicBrainz::new( + "https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212", + ).unwrap()), + properties: HashMap::from([ + (String::from("MusicButler"), vec![ + String::from("https://www.musicbutler.io/artist-page/283448581"), + ]), + (String::from("Bandcamp"), vec![ + String::from("https://arkonamoscow.bandcamp.com/"), + ]), + (String::from("Qobuz"), vec![String::from( "https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums", - ).unwrap()), - }, + )]), + ]), albums: vec![Album { id: AlbumId { year: 2011, @@ -198,18 +199,17 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { name: String::from("Eluveitie"), }, sort: None, - properties: ArtistProperties { - musicbrainz: Some(MusicBrainz::new( - "https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38", - ).unwrap()), - musicbutler: vec![ - MusicButler::new("https://www.musicbutler.io/artist-page/269358403").unwrap(), - ], - bandcamp: vec![], - qobuz: Some(Qobuz::new( + musicbrainz: Some(MusicBrainz::new( + "https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38", + ).unwrap()), + properties: HashMap::from([ + (String::from("MusicButler"), vec![ + String::from("https://www.musicbutler.io/artist-page/269358403"), + ]), + (String::from("Qobuz"), vec![String::from( "https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums", - ).unwrap()), - }, + )]), + ]), albums: vec![ Album { id: AlbumId { @@ -432,18 +432,17 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { name: String::from("Frontside"), }, sort: None, - properties: ArtistProperties { - musicbrainz: Some(MusicBrainz::new( - "https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490", - ).unwrap()), - musicbutler: vec![ - MusicButler::new("https://www.musicbutler.io/artist-page/826588800").unwrap(), - ], - bandcamp: vec![], - qobuz: Some(Qobuz::new( + musicbrainz: Some(MusicBrainz::new( + "https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490", + ).unwrap()), + properties: HashMap::from([ + (String::from("MusicButler"), vec![ + String::from("https://www.musicbutler.io/artist-page/826588800"), + ]), + (String::from("Qobuz"), vec![String::from( "https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums", - ).unwrap()), - }, + )]), + ]), albums: vec![Album { id: AlbumId { year: 2001, @@ -581,18 +580,17 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { sort: Some(ArtistId { name: String::from("Heaven’s Basement"), }), - properties: ArtistProperties { - musicbrainz: Some(MusicBrainz::new( - "https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc", - ).unwrap()), - musicbutler: vec![ - MusicButler::new("https://www.musicbutler.io/artist-page/291158685").unwrap(), - ], - bandcamp: vec![], - qobuz: Some(Qobuz::new( + musicbrainz: Some(MusicBrainz::new( + "https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc", + ).unwrap()), + properties: HashMap::from([ + (String::from("MusicButler"), vec![ + String::from("https://www.musicbutler.io/artist-page/291158685"), + ]), + (String::from("Qobuz"), vec![String::from( "https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums", - ).unwrap()), - }, + )]), + ]), albums: vec![Album { id: AlbumId { year: 2011, @@ -702,18 +700,17 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { name: String::from("Metallica"), }, sort: None, - properties: ArtistProperties { - musicbrainz: Some(MusicBrainz::new( - "https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab", - ).unwrap()), - musicbutler: vec![ - MusicButler::new("https://www.musicbutler.io/artist-page/3996865").unwrap(), - ], - bandcamp: vec![], - qobuz: Some(Qobuz::new( + musicbrainz: Some(MusicBrainz::new( + "https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab", + ).unwrap()), + properties: HashMap::from([ + (String::from("MusicButler"), vec![ + String::from("https://www.musicbutler.io/artist-page/3996865"), + ]), + (String::from("Qobuz"), vec![String::from( "https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums", - ).unwrap()), - }, + )]), + ]), albums: vec![ Album { id: AlbumId {