diff --git a/src/bin/musichoard-edit.rs b/src/bin/musichoard-edit.rs index 1881574..3941786c 100644 --- a/src/bin/musichoard-edit.rs +++ b/src/bin/musichoard-edit.rs @@ -51,8 +51,6 @@ enum ArtistCommand { Remove, #[structopt(about = "Edit the artist's sort name")] Sort(SortCommand), - #[structopt(name = "musicbrainz", about = "Edit the MusicBrainz URL of an artist")] - MusicBrainz(MusicBrainzCommand), #[structopt(about = "Edit a property of an artist")] Property(PropertyCommand), #[structopt(about = "Modify the artist's album information")] @@ -73,20 +71,6 @@ struct SortValue { sort: String, } -#[derive(StructOpt, Debug)] -enum MusicBrainzCommand { - #[structopt(about = "Set the MusicBrainz URL overwriting any existing value")] - Set(MusicBrainzValue), - #[structopt(about = "Clear the MusicBrainz URL)")] - Clear, -} - -#[derive(StructOpt, Debug)] -struct MusicBrainzValue { - #[structopt(help = "The MusicBrainz URL")] - url: String, -} - #[derive(StructOpt, Debug)] enum PropertyCommand { #[structopt(about = "Add values to the property without overwriting existing values")] @@ -173,9 +157,6 @@ impl ArtistCommand { ArtistCommand::Sort(sort_command) => { sort_command.handle(music_hoard, artist_name); } - ArtistCommand::MusicBrainz(musicbrainz_command) => { - musicbrainz_command.handle(music_hoard, artist_name) - } ArtistCommand::Property(property_command) => { property_command.handle(music_hoard, artist_name); } @@ -190,10 +171,7 @@ impl SortCommand { fn handle(self, music_hoard: &mut MH, artist_name: &str) { match self { SortCommand::Set(artist_sort_value) => music_hoard - .set_artist_sort( - ArtistId::new(artist_name), - ArtistId::new(artist_sort_value.sort), - ) + .set_artist_sort(ArtistId::new(artist_name), artist_sort_value.sort) .expect("faild to set artist sort name"), SortCommand::Clear => music_hoard .clear_artist_sort(ArtistId::new(artist_name)) @@ -202,19 +180,6 @@ impl SortCommand { } } -impl MusicBrainzCommand { - fn handle(self, music_hoard: &mut MH, artist_name: &str) { - match self { - MusicBrainzCommand::Set(musicbrainz_value) => music_hoard - .set_artist_musicbrainz(ArtistId::new(artist_name), musicbrainz_value.url) - .expect("failed to set MusicBrainz URL"), - MusicBrainzCommand::Clear => music_hoard - .clear_artist_musicbrainz(ArtistId::new(artist_name)) - .expect("failed to clear MusicBrainz URL"), - } - } -} - impl PropertyCommand { fn handle(self, music_hoard: &mut MH, artist_name: &str) { match self { diff --git a/src/core/collection/album.rs b/src/core/collection/album.rs index 59e48a8..27c171e 100644 --- a/src/core/collection/album.rs +++ b/src/core/collection/album.rs @@ -22,6 +22,12 @@ pub struct AlbumMeta { pub id: AlbumId, pub date: AlbumDate, pub seq: AlbumSeq, + pub info: AlbumInfo, +} + +/// Album non-identifier metadata. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AlbumInfo { pub musicbrainz: MbRefOption, pub primary_type: Option, pub secondary_types: Vec, @@ -144,8 +150,9 @@ impl Album { primary_type: Option, secondary_types: Vec, ) -> Self { + let info = AlbumInfo::new(MbRefOption::None, primary_type, secondary_types); Album { - meta: AlbumMeta::new(id, date, primary_type, secondary_types), + meta: AlbumMeta::new(id, date, info), tracks: vec![], } } @@ -179,16 +186,13 @@ impl AlbumMeta { pub fn new, Date: Into>( id: Id, date: Date, - primary_type: Option, - secondary_types: Vec, + info: AlbumInfo, ) -> Self { AlbumMeta { id: id.into(), date: date.into(), seq: AlbumSeq::default(), - musicbrainz: MbRefOption::None, - primary_type, - secondary_types, + info, } } @@ -203,13 +207,29 @@ impl AlbumMeta { pub fn clear_seq(&mut self) { self.seq = AlbumSeq::default(); } +} - pub fn set_musicbrainz_ref(&mut self, mbref: MbAlbumRef) { - self.musicbrainz.replace(mbref); +impl Default for AlbumInfo { + fn default() -> Self { + AlbumInfo { + musicbrainz: MbRefOption::None, + primary_type: None, + secondary_types: Vec::new(), + } } +} - pub fn clear_musicbrainz_ref(&mut self) { - self.musicbrainz.take(); +impl AlbumInfo { + pub fn new( + musicbrainz: MbRefOption, + primary_type: Option, + secondary_types: Vec, + ) -> Self { + AlbumInfo { + musicbrainz, + primary_type, + secondary_types, + } } } @@ -230,6 +250,12 @@ impl Merge for AlbumMeta { assert_eq!(self.id, other.id); self.seq = std::cmp::max(self.seq, other.seq); + self.info.merge_in_place(other.info); + } +} + +impl Merge for AlbumInfo { + fn merge_in_place(&mut self, other: Self) { self.musicbrainz = self.musicbrainz.take().or(other.musicbrainz); self.primary_type = self.primary_type.take().or(other.primary_type); self.secondary_types.merge_in_place(other.secondary_types); @@ -354,40 +380,4 @@ mod tests { let merged = left.clone().merge(right); assert_eq!(expected, merged); } - - #[test] - fn set_clear_musicbrainz_url() { - const MUSICBRAINZ: &str = - "https://musicbrainz.org/release-group/c12897a3-af7a-3466-8892-58af84765813"; - const MUSICBRAINZ_2: &str = - "https://musicbrainz.org/release-group/0eaa9306-e6df-47be-94ce-04bfe3df782c"; - - let mut album = Album::new(AlbumId::new("an album"), AlbumDate::default(), None, vec![]); - - let mut expected: MbRefOption = MbRefOption::None; - assert_eq!(album.meta.musicbrainz, expected); - - // Setting a URL on an album. - album - .meta - .set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap()); - expected.replace(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap()); - assert_eq!(album.meta.musicbrainz, expected); - - album - .meta - .set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap()); - assert_eq!(album.meta.musicbrainz, expected); - - album - .meta - .set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ_2).unwrap()); - expected.replace(MbAlbumRef::from_url_str(MUSICBRAINZ_2).unwrap()); - assert_eq!(album.meta.musicbrainz, expected); - - // Clearing URLs. - album.meta.clear_musicbrainz_ref(); - expected.take(); - assert_eq!(album.meta.musicbrainz, expected); - } } diff --git a/src/core/collection/artist.rs b/src/core/collection/artist.rs index 2a9ff05..730ef87 100644 --- a/src/core/collection/artist.rs +++ b/src/core/collection/artist.rs @@ -21,7 +21,13 @@ pub struct Artist { #[derive(Clone, Debug, PartialEq, Eq)] pub struct ArtistMeta { pub id: ArtistId, - pub sort: Option, + pub sort: Option, + pub info: ArtistInfo, +} + +/// Artist non-identifier metadata. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ArtistInfo { pub musicbrainz: MbRefOption, pub properties: HashMap>, } @@ -75,25 +81,39 @@ impl ArtistMeta { ArtistMeta { id: id.into(), sort: None, - musicbrainz: MbRefOption::None, - properties: HashMap::new(), + info: ArtistInfo::default(), } } - pub fn get_sort_key(&self) -> (&ArtistId,) { - (self.sort.as_ref().unwrap_or(&self.id),) + pub fn get_sort_key(&self) -> (&str,) { + (self.sort.as_ref().unwrap_or(&self.id.name),) } - pub fn set_sort_key>(&mut self, sort: SORT) { + pub fn set_sort_key>(&mut self, sort: S) { self.sort = Some(sort.into()); } pub fn clear_sort_key(&mut self) { self.sort.take(); } +} - pub fn set_musicbrainz_ref(&mut self, mbref: MbArtistRef) { - self.musicbrainz.replace(mbref); +impl Default for ArtistInfo { + fn default() -> Self { + Self::new(MbRefOption::None) + } +} + +impl ArtistInfo { + pub fn new(musicbrainz: MbRefOption) -> Self { + ArtistInfo { + musicbrainz, + properties: HashMap::new(), + } + } + + pub fn set_musicbrainz_ref(&mut self, mbref: MbRefOption) { + self.musicbrainz = mbref } pub fn clear_musicbrainz_ref(&mut self) { @@ -162,6 +182,12 @@ impl Merge for ArtistMeta { assert_eq!(self.id, other.id); self.sort = self.sort.take().or(other.sort); + self.info.merge_in_place(other.info); + } +} + +impl Merge for ArtistInfo { + fn merge_in_place(&mut self, other: Self) { self.musicbrainz = self.musicbrainz.take().or(other.musicbrainz); self.properties.merge_in_place(other.properties); } @@ -207,14 +233,14 @@ mod tests { #[test] fn artist_sort_set_clear() { let artist_id = ArtistId::new("an artist"); - let sort_id_1 = ArtistId::new("sort id 1"); - let sort_id_2 = ArtistId::new("sort id 2"); + let sort_id_1 = String::from("sort id 1"); + let sort_id_2 = String::from("sort id 2"); let mut artist = Artist::new(&artist_id.name); assert_eq!(artist.meta.id, artist_id); assert_eq!(artist.meta.sort, None); - assert_eq!(artist.meta.get_sort_key(), (&artist_id,)); + assert_eq!(artist.meta.get_sort_key(), (artist_id.name.as_str(),)); assert!(artist.meta < ArtistMeta::new(sort_id_1.clone())); assert!(artist.meta < ArtistMeta::new(sort_id_2.clone())); assert!(artist < Artist::new(sort_id_1.clone())); @@ -224,7 +250,7 @@ mod tests { assert_eq!(artist.meta.id, artist_id); assert_eq!(artist.meta.sort.as_ref(), Some(&sort_id_1)); - assert_eq!(artist.meta.get_sort_key(), (&sort_id_1,)); + assert_eq!(artist.meta.get_sort_key(), (sort_id_1.as_str(),)); assert!(artist.meta > ArtistMeta::new(artist_id.clone())); assert!(artist.meta < ArtistMeta::new(sort_id_2.clone())); assert!(artist > Artist::new(artist_id.clone())); @@ -234,7 +260,7 @@ mod tests { assert_eq!(artist.meta.id, artist_id); assert_eq!(artist.meta.sort.as_ref(), Some(&sort_id_2)); - assert_eq!(artist.meta.get_sort_key(), (&sort_id_2,)); + assert_eq!(artist.meta.get_sort_key(), (sort_id_2.as_str(),)); assert!(artist.meta > ArtistMeta::new(artist_id.clone())); assert!(artist.meta > ArtistMeta::new(sort_id_1.clone())); assert!(artist > Artist::new(artist_id.clone())); @@ -244,7 +270,7 @@ mod tests { assert_eq!(artist.meta.id, artist_id); assert_eq!(artist.meta.sort, None); - assert_eq!(artist.meta.get_sort_key(), (&artist_id,)); + assert_eq!(artist.meta.get_sort_key(), (artist_id.name.as_str(),)); assert!(artist.meta < ArtistMeta::new(sort_id_1.clone())); assert!(artist.meta < ArtistMeta::new(sort_id_2.clone())); assert!(artist < Artist::new(sort_id_1.clone())); @@ -256,160 +282,132 @@ mod tests { let mut artist = Artist::new(ArtistId::new("an artist")); let mut expected: MbRefOption = MbRefOption::None; - assert_eq!(artist.meta.musicbrainz, expected); + assert_eq!(artist.meta.info.musicbrainz, expected); // Setting a URL on an artist. - artist - .meta - .set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); + artist.meta.info.set_musicbrainz_ref(MbRefOption::Some( + MbArtistRef::from_url_str(MUSICBRAINZ).unwrap(), + )); expected.replace(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); - assert_eq!(artist.meta.musicbrainz, expected); + assert_eq!(artist.meta.info.musicbrainz, expected); - artist - .meta - .set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); - assert_eq!(artist.meta.musicbrainz, expected); + artist.meta.info.set_musicbrainz_ref(MbRefOption::Some( + MbArtistRef::from_url_str(MUSICBRAINZ).unwrap(), + )); + assert_eq!(artist.meta.info.musicbrainz, expected); - artist - .meta - .set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap()); + artist.meta.info.set_musicbrainz_ref(MbRefOption::Some( + MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap(), + )); expected.replace(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap()); - assert_eq!(artist.meta.musicbrainz, expected); + assert_eq!(artist.meta.info.musicbrainz, expected); // Clearing URLs. - artist.meta.clear_musicbrainz_ref(); + artist.meta.info.clear_musicbrainz_ref(); expected.take(); - assert_eq!(artist.meta.musicbrainz, expected); + assert_eq!(artist.meta.info.musicbrainz, expected); } #[test] fn add_to_remove_from_property() { let mut artist = Artist::new(ArtistId::new("an artist")); + let info = &mut artist.meta.info; let mut expected: Vec = vec![]; - assert!(artist.meta.properties.is_empty()); + assert!(info.properties.is_empty()); // Adding a single URL. - artist - .meta - .add_to_property("MusicButler", vec![MUSICBUTLER]); + info.add_to_property("MusicButler", vec![MUSICBUTLER]); expected.push(MUSICBUTLER.to_owned()); - assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected)); + assert_eq!(info.properties.get("MusicButler"), Some(&expected)); // Adding a URL that already exists is ok, but does not do anything. - artist - .meta - .add_to_property("MusicButler", vec![MUSICBUTLER]); - assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected)); + info.add_to_property("MusicButler", vec![MUSICBUTLER]); + assert_eq!(info.properties.get("MusicButler"), Some(&expected)); // Adding another single URL. - artist - .meta - .add_to_property("MusicButler", vec![MUSICBUTLER_2]); + info.add_to_property("MusicButler", vec![MUSICBUTLER_2]); expected.push(MUSICBUTLER_2.to_owned()); - assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected)); + assert_eq!(info.properties.get("MusicButler"), Some(&expected)); - artist - .meta - .add_to_property("MusicButler", vec![MUSICBUTLER_2]); - assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected)); + info.add_to_property("MusicButler", vec![MUSICBUTLER_2]); + assert_eq!(info.properties.get("MusicButler"), Some(&expected)); // Removing a URL. - artist - .meta - .remove_from_property("MusicButler", vec![MUSICBUTLER]); + info.remove_from_property("MusicButler", vec![MUSICBUTLER]); expected.retain(|url| url != MUSICBUTLER); - assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected)); + assert_eq!(info.properties.get("MusicButler"), Some(&expected)); // Removing URls that do not exist is okay, they will be ignored. - artist - .meta - .remove_from_property("MusicButler", vec![MUSICBUTLER]); - assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected)); + info.remove_from_property("MusicButler", vec![MUSICBUTLER]); + assert_eq!(info.properties.get("MusicButler"), Some(&expected)); // Removing a URL. - artist - .meta - .remove_from_property("MusicButler", vec![MUSICBUTLER_2]); + info.remove_from_property("MusicButler", vec![MUSICBUTLER_2]); expected.retain(|url| url.as_str() != MUSICBUTLER_2); - assert!(artist.meta.properties.is_empty()); + assert!(info.properties.is_empty()); - artist - .meta - .remove_from_property("MusicButler", vec![MUSICBUTLER_2]); - assert!(artist.meta.properties.is_empty()); + info.remove_from_property("MusicButler", vec![MUSICBUTLER_2]); + assert!(info.properties.is_empty()); // Adding URLs if some exist is okay, they will be ignored. - artist - .meta - .add_to_property("MusicButler", vec![MUSICBUTLER]); + info.add_to_property("MusicButler", vec![MUSICBUTLER]); expected.push(MUSICBUTLER.to_owned()); - assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected)); + assert_eq!(info.properties.get("MusicButler"), Some(&expected)); - artist - .meta - .add_to_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); + info.add_to_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); expected.push(MUSICBUTLER_2.to_owned()); - assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected)); + assert_eq!(info.properties.get("MusicButler"), Some(&expected)); // Removing URLs if some do not exist is okay, they will be ignored. - artist - .meta - .remove_from_property("MusicButler", vec![MUSICBUTLER]); + info.remove_from_property("MusicButler", vec![MUSICBUTLER]); expected.retain(|url| url.as_str() != MUSICBUTLER); - assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected)); + assert_eq!(info.properties.get("MusicButler"), Some(&expected)); - artist - .meta - .remove_from_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); + info.remove_from_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); expected.retain(|url| url.as_str() != MUSICBUTLER_2); - assert!(artist.meta.properties.is_empty()); + assert!(info.properties.is_empty()); // Adding mutliple URLs without clashes. - artist - .meta - .add_to_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); + info.add_to_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); expected.push(MUSICBUTLER.to_owned()); expected.push(MUSICBUTLER_2.to_owned()); - assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected)); + assert_eq!(info.properties.get("MusicButler"), Some(&expected)); // Removing multiple URLs without clashes. - artist - .meta - .remove_from_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); + info.remove_from_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); expected.clear(); - assert!(artist.meta.properties.is_empty()); + assert!(info.properties.is_empty()); } #[test] fn set_clear_musicbutler_urls() { let mut artist = Artist::new(ArtistId::new("an artist")); + let info = &mut artist.meta.info; let mut expected: Vec = vec![]; - assert!(artist.meta.properties.is_empty()); + assert!(info.properties.is_empty()); // Set URLs. - artist.meta.set_property("MusicButler", vec![MUSICBUTLER]); + info.set_property("MusicButler", vec![MUSICBUTLER]); expected.push(MUSICBUTLER.to_owned()); - assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected)); + assert_eq!(info.properties.get("MusicButler"), Some(&expected)); - artist.meta.set_property("MusicButler", vec![MUSICBUTLER_2]); + info.set_property("MusicButler", vec![MUSICBUTLER_2]); expected.clear(); expected.push(MUSICBUTLER_2.to_owned()); - assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected)); + assert_eq!(info.properties.get("MusicButler"), Some(&expected)); - artist - .meta - .set_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); + info.set_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); expected.clear(); expected.push(MUSICBUTLER.to_owned()); expected.push(MUSICBUTLER_2.to_owned()); - assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected)); + assert_eq!(info.properties.get("MusicButler"), Some(&expected)); // Clear URLs. - artist.meta.clear_property("MusicButler"); + info.clear_property("MusicButler"); expected.clear(); - assert!(artist.meta.properties.is_empty()); + assert!(info.properties.is_empty()); } #[test] @@ -417,14 +415,15 @@ mod tests { let left = FULL_COLLECTION[0].to_owned(); let mut right = FULL_COLLECTION[1].to_owned(); right.meta.id = left.meta.id.clone(); - right.meta.musicbrainz = MbRefOption::None; - right.meta.properties = HashMap::new(); + right.meta.info.musicbrainz = MbRefOption::None; + right.meta.info.properties = HashMap::new(); let mut expected = left.clone(); - expected.meta.properties = expected + expected.meta.info.properties = expected .meta + .info .properties - .merge(right.clone().meta.properties); + .merge(right.clone().meta.info.properties); expected.albums.append(&mut right.albums.clone()); expected.albums.sort_unstable(); @@ -445,10 +444,11 @@ mod tests { left.albums.sort_unstable(); let mut expected = left.clone(); - expected.meta.properties = expected + expected.meta.info.properties = expected .meta + .info .properties - .merge(right.clone().meta.properties); + .merge(right.clone().meta.info.properties); expected.albums.append(&mut right.albums.clone()); expected.albums.sort_unstable(); expected.albums.dedup(); diff --git a/src/core/musichoard/base.rs b/src/core/musichoard/base.rs index 70baae9..53d089a 100644 --- a/src/core/musichoard/base.rs +++ b/src/core/musichoard/base.rs @@ -97,7 +97,7 @@ impl IMusicHoardBasePrivate for MusicHoard #[cfg(test)] mod tests { - use crate::core::{collection::artist::ArtistId, testmod::FULL_COLLECTION}; + use crate::core::testmod::FULL_COLLECTION; use super::*; @@ -190,7 +190,7 @@ mod tests { let mut right: Vec = vec![left.last().unwrap().clone()]; assert!(right.first().unwrap() > left.first().unwrap()); - let artist_sort = Some(ArtistId::new("Album_Artist 0")); + let artist_sort = Some(String::from("Album_Artist 0")); right[0].meta.sort = artist_sort.clone(); assert!(right.first().unwrap() < left.first().unwrap()); diff --git a/src/core/musichoard/database.rs b/src/core/musichoard/database.rs index 8c377c8..c384bf9 100644 --- a/src/core/musichoard/database.rs +++ b/src/core/musichoard/database.rs @@ -1,12 +1,14 @@ -use crate::core::{ - collection::{ - album::{Album, AlbumId, AlbumSeq}, - artist::{Artist, ArtistId}, - musicbrainz::MbArtistRef, - Collection, +use crate::{ + collection::{album::AlbumInfo, artist::ArtistInfo}, + core::{ + collection::{ + album::{Album, AlbumId, AlbumSeq}, + artist::{Artist, ArtistId}, + Collection, + }, + interface::database::IDatabase, + musichoard::{base::IMusicHoardBasePrivate, Error, MusicHoard, NoDatabase}, }, - interface::database::IDatabase, - musichoard::{base::IMusicHoardBasePrivate, Error, MusicHoard, NoDatabase}, }; pub trait IMusicHoardDatabase { @@ -15,20 +17,19 @@ pub trait IMusicHoardDatabase { fn add_artist>(&mut self, artist_id: IntoId) -> Result<(), Error>; fn remove_artist>(&mut self, artist_id: Id) -> Result<(), Error>; - fn set_artist_sort, IntoId: Into>( + fn set_artist_sort, S: Into>( &mut self, artist_id: Id, - artist_sort: IntoId, + artist_sort: S, ) -> Result<(), Error>; fn clear_artist_sort>(&mut self, artist_id: Id) -> Result<(), Error>; - fn set_artist_musicbrainz, S: AsRef>( + fn set_artist_info>( &mut self, artist_id: Id, - url: S, + info: ArtistInfo, ) -> Result<(), Error>; - fn clear_artist_musicbrainz>(&mut self, artist_id: Id) - -> Result<(), Error>; + fn clear_artist_info>(&mut self, artist_id: Id) -> Result<(), Error>; fn add_to_artist_property, S: AsRef + Into>( &mut self, @@ -65,6 +66,17 @@ pub trait IMusicHoardDatabase { artist_id: ArtistIdRef, album_id: AlbumIdRef, ) -> Result<(), Error>; + fn set_album_info, AlbumIdRef: AsRef>( + &mut self, + artist_id: Id, + album_id: AlbumIdRef, + info: AlbumInfo, + ) -> Result<(), Error>; + fn clear_album_info, AlbumIdRef: AsRef>( + &mut self, + artist_id: Id, + album_id: AlbumIdRef, + ) -> Result<(), Error>; } impl IMusicHoardDatabase for MusicHoard { @@ -100,10 +112,10 @@ impl IMusicHoardDatabase for MusicHoard, IntoId: Into>( + fn set_artist_sort, S: Into>( &mut self, artist_id: Id, - artist_sort: IntoId, + artist_sort: S, ) -> Result<(), Error> { self.update_artist_and( artist_id.as_ref(), @@ -120,23 +132,17 @@ impl IMusicHoardDatabase for MusicHoard, S: AsRef>( + fn set_artist_info>( &mut self, artist_id: Id, - url: S, + info: ArtistInfo, ) -> Result<(), Error> { - let mb = MbArtistRef::from_url_str(url)?; - self.update_artist(artist_id.as_ref(), |artist| { - artist.meta.set_musicbrainz_ref(mb) - }) + self.update_artist(artist_id.as_ref(), |artist| artist.meta.info = info) } - fn clear_artist_musicbrainz>( - &mut self, - artist_id: Id, - ) -> Result<(), Error> { + fn clear_artist_info>(&mut self, artist_id: Id) -> Result<(), Error> { self.update_artist(artist_id.as_ref(), |artist| { - artist.meta.clear_musicbrainz_ref() + artist.meta.info = ArtistInfo::default() }) } @@ -147,7 +153,7 @@ impl IMusicHoardDatabase for MusicHoard, ) -> Result<(), Error> { self.update_artist(artist_id.as_ref(), |artist| { - artist.meta.add_to_property(property, values) + artist.meta.info.add_to_property(property, values) }) } @@ -158,7 +164,7 @@ impl IMusicHoardDatabase for MusicHoard, ) -> Result<(), Error> { self.update_artist(artist_id.as_ref(), |artist| { - artist.meta.remove_from_property(property, values) + artist.meta.info.remove_from_property(property, values) }) } @@ -169,7 +175,7 @@ impl IMusicHoardDatabase for MusicHoard, ) -> Result<(), Error> { self.update_artist(artist_id.as_ref(), |artist| { - artist.meta.set_property(property, values) + artist.meta.info.set_property(property, values) }) } @@ -179,7 +185,7 @@ impl IMusicHoardDatabase for MusicHoard Result<(), Error> { self.update_artist(artist_id.as_ref(), |artist| { - artist.meta.clear_property(property) + artist.meta.info.clear_property(property) }) } @@ -194,7 +200,6 @@ impl IMusicHoardDatabase for MusicHoard IMusicHoardDatabase for MusicHoard, AlbumIdRef: AsRef>( + &mut self, + artist_id: Id, + album_id: AlbumIdRef, + info: AlbumInfo, + ) -> Result<(), Error> { + self.update_album(artist_id.as_ref(), album_id.as_ref(), |album| { + album.meta.info = info + }) + } + + fn clear_album_info, AlbumIdRef: AsRef>( + &mut self, + artist_id: Id, + album_id: AlbumIdRef, + ) -> Result<(), Error> { + self.update_album(artist_id.as_ref(), album_id.as_ref(), |album| { + album.meta.info = AlbumInfo::default() + }) + } } pub trait IMusicHoardDatabasePrivate { @@ -272,24 +297,34 @@ impl MusicHoard { self.update_artist_and(artist_id, fn_artist, |_| {}) } - fn update_album_and( + fn update_album_and( &mut self, artist_id: &ArtistId, album_id: &AlbumId, fn_album: FnAlbum, fn_artist: FnArtist, - fn_coll: FnColl, ) -> Result<(), Error> where FnAlbum: FnOnce(&mut Album), FnArtist: FnOnce(&mut Artist), - FnColl: FnOnce(&mut Collection), { let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id)?; let album = Self::get_album_mut_or_err(artist, album_id)?; fn_album(album); fn_artist(artist); - self.update_collection(fn_coll) + self.update_collection(|_| {}) + } + + fn update_album( + &mut self, + artist_id: &ArtistId, + album_id: &AlbumId, + fn_album: FnAlbum, + ) -> Result<(), Error> + where + FnAlbum: FnOnce(&mut Album), + { + self.update_album_and(artist_id, album_id, fn_album, |_| {}) } } @@ -298,7 +333,10 @@ mod tests { use mockall::{predicate, Sequence}; use crate::{ - collection::musicbrainz::MbRefOption, + collection::{ + album::{AlbumPrimaryType, AlbumSecondaryType}, + musicbrainz::{MbArtistRef, MbRefOption}, + }, core::{ collection::{album::AlbumDate, artist::ArtistId}, interface::database::{self, MockIDatabase}, @@ -309,8 +347,7 @@ mod tests { use super::*; - static MUSICBRAINZ: &str = - "https://musicbrainz.org/artist/d368baa8-21ca-4759-9731-0b2753071ad8"; + static MBID: &str = "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/"; @@ -370,12 +407,12 @@ mod tests { let mut music_hoard: MH = MusicHoard::database(database).unwrap(); let artist_1_id = ArtistId::new("the artist"); - let artist_1_sort = ArtistId::new("artist, the"); + let artist_1_sort = String::from("artist, the"); // Must be after "artist, the", but before "the artist" let artist_2_id = ArtistId::new("b-artist"); - assert!(artist_1_sort < artist_2_id); + assert!(artist_1_sort < artist_2_id.name); assert!(artist_2_id < artist_1_id); assert!(music_hoard.add_artist(artist_1_id.clone()).is_ok()); @@ -416,24 +453,20 @@ mod tests { fn collection_error() { let mut database = MockIDatabase::new(); database.expect_load().times(1).returning(|| Ok(vec![])); - database.expect_save().times(1).returning(|_| Ok(())); + let mut music_hoard = MusicHoard::database(database).unwrap(); let artist_id = ArtistId::new("an artist"); - let mut music_hoard = MusicHoard::database(database).unwrap(); - assert!(music_hoard.add_artist(artist_id.clone()).is_ok()); - let actual_err = music_hoard - .set_artist_musicbrainz(&artist_id, MUSICBUTLER) + .set_artist_info(&artist_id, ArtistInfo::default()) .unwrap_err(); - let expected_err = Error::CollectionError(format!( - "an error occurred when processing a URL: invalid artist MusicBrainz URL: {MUSICBUTLER}" - )); + let expected_err = + Error::CollectionError(format!("artist '{artist_id}' is not in the collection")); assert_eq!(actual_err, expected_err); assert_eq!(actual_err.to_string(), expected_err.to_string()); } #[test] - fn set_clear_musicbrainz_url() { + fn set_clear_artist_info() { let mut database = MockIDatabase::new(); database.expect_load().times(1).returning(|| Ok(vec![])); database.expect_save().times(3).returning(|_| Ok(())); @@ -445,29 +478,31 @@ mod tests { assert!(music_hoard.add_artist(artist_id.clone()).is_ok()); let mut expected: MbRefOption = MbRefOption::None; - assert_eq!(music_hoard.collection[0].meta.musicbrainz, expected); + assert_eq!(music_hoard.collection[0].meta.info.musicbrainz, expected); + + let info = ArtistInfo::new(MbRefOption::Some(MbArtistRef::from_uuid_str(MBID).unwrap())); // Setting a URL on an artist not in the collection is an error. assert!(music_hoard - .set_artist_musicbrainz(&artist_id_2, MUSICBRAINZ) + .set_artist_info(&artist_id_2, info.clone()) .is_err()); - assert_eq!(music_hoard.collection[0].meta.musicbrainz, expected); + assert_eq!(music_hoard.collection[0].meta.info.musicbrainz, expected); // Setting a URL on an artist. assert!(music_hoard - .set_artist_musicbrainz(&artist_id, MUSICBRAINZ) + .set_artist_info(&artist_id, info.clone()) .is_ok()); - expected.replace(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); - assert_eq!(music_hoard.collection[0].meta.musicbrainz, expected); + expected.replace(MbArtistRef::from_uuid_str(MBID).unwrap()); + assert_eq!(music_hoard.collection[0].meta.info.musicbrainz, expected); // Clearing URLs on an artist that does not exist is an error. - assert!(music_hoard.clear_artist_musicbrainz(&artist_id_2).is_err()); - assert_eq!(music_hoard.collection[0].meta.musicbrainz, expected); + assert!(music_hoard.clear_artist_info(&artist_id_2).is_err()); + assert_eq!(music_hoard.collection[0].meta.info.musicbrainz, expected); // Clearing URLs. - assert!(music_hoard.clear_artist_musicbrainz(&artist_id).is_ok()); + assert!(music_hoard.clear_artist_info(&artist_id).is_ok()); expected.take(); - assert_eq!(music_hoard.collection[0].meta.musicbrainz, expected); + assert_eq!(music_hoard.collection[0].meta.info.musicbrainz, expected); } #[test] @@ -483,13 +518,13 @@ mod tests { assert!(music_hoard.add_artist(artist_id.clone()).is_ok()); let mut expected: Vec = vec![]; - assert!(music_hoard.collection[0].meta.properties.is_empty()); + assert!(music_hoard.collection[0].meta.info.properties.is_empty()); // Adding URLs to an artist not in the collection is an error. assert!(music_hoard .add_to_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .is_err()); - assert!(music_hoard.collection[0].meta.properties.is_empty()); + assert!(music_hoard.collection[0].meta.info.properties.is_empty()); // Adding mutliple URLs without clashes. assert!(music_hoard @@ -497,19 +532,15 @@ mod tests { .is_ok()); expected.push(MUSICBUTLER.to_owned()); expected.push(MUSICBUTLER_2.to_owned()); - assert_eq!( - music_hoard.collection[0].meta.properties.get("MusicButler"), - Some(&expected) - ); + let info = &music_hoard.collection[0].meta.info; + assert_eq!(info.properties.get("MusicButler"), Some(&expected)); // Removing URLs from an artist not in the collection is an error. assert!(music_hoard .remove_from_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .is_err()); - assert_eq!( - music_hoard.collection[0].meta.properties.get("MusicButler"), - Some(&expected) - ); + let info = &music_hoard.collection[0].meta.info; + assert_eq!(info.properties.get("MusicButler"), Some(&expected)); // Removing multiple URLs without clashes. assert!(music_hoard @@ -520,7 +551,7 @@ mod tests { ) .is_ok()); expected.clear(); - assert!(music_hoard.collection[0].meta.properties.is_empty()); + assert!(music_hoard.collection[0].meta.info.properties.is_empty()); } #[test] @@ -536,13 +567,13 @@ mod tests { assert!(music_hoard.add_artist(artist_id.clone()).is_ok()); let mut expected: Vec = vec![]; - assert!(music_hoard.collection[0].meta.properties.is_empty()); + assert!(music_hoard.collection[0].meta.info.properties.is_empty()); // Seting URL on an artist not in the collection is an error. assert!(music_hoard .set_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .is_err()); - assert!(music_hoard.collection[0].meta.properties.is_empty()); + assert!(music_hoard.collection[0].meta.info.properties.is_empty()); // Set URLs. assert!(music_hoard @@ -551,10 +582,8 @@ mod tests { expected.clear(); expected.push(MUSICBUTLER.to_owned()); expected.push(MUSICBUTLER_2.to_owned()); - assert_eq!( - music_hoard.collection[0].meta.properties.get("MusicButler"), - Some(&expected) - ); + let info = &music_hoard.collection[0].meta.info; + assert_eq!(info.properties.get("MusicButler"), Some(&expected)); // Clearing URLs on an artist that does not exist is an error. assert!(music_hoard @@ -566,7 +595,7 @@ mod tests { .clear_artist_property(&artist_id, "MusicButler") .is_ok()); expected.clear(); - assert!(music_hoard.collection[0].meta.properties.is_empty()); + assert!(music_hoard.collection[0].meta.info.properties.is_empty()); } #[test] @@ -614,6 +643,65 @@ mod tests { assert_eq!(music_hoard.collection[0].albums[0].meta.seq, AlbumSeq(0)); } + #[test] + fn set_clear_album_info() { + let mut database = MockIDatabase::new(); + + let artist_id = ArtistId::new("an artist"); + let album_id = AlbumId::new("an album"); + let album_id_2 = AlbumId::new("another album"); + + let mut database_result = vec![Artist::new(artist_id.clone())]; + database_result[0].albums.push(Album::new( + album_id.clone(), + AlbumDate::default(), + None, + vec![], + )); + + database + .expect_load() + .times(1) + .return_once(|| Ok(database_result)); + database.expect_save().times(2).returning(|_| Ok(())); + let mut music_hoard = MusicHoard::database(database).unwrap(); + + let meta = &music_hoard.collection[0].albums[0].meta; + assert_eq!(meta.info.musicbrainz, MbRefOption::None); + assert_eq!(meta.info.primary_type, None); + assert_eq!(meta.info.secondary_types, Vec::new()); + + let info = AlbumInfo::new( + MbRefOption::CannotHaveMbid, + Some(AlbumPrimaryType::Album), + vec![AlbumSecondaryType::Live], + ); + + // Seting info on an album not belonging to the artist is an error. + assert!(music_hoard + .set_album_info(&artist_id, &album_id_2, info.clone()) + .is_err()); + let meta = &music_hoard.collection[0].albums[0].meta; + assert_eq!(meta.info, AlbumInfo::default()); + + // Set info. + assert!(music_hoard + .set_album_info(&artist_id, &album_id, info.clone()) + .is_ok()); + let meta = &music_hoard.collection[0].albums[0].meta; + assert_eq!(meta.info, info); + + // Clearing info on an album that does not exist is an error. + assert!(music_hoard + .clear_album_info(&artist_id, &album_id_2) + .is_err()); + + // Clear info. + assert!(music_hoard.clear_album_info(&artist_id, &album_id).is_ok()); + let meta = &music_hoard.collection[0].albums[0].meta; + assert_eq!(meta.info, AlbumInfo::default()); + } + #[test] fn load_database() { let mut database = MockIDatabase::new(); diff --git a/src/core/musichoard/library.rs b/src/core/musichoard/library.rs index 47e9357..36b5f1e 100644 --- a/src/core/musichoard/library.rs +++ b/src/core/musichoard/library.rs @@ -52,7 +52,7 @@ impl MusicHoard { name: item.album_artist, }; - let artist_sort = item.album_artist_sort.map(|s| ArtistId { name: s }); + let artist_sort = item.album_artist_sort; let album_id = AlbumId { title: item.album_title, diff --git a/src/core/musichoard/mod.rs b/src/core/musichoard/mod.rs index c6a3999..435ec7d 100644 --- a/src/core/musichoard/mod.rs +++ b/src/core/musichoard/mod.rs @@ -20,12 +20,9 @@ use crate::core::collection::{ Collection, }; -use crate::core::{ - collection, - interface::{ - database::{LoadError as DatabaseLoadError, SaveError as DatabaseSaveError}, - library::Error as LibraryError, - }, +use crate::core::interface::{ + database::{LoadError as DatabaseLoadError, SaveError as DatabaseSaveError}, + library::Error as LibraryError, }; /// The Music Hoard. It is responsible for pulling information from both the library and the @@ -79,12 +76,6 @@ impl Display for Error { } } -impl From for Error { - fn from(err: collection::Error) -> Self { - Error::CollectionError(err.to_string()) - } -} - impl From for Error { fn from(err: LibraryError) -> Error { Error::LibraryError(err.to_string()) diff --git a/src/core/testmod.rs b/src/core/testmod.rs index b7f1ca0..32dab21 100644 --- a/src/core/testmod.rs +++ b/src/core/testmod.rs @@ -2,8 +2,8 @@ use once_cell::sync::Lazy; use std::collections::HashMap; use crate::core::collection::{ - album::{Album, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSeq}, - artist::{Artist, ArtistId, ArtistMeta}, + album::{Album, AlbumId, AlbumInfo, AlbumMeta, AlbumPrimaryType, AlbumSeq}, + artist::{Artist, ArtistId, ArtistInfo, ArtistMeta}, musicbrainz::{MbAlbumRef, MbArtistRef, MbRefOption}, track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, }; diff --git a/src/external/database/serde/deserialize.rs b/src/external/database/serde/deserialize.rs index 7dfd676..acfed8a 100644 --- a/src/external/database/serde/deserialize.rs +++ b/src/external/database/serde/deserialize.rs @@ -4,8 +4,8 @@ use serde::{de::Visitor, Deserialize, Deserializer}; use crate::{ collection::{ - album::AlbumMeta, - artist::ArtistMeta, + album::{AlbumInfo, AlbumMeta}, + artist::{ArtistInfo, ArtistMeta}, musicbrainz::{MbAlbumRef, MbArtistRef, MbRefOption, Mbid}, }, core::collection::{ @@ -114,9 +114,11 @@ impl From for Artist { Artist { meta: ArtistMeta { id: ArtistId::new(artist.name), - sort: artist.sort.map(ArtistId::new), - musicbrainz: artist.musicbrainz.into(), - properties: artist.properties, + sort: artist.sort, + info: ArtistInfo { + musicbrainz: artist.musicbrainz.into(), + properties: artist.properties, + }, }, albums: artist.albums.into_iter().map(Into::into).collect(), } @@ -130,9 +132,11 @@ impl From for Album { id: AlbumId { title: album.title }, date: AlbumDate::default(), seq: AlbumSeq(album.seq), - musicbrainz: album.musicbrainz.into(), - primary_type: album.primary_type.map(Into::into), - secondary_types: album.secondary_types.into_iter().map(Into::into).collect(), + info: AlbumInfo { + musicbrainz: album.musicbrainz.into(), + primary_type: album.primary_type.map(Into::into), + secondary_types: album.secondary_types.into_iter().map(Into::into).collect(), + }, }, tracks: vec![], } diff --git a/src/external/database/serde/serialize.rs b/src/external/database/serde/serialize.rs index 33f3b1d..d7e8762 100644 --- a/src/external/database/serde/serialize.rs +++ b/src/external/database/serde/serialize.rs @@ -72,10 +72,11 @@ impl<'a> From<&'a Artist> for SerializeArtist<'a> { fn from(artist: &'a Artist) -> Self { SerializeArtist { name: &artist.meta.id.name, - sort: artist.meta.sort.as_ref().map(|id| id.name.as_ref()), - musicbrainz: (&artist.meta.musicbrainz).into(), + sort: artist.meta.sort.as_deref(), + musicbrainz: (&artist.meta.info.musicbrainz).into(), properties: artist .meta + .info .properties .iter() .map(|(k, v)| (k.as_ref(), v)) @@ -90,10 +91,11 @@ impl<'a> From<&'a Album> for SerializeAlbum<'a> { SerializeAlbum { title: &album.meta.id.title, seq: album.meta.seq.0, - musicbrainz: (&album.meta.musicbrainz).into(), - primary_type: album.meta.primary_type.map(Into::into), + musicbrainz: (&album.meta.info.musicbrainz).into(), + primary_type: album.meta.info.primary_type.map(Into::into), secondary_types: album .meta + .info .secondary_types .iter() .copied() diff --git a/src/external/musicbrainz/api/mod.rs b/src/external/musicbrainz/api/mod.rs index 2bb9f43..70e9299 100644 --- a/src/external/musicbrainz/api/mod.rs +++ b/src/external/musicbrainz/api/mod.rs @@ -4,8 +4,7 @@ use serde::{de::Visitor, Deserialize, Deserializer}; use crate::{ collection::{ - album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType}, - artist::ArtistId, + album::{AlbumDate, AlbumPrimaryType, AlbumSecondaryType}, musicbrainz::Mbid, Error as CollectionError, }, @@ -62,8 +61,8 @@ impl MusicBrainzClient { #[derive(Clone, Debug, PartialEq, Eq)] pub struct MbArtistMeta { pub id: Mbid, - pub name: ArtistId, - pub sort_name: ArtistId, + pub name: String, + pub sort_name: String, pub disambiguation: Option, } @@ -80,8 +79,8 @@ impl From for MbArtistMeta { fn from(value: SerdeMbArtistMeta) -> Self { MbArtistMeta { id: value.id.into(), - name: value.name.into(), - sort_name: value.sort_name.into(), + name: value.name, + sort_name: value.sort_name, disambiguation: value.disambiguation, } } @@ -90,7 +89,7 @@ impl From for MbArtistMeta { #[derive(Clone, Debug, PartialEq, Eq)] pub struct MbReleaseGroupMeta { pub id: Mbid, - pub title: AlbumId, + pub title: String, pub first_release_date: AlbumDate, pub primary_type: AlbumPrimaryType, pub secondary_types: Option>, @@ -110,7 +109,7 @@ impl From for MbReleaseGroupMeta { fn from(value: SerdeMbReleaseGroupMeta) -> Self { MbReleaseGroupMeta { id: value.id.into(), - title: value.title.into(), + title: value.title, first_release_date: value.first_release_date.into(), primary_type: value.primary_type.into(), secondary_types: value diff --git a/src/testmod/full.rs b/src/testmod/full.rs index 8e346aa..ea0f159 100644 --- a/src/testmod/full.rs +++ b/src/testmod/full.rs @@ -7,19 +7,21 @@ macro_rules! full_collection { name: "Album_Artist ‘A’".to_string(), }, sort: None, - musicbrainz: MbRefOption::Some(MbArtistRef::from_url_str( - "https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000" - ).unwrap()), - properties: HashMap::from([ - (String::from("MusicButler"), vec![ - String::from("https://www.musicbutler.io/artist-page/000000000"), + info: ArtistInfo { + musicbrainz: MbRefOption::Some(MbArtistRef::from_url_str( + "https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000" + ).unwrap()), + 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", + ) + ]), ]), - (String::from("Qobuz"), vec![ - String::from( - "https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums", - ) - ]), - ]), + }, }, albums: vec![ Album { @@ -29,11 +31,13 @@ macro_rules! full_collection { }, date: 1998.into(), seq: AlbumSeq(1), - musicbrainz: MbRefOption::Some(MbAlbumRef::from_url_str( - "https://musicbrainz.org/release-group/00000000-0000-0000-0000-000000000000" - ).unwrap()), - primary_type: Some(AlbumPrimaryType::Album), - secondary_types: vec![], + info: AlbumInfo { + musicbrainz: MbRefOption::Some(MbAlbumRef::from_url_str( + "https://musicbrainz.org/release-group/00000000-0000-0000-0000-000000000000" + ).unwrap()), + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + }, }, tracks: vec![ Track { @@ -92,9 +96,11 @@ macro_rules! full_collection { }, date: (2015, 4).into(), seq: AlbumSeq(1), - musicbrainz: MbRefOption::None, - primary_type: Some(AlbumPrimaryType::Album), - secondary_types: vec![], + info: AlbumInfo { + musicbrainz: MbRefOption::None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + }, }, tracks: vec![ Track { @@ -129,23 +135,25 @@ macro_rules! full_collection { name: "Album_Artist ‘B’".to_string(), }, sort: None, - musicbrainz: MbRefOption::Some(MbArtistRef::from_url_str( - "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111" - ).unwrap()), - 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"), + info: ArtistInfo { + musicbrainz: MbRefOption::Some(MbArtistRef::from_url_str( + "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111" + ).unwrap()), + 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", + ) + ]), ]), - (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", - ) - ]), - ]), + }, }, albums: vec![ Album { @@ -155,9 +163,11 @@ macro_rules! full_collection { }, date: (2003, 6, 6).into(), seq: AlbumSeq(1), - musicbrainz: MbRefOption::None, - primary_type: Some(AlbumPrimaryType::Album), - secondary_types: vec![], + info: AlbumInfo { + musicbrainz: MbRefOption::None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + }, }, tracks: vec![ Track { @@ -194,11 +204,13 @@ macro_rules! full_collection { }, date: 2008.into(), seq: AlbumSeq(3), - musicbrainz: MbRefOption::Some(MbAlbumRef::from_url_str( - "https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111111" - ).unwrap()), - primary_type: Some(AlbumPrimaryType::Album), - secondary_types: vec![], + info: AlbumInfo { + musicbrainz: MbRefOption::Some(MbAlbumRef::from_url_str( + "https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111111" + ).unwrap()), + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + }, }, tracks: vec![ Track { @@ -235,11 +247,13 @@ macro_rules! full_collection { }, date: 2009.into(), seq: AlbumSeq(2), - musicbrainz: MbRefOption::Some(MbAlbumRef::from_url_str( - "https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111112" - ).unwrap()), - primary_type: Some(AlbumPrimaryType::Album), - secondary_types: vec![], + info: AlbumInfo { + musicbrainz: MbRefOption::Some(MbAlbumRef::from_url_str( + "https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111112" + ).unwrap()), + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + }, }, tracks: vec![ Track { @@ -276,9 +290,11 @@ macro_rules! full_collection { }, date: 2015.into(), seq: AlbumSeq(4), - musicbrainz: MbRefOption::None, - primary_type: Some(AlbumPrimaryType::Album), - secondary_types: vec![], + info: AlbumInfo { + musicbrainz: MbRefOption::None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + }, }, tracks: vec![ Track { @@ -315,11 +331,11 @@ macro_rules! full_collection { id: ArtistId { name: "The Album_Artist ‘C’".to_string(), }, - sort: Some(ArtistId { - name: "Album_Artist ‘C’, The".to_string(), - }), - musicbrainz: MbRefOption::CannotHaveMbid, - properties: HashMap::new(), + sort: Some("Album_Artist ‘C’, The".to_string()), + info: ArtistInfo { + musicbrainz: MbRefOption::CannotHaveMbid, + properties: HashMap::new(), + }, }, albums: vec![ Album { @@ -329,9 +345,11 @@ macro_rules! full_collection { }, date: 1985.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: Some(AlbumPrimaryType::Album), - secondary_types: vec![], + info: AlbumInfo { + musicbrainz: MbRefOption::None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + }, }, tracks: vec![ Track { @@ -368,9 +386,11 @@ macro_rules! full_collection { }, date: 2018.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: Some(AlbumPrimaryType::Album), - secondary_types: vec![], + info: AlbumInfo { + musicbrainz: MbRefOption::None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + }, }, tracks: vec![ Track { @@ -408,8 +428,10 @@ macro_rules! full_collection { name: "Album_Artist ‘D’".to_string(), }, sort: None, - musicbrainz: MbRefOption::None, - properties: HashMap::new(), + info: ArtistInfo { + musicbrainz: MbRefOption::None, + properties: HashMap::new(), + }, }, albums: vec![ Album { @@ -419,9 +441,11 @@ macro_rules! full_collection { }, date: 1995.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: Some(AlbumPrimaryType::Album), - secondary_types: vec![], + info: AlbumInfo { + musicbrainz: MbRefOption::None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + }, }, tracks: vec![ Track { @@ -458,9 +482,11 @@ macro_rules! full_collection { }, date: 2028.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: Some(AlbumPrimaryType::Album), - secondary_types: vec![], + info: AlbumInfo { + musicbrainz: MbRefOption::None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + }, }, tracks: vec![ Track { diff --git a/src/testmod/library.rs b/src/testmod/library.rs index 003a14a..1024f23 100644 --- a/src/testmod/library.rs +++ b/src/testmod/library.rs @@ -8,8 +8,10 @@ macro_rules! library_collection { name: "Album_Artist ‘A’".to_string(), }, sort: None, - musicbrainz: MbRefOption::None, - properties: HashMap::new(), + info: ArtistInfo { + musicbrainz: MbRefOption::None, + properties: HashMap::new(), + }, }, albums: vec![ Album { @@ -19,9 +21,7 @@ macro_rules! library_collection { }, date: 1998.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: None, - secondary_types: vec![], + info: AlbumInfo::default(), }, tracks: vec![ Track { @@ -80,9 +80,7 @@ macro_rules! library_collection { }, date: (2015, 4).into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: None, - secondary_types: vec![], + info: AlbumInfo::default(), }, tracks: vec![ Track { @@ -117,8 +115,10 @@ macro_rules! library_collection { name: "Album_Artist ‘B’".to_string(), }, sort: None, - musicbrainz: MbRefOption::None, - properties: HashMap::new(), + info: ArtistInfo { + musicbrainz: MbRefOption::None, + properties: HashMap::new(), + }, }, albums: vec![ Album { @@ -128,9 +128,7 @@ macro_rules! library_collection { }, date: (2003, 6, 6).into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: None, - secondary_types: vec![], + info: AlbumInfo::default(), }, tracks: vec![ Track { @@ -167,9 +165,7 @@ macro_rules! library_collection { }, date: 2008.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: None, - secondary_types: vec![], + info: AlbumInfo::default(), }, tracks: vec![ Track { @@ -206,9 +202,7 @@ macro_rules! library_collection { }, date: 2009.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: None, - secondary_types: vec![], + info: AlbumInfo::default(), }, tracks: vec![ Track { @@ -245,9 +239,7 @@ macro_rules! library_collection { }, date: 2015.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: None, - secondary_types: vec![], + info: AlbumInfo::default(), }, tracks: vec![ Track { @@ -284,11 +276,11 @@ macro_rules! library_collection { id: ArtistId { name: "The Album_Artist ‘C’".to_string(), }, - sort: Some(ArtistId { - name: "Album_Artist ‘C’, The".to_string(), - }), - musicbrainz: MbRefOption::None, - properties: HashMap::new(), + sort: Some("Album_Artist ‘C’, The".to_string()), + info: ArtistInfo { + musicbrainz: MbRefOption::None, + properties: HashMap::new(), + }, }, albums: vec![ Album { @@ -298,9 +290,7 @@ macro_rules! library_collection { }, date: 1985.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: None, - secondary_types: vec![], + info: AlbumInfo::default(), }, tracks: vec![ Track { @@ -337,9 +327,7 @@ macro_rules! library_collection { }, date: 2018.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: None, - secondary_types: vec![], + info: AlbumInfo::default(), }, tracks: vec![ Track { @@ -377,8 +365,10 @@ macro_rules! library_collection { name: "Album_Artist ‘D’".to_string(), }, sort: None, - musicbrainz: MbRefOption::None, - properties: HashMap::new(), + info: ArtistInfo { + musicbrainz: MbRefOption::None, + properties: HashMap::new(), + }, }, albums: vec![ Album { @@ -388,9 +378,7 @@ macro_rules! library_collection { }, date: 1995.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: None, - secondary_types: vec![], + info: AlbumInfo::default(), }, tracks: vec![ Track { @@ -427,9 +415,7 @@ macro_rules! library_collection { }, date: 2028.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: None, - secondary_types: vec![], + info: AlbumInfo::default(), }, tracks: vec![ Track { diff --git a/src/tui/app/machine/browse_state.rs b/src/tui/app/machine/browse_state.rs index 7f26314..a215740 100644 --- a/src/tui/app/machine/browse_state.rs +++ b/src/tui/app/machine/browse_state.rs @@ -73,7 +73,7 @@ impl IAppInteractBrowse for AppMachine { } fn fetch_musicbrainz(self) -> Self::APP { - AppMachine::app_fetch_new(self.inner) + AppMachine::app_fetch_first(self.inner) } } diff --git a/src/tui/app/machine/fetch_state.rs b/src/tui/app/machine/fetch_state.rs index cf53244..a8efb7a 100644 --- a/src/tui/app/machine/fetch_state.rs +++ b/src/tui/app/machine/fetch_state.rs @@ -4,9 +4,9 @@ use std::{ }; use musichoard::collection::{ - album::AlbumMeta, - artist::{Artist, ArtistMeta}, - musicbrainz::{IMusicBrainzRef, MbRefOption, Mbid}, + album::{Album, AlbumMeta}, + artist::{Artist, ArtistId, ArtistMeta}, + musicbrainz::{IMusicBrainzRef, MbArtistRef, MbRefOption, Mbid}, }; use crate::tui::{ @@ -46,12 +46,27 @@ impl FetchState { } } +enum FetchError { + NothingToFetch, + SubmitError(DaemonError), +} + +impl From for FetchError { + fn from(value: DaemonError) -> Self { + FetchError::SubmitError(value) + } +} + impl AppMachine { fn fetch_state(inner: AppInner, state: FetchState) -> Self { AppMachine::new(inner, state) } - pub fn app_fetch_new(inner: AppInner) -> App { + pub fn app_fetch_first(inner: AppInner) -> App { + Self::app_fetch_new(inner, true) + } + + fn app_fetch_new(inner: AppInner, first: bool) -> App { let coll = inner.music_hoard.get_collection(); let artist = match inner.selection.state_artist(coll) { Some(artist_state) => &coll[artist_state.index], @@ -61,16 +76,38 @@ impl AppMachine { }; let (fetch_tx, fetch_rx) = mpsc::channel::(); - if let Err(err) = Self::submit_fetch_job(&*inner.musicbrainz, fetch_tx, artist) { - return AppMachine::error_state(inner, err.to_string()).into(); - } - let fetch = FetchState::new(fetch_rx); - AppMachine::app_fetch(inner, fetch, true) + match Self::submit_fetch_job(&*inner.musicbrainz, fetch_tx, artist) { + Ok(()) => AppMachine::fetch_state(inner, fetch).into(), + Err(FetchError::NothingToFetch) => { + if first { + AppMachine::match_state(inner, MatchState::new(None, fetch)).into() + } else { + AppMachine::browse_state(inner).into() + } + } + Err(FetchError::SubmitError(daemon_err)) => { + AppMachine::error_state(inner, daemon_err.to_string()).into() + } + } } - pub fn app_fetch_next(inner: AppInner, fetch: FetchState) -> App { - Self::app_fetch(inner, fetch, false) + pub fn app_fetch_next(inner: AppInner, mut fetch: FetchState) -> App { + match fetch.try_recv() { + Ok(fetch_result) => match fetch_result { + Ok(next_match) => { + let current = Some(next_match); + AppMachine::match_state(inner, MatchState::new(current, fetch)).into() + } + Err(fetch_err) => { + AppMachine::error_state(inner, format!("fetch failed: {fetch_err}")).into() + } + }, + Err(recv_err) => match recv_err { + TryRecvError::Empty => AppMachine::fetch_state(inner, fetch).into(), + TryRecvError::Disconnected => Self::app_fetch_new(inner, false), + }, + } } pub fn app_lookup_artist( @@ -86,10 +123,13 @@ impl AppMachine { pub fn app_lookup_album( inner: AppInner, fetch: FetchState, + artist_id: &ArtistId, album: &AlbumMeta, mbid: Mbid, ) -> App { - let f = Self::submit_lookup_release_group_job; + let f = |mb: &dyn IMbJobSender, rs, album, mbid| { + Self::submit_lookup_release_group_job(mb, rs, artist_id, album, mbid) + }; Self::app_lookup(f, inner, fetch, album, mbid) } @@ -111,48 +151,41 @@ impl AppMachine { Self::app_fetch_next(inner, fetch) } - fn app_fetch(inner: AppInner, mut fetch: FetchState, first: bool) -> App { - match fetch.try_recv() { - Ok(fetch_result) => match fetch_result { - Ok(next_match) => { - let current = Some(next_match); - AppMachine::match_state(inner, MatchState::new(current, fetch)).into() - } - Err(fetch_err) => { - AppMachine::error_state(inner, format!("fetch failed: {fetch_err}")).into() - } - }, - Err(recv_err) => match recv_err { - TryRecvError::Empty => AppMachine::fetch_state(inner, fetch).into(), - TryRecvError::Disconnected => { - if first { - AppMachine::match_state(inner, MatchState::new(None, fetch)).into() - } else { - AppMachine::browse_state(inner).into() - } - } - }, - } - } - fn submit_fetch_job( musicbrainz: &dyn IMbJobSender, result_sender: ResultSender, artist: &Artist, - ) -> Result<(), DaemonError> { - let requests = match artist.meta.musicbrainz { + ) -> Result<(), FetchError> { + let requests = match artist.meta.info.musicbrainz { MbRefOption::Some(ref arid) => { - let arid = arid.mbid(); - let albums = artist.albums.iter(); - albums - .filter(|album| matches!(album.meta.musicbrainz, MbRefOption::None)) - .map(|album| MbParams::search_release_group(arid.clone(), album.meta.clone())) - .collect() + Self::fetch_albums_requests(&artist.meta.id, arid, &artist.albums) } - MbRefOption::CannotHaveMbid => return Ok(()), - MbRefOption::None => VecDeque::from([MbParams::search_artist(artist.meta.clone())]), + MbRefOption::CannotHaveMbid => VecDeque::new(), + MbRefOption::None => Self::fetch_artist_request(&artist.meta), }; - musicbrainz.submit_background_job(result_sender, requests) + if requests.is_empty() { + return Err(FetchError::NothingToFetch); + } + Ok(musicbrainz.submit_background_job(result_sender, requests)?) + } + + fn fetch_albums_requests( + artist: &ArtistId, + arid: &MbArtistRef, + albums: &[Album], + ) -> VecDeque { + let arid = arid.mbid(); + albums + .iter() + .filter(|album| matches!(album.meta.info.musicbrainz, MbRefOption::None)) + .map(|album| { + MbParams::search_release_group(artist.clone(), arid.clone(), album.meta.clone()) + }) + .collect() + } + + fn fetch_artist_request(meta: &ArtistMeta) -> VecDeque { + VecDeque::from([MbParams::search_artist(meta.clone())]) } fn submit_lookup_artist_job( @@ -168,10 +201,15 @@ impl AppMachine { fn submit_lookup_release_group_job( musicbrainz: &dyn IMbJobSender, result_sender: ResultSender, + artist_id: &ArtistId, album: &AlbumMeta, mbid: Mbid, ) -> Result<(), DaemonError> { - let requests = VecDeque::from([MbParams::lookup_release_group(album.clone(), mbid)]); + let requests = VecDeque::from([MbParams::lookup_release_group( + artist_id.clone(), + album.clone(), + mbid, + )]); musicbrainz.submit_foreground_job(result_sender, requests) } } @@ -207,12 +245,16 @@ impl IAppEventFetch for AppMachine { #[cfg(test)] mod tests { use mockall::predicate; - use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::Mbid}; + use musichoard::collection::{ + album::AlbumMeta, + artist::{ArtistId, ArtistMeta}, + musicbrainz::Mbid, + }; use crate::tui::{ app::{ machine::tests::{inner, music_hoard}, - Delta, IApp, IAppAccess, IAppInteractBrowse, MatchStateInfo, MissOption, SearchOption, + Delta, IApp, IAppAccess, IAppInteractBrowse, MatchOption, MatchStateInfo, }, lib::interface::musicbrainz::{ self, @@ -257,18 +299,23 @@ mod tests { #[test] fn fetch_no_artist() { - let app = AppMachine::app_fetch_new(inner(music_hoard(vec![]))); + let app = AppMachine::app_fetch_first(inner(music_hoard(vec![]))); assert!(matches!(app.state(), AppState::Error(_))); } fn search_release_group_expectation( job_sender: &mut MockIMbJobSender, - arid: &Mbid, + artist_id: &ArtistId, + artist_mbid: &Mbid, albums: &[AlbumMeta], ) { let mut requests = VecDeque::new(); for album in albums.iter() { - requests.push_back(MbParams::search_release_group(arid.clone(), album.clone())); + requests.push_back(MbParams::search_release_group( + artist_id.clone(), + artist_mbid.clone(), + album.clone(), + )); } job_sender .expect_submit_background_job() @@ -281,13 +328,19 @@ mod tests { fn fetch_albums() { let mut mb_job_sender = MockIMbJobSender::new(); - let arid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap(); + let artist_id = COLLECTION[1].meta.id.clone(); + let artist_mbid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap(); let album_1_meta = COLLECTION[1].albums[0].meta.clone(); let album_4_meta = COLLECTION[1].albums[3].meta.clone(); // Other albums have an MBID and so they will be skipped. - search_release_group_expectation(&mut mb_job_sender, &arid, &[album_1_meta, album_4_meta]); + search_release_group_expectation( + &mut mb_job_sender, + &artist_id, + &artist_mbid, + &[album_1_meta, album_4_meta], + ); let music_hoard = music_hoard(COLLECTION.to_owned()); let inner = AppInner::new(music_hoard, mb_job_sender); @@ -297,11 +350,19 @@ mod tests { let app = browse.increment_selection(Delta::Line); let app = app.unwrap_browse().fetch_musicbrainz(); - assert!(matches!(app, AppState::Match(_))); + assert!(matches!(app, AppState::Fetch(_))); } - fn lookup_album_expectation(job_sender: &mut MockIMbJobSender, album: &AlbumMeta) { - let requests = VecDeque::from([MbParams::lookup_release_group(album.clone(), mbid())]); + fn lookup_album_expectation( + job_sender: &mut MockIMbJobSender, + artist_id: &ArtistId, + album: &AlbumMeta, + ) { + let requests = VecDeque::from([MbParams::lookup_release_group( + artist_id.clone(), + album.clone(), + mbid(), + )]); job_sender .expect_submit_foreground_job() .with(predicate::always(), predicate::eq(requests)) @@ -313,8 +374,9 @@ mod tests { fn lookup_album() { let mut mb_job_sender = MockIMbJobSender::new(); + let artist_id = COLLECTION[1].meta.id.clone(); let album = COLLECTION[1].albums[0].meta.clone(); - lookup_album_expectation(&mut mb_job_sender, &album); + lookup_album_expectation(&mut mb_job_sender, &artist_id, &album); let music_hoard = music_hoard(COLLECTION.to_owned()); let inner = AppInner::new(music_hoard, mb_job_sender); @@ -322,7 +384,7 @@ mod tests { let (_fetch_tx, fetch_rx) = mpsc::channel(); let fetch = FetchState::new(fetch_rx); - AppMachine::app_lookup_album(inner, fetch, &album, mbid()); + AppMachine::app_lookup_album(inner, fetch, &artist_id, &album, mbid()); } fn search_artist_expectation(job_sender: &mut MockIMbJobSender, artist: &ArtistMeta) { @@ -351,7 +413,7 @@ mod tests { let app = browse.increment_selection(Delta::Line); let app = app.unwrap_browse().fetch_musicbrainz(); - assert!(matches!(app, AppState::Match(_))); + assert!(matches!(app, AppState::Fetch(_))); } fn lookup_artist_expectation(job_sender: &mut MockIMbJobSender, artist: &ArtistMeta) { @@ -442,15 +504,15 @@ mod tests { let inner = inner(music_hoard(COLLECTION.clone())); let fetch = FetchState::new(rx); - let mut app = AppMachine::app_fetch(inner, fetch, true); + let mut app = AppMachine::app_fetch_next(inner, fetch); assert!(matches!(app, AppState::Match(_))); let public = app.get(); let match_state = public.state.unwrap_match(); let match_options = vec![ artist_match.into(), - SearchOption::None(MissOption::CannotHaveMbid), - SearchOption::None(MissOption::ManualInputMbid), + MatchOption::CannotHaveMbid, + MatchOption::ManualInputMbid, ]; let expected = MatchStateInfo::artist_search(artist, match_options); assert_eq!(match_state.info, Some(expected).as_ref()); @@ -465,7 +527,7 @@ mod tests { let inner = inner(music_hoard(COLLECTION.clone())); let fetch = FetchState::new(rx); - let app = AppMachine::app_fetch(inner, fetch, true); + let app = AppMachine::app_fetch_next(inner, fetch); assert!(matches!(app, AppState::Error(_))); } @@ -475,26 +537,28 @@ mod tests { let inner = inner(music_hoard(COLLECTION.clone())); let fetch = FetchState::new(rx); - let app = AppMachine::app_fetch(inner, fetch, true); + let app = AppMachine::app_fetch_next(inner, fetch); assert!(matches!(app, AppState::Fetch(_))); } #[test] - fn recv_err_disconnected_first() { - let (_, rx) = mpsc::channel::(); + fn recv_err_empty_first() { + let mut collection = COLLECTION.clone(); + collection[0].albums.clear(); - let inner = inner(music_hoard(COLLECTION.clone())); - let fetch = FetchState::new(rx); - let app = AppMachine::app_fetch(inner, fetch, true); + let app = AppMachine::app_fetch_first(inner(music_hoard(collection))); assert!(matches!(app, AppState::Match(_))); } #[test] - fn recv_err_disconnected_next() { - let (_, rx) = mpsc::channel::(); + fn recv_err_empty_next() { + let mut collection = COLLECTION.clone(); + collection[0].albums.clear(); + let (_, rx) = mpsc::channel::(); let fetch = FetchState::new(rx); - let app = AppMachine::app_fetch_next(inner(music_hoard(COLLECTION.clone())), fetch); + + let app = AppMachine::app_fetch_next(inner(music_hoard(collection)), fetch); assert!(matches!(app, AppState::Browse(_))); } @@ -504,7 +568,7 @@ mod tests { let inner = inner(music_hoard(COLLECTION.clone())); let fetch = FetchState::new(rx); - let app = AppMachine::app_fetch(inner, fetch, true); + let app = AppMachine::app_fetch_next(inner, fetch); assert!(matches!(app, AppState::Fetch(_))); let artist = COLLECTION[3].meta.clone(); diff --git a/src/tui/app/machine/match_state.rs b/src/tui/app/machine/match_state.rs index d633319..5db46a0 100644 --- a/src/tui/app/machine/match_state.rs +++ b/src/tui/app/machine/match_state.rs @@ -1,14 +1,81 @@ use std::cmp; -use musichoard::collection::musicbrainz::Mbid; - -use crate::tui::app::{ - machine::{fetch_state::FetchState, input::Input, App, AppInner, AppMachine}, - AlbumMatches, AppPublicState, AppState, ArtistMatches, IAppInteractMatch, ListOption, - LookupOption, MatchStateInfo, MatchStatePublic, MissOption, SearchOption, WidgetState, +use musichoard::collection::{ + album::{AlbumInfo, AlbumMeta}, + artist::{ArtistInfo, ArtistMeta}, + musicbrainz::{MbRefOption, Mbid}, }; -impl ListOption { +use crate::tui::{ + app::{ + machine::{fetch_state::FetchState, input::Input, App, AppInner, AppMachine}, + AlbumMatches, AppPublicState, AppState, ArtistMatches, IAppInteractMatch, ListOption, + MatchOption, MatchStateInfo, MatchStatePublic, WidgetState, + }, + lib::interface::musicbrainz::api::{Lookup, Match}, +}; + +trait GetInfoMeta { + type InfoType; +} +impl GetInfoMeta for ArtistMeta { + type InfoType = ArtistInfo; +} +impl GetInfoMeta for AlbumMeta { + type InfoType = AlbumInfo; +} + +trait GetInfo { + type InfoType; + fn into_info(self, info: Self::InfoType) -> InfoOption; +} + +enum InfoOption { + Info(T), + NeedInput, +} + +macro_rules! impl_match_option_artist_into_info { + ($holder:ident) => { + impl GetInfo for MatchOption<$holder> { + type InfoType = ArtistInfo; + + fn into_info(self, mut info: Self::InfoType) -> InfoOption { + match self { + MatchOption::Some(option) => info.musicbrainz = option.item.info.musicbrainz, + MatchOption::CannotHaveMbid => info.musicbrainz = MbRefOption::CannotHaveMbid, + MatchOption::ManualInputMbid => return InfoOption::NeedInput, + } + InfoOption::Info(info) + } + } + }; +} + +impl_match_option_artist_into_info!(Match); +impl_match_option_artist_into_info!(Lookup); + +macro_rules! impl_match_option_album_into_info { + ($holder:ident) => { + impl GetInfo for MatchOption<$holder> { + type InfoType = AlbumInfo; + + fn into_info(self, mut info: Self::InfoType) -> InfoOption { + match self { + MatchOption::Some(option) => info = option.item.info, + MatchOption::CannotHaveMbid => info.musicbrainz = MbRefOption::CannotHaveMbid, + MatchOption::ManualInputMbid => return InfoOption::NeedInput, + } + InfoOption::Info(info) + } + } + }; +} + +impl_match_option_album_into_info!(Match); +impl_match_option_album_into_info!(Lookup); + +impl ListOption { fn len(&self) -> usize { match self { ListOption::Lookup(list) => list.len(), @@ -18,26 +85,35 @@ impl ListOption { fn push_cannot_have_mbid(&mut self) { match self { - ListOption::Lookup(list) => list.push(LookupOption::None(MissOption::CannotHaveMbid)), - ListOption::Search(list) => list.push(SearchOption::None(MissOption::CannotHaveMbid)), + ListOption::Lookup(list) => list.push(MatchOption::CannotHaveMbid), + ListOption::Search(list) => list.push(MatchOption::CannotHaveMbid), } } fn push_manual_input_mbid(&mut self) { match self { - ListOption::Lookup(list) => list.push(LookupOption::None(MissOption::ManualInputMbid)), - ListOption::Search(list) => list.push(SearchOption::None(MissOption::ManualInputMbid)), + ListOption::Lookup(list) => list.push(MatchOption::ManualInputMbid), + ListOption::Search(list) => list.push(MatchOption::ManualInputMbid), } } +} - fn is_manual_input_mbid(&self, index: usize) -> bool { +trait ExtractInfo { + type InfoType; + fn extract_info(&mut self, index: usize, info: Self::InfoType) -> InfoOption; +} + +impl ExtractInfo for ListOption +where + MatchOption>: GetInfo, + MatchOption>: GetInfo, +{ + type InfoType = T::InfoType; + + fn extract_info(&mut self, index: usize, info: Self::InfoType) -> InfoOption { match self { - ListOption::Lookup(list) => { - list.get(index) == Some(&LookupOption::None(MissOption::ManualInputMbid)) - } - ListOption::Search(list) => { - list.get(index) == Some(&SearchOption::None(MissOption::ManualInputMbid)) - } + ListOption::Lookup(ref mut list) => list.swap_remove(index).into_info(info), + ListOption::Search(ref mut list) => list.swap_remove(index).into_info(info), } } } @@ -54,10 +130,6 @@ impl ArtistMatches { fn push_manual_input_mbid(&mut self) { self.list.push_manual_input_mbid(); } - - fn is_manual_input_mbid(&self, index: usize) -> bool { - self.list.is_manual_input_mbid(index) - } } impl AlbumMatches { @@ -72,10 +144,6 @@ impl AlbumMatches { fn push_manual_input_mbid(&mut self) { self.list.push_manual_input_mbid(); } - - fn is_manual_input_mbid(&self, index: usize) -> bool { - self.list.is_manual_input_mbid(index) - } } impl MatchStateInfo { @@ -99,13 +167,6 @@ impl MatchStateInfo { Self::Album(a) => a.push_manual_input_mbid(), } } - - fn is_manual_input_mbid(&self, index: usize) -> bool { - match self { - Self::Artist(a) => a.is_manual_input_mbid(index), - Self::Album(a) => a.is_manual_input_mbid(index), - } - } } pub struct MatchState { @@ -146,11 +207,23 @@ impl AppMachine { AppMachine::app_lookup_artist(self.inner, self.state.fetch, matching, mbid) } MatchStateInfo::Album(album_matches) => { + let artist_id = &album_matches.artist; let matching = &album_matches.matching; - AppMachine::app_lookup_album(self.inner, self.state.fetch, matching, mbid) + AppMachine::app_lookup_album( + self.inner, + self.state.fetch, + artist_id, + matching, + mbid, + ) } } } + + fn get_input(mut self) -> App { + self.input.replace(Input::default()); + self.into() + } } impl From> for App { @@ -197,17 +270,32 @@ impl IAppInteractMatch for AppMachine { fn select(mut self) -> Self::APP { if let Some(index) = self.state.state.list.selected() { // selected() implies current exists - if self - .state - .current - .as_ref() - .unwrap() - .is_manual_input_mbid(index) - { - self.input.replace(Input::default()); - return self.into(); + + let mh = &mut self.inner.music_hoard; + let result = match self.state.current.as_mut().unwrap() { + MatchStateInfo::Artist(ref mut matches) => { + let info: ArtistInfo = matches.matching.info.clone(); + match matches.list.extract_info(index, info) { + InfoOption::Info(info) => mh.set_artist_info(&matches.matching.id, info), + InfoOption::NeedInput => return self.get_input(), + } + } + MatchStateInfo::Album(matches) => { + let info: AlbumInfo = matches.matching.info.clone(); + match matches.list.extract_info(index, info) { + InfoOption::Info(info) => { + mh.set_album_info(&matches.artist, &matches.matching.id, info) + } + InfoOption::NeedInput => return self.get_input(), + } + } + }; + + if let Err(err) = result { + return AppMachine::error_state(self.inner, err.to_string()).into(); } } + AppMachine::app_fetch_next(self.inner, self.state.fetch) } @@ -220,9 +308,9 @@ impl IAppInteractMatch for AppMachine { mod tests { use std::{collections::VecDeque, sync::mpsc}; - use mockall::predicate; + use mockall::predicate::{self, eq}; use musichoard::collection::{ - album::{AlbumDate, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType}, + album::{AlbumDate, AlbumId, AlbumInfo, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType}, artist::{ArtistId, ArtistMeta}, }; @@ -258,8 +346,18 @@ mod tests { } } + fn mbid() -> Mbid { + "00000000-0000-0000-0000-000000000000".try_into().unwrap() + } + + fn artist_meta() -> ArtistMeta { + let mut meta = ArtistMeta::new(ArtistId::new("Artist")); + meta.info.musicbrainz = MbRefOption::Some(mbid().into()); + meta + } + fn artist_match() -> MatchStateInfo { - let artist = ArtistMeta::new(ArtistId::new("Artist")); + let artist = artist_meta(); let artist_1 = artist.clone(); let artist_match_1 = Match::new(100, artist_1); @@ -273,40 +371,44 @@ mod tests { } fn artist_lookup() -> MatchStateInfo { - let artist = ArtistMeta::new(ArtistId::new("Artist")); + let artist = artist_meta(); let lookup = Lookup::new(artist.clone()); MatchStateInfo::artist_lookup(artist, lookup) } - fn album_match() -> MatchStateInfo { - let album = AlbumMeta::new( + fn album_meta() -> AlbumMeta { + AlbumMeta::new( AlbumId::new("Album"), AlbumDate::new(Some(1990), Some(5), None), - Some(AlbumPrimaryType::Album), - vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation], - ); + AlbumInfo::new( + MbRefOption::Some(mbid().into()), + Some(AlbumPrimaryType::Album), + vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation], + ), + ) + } + + fn album_match() -> MatchStateInfo { + let artist_id = ArtistId::new("Artist"); + let album = album_meta(); let album_1 = album.clone(); let album_match_1 = Match::new(100, album_1); let mut album_2 = album.clone(); album_2.id.title.push_str(" extra title part"); - album_2.secondary_types.pop(); + album_2.info.secondary_types.pop(); let album_match_2 = Match::new(100, album_2); let list = vec![album_match_1.clone(), album_match_2.clone()]; - MatchStateInfo::album_search(album, list) + MatchStateInfo::album_search(artist_id, album, list) } fn album_lookup() -> MatchStateInfo { - let album = AlbumMeta::new( - AlbumId::new("Album"), - AlbumDate::new(Some(1990), Some(5), None), - Some(AlbumPrimaryType::Album), - vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation], - ); + let artist_id = ArtistId::new("Artist"); + let album = album_meta(); let lookup = Lookup::new(album.clone()); - MatchStateInfo::album_lookup(album, lookup) + MatchStateInfo::album_lookup(artist_id, album, lookup) } fn fetch_state() -> FetchState { @@ -361,11 +463,34 @@ mod tests { fn match_state_flow(mut matches_info: MatchStateInfo, len: usize) { // tx must exist for rx to return Empty rather than Disconnected. - #[allow(unused_variables)] - let (tx, rx) = mpsc::channel(); + let (_tx, rx) = mpsc::channel(); let app_matches = MatchState::new(Some(matches_info.clone()), FetchState::new(rx)); - let matches = AppMachine::match_state(inner(music_hoard(vec![])), app_matches); + let mut music_hoard = music_hoard(vec![]); + let artist_id = ArtistId::new("Artist"); + match matches_info { + MatchStateInfo::Album(_) => { + let album_id = AlbumId::new("Album"); + let mut info = album_meta().info; + info.musicbrainz = MbRefOption::CannotHaveMbid; + music_hoard + .expect_set_album_info() + .with(eq(artist_id.clone()), eq(album_id.clone()), eq(info)) + .times(1) + .return_once(|_, _, _| Ok(())); + } + MatchStateInfo::Artist(_) => { + let mut info = artist_meta().info; + info.musicbrainz = MbRefOption::CannotHaveMbid; + music_hoard + .expect_set_artist_info() + .with(eq(artist_id.clone()), eq(info)) + .times(1) + .return_once(|_, _| Ok(())); + } + } + + let matches = AppMachine::match_state(inner(music_hoard), app_matches); matches_info.push_cannot_have_mbid(); matches_info.push_manual_input_mbid(); @@ -430,6 +555,75 @@ mod tests { match_state_flow(album_lookup(), 1); } + #[test] + fn set_artist_info() { + let matches_info = artist_match(); + + let (_tx, rx) = mpsc::channel(); + let app_matches = MatchState::new(Some(matches_info.clone()), FetchState::new(rx)); + + let mut music_hoard = music_hoard(vec![]); + match matches_info { + MatchStateInfo::Album(_) => panic!(), + MatchStateInfo::Artist(_) => { + let meta = artist_meta(); + music_hoard + .expect_set_artist_info() + .with(eq(meta.id), eq(meta.info)) + .times(1) + .return_once(|_, _| Ok(())); + } + } + + let matches = AppMachine::match_state(inner(music_hoard), app_matches); + matches.select().unwrap_fetch(); + } + + #[test] + fn set_album_info() { + let matches_info = album_match(); + + let (_tx, rx) = mpsc::channel(); + let app_matches = MatchState::new(Some(matches_info.clone()), FetchState::new(rx)); + + let mut music_hoard = music_hoard(vec![]); + match matches_info { + MatchStateInfo::Artist(_) => panic!(), + MatchStateInfo::Album(matches) => { + let meta = album_meta(); + music_hoard + .expect_set_album_info() + .with(eq(matches.artist), eq(meta.id), eq(meta.info)) + .times(1) + .return_once(|_, _, _| Ok(())); + } + } + + let matches = AppMachine::match_state(inner(music_hoard), app_matches); + matches.select().unwrap_fetch(); + } + + #[test] + fn set_info_error() { + let matches_info = artist_match(); + + let (_tx, rx) = mpsc::channel(); + let app_matches = MatchState::new(Some(matches_info.clone()), FetchState::new(rx)); + + let mut music_hoard = music_hoard(vec![]); + match matches_info { + MatchStateInfo::Album(_) => panic!(), + MatchStateInfo::Artist(_) => { + music_hoard.expect_set_artist_info().return_once(|_, _| { + Err(musichoard::Error::DatabaseError(String::from("error"))) + }); + } + } + + let matches = AppMachine::match_state(inner(music_hoard), app_matches); + matches.select().unwrap_error(); + } + #[test] fn abort() { let mut album_match = album_match(); @@ -451,10 +645,11 @@ mod tests { #[test] fn select_empty() { - // Note that what really matters in this test is actually that the transmit channel has - // disconnected and so the receive within FetchState concludes there are no more matches. + // This test will become obsolete with #203 so it just needs to work well enough for + // coverage. We expect the error state, because after selecting, fetch will be invoked, but + // with an empty collection, an error will be raised. let matches = AppMachine::match_state(inner(music_hoard(vec![])), match_state(None)); - matches.select().unwrap_browse(); + matches.select().unwrap_error(); } #[test] @@ -474,10 +669,6 @@ mod tests { input.confirm().unwrap_error(); } - fn mbid() -> Mbid { - "00000000-0000-0000-0000-000000000000".try_into().unwrap() - } - fn input_mbid(mut app: App) -> App { let mbid = mbid().uuid().to_string(); for c in mbid.chars() { @@ -518,15 +709,21 @@ mod tests { #[test] fn select_manual_input_album() { let mut mb_job_sender = MockIMbJobSender::new(); - let album = AlbumMeta::new("Album", 1990, None, vec![]); - let requests = VecDeque::from([MbParams::lookup_release_group(album.clone(), mbid())]); + let artist_id = ArtistId::new("Artist"); + let album = AlbumMeta::new("Album", 1990, AlbumInfo::default()); + let requests = VecDeque::from([MbParams::lookup_release_group( + artist_id.clone(), + album.clone(), + mbid(), + )]); mb_job_sender .expect_submit_foreground_job() .with(predicate::always(), predicate::eq(requests)) .return_once(|_, _| Ok(())); let matches_vec: Vec> = vec![]; - let album_match = MatchStateInfo::album_search(album.clone(), matches_vec); + let album_match = + MatchStateInfo::album_search(artist_id.clone(), album.clone(), matches_vec); let matches = AppMachine::match_state( inner_with_mb(music_hoard(vec![]), mb_job_sender), match_state(Some(album_match)), diff --git a/src/tui/app/machine/search_state.rs b/src/tui/app/machine/search_state.rs index dbcb9d8..ce6d519 100644 --- a/src/tui/app/machine/search_state.rs +++ b/src/tui/app/machine/search_state.rs @@ -182,7 +182,7 @@ impl IAppInteractSearchPrivate for AppMachine { if let Some(ref probe_sort) = probe.meta.sort { if !result { - let name = Self::normalize_search(&probe_sort.name, !case_sens, !char_sens); + let name = Self::normalize_search(probe_sort, !case_sens, !char_sens); result = name.starts_with(search); } } diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index 67c8a35..98ef76a 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -4,7 +4,11 @@ mod selection; pub use machine::App; pub use selection::{Category, Delta, Selection, WidgetState}; -use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, Collection}; +use musichoard::collection::{ + album::AlbumMeta, + artist::{ArtistId, ArtistMeta}, + Collection, +}; use crate::tui::lib::interface::musicbrainz::api::Match; @@ -177,38 +181,21 @@ pub struct AppPublicInner<'app> { pub type InputPublic<'app> = &'app tui_input::Input; #[derive(Clone, Debug, PartialEq, Eq)] -pub enum MissOption { +pub enum MatchOption { + Some(T), CannotHaveMbid, ManualInputMbid, } -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SearchOption { - Match(Match), - None(MissOption), -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum LookupOption { - Match(Lookup), - None(MissOption), -} - #[derive(Clone, Debug, PartialEq, Eq)] pub enum ListOption { - Search(Vec>), - Lookup(Vec>), + Search(Vec>>), + Lookup(Vec>>), } -impl From> for SearchOption { - fn from(value: Match) -> Self { - SearchOption::Match(value) - } -} - -impl From> for LookupOption { - fn from(value: Lookup) -> Self { - LookupOption::Match(value) +impl From for MatchOption { + fn from(value: T) -> Self { + MatchOption::Some(value) } } @@ -220,6 +207,7 @@ pub struct ArtistMatches { #[derive(Clone, Debug, PartialEq, Eq)] pub struct AlbumMatches { + pub artist: ArtistId, pub matching: AlbumMeta, pub list: ListOption, } @@ -231,7 +219,7 @@ pub enum MatchStateInfo { } impl MatchStateInfo { - pub fn artist_search>>( + pub fn artist_search>>>( matching: ArtistMeta, list: Vec, ) -> Self { @@ -239,22 +227,38 @@ impl MatchStateInfo { MatchStateInfo::Artist(ArtistMatches { matching, list }) } - pub fn album_search>>( + pub fn album_search>>>( + artist: ArtistId, matching: AlbumMeta, list: Vec, ) -> Self { let list = ListOption::Search(list.into_iter().map(Into::into).collect()); - MatchStateInfo::Album(AlbumMatches { matching, list }) + MatchStateInfo::Album(AlbumMatches { + artist, + matching, + list, + }) } - pub fn artist_lookup>>(matching: ArtistMeta, item: M) -> Self { + pub fn artist_lookup>>>( + matching: ArtistMeta, + item: M, + ) -> Self { let list = ListOption::Lookup(vec![item.into()]); MatchStateInfo::Artist(ArtistMatches { matching, list }) } - pub fn album_lookup>>(matching: AlbumMeta, item: M) -> Self { + pub fn album_lookup>>>( + artist: ArtistId, + matching: AlbumMeta, + item: M, + ) -> Self { let list = ListOption::Lookup(vec![item.into()]); - MatchStateInfo::Album(AlbumMatches { matching, list }) + MatchStateInfo::Album(AlbumMatches { + artist, + matching, + list, + }) } } diff --git a/src/tui/app/selection/artist.rs b/src/tui/app/selection/artist.rs index 55877b7..045455a 100644 --- a/src/tui/app/selection/artist.rs +++ b/src/tui/app/selection/artist.rs @@ -205,14 +205,14 @@ impl KeySelectArtist { let artist = &artists[index]; let key = artist.meta.get_sort_key(); KeySelectArtist { - key: (key.0.to_owned(),), + key: (key.0.into(),), album: KeySelectAlbum::get(&artist.albums, &selection.album), } }) } - pub fn get_sort_key(&self) -> (&ArtistId,) { - (&self.key.0,) + pub fn get_sort_key(&self) -> (&str,) { + (&self.key.0.name,) } } diff --git a/src/tui/lib/external/musicbrainz/api/mod.rs b/src/tui/lib/external/musicbrainz/api/mod.rs index 0a5a2cb..f248f1d 100644 --- a/src/tui/lib/external/musicbrainz/api/mod.rs +++ b/src/tui/lib/external/musicbrainz/api/mod.rs @@ -4,8 +4,8 @@ use std::collections::HashMap; use musichoard::{ collection::{ - album::{AlbumDate, AlbumMeta, AlbumSeq}, - artist::{ArtistId, ArtistMeta}, + album::{AlbumDate, AlbumInfo, AlbumMeta, AlbumSeq}, + artist::{ArtistInfo, ArtistMeta}, musicbrainz::{MbRefOption, Mbid}, }, external::musicbrainz::{ @@ -93,15 +93,15 @@ impl IMusicBrainz for MusicBrainz { } fn from_lookup_artist_response(entity: LookupArtistResponse) -> Lookup { - let sort: Option = Some(entity.meta.sort_name) - .filter(|s| s != &entity.meta.name) - .map(Into::into); + let sort = Some(entity.meta.sort_name).filter(|s| s != &entity.meta.name); Lookup { item: ArtistMeta { - id: entity.meta.name, + id: entity.meta.name.into(), sort, - musicbrainz: MbRefOption::Some(entity.meta.id.into()), - properties: HashMap::new(), + info: ArtistInfo { + musicbrainz: MbRefOption::Some(entity.meta.id.into()), + properties: HashMap::new(), + }, }, disambiguation: entity.meta.disambiguation, } @@ -110,28 +110,30 @@ fn from_lookup_artist_response(entity: LookupArtistResponse) -> Lookup Lookup { Lookup { item: AlbumMeta { - id: entity.meta.title, + id: entity.meta.title.into(), date: entity.meta.first_release_date, seq: AlbumSeq::default(), - musicbrainz: MbRefOption::Some(entity.meta.id.into()), - primary_type: Some(entity.meta.primary_type), - secondary_types: entity.meta.secondary_types.unwrap_or_default(), + info: AlbumInfo { + musicbrainz: MbRefOption::Some(entity.meta.id.into()), + primary_type: Some(entity.meta.primary_type), + secondary_types: entity.meta.secondary_types.unwrap_or_default(), + }, }, disambiguation: None, } } fn from_search_artist_response_artist(entity: SearchArtistResponseArtist) -> Match { - let sort: Option = Some(entity.meta.sort_name) - .filter(|s| s != &entity.meta.name) - .map(Into::into); + let sort = Some(entity.meta.sort_name).filter(|s| s != &entity.meta.name); Match { score: entity.score, item: ArtistMeta { - id: entity.meta.name, + id: entity.meta.name.into(), sort, - musicbrainz: MbRefOption::Some(entity.meta.id.into()), - properties: HashMap::new(), + info: ArtistInfo { + musicbrainz: MbRefOption::Some(entity.meta.id.into()), + properties: HashMap::new(), + }, }, disambiguation: entity.meta.disambiguation, } @@ -143,12 +145,14 @@ fn from_search_release_group_response_release_group( Match { score: entity.score, item: AlbumMeta { - id: entity.meta.title, + id: entity.meta.title.into(), date: entity.meta.first_release_date, seq: AlbumSeq::default(), - musicbrainz: MbRefOption::Some(entity.meta.id.into()), - primary_type: Some(entity.meta.primary_type), - secondary_types: entity.meta.secondary_types.unwrap_or_default(), + info: AlbumInfo { + musicbrainz: MbRefOption::Some(entity.meta.id.into()), + primary_type: Some(entity.meta.primary_type), + secondary_types: entity.meta.secondary_types.unwrap_or_default(), + }, }, disambiguation: None, } diff --git a/src/tui/lib/external/musicbrainz/daemon/mod.rs b/src/tui/lib/external/musicbrainz/daemon/mod.rs index 5db771c..dc75fd2 100644 --- a/src/tui/lib/external/musicbrainz/daemon/mod.rs +++ b/src/tui/lib/external/musicbrainz/daemon/mod.rs @@ -245,15 +245,15 @@ impl JobInstance { .map(|rv| MatchStateInfo::artist_lookup(params.artist, rv)), LookupParams::ReleaseGroup(params) => musicbrainz .lookup_release_group(¶ms.mbid) - .map(|rv| MatchStateInfo::album_lookup(params.album, rv)), + .map(|rv| MatchStateInfo::album_lookup(params.artist_id, params.album, rv)), }, MbParams::Search(search) => match search { SearchParams::Artist(params) => musicbrainz .search_artist(¶ms.artist) .map(|rv| MatchStateInfo::artist_search(params.artist, rv)), SearchParams::ReleaseGroup(params) => musicbrainz - .search_release_group(¶ms.arid, ¶ms.album) - .map(|rv| MatchStateInfo::album_search(params.album, rv)), + .search_release_group(¶ms.artist_mbid, ¶ms.album) + .map(|rv| MatchStateInfo::album_search(params.artist_id, params.album, rv)), }, }; self.return_result(event_sender, result) @@ -315,7 +315,7 @@ mod tests { use mockall::{predicate, Sequence}; use musichoard::collection::{ album::AlbumMeta, - artist::ArtistMeta, + artist::{ArtistId, ArtistMeta}, musicbrainz::{IMusicBrainzRef, MbRefOption, Mbid}, }; @@ -397,9 +397,10 @@ mod tests { } fn lookup_release_group_requests() -> VecDeque { + let artist_id = COLLECTION[1].meta.id.clone(); let album = COLLECTION[1].albums[0].meta.clone(); let mbid = mbid(); - VecDeque::from([MbParams::lookup_release_group(album, mbid)]) + VecDeque::from([MbParams::lookup_release_group(artist_id, album, mbid)]) } fn search_artist_requests() -> VecDeque { @@ -418,20 +419,25 @@ mod tests { } fn search_albums_requests() -> VecDeque { - let mbref = mb_ref_opt_as_ref(&COLLECTION[1].meta.musicbrainz); + let mbref = mb_ref_opt_as_ref(&COLLECTION[1].meta.info.musicbrainz); let arid = mb_ref_opt_unwrap(mbref).mbid().clone(); + let artist_id = COLLECTION[1].meta.id.clone(); let album_1 = COLLECTION[1].albums[0].meta.clone(); let album_4 = COLLECTION[1].albums[3].meta.clone(); VecDeque::from([ - MbParams::search_release_group(arid.clone(), album_1), - MbParams::search_release_group(arid.clone(), album_4), + MbParams::search_release_group(artist_id.clone(), arid.clone(), album_1), + MbParams::search_release_group(artist_id.clone(), arid.clone(), album_4), ]) } + fn album_artist_id() -> ArtistId { + COLLECTION[1].meta.id.clone() + } + fn album_arid_expectation() -> Mbid { - let mbref = mb_ref_opt_as_ref(&COLLECTION[1].meta.musicbrainz); + let mbref = mb_ref_opt_as_ref(&COLLECTION[1].meta.info.musicbrainz); mb_ref_opt_unwrap(mbref).mbid().clone() } @@ -611,7 +617,11 @@ mod tests { assert_eq!(result, Ok(())); let result = result_receiver.try_recv().unwrap(); - assert_eq!(result, Ok(MatchStateInfo::album_lookup(album, lookup))); + let artist_id = album_artist_id(); + assert_eq!( + result, + Ok(MatchStateInfo::album_lookup(artist_id, album, lookup)) + ); } fn search_artist_expectation( @@ -701,11 +711,27 @@ mod tests { let result = daemon.execute_next_job(); assert_eq!(result, Ok(())); - let result = result_receiver.try_recv().unwrap(); - assert_eq!(result, Ok(MatchStateInfo::album_search(album_1, matches_1))); + let artist_id = album_artist_id(); let result = result_receiver.try_recv().unwrap(); - assert_eq!(result, Ok(MatchStateInfo::album_search(album_4, matches_4))); + assert_eq!( + result, + Ok(MatchStateInfo::album_search( + artist_id.clone(), + album_1, + matches_1 + )) + ); + + let result = result_receiver.try_recv().unwrap(); + assert_eq!( + result, + Ok(MatchStateInfo::album_search( + artist_id.clone(), + album_4, + matches_4 + )) + ); } #[test] diff --git a/src/tui/lib/interface/musicbrainz/daemon/mod.rs b/src/tui/lib/interface/musicbrainz/daemon/mod.rs index 964ec56..0a2b468 100644 --- a/src/tui/lib/interface/musicbrainz/daemon/mod.rs +++ b/src/tui/lib/interface/musicbrainz/daemon/mod.rs @@ -1,6 +1,10 @@ use std::{collections::VecDeque, fmt, sync::mpsc}; -use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::Mbid}; +use musichoard::collection::{ + album::AlbumMeta, + artist::{ArtistId, ArtistMeta}, + musicbrainz::Mbid, +}; use crate::tui::{app::MatchStateInfo, lib::interface::musicbrainz::api::Error as MbApiError}; @@ -60,6 +64,7 @@ pub struct LookupArtistParams { #[derive(Clone, Debug, PartialEq, Eq)] pub struct LookupReleaseGroupParams { + pub artist_id: ArtistId, pub album: AlbumMeta, pub mbid: Mbid, } @@ -77,7 +82,8 @@ pub struct SearchArtistParams { #[derive(Clone, Debug, PartialEq, Eq)] pub struct SearchReleaseGroupParams { - pub arid: Mbid, + pub artist_id: ArtistId, + pub artist_mbid: Mbid, pub album: AlbumMeta, } @@ -86,8 +92,9 @@ impl MbParams { MbParams::Lookup(LookupParams::Artist(LookupArtistParams { artist, mbid })) } - pub fn lookup_release_group(album: AlbumMeta, mbid: Mbid) -> Self { + pub fn lookup_release_group(artist_id: ArtistId, album: AlbumMeta, mbid: Mbid) -> Self { MbParams::Lookup(LookupParams::ReleaseGroup(LookupReleaseGroupParams { + artist_id, album, mbid, })) @@ -97,9 +104,10 @@ impl MbParams { MbParams::Search(SearchParams::Artist(SearchArtistParams { artist })) } - pub fn search_release_group(arid: Mbid, album: AlbumMeta) -> Self { + pub fn search_release_group(artist_id: ArtistId, artist_mbid: Mbid, album: AlbumMeta) -> Self { MbParams::Search(SearchParams::ReleaseGroup(SearchReleaseGroupParams { - arid, + artist_id, + artist_mbid, album, })) } diff --git a/src/tui/lib/mod.rs b/src/tui/lib/mod.rs index 31643b2..8adc147 100644 --- a/src/tui/lib/mod.rs +++ b/src/tui/lib/mod.rs @@ -2,7 +2,11 @@ pub mod external; pub mod interface; use musichoard::{ - collection::Collection, + collection::{ + album::{AlbumId, AlbumInfo}, + artist::{ArtistId, ArtistInfo}, + Collection, + }, interface::{database::IDatabase, library::ILibrary}, IMusicHoardBase, IMusicHoardDatabase, IMusicHoardLibrary, MusicHoard, }; @@ -15,6 +19,15 @@ pub trait IMusicHoard { fn rescan_library(&mut self) -> Result<(), musichoard::Error>; fn reload_database(&mut self) -> Result<(), musichoard::Error>; fn get_collection(&self) -> &Collection; + + fn set_artist_info(&mut self, id: &ArtistId, info: ArtistInfo) + -> Result<(), musichoard::Error>; + fn set_album_info( + &mut self, + artist_id: &ArtistId, + album_id: &AlbumId, + info: AlbumInfo, + ) -> Result<(), musichoard::Error>; } // GRCOV_EXCL_START @@ -30,5 +43,22 @@ impl IMusicHoard for MusicHoard &Collection { ::get_collection(self) } + + fn set_artist_info( + &mut self, + id: &ArtistId, + info: ArtistInfo, + ) -> Result<(), musichoard::Error> { + ::set_artist_info(self, id, info) + } + + fn set_album_info( + &mut self, + artist_id: &ArtistId, + album_id: &AlbumId, + info: AlbumInfo, + ) -> Result<(), musichoard::Error> { + ::set_album_info(self, artist_id, album_id, info) + } } // GRCOV_EXCL_STOP diff --git a/src/tui/testmod.rs b/src/tui/testmod.rs index ba3955a..5b7ddc2 100644 --- a/src/tui/testmod.rs +++ b/src/tui/testmod.rs @@ -1,8 +1,8 @@ use std::collections::HashMap; use musichoard::collection::{ - album::{Album, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSeq}, - artist::{Artist, ArtistId, ArtistMeta}, + album::{Album, AlbumId, AlbumInfo, AlbumMeta, AlbumPrimaryType, AlbumSeq}, + artist::{Artist, ArtistId, ArtistInfo, ArtistMeta}, musicbrainz::{MbAlbumRef, MbArtistRef, MbRefOption}, track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, }; diff --git a/src/tui/ui/browse_state.rs b/src/tui/ui/browse_state.rs index e01845c..72254ff 100644 --- a/src/tui/ui/browse_state.rs +++ b/src/tui/ui/browse_state.rs @@ -169,7 +169,10 @@ impl<'a, 'b> AlbumState<'a, 'b> { .map(|a| UiDisplay::display_date(&a.meta.date, &a.meta.seq)) .unwrap_or_default(), album - .map(|a| UiDisplay::display_type(&a.meta.primary_type, &a.meta.secondary_types)) + .map(|a| UiDisplay::display_type( + &a.meta.info.primary_type, + &a.meta.info.secondary_types + )) .unwrap_or_default(), album .map(|a| UiDisplay::display_album_status(&a.get_status())) diff --git a/src/tui/ui/display.rs b/src/tui/ui/display.rs index 49972c2..511217c 100644 --- a/src/tui/ui/display.rs +++ b/src/tui/ui/display.rs @@ -5,7 +5,10 @@ use musichoard::collection::{ track::{TrackFormat, TrackQuality}, }; -use crate::tui::app::{LookupOption, MatchStateInfo, MissOption, SearchOption}; +use crate::tui::{ + app::{MatchOption, MatchStateInfo}, + lib::interface::musicbrainz::api::{Lookup, Match}, +}; pub struct UiDisplay; @@ -133,23 +136,25 @@ impl UiDisplay { } } - pub fn display_search_option_artist(match_option: &SearchOption) -> String { + pub fn display_search_option_artist(match_option: &MatchOption>) -> String { match match_option { - SearchOption::Match(match_artist) => format!( + MatchOption::Some(match_artist) => format!( "{} ({}%)", Self::display_option_artist(&match_artist.item, &match_artist.disambiguation), match_artist.score, ), - SearchOption::None(miss) => Self::display_miss_option(miss).to_string(), + MatchOption::CannotHaveMbid => Self::display_cannot_have_mbid().to_string(), + MatchOption::ManualInputMbid => Self::display_manual_input_mbid().to_string(), } } - pub fn display_lookup_option_artist(lookup_option: &LookupOption) -> String { + pub fn display_lookup_option_artist(lookup_option: &MatchOption>) -> String { match lookup_option { - LookupOption::Match(match_artist) => { + MatchOption::Some(match_artist) => { Self::display_option_artist(&match_artist.item, &match_artist.disambiguation) } - LookupOption::None(miss) => Self::display_miss_option(miss).to_string(), + MatchOption::CannotHaveMbid => Self::display_cannot_have_mbid().to_string(), + MatchOption::ManualInputMbid => Self::display_manual_input_mbid().to_string(), } } @@ -165,21 +170,23 @@ impl UiDisplay { ) } - pub fn display_search_option_album(match_option: &SearchOption) -> String { + pub fn display_search_option_album(match_option: &MatchOption>) -> String { match match_option { - SearchOption::Match(match_album) => format!( + MatchOption::Some(match_album) => format!( "{} ({}%)", Self::display_option_album(&match_album.item), match_album.score, ), - SearchOption::None(miss) => Self::display_miss_option(miss).to_string(), + MatchOption::CannotHaveMbid => Self::display_cannot_have_mbid().to_string(), + MatchOption::ManualInputMbid => Self::display_manual_input_mbid().to_string(), } } - pub fn display_lookup_option_album(lookup_option: &LookupOption) -> String { + pub fn display_lookup_option_album(lookup_option: &MatchOption>) -> String { match lookup_option { - LookupOption::Match(match_album) => Self::display_option_album(&match_album.item), - LookupOption::None(miss) => Self::display_miss_option(miss).to_string(), + MatchOption::Some(match_album) => Self::display_option_album(&match_album.item), + MatchOption::CannotHaveMbid => Self::display_cannot_have_mbid().to_string(), + MatchOption::ManualInputMbid => Self::display_manual_input_mbid().to_string(), } } @@ -188,17 +195,10 @@ impl UiDisplay { "{:010} | {} [{}]", UiDisplay::display_album_date(&album.date), album.id.title, - UiDisplay::display_type(&album.primary_type, &album.secondary_types), + UiDisplay::display_type(&album.info.primary_type, &album.info.secondary_types), ) } - fn display_miss_option(miss_option: &MissOption) -> &'static str { - match miss_option { - MissOption::CannotHaveMbid => Self::display_cannot_have_mbid(), - MissOption::ManualInputMbid => Self::display_manual_input_mbid(), - } - } - fn display_cannot_have_mbid() -> &'static str { "-- Cannot have a MusicBrainz Identifier --" } diff --git a/src/tui/ui/info_state.rs b/src/tui/ui/info_state.rs index 8936d90..684b914 100644 --- a/src/tui/ui/info_state.rs +++ b/src/tui/ui/info_state.rs @@ -76,10 +76,10 @@ impl<'a> ArtistOverlay<'a> { Properties: {}", artist.map(|a| a.meta.id.name.as_str()).unwrap_or(""), artist - .map(|a| UiDisplay::display_mb_ref_option_as_url(&a.meta.musicbrainz)) + .map(|a| UiDisplay::display_mb_ref_option_as_url(&a.meta.info.musicbrainz)) .unwrap_or_default(), Self::opt_hashmap_to_string( - artist.map(|a| &a.meta.properties), + artist.map(|a| &a.meta.info.properties), &double_item_indent, &double_list_indent ), @@ -104,7 +104,7 @@ impl<'a> AlbumOverlay<'a> { MusicBrainz: {}", album.map(|a| a.meta.id.title.as_str()).unwrap_or(""), album - .map(|a| UiDisplay::display_mb_ref_option_as_url(&a.meta.musicbrainz)) + .map(|a| UiDisplay::display_mb_ref_option_as_url(&a.meta.info.musicbrainz)) .unwrap_or_default(), )); diff --git a/src/tui/ui/mod.rs b/src/tui/ui/mod.rs index 94c441b..d0f13fd 100644 --- a/src/tui/ui/mod.rs +++ b/src/tui/ui/mod.rs @@ -201,8 +201,9 @@ impl IUi for Ui { #[cfg(test)] mod tests { use musichoard::collection::{ - album::{AlbumDate, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType}, + album::{AlbumDate, AlbumId, AlbumInfo, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType}, artist::{Artist, ArtistId, ArtistMeta}, + musicbrainz::MbRefOption, }; use crate::tui::{ @@ -373,31 +374,40 @@ mod tests { info } + fn album_artist_id() -> ArtistId { + ArtistId::new("Artist") + } + fn album_meta() -> AlbumMeta { AlbumMeta::new( AlbumId::new("An Album"), AlbumDate::new(Some(1990), Some(5), None), - Some(AlbumPrimaryType::Album), - vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation], + AlbumInfo::new( + MbRefOption::None, + Some(AlbumPrimaryType::Album), + vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation], + ), ) } fn album_matches() -> MatchStateInfo { + let artist_id = album_artist_id(); let album = album_meta(); let album_match = Match::new(80, album.clone()); let list = vec![album_match.clone(), album_match.clone()]; - let mut info = MatchStateInfo::album_search(album, list); + let mut info = MatchStateInfo::album_search(artist_id, album, list); info.push_cannot_have_mbid(); info.push_manual_input_mbid(); info } fn album_lookup() -> MatchStateInfo { + let artist_id = album_artist_id(); let album = album_meta(); let album_lookup = Lookup::new(album.clone()); - let mut info = MatchStateInfo::album_lookup(album, album_lookup); + let mut info = MatchStateInfo::album_lookup(artist_id, album, album_lookup); info.push_cannot_have_mbid(); info.push_manual_input_mbid(); info diff --git a/tests/testlib.rs b/tests/testlib.rs index 8839bf4..e0cd704 100644 --- a/tests/testlib.rs +++ b/tests/testlib.rs @@ -2,8 +2,8 @@ use once_cell::sync::Lazy; use std::collections::HashMap; use musichoard::collection::{ - album::{Album, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType, AlbumSeq}, - artist::{Artist, ArtistId, ArtistMeta}, + album::{Album, AlbumId, AlbumInfo, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType, AlbumSeq}, + artist::{Artist, ArtistId, ArtistInfo, ArtistMeta}, musicbrainz::{MbArtistRef, MbRefOption}, track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, Collection, @@ -16,23 +16,23 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { id: ArtistId { name: String::from("Аркона"), }, - sort: Some(ArtistId{ - name: String::from("Arkona") - }), - musicbrainz: MbRefOption::Some(MbArtistRef::from_url_str( - "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"), + sort: Some(String::from("Arkona")), + info: ArtistInfo { + musicbrainz: MbRefOption::Some(MbArtistRef::from_url_str( + "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", + )]), ]), - (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", - )]), - ]), + }, }, albums: vec![Album { meta: AlbumMeta { @@ -41,9 +41,11 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { }, date: 2011.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: Some(AlbumPrimaryType::Album), - secondary_types: vec![], + info: AlbumInfo { + musicbrainz: MbRefOption::None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + }, }, tracks: vec![ Track { @@ -209,17 +211,19 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { name: String::from("Eluveitie"), }, sort: None, - musicbrainz: MbRefOption::Some(MbArtistRef::from_url_str( - "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"), + info: ArtistInfo { + musicbrainz: MbRefOption::Some(MbArtistRef::from_url_str( + "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", + )]), ]), - (String::from("Qobuz"), vec![String::from( - "https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums", - )]), - ]), + }, }, albums: vec![ Album { @@ -229,9 +233,11 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { }, date: 2004.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: Some(AlbumPrimaryType::Ep), - secondary_types: vec![], + info: AlbumInfo { + musicbrainz: MbRefOption::None, + primary_type: Some(AlbumPrimaryType::Ep), + secondary_types: vec![], + }, }, tracks: vec![ Track { @@ -309,9 +315,11 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { }, date: 2008.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: Some(AlbumPrimaryType::Album), - secondary_types: vec![], + info: AlbumInfo { + musicbrainz: MbRefOption::None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + }, }, tracks: vec![ Track { @@ -456,17 +464,19 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { name: String::from("Frontside"), }, sort: None, - musicbrainz: MbRefOption::Some(MbArtistRef::from_url_str( - "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"), + info: ArtistInfo { + musicbrainz: MbRefOption::Some(MbArtistRef::from_url_str( + "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", + )]), ]), - (String::from("Qobuz"), vec![String::from( - "https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums", - )]), - ]), + }, }, albums: vec![Album { meta: AlbumMeta { @@ -475,9 +485,11 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { }, date: 2001.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: Some(AlbumPrimaryType::Album), - secondary_types: vec![], + info: AlbumInfo { + musicbrainz: MbRefOption::None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + }, }, tracks: vec![ Track { @@ -609,20 +621,20 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { id: ArtistId { name: String::from("Heaven’s Basement"), }, - sort: Some(ArtistId { - name: String::from("Heaven’s Basement"), - }), - musicbrainz: MbRefOption::Some(MbArtistRef::from_url_str( - "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"), + sort: Some(String::from("Heaven’s Basement")), + info: ArtistInfo { + musicbrainz: MbRefOption::Some(MbArtistRef::from_url_str( + "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", + )]), ]), - (String::from("Qobuz"), vec![String::from( - "https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums", - )]), - ]), + }, }, albums: vec![Album { meta: AlbumMeta { @@ -631,9 +643,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { }, date: 2011.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: None, - secondary_types: vec![], + info: AlbumInfo::default(), }, tracks: vec![ Track { @@ -655,9 +665,11 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { }, date: 2011.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: Some(AlbumPrimaryType::Album), - secondary_types: vec![], + info: AlbumInfo { + musicbrainz: MbRefOption::None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + }, }, tracks: vec![ Track { @@ -746,17 +758,19 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { name: String::from("Metallica"), }, sort: None, - musicbrainz: MbRefOption::Some(MbArtistRef::from_url_str( - "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"), + info: ArtistInfo { + musicbrainz: MbRefOption::Some(MbArtistRef::from_url_str( + "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", + )]), ]), - (String::from("Qobuz"), vec![String::from( - "https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums", - )]), - ]), + }, }, albums: vec![ Album { @@ -766,9 +780,11 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { }, date: 1984.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: Some(AlbumPrimaryType::Album), - secondary_types: vec![], + info: AlbumInfo { + musicbrainz: MbRefOption::None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![], + }, }, tracks: vec![ Track { @@ -868,9 +884,11 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { }, date: 1999.into(), seq: AlbumSeq(0), - musicbrainz: MbRefOption::None, - primary_type: Some(AlbumPrimaryType::Album), - secondary_types: vec![AlbumSecondaryType::Live], + info: AlbumInfo { + musicbrainz: MbRefOption::None, + primary_type: Some(AlbumPrimaryType::Album), + secondary_types: vec![AlbumSecondaryType::Live], + }, }, tracks: vec![ Track {