diff --git a/.gitea/workflows/gitea-ci.yaml b/.gitea/workflows/gitea-ci.yaml index 72c9002..c701061 100644 --- a/.gitea/workflows/gitea-ci.yaml +++ b/.gitea/workflows/gitea-ci.yaml @@ -30,6 +30,7 @@ jobs: --ignore-not-existing --ignore "tests/*" --ignore "src/main.rs" + --ignore "src/bin/mh-edit.rs" --excl-start "GRCOV_EXCL_START|mod tests \{" --excl-stop "GRCOV_EXCL_STOP" --output-path ./target/debug/coverage/ diff --git a/Cargo.lock b/Cargo.lock index 491d365..4ad4190 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,6 +369,7 @@ dependencies = [ "mockall", "once_cell", "openssh", + "paste", "ratatui", "serde", "serde_json", @@ -456,6 +457,12 @@ 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 33dc5d7..9fe2d21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ 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} @@ -23,14 +24,19 @@ tempfile = "3.5.0" [features] default = ["database-json", "library-beets"] +bin = ["structopt"] database-json = ["serde_json"] library-beets = [] ssh-library = ["openssh", "tokio"] -tui = ["structopt", "crossterm", "ratatui"] +tui = ["crossterm", "ratatui"] [[bin]] name = "musichoard" -required-features = ["database-json", "library-beets", "ssh-library", "tui"] +required-features = ["bin", "database-json", "library-beets", "ssh-library", "tui"] + +[[bin]] +name = "mh-edit" +required-features = ["bin", "database-json"] [package.metadata.docs.rs] all-features = true diff --git a/README.md b/README.md index d9674e0..36f6a45 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ grcov codecov/debug/profraw \ --ignore-not-existing \ --ignore "tests/*" \ --ignore "src/main.rs" \ + --ignore "src/bin/mh-edit.rs" \ --excl-start "GRCOV_EXCL_START|mod tests \{" \ --excl-stop "GRCOV_EXCL_STOP" \ --output-path ./codecov/debug/coverage/ diff --git a/src/bin/mh-edit.rs b/src/bin/mh-edit.rs new file mode 100644 index 0000000..daef105 --- /dev/null +++ b/src/bin/mh-edit.rs @@ -0,0 +1,179 @@ +use paste::paste; +use std::path::PathBuf; +use structopt::{clap::AppSettings, StructOpt}; + +use musichoard::{ + database::json::{backend::JsonDatabaseFileBackend, JsonDatabase}, + library::NoLibrary, + ArtistId, MusicHoard, +}; + +type MH = MusicHoard>; + +#[derive(StructOpt, Debug)] +#[structopt(about = "mh-edit: edit the MusicHoard database", + global_settings=&[AppSettings::DeriveDisplayOrder])] +struct Opt { + #[structopt( + long = "database", + name = "database file path", + default_value = "database.json" + )] + database_file_path: PathBuf, + + #[structopt(subcommand)] + category: Category, +} + +#[derive(StructOpt, Debug)] +enum Category { + #[structopt(about = "Edit artist information")] + Artist(ArtistCommand), +} + +impl Category { + fn handle(self, music_hoard: &mut MH) { + match self { + Category::Artist(artist_command) => artist_command.handle(music_hoard), + } + } +} + +#[derive(StructOpt, Debug)] +enum ArtistCommand { + #[structopt(about = "Add a new artist to the collection")] + New(ArtistValue), + #[structopt(about = "Delete an artist from the collection")] + Delete(ArtistValue), + #[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), +} + +#[derive(StructOpt, Debug)] +struct ArtistValue { + #[structopt(help = "The name of the artist")] + artist: String, +} + +#[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)")] + Clear(ArtistValue), +} + +#[derive(StructOpt, Debug)] +struct SingleUrlValue { + #[structopt(help = "The name of the artist")] + artist: String, + #[structopt(help = "The URL")] + url: String, +} + +#[derive(StructOpt, Debug)] +struct MultiUrlValue { + #[structopt(help = "The name of the artist")] + artist: String, + #[structopt(help = "The list of URLs")] + urls: 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) + }; +} + +impl ArtistCommand { + fn handle(self, music_hoard: &mut MH) { + match self { + ArtistCommand::New(artist_value) => { + music_hoard + .new_artist(ArtistId::new(artist_value.artist)) + .expect("failed to add new artist"); + } + ArtistCommand::Delete(artist_value) => { + music_hoard + .delete_artist(ArtistId::new(artist_value.artist)) + .expect("failed to delete artist"); + } + ArtistCommand::MusicBrainz(url_command) => { + single_url_command_dispatch!(url_command, music_hoard, musicbrainz) + } + 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) + } + } + } +} + +fn main() { + let opt = Opt::from_args(); + + let lib: Option = None; + let db = Some(JsonDatabase::new(JsonDatabaseFileBackend::new( + &opt.database_file_path, + ))); + + let mut music_hoard = MusicHoard::new(lib, db); + music_hoard + .load_from_database() + .expect("failed to load database"); + + opt.category.handle(&mut music_hoard); + + music_hoard + .save_to_database() + .expect("failed to save database"); +} diff --git a/src/lib.rs b/src/lib.rs index 93fe3c6..5d95931 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,13 +6,14 @@ pub mod library; use std::{ cmp::Ordering, collections::{HashMap, HashSet}, - fmt, + fmt::{self, Debug, Display}, iter::Peekable, mem, }; use database::IDatabase; use library::{ILibrary, Item, Query}; +use paste::paste; use serde::{Deserialize, Serialize}; use url::Url; use uuid::Uuid; @@ -41,7 +42,7 @@ struct InvalidUrlError { url: String, } -impl fmt::Display for InvalidUrlError { +impl Display for InvalidUrlError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "invalid url of type {:?}: {}", self.url_type, self.url) } @@ -79,6 +80,20 @@ impl MusicBrainz { } } +impl TryFrom<&str> for MusicBrainz { + type Error = Error; + + fn try_from(value: &str) -> Result { + MusicBrainz::new(value) + } +} + +impl Display for MusicBrainz { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + impl IUrl for MusicBrainz { fn url(&self) -> &str { self.0.as_str() @@ -111,6 +126,10 @@ impl MusicButler { Ok(MusicButler(url)) } + pub fn as_str(&self) -> &str { + self.0.as_str() + } + fn invalid_url_error>(url: S) -> InvalidUrlError { InvalidUrlError { url_type: UrlType::MusicButler, @@ -119,6 +138,14 @@ impl MusicButler { } } +impl TryFrom<&str> for MusicButler { + type Error = Error; + + fn try_from(value: &str) -> Result { + MusicButler::new(value) + } +} + impl IUrl for MusicButler { fn url(&self) -> &str { self.0.as_str() @@ -144,6 +171,10 @@ impl Bandcamp { Ok(Bandcamp(url)) } + pub fn as_str(&self) -> &str { + self.0.as_str() + } + fn invalid_url_error>(url: S) -> InvalidUrlError { InvalidUrlError { url_type: UrlType::Bandcamp, @@ -152,6 +183,14 @@ impl Bandcamp { } } +impl TryFrom<&str> for Bandcamp { + type Error = Error; + + fn try_from(value: &str) -> Result { + Bandcamp::new(value) + } +} + impl IUrl for Bandcamp { fn url(&self) -> &str { self.0.as_str() @@ -185,6 +224,20 @@ impl Qobuz { } } +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) + } +} + impl IUrl for Qobuz { fn url(&self) -> &str { self.0.as_str() @@ -279,6 +332,24 @@ pub struct ArtistId { pub name: String, } +impl AsRef for ArtistId { + fn as_ref(&self) -> &ArtistId { + self + } +} + +impl ArtistId { + pub fn new>(name: S) -> ArtistId { + ArtistId { name: name.into() } + } +} + +impl Display for ArtistId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name) + } +} + /// The artist properties. #[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] pub struct ArtistProperties { @@ -307,6 +378,165 @@ pub struct Artist { pub albums: Vec, } +macro_rules! artist_unique_url_dispatch { + ($field:ident) => { + paste! { + fn []>(&mut self, url: S) -> Result<(), Error> { + Self::add_unique_url(&mut self.properties.$field, url) + } + + fn []>(&mut self, url: S) -> Result<(), Error> { + Self::remove_unique_url(&mut self.properties.$field, url) + } + + fn []>(&mut self, url: S) -> Result<(), Error> { + Self::set_unique_url(&mut self.properties.$field, url) + } + + fn [](&mut self) { + Self::clear_unique_url(&mut self.properties.$field); + } + } + }; +} + +macro_rules! artist_multi_url_dispatch { + ($field:ident) => { + paste! { + fn []>(&mut self, urls: Vec) -> Result<(), Error> { + Self::add_multi_urls(&mut self.properties.$field, urls) + } + + fn []>(&mut self, urls: Vec) -> Result<(), Error> { + Self::remove_multi_urls(&mut self.properties.$field, urls) + } + + fn []>(&mut self, urls: Vec) -> Result<(), Error> { + Self::set_multi_urls(&mut self.properties.$field, urls) + } + + fn [](&mut self) { + Self::clear_multi_urls(&mut self.properties.$field); + } + } + }; +} + +impl Artist { + pub fn new>(id: ID) -> Self { + Artist { + id: id.into(), + properties: ArtistProperties::default(), + albums: vec![], + } + } + + fn add_unique_url, T: for<'a> TryFrom<&'a str, Error = Error> + Eq + Display>( + container: &mut Option, + url: S, + ) -> Result<(), Error> { + let url: T = url.as_ref().try_into()?; + + match container { + Some(current) => { + if current != &url { + return Err(Error::CollectionError(format!( + "artist already has a different URL: {}", + current + ))); + } + } + None => { + _ = container.insert(url); + } + } + + Ok(()) + } + + fn remove_unique_url, T: for<'a> TryFrom<&'a str, Error = Error> + Eq>( + container: &mut Option, + url: S, + ) -> Result<(), Error> { + let url: T = url.as_ref().try_into()?; + + if container == &Some(url) { + _ = container.take(); + } + + Ok(()) + } + + fn set_unique_url, T: for<'a> TryFrom<&'a str, Error = Error>>( + container: &mut Option, + url: S, + ) -> Result<(), Error> { + _ = container.insert(url.as_ref().try_into()?); + Ok(()) + } + + fn clear_unique_url(container: &mut Option) { + _ = container.take(); + } + + fn add_multi_urls, T: for<'a> TryFrom<&'a str, Error = Error> + Eq>( + container: &mut Vec, + urls: Vec, + ) -> Result<(), Error> { + let mut new_urls = urls + .iter() + .map(|url| url.as_ref().try_into()) + .filter(|res| { + res.as_ref() + .map(|url| !container.contains(url)) + .unwrap_or(true) // Propagate errors. + }) + .collect::, Error>>()?; + + container.append(&mut new_urls); + Ok(()) + } + + fn remove_multi_urls, T: for<'a> TryFrom<&'a str, Error = Error> + Eq>( + container: &mut Vec, + urls: Vec, + ) -> Result<(), Error> { + let urls = urls + .iter() + .map(|url| url.as_ref().try_into()) + .collect::, Error>>()?; + + container.retain(|url| !urls.contains(url)); + Ok(()) + } + + fn set_multi_urls, T: for<'a> TryFrom<&'a str, Error = Error>>( + container: &mut Vec, + urls: Vec, + ) -> Result<(), Error> { + let mut urls = urls + .iter() + .map(|url| url.as_ref().try_into()) + .collect::, Error>>()?; + + container.clear(); + container.append(&mut urls); + Ok(()) + } + + fn clear_multi_urls(container: &mut Vec) { + container.clear(); + } + + artist_unique_url_dispatch!(musicbrainz); + + artist_multi_url_dispatch!(musicbutler); + + artist_multi_url_dispatch!(bandcamp); + + artist_unique_url_dispatch!(qobuz); +} + impl PartialOrd for Artist { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -398,6 +628,8 @@ where /// Error type for `musichoard`. #[derive(Debug, PartialEq, Eq)] pub enum Error { + /// The [`MusicHoard`] is not able to read/write its in-memory collection. + CollectionError(String), /// The [`MusicHoard`] failed to read/write from/to the library. LibraryError(String), /// The [`MusicHoard`] failed to read/write from/to the database. @@ -408,9 +640,10 @@ pub enum Error { InvalidUrlError(String), } -impl fmt::Display for Error { +impl Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { + Self::CollectionError(ref s) => write!(f, "failed to read/write the collection: {s}"), Self::LibraryError(ref s) => write!(f, "failed to read/write from/to the library: {s}"), Self::DatabaseError(ref s) => { write!(f, "failed to read/write from/to the database: {s}") @@ -465,6 +698,81 @@ pub struct MusicHoard { collection: Collection, } +macro_rules! music_hoard_unique_url_dispatch { + ($field:ident) => { + paste! { + pub fn [], S: AsRef>( + &mut self, + artist_id: ID, + url: S, + ) -> Result<(), Error> { + self.get_artist_or_err(artist_id.as_ref())?.[](url) + } + + pub fn [], S: AsRef>( + &mut self, + artist_id: ID, + url: S, + ) -> Result<(), Error> { + self.get_artist_or_err(artist_id.as_ref())?.[](url) + } + + pub fn [], S: AsRef>( + &mut self, + artist_id: ID, + url: S, + ) -> Result<(), Error> { + self.get_artist_or_err(artist_id.as_ref())?.[](url) + } + + pub fn []>( + &mut self, + artist_id: ID, + ) -> Result<(), Error> { + self.get_artist_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> { + self.get_artist_or_err(artist_id.as_ref())?.[](urls) + } + + pub fn [], S: AsRef>( + &mut self, + artist_id: ID, + urls: Vec, + ) -> Result<(), Error> { + self.get_artist_or_err(artist_id.as_ref())?.[](urls) + } + + pub fn [], S: AsRef>( + &mut self, + artist_id: ID, + urls: Vec, + ) -> Result<(), Error> { + self.get_artist_or_err(artist_id.as_ref())?.[](urls) + } + + pub fn []>( + &mut self, artist_id: ID, + ) -> Result<(), Error> { + self.get_artist_or_err(artist_id.as_ref())?.[](); + Ok(()) + } + } + }; +} + impl MusicHoard { /// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`]. pub fn new(library: Option, database: Option) -> Self { @@ -475,6 +783,10 @@ impl MusicHoard { } } + pub fn get_collection(&self) -> &Collection { + &self.collection + } + pub fn rescan_library(&mut self) -> Result<(), Error> { match self.library { Some(ref mut library) => { @@ -517,10 +829,49 @@ impl MusicHoard { } } - pub fn get_collection(&self) -> &Collection { - &self.collection + pub fn new_artist>(&mut self, artist_id: ID) -> Result<(), Error> { + let artist_id: ArtistId = artist_id.into(); + if let Ok(artist) = self.get_artist_or_err(&artist_id) { + return Err(Error::CollectionError(format!( + "artist '{}' is already in the collection", + artist.id + ))); + } + + let new_artist = vec![Artist::new(artist_id)]; + + let collection = mem::take(&mut self.collection); + self.collection = Self::merge(collection, new_artist); + + Ok(()) } + pub fn delete_artist>(&mut self, artist_id: ID) -> Result<(), Error> { + let index_opt = self + .collection + .iter() + .position(|a| &a.id == artist_id.as_ref()); + + match index_opt { + Some(index) => { + self.collection.remove(index); + Ok(()) + } + None => Err(Error::CollectionError(format!( + "artist '{}' is not in the collection", + artist_id.as_ref() + ))), + } + } + + music_hoard_unique_url_dispatch!(musicbrainz); + + music_hoard_multi_url_dispatch!(musicbutler); + + music_hoard_multi_url_dispatch!(bandcamp); + + music_hoard_unique_url_dispatch!(qobuz); + fn sort(collection: &mut [Artist]) { collection.sort_unstable(); for artist in collection.iter_mut() { @@ -572,11 +923,7 @@ impl MusicHoard { .unwrap() } else { album_ids.insert(artist_id.clone(), HashSet::::new()); - artists.push(Artist { - id: artist_id.clone(), - properties: ArtistProperties::default(), - albums: vec![], - }); + artists.push(Artist::new(artist_id.clone())); artists.last_mut().unwrap() }; @@ -605,6 +952,15 @@ impl MusicHoard { artists } + + fn get_artist_or_err(&mut self, artist_id: &ArtistId) -> Result<&mut Artist, Error> { + self.collection + .iter_mut() + .find(|a| &a.id == artist_id) + .ok_or_else(|| { + Error::CollectionError(format!("artist '{}' is not in the collection", artist_id)) + }) + } } #[cfg(test)] @@ -623,6 +979,17 @@ mod tests { use super::*; + static MUSICBRAINZ: &str = + "https://musicbrainz.org/artist/d368baa8-21ca-4759-9731-0b2753071ad8"; + static MUSICBRAINZ_2: &str = + "https://musicbrainz.org/artist/823869a5-5ded-4f6b-9fb7-2a9344d83c6b"; + static MUSICBUTLER: &str = "https://www.musicbutler.io/artist-page/483340948"; + static MUSICBUTLER_2: &str = "https://www.musicbutler.io/artist-page/658903042/"; + static BANDCAMP: &str = "https://thelasthangmen.bandcamp.com/"; + static BANDCAMP_2: &str = "https://viciouscrusade.bandcamp.com/"; + static QOBUZ: &str = "https://www.qobuz.com/nl-nl/interpreter/the-last-hangmen/1244413"; + static QOBUZ_2: &str = "https://www.qobuz.com/nl-nl/interpreter/vicious-crusade/7522386"; + pub static COLLECTION: Lazy> = Lazy::new(|| collection!()); pub fn artist_to_items(artist: &Artist) -> Vec { @@ -694,30 +1061,656 @@ mod tests { #[test] fn urls() { - let musicbrainz = "https://musicbrainz.org/artist/d368baa8-21ca-4759-9731-0b2753071ad8"; - let musicbutler = "https://www.musicbutler.io/artist-page/483340948"; - let bandcamp = "https://thelasthangmen.bandcamp.com/"; - let qobuz = "https://www.qobuz.com/nl-nl/interpreter/the-last-hangmen/1244413"; + 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!(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!(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!(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()); + } - assert!(Qobuz::new(musicbrainz).is_err()); - assert!(Qobuz::new(musicbutler).is_err()); - assert!(Qobuz::new(bandcamp).is_err()); - assert!(Qobuz::new(qobuz).is_ok()); + #[test] + fn artist_new_delete() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoard::::new(None, None); + + assert!(music_hoard.new_artist(artist_id.clone()).is_ok()); + + let actual_err = music_hoard.new_artist(artist_id.clone()).unwrap_err(); + let expected_err = Error::CollectionError(String::from( + "artist 'an artist' is already in the collection", + )); + assert_eq!(actual_err, expected_err); + assert_eq!(actual_err.to_string(), expected_err.to_string()); + + let actual_err = music_hoard.delete_artist(&artist_id_2).unwrap_err(); + let expected_err = Error::CollectionError(String::from( + "artist 'another artist' is not in the collection", + )); + assert_eq!(actual_err, expected_err); + assert_eq!(actual_err.to_string(), expected_err.to_string()); + + assert!(music_hoard.delete_artist(&artist_id).is_ok()); + } + + #[test] + fn add_remove_musicbrainz_url() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoard::::new(None, None); + + assert!(music_hoard.new_artist(artist_id.clone()).is_ok()); + + let mut expected: Option = None; + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Adding incorect URL is an error. + assert!(music_hoard + .add_musicbrainz_url(&artist_id, MUSICBUTLER) + .is_err()); + assert!(music_hoard + .add_musicbrainz_url(&artist_id, BANDCAMP) + .is_err()); + assert!(music_hoard.add_musicbrainz_url(&artist_id, QOBUZ).is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Adding URL to an artist not in the collection is an error. + assert!(music_hoard + .add_musicbrainz_url(&artist_id_2, MUSICBRAINZ) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Adding URL to artist. + assert!(music_hoard + .add_musicbrainz_url(&artist_id, MUSICBRAINZ) + .is_ok()); + _ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Adding the same URL again is ok, but does not do anything. + assert!(music_hoard + .add_musicbrainz_url(&artist_id, MUSICBRAINZ) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Adding further URLs is an error. + assert!(music_hoard + .add_musicbrainz_url(&artist_id, MUSICBRAINZ_2) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Removing a URL from an artist not in the collection is an error. + assert!(music_hoard + .remove_musicbrainz_url(&artist_id_2, MUSICBRAINZ) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Removing a URL not in the collection is okay, but does not do anything. + assert!(music_hoard + .remove_musicbrainz_url(&artist_id, MUSICBRAINZ_2) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Removing a URL in the collection removes it. + assert!(music_hoard + .remove_musicbrainz_url(&artist_id, MUSICBRAINZ) + .is_ok()); + _ = expected.take(); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + assert!(music_hoard + .remove_musicbrainz_url(&artist_id, MUSICBRAINZ) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + } + + #[test] + fn set_clear_musicbrainz_url() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoard::::new(None, None); + + assert!(music_hoard.new_artist(artist_id.clone()).is_ok()); + + let mut expected: Option = None; + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Setting an incorrect URL is an error. + assert!(music_hoard + .set_musicbrainz_url(&artist_id, MUSICBUTLER) + .is_err()); + assert!(music_hoard + .set_musicbrainz_url(&artist_id, BANDCAMP) + .is_err()); + assert!(music_hoard.set_musicbrainz_url(&artist_id, QOBUZ).is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Setting a URL on an artist not in the collection is an error. + assert!(music_hoard + .set_musicbrainz_url(&artist_id_2, MUSICBRAINZ) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Setting a URL on an artist. + assert!(music_hoard + .set_musicbrainz_url(&artist_id, MUSICBRAINZ) + .is_ok()); + _ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + assert!(music_hoard + .set_musicbrainz_url(&artist_id, MUSICBRAINZ) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + assert!(music_hoard + .set_musicbrainz_url(&artist_id, MUSICBRAINZ_2) + .is_ok()); + _ = expected.insert(MusicBrainz::new(MUSICBRAINZ_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Clearing URLs on an artist that does not exist is an error. + assert!(music_hoard.clear_musicbrainz_url(&artist_id_2).is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + + // Clearing URLs. + assert!(music_hoard.clear_musicbrainz_url(&artist_id).is_ok()); + _ = expected.take(); + assert_eq!(music_hoard.collection[0].properties.musicbrainz, expected); + } + + #[test] + fn add_remove_musicbutler_urls() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoard::::new(None, None); + + assert!(music_hoard.new_artist(artist_id.clone()).is_ok()); + + let mut expected: Vec = vec![]; + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // If any URL is incorrect adding URLs is an error. + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBRAINZ, MUSICBRAINZ_2]) + .is_err()); + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) + .is_err()); + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![QOBUZ, QOBUZ_2]) + .is_err()); + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBRAINZ, MUSICBUTLER, BANDCAMP, QOBUZ]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Adding URLs to an artist not in the collection is an error. + assert!(music_hoard + .add_musicbutler_urls(&artist_id_2, vec![MUSICBUTLER]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Adding a single URL. + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) + .is_ok()); + expected.push(MusicButler::new(MUSICBUTLER).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Adding a URL that already exists is ok, but does not do anything. + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Adding another single URL. + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER_2]) + .is_ok()); + expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER_2]) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Removing URLs from an artist not in the collection is an error. + assert!(music_hoard + .remove_musicbutler_urls(&artist_id_2, vec![MUSICBUTLER]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Removing a URL. + assert!(music_hoard + .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) + .is_ok()); + expected.retain(|url| url.as_str() != MUSICBUTLER); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Removing URls that do not exist is okay, they will be ignored. + assert!(music_hoard + .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Removing a URL. + assert!(music_hoard + .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER_2]) + .is_ok()); + expected.retain(|url| url.as_str() != MUSICBUTLER_2); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + assert!(music_hoard + .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER_2]) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Adding URLs if some exist is okay, they will be ignored. + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) + .is_ok()); + expected.push(MusicButler::new(MUSICBUTLER).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .is_ok()); + expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Removing URLs if some do not exist is okay, they will be ignored. + assert!(music_hoard + .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) + .is_ok()); + expected.retain(|url| url.as_str() != MUSICBUTLER); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + assert!(music_hoard + .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .is_ok()); + expected.retain(|url| url.as_str() != MUSICBUTLER_2); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Adding mutliple URLs without clashes. + assert!(music_hoard + .add_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .is_ok()); + expected.push(MusicButler::new(MUSICBUTLER).unwrap()); + expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Removing multiple URLs without clashes. + assert!(music_hoard + .remove_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .is_ok()); + expected.clear(); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + } + + #[test] + fn set_clear_musicbutler_urls() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoard::::new(None, None); + + assert!(music_hoard.new_artist(artist_id.clone()).is_ok()); + + let mut expected: Vec = vec![]; + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // If any URL is incorrect setting URLs is an error. + assert!(music_hoard + .set_musicbutler_urls(&artist_id, vec![MUSICBRAINZ, MUSICBRAINZ_2]) + .is_err()); + assert!(music_hoard + .set_musicbutler_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) + .is_err()); + assert!(music_hoard + .set_musicbutler_urls(&artist_id, vec![QOBUZ, QOBUZ_2]) + .is_err()); + assert!(music_hoard + .set_musicbutler_urls(&artist_id, vec![MUSICBRAINZ, MUSICBUTLER, BANDCAMP, QOBUZ]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Seting URL on an artist not in the collection is an error. + assert!(music_hoard + .set_musicbutler_urls(&artist_id_2, vec![MUSICBUTLER]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Set URLs. + assert!(music_hoard + .set_musicbutler_urls(&artist_id, vec![MUSICBUTLER]) + .is_ok()); + expected.push(MusicButler::new(MUSICBUTLER).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + assert!(music_hoard + .set_musicbutler_urls(&artist_id, vec![MUSICBUTLER_2]) + .is_ok()); + expected.clear(); + expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + assert!(music_hoard + .set_musicbutler_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .is_ok()); + expected.clear(); + expected.push(MusicButler::new(MUSICBUTLER).unwrap()); + expected.push(MusicButler::new(MUSICBUTLER_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + + // Clearing URLs on an artist that does not exist is an error. + assert!(music_hoard.clear_musicbutler_urls(&artist_id_2).is_err()); + + // Clear URLs. + assert!(music_hoard.clear_musicbutler_urls(&artist_id).is_ok()); + expected.clear(); + assert_eq!(music_hoard.collection[0].properties.musicbutler, expected); + } + + #[test] + fn add_remove_bandcamp_urls() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoard::::new(None, None); + + assert!(music_hoard.new_artist(artist_id.clone()).is_ok()); + + let mut expected: Vec = vec![]; + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // If any URL is incorrect adding URLs is an error. + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![MUSICBRAINZ, MUSICBRAINZ_2]) + .is_err()); + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .is_err()); + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![QOBUZ, QOBUZ_2]) + .is_err()); + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![MUSICBRAINZ, MUSICBUTLER, BANDCAMP, QOBUZ]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Adding URLs to an artist not in the collection is an error. + assert!(music_hoard + .add_bandcamp_urls(&artist_id_2, vec![BANDCAMP]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Adding a single URL. + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![BANDCAMP]) + .is_ok()); + expected.push(Bandcamp::new(BANDCAMP).unwrap()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Adding a URL that already exists is ok, but does not do anything. + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![BANDCAMP]) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Adding another single URL. + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![BANDCAMP_2]) + .is_ok()); + expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![BANDCAMP_2]) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Removing URLs from an artist not in the collection is an error. + assert!(music_hoard + .remove_bandcamp_urls(&artist_id_2, vec![BANDCAMP]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Removing a URL. + assert!(music_hoard + .remove_bandcamp_urls(&artist_id, vec![BANDCAMP]) + .is_ok()); + expected.retain(|url| url.as_str() != BANDCAMP); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Removing URls that do not exist is okay, they will be ignored. + assert!(music_hoard + .remove_bandcamp_urls(&artist_id, vec![BANDCAMP]) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Removing a URL. + assert!(music_hoard + .remove_bandcamp_urls(&artist_id, vec![BANDCAMP_2]) + .is_ok()); + expected.retain(|url| url.as_str() != BANDCAMP_2); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + assert!(music_hoard + .remove_bandcamp_urls(&artist_id, vec![BANDCAMP_2]) + .is_ok()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Adding URLs if some exist is okay, they will be ignored. + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![BANDCAMP]) + .is_ok()); + expected.push(Bandcamp::new(BANDCAMP).unwrap()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) + .is_ok()); + expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Removing URLs if some do not exist is okay, they will be ignored. + assert!(music_hoard + .remove_bandcamp_urls(&artist_id, vec![BANDCAMP]) + .is_ok()); + expected.retain(|url| url.as_str() != BANDCAMP); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + assert!(music_hoard + .remove_bandcamp_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) + .is_ok()); + expected.retain(|url| url.as_str() != BANDCAMP_2); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Adding mutliple URLs without clashes. + assert!(music_hoard + .add_bandcamp_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) + .is_ok()); + expected.push(Bandcamp::new(BANDCAMP).unwrap()); + expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Removing multiple URLs without clashes. + assert!(music_hoard + .remove_bandcamp_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) + .is_ok()); + expected.clear(); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + } + + #[test] + fn set_clear_bandcamp_urls() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoard::::new(None, None); + + assert!(music_hoard.new_artist(artist_id.clone()).is_ok()); + + let mut expected: Vec = vec![]; + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // If any URL is incorrect setting URLs is an error. + assert!(music_hoard + .set_bandcamp_urls(&artist_id, vec![MUSICBRAINZ, MUSICBRAINZ_2]) + .is_err()); + assert!(music_hoard + .set_bandcamp_urls(&artist_id, vec![MUSICBUTLER, MUSICBUTLER_2]) + .is_err()); + assert!(music_hoard + .set_bandcamp_urls(&artist_id, vec![QOBUZ, QOBUZ_2]) + .is_err()); + assert!(music_hoard + .set_bandcamp_urls(&artist_id, vec![MUSICBRAINZ, MUSICBUTLER, BANDCAMP, QOBUZ]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Seting URL on an artist not in the collection is an error. + assert!(music_hoard + .set_bandcamp_urls(&artist_id_2, vec![BANDCAMP]) + .is_err()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Set URLs. + assert!(music_hoard + .set_bandcamp_urls(&artist_id, vec![BANDCAMP]) + .is_ok()); + expected.push(Bandcamp::new(BANDCAMP).unwrap()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + assert!(music_hoard + .set_bandcamp_urls(&artist_id, vec![BANDCAMP_2]) + .is_ok()); + expected.clear(); + expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + assert!(music_hoard + .set_bandcamp_urls(&artist_id, vec![BANDCAMP, BANDCAMP_2]) + .is_ok()); + expected.clear(); + expected.push(Bandcamp::new(BANDCAMP).unwrap()); + expected.push(Bandcamp::new(BANDCAMP_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + + // Clearing URLs on an artist that does not exist is an error. + assert!(music_hoard.clear_bandcamp_urls(&artist_id_2).is_err()); + + // Clear URLs. + assert!(music_hoard.clear_bandcamp_urls(&artist_id).is_ok()); + expected.clear(); + assert_eq!(music_hoard.collection[0].properties.bandcamp, expected); + } + + #[test] + fn add_remove_qobuz_url() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoard::::new(None, None); + + assert!(music_hoard.new_artist(artist_id.clone()).is_ok()); + + let mut expected: Option = None; + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Adding incorect URL is an error. + assert!(music_hoard.add_qobuz_url(&artist_id, MUSICBRAINZ).is_err()); + assert!(music_hoard.add_qobuz_url(&artist_id, MUSICBUTLER).is_err()); + assert!(music_hoard.add_qobuz_url(&artist_id, BANDCAMP).is_err()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Adding URL to an artist not in the collection is an error. + assert!(music_hoard.add_qobuz_url(&artist_id_2, QOBUZ).is_err()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Adding URL to artist. + assert!(music_hoard.add_qobuz_url(&artist_id, QOBUZ).is_ok()); + _ = expected.insert(Qobuz::new(QOBUZ).unwrap()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Adding the same URL again is ok, but does not do anything. + assert!(music_hoard.add_qobuz_url(&artist_id, QOBUZ).is_ok()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Adding further URLs is an error. + assert!(music_hoard.add_qobuz_url(&artist_id, QOBUZ_2).is_err()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Removing a URL from an artist not in the collection is an error. + assert!(music_hoard.remove_qobuz_url(&artist_id_2, QOBUZ).is_err()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Removing a URL not in the collection is okay, but does not do anything. + assert!(music_hoard.remove_qobuz_url(&artist_id, QOBUZ_2).is_ok()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Removing a URL in the collection removes it. + assert!(music_hoard.remove_qobuz_url(&artist_id, QOBUZ).is_ok()); + _ = expected.take(); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + assert!(music_hoard.remove_qobuz_url(&artist_id, QOBUZ).is_ok()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + } + + #[test] + fn set_clear_qobuz_url() { + let artist_id = ArtistId::new("an artist"); + let artist_id_2 = ArtistId::new("another artist"); + let mut music_hoard = MusicHoard::::new(None, None); + + assert!(music_hoard.new_artist(artist_id.clone()).is_ok()); + + let mut expected: Option = None; + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Setting an incorrect URL is an error. + assert!(music_hoard.set_qobuz_url(&artist_id, MUSICBUTLER).is_err()); + assert!(music_hoard.set_qobuz_url(&artist_id, BANDCAMP).is_err()); + assert!(music_hoard.set_qobuz_url(&artist_id, MUSICBRAINZ).is_err()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Setting a URL on an artist not in the collection is an error. + assert!(music_hoard.set_qobuz_url(&artist_id_2, QOBUZ).is_err()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Setting a URL on an artist. + assert!(music_hoard.set_qobuz_url(&artist_id, QOBUZ).is_ok()); + _ = expected.insert(Qobuz::new(QOBUZ).unwrap()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + assert!(music_hoard.set_qobuz_url(&artist_id, QOBUZ).is_ok()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + assert!(music_hoard.set_qobuz_url(&artist_id, QOBUZ_2).is_ok()); + _ = expected.insert(Qobuz::new(QOBUZ_2).unwrap()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Clearing URLs on an artist that does not exist is an error. + assert!(music_hoard.clear_qobuz_url(&artist_id_2).is_err()); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); + + // Clearing URLs. + assert!(music_hoard.clear_qobuz_url(&artist_id).is_ok()); + _ = expected.take(); + assert_eq!(music_hoard.collection[0].properties.qobuz, expected); } #[test]