From bd7e9ceb4db8b54b9bfc847b8d4adc6e2c9ac7a2 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 8 Mar 2024 23:28:52 +0100 Subject: [PATCH] Connect release groups to musicbrainz id (#157) Closes #46 Reviewed-on: https://git.thenineworlds.net/wojtek/musichoard/pulls/157 --- src/core/collection/album.rs | 68 +++++++--- src/core/collection/artist.rs | 147 +-------------------- src/core/collection/mod.rs | 1 + src/core/collection/musicbrainz.rs | 173 +++++++++++++++++++++++++ src/core/database/json/testmod.rs | 31 +++-- src/core/database/serde/deserialize.rs | 55 +++++--- src/core/database/serde/serialize.rs | 18 +-- src/core/musichoard/musichoard.rs | 40 +++--- src/core/testmod.rs | 5 +- src/tests.rs | 28 +++- src/tui/testmod.rs | 5 +- src/tui/ui.rs | 11 +- tests/files/database/database.json | 2 +- tests/testlib.rs | 23 +++- 14 files changed, 365 insertions(+), 242 deletions(-) create mode 100644 src/core/collection/musicbrainz.rs diff --git a/src/core/collection/album.rs b/src/core/collection/album.rs index ccbd707..f7ed42d 100644 --- a/src/core/collection/album.rs +++ b/src/core/collection/album.rs @@ -5,6 +5,7 @@ use std::{ use crate::core::collection::{ merge::{Merge, MergeSorted, WithId}, + musicbrainz::MusicBrainz, track::{Track, TrackFormat}, }; @@ -14,6 +15,7 @@ pub struct Album { pub id: AlbumId, pub date: AlbumDate, pub seq: AlbumSeq, + pub musicbrainz: Option, pub tracks: Vec, } @@ -50,6 +52,24 @@ impl AlbumDate { } } +impl From for AlbumDate { + fn from(value: u32) -> Self { + AlbumDate::new(value, AlbumMonth::default(), 0) + } +} + +impl> From<(u32, M)> for AlbumDate { + fn from(value: (u32, M)) -> Self { + AlbumDate::new(value.0, value.1, 0) + } +} + +impl> From<(u32, M, u8)> for AlbumDate { + fn from(value: (u32, M, u8)) -> Self { + AlbumDate::new(value.0, value.1, value.2) + } +} + #[repr(u8)] #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)] pub enum AlbumMonth { @@ -115,6 +135,16 @@ impl AlbumStatus { } impl Album { + pub fn new, Date: Into>(id: Id, date: Date) -> Self { + Album { + id: id.into(), + date: date.into(), + seq: AlbumSeq::default(), + musicbrainz: None, + tracks: vec![], + } + } + pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) { (&self.date, &self.seq, &self.id) } @@ -202,6 +232,21 @@ mod tests { assert_eq!(>::into(255), AlbumMonth::None); } + #[test] + fn album_date_from() { + let date: AlbumDate = 1986.into(); + assert_eq!(date, AlbumDate::new(1986, AlbumMonth::default(), 0)); + + let date: AlbumDate = (1986, 5).into(); + assert_eq!(date, AlbumDate::new(1986, AlbumMonth::May, 0)); + + let date: AlbumDate = (1986, AlbumMonth::June).into(); + assert_eq!(date, AlbumDate::new(1986, AlbumMonth::June, 0)); + + let date: AlbumDate = (1986, 6, 8).into(); + assert_eq!(date, AlbumDate::new(1986, AlbumMonth::June, 8)); + } + #[test] fn same_date_seq_cmp() { let date = AlbumDate::new(2024, 3, 2); @@ -209,22 +254,14 @@ mod tests { let album_id_1 = AlbumId { title: String::from("album z"), }; - let album_1 = Album { - id: album_id_1, - date: date.clone(), - seq: AlbumSeq(1), - tracks: vec![], - }; + let mut album_1 = Album::new(album_id_1, date.clone()); + album_1.set_seq(AlbumSeq(1)); let album_id_2 = AlbumId { title: String::from("album a"), }; - let album_2 = Album { - id: album_id_2, - date: date.clone(), - seq: AlbumSeq(2), - tracks: vec![], - }; + let mut album_2 = Album::new(album_id_2, date.clone()); + album_2.set_seq(AlbumSeq(2)); assert_ne!(album_1, album_2); assert!(album_1 < album_2); @@ -232,12 +269,7 @@ mod tests { #[test] fn set_clear_seq() { - let mut album = Album { - id: "an album".into(), - date: AlbumDate::default(), - seq: AlbumSeq::default(), - tracks: vec![], - }; + let mut album = Album::new("An album", AlbumDate::default()); assert_eq!(album.seq, AlbumSeq(0)); diff --git a/src/core/collection/artist.rs b/src/core/collection/artist.rs index 372dc20..bdcb9d0 100644 --- a/src/core/collection/artist.rs +++ b/src/core/collection/artist.rs @@ -2,16 +2,12 @@ use std::{ collections::HashMap, fmt::{self, Debug, Display}, mem, - str::FromStr, }; -use url::Url; -use uuid::Uuid; - use crate::core::collection::{ album::Album, merge::{Merge, MergeCollections, WithId}, - Error, + musicbrainz::MusicBrainz, }; /// An artist. @@ -40,7 +36,7 @@ pub struct ArtistId { impl Artist { /// Create new [`Artist`] with the given [`ArtistId`]. - pub fn new>(id: ID) -> Self { + pub fn new>(id: Id) -> Self { Artist { id: id.into(), sort: None, @@ -164,92 +160,6 @@ impl Display for ArtistId { } } -/// An object with the [`IMbid`] trait contains a [MusicBrainz -/// Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). -pub trait IMbid { - fn mbid(&self) -> &str; -} - -/// MusicBrainz reference. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct MusicBrainz(Url); - -impl MusicBrainz { - /// Validate and wrap a MusicBrainz URL. - pub fn new_from_str>(url: S) -> Result { - let url = Url::parse(url.as_ref())?; - Self::new_from_url(url) - } - - /// Validate and wrap a MusicBrainz URL. - pub fn new_from_url(url: Url) -> Result { - if !url - .domain() - .map(|u| u.ends_with("musicbrainz.org")) - .unwrap_or(false) - { - return Err(Self::invalid_url_error(url)); - } - - match url.path_segments().and_then(|mut ps| ps.nth(1)) { - Some(segment) => Uuid::try_parse(segment)?, - None => return Err(Self::invalid_url_error(url)), - }; - - Ok(MusicBrainz(url)) - } - - fn invalid_url_error(url: U) -> Error { - Error::UrlError(format!("invalid MusicBrainz URL: {url}")) - } -} - -impl AsRef for MusicBrainz { - fn as_ref(&self) -> &str { - self.0.as_ref() - } -} - -impl FromStr for MusicBrainz { - type Err = Error; - - fn from_str(s: &str) -> Result { - MusicBrainz::new_from_str(s) - } -} - -// A blanket TryFrom would be better, but https://stackoverflow.com/a/64407892 -macro_rules! impl_try_from_for_musicbrainz { - ($from:ty) => { - impl TryFrom<$from> for MusicBrainz { - type Error = Error; - - fn try_from(value: $from) -> Result { - MusicBrainz::new_from_str(value) - } - } - }; -} - -impl_try_from_for_musicbrainz!(&str); -impl_try_from_for_musicbrainz!(&String); -impl_try_from_for_musicbrainz!(String); - -impl TryFrom for MusicBrainz { - type Error = Error; - - fn try_from(value: Url) -> Result { - MusicBrainz::new_from_url(value) - } -} - -impl IMbid for MusicBrainz { - fn mbid(&self) -> &str { - // The URL is assumed to have been validated. - self.0.path_segments().and_then(|mut ps| ps.nth(1)).unwrap() - } -} - #[cfg(test)] mod tests { use crate::core::testmod::FULL_COLLECTION; @@ -263,40 +173,6 @@ mod tests { static MUSICBUTLER: &str = "https://www.musicbutler.io/artist-page/483340948"; static MUSICBUTLER_2: &str = "https://www.musicbutler.io/artist-page/658903042/"; - #[test] - fn musicbrainz() { - let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8"; - let url_str = format!("https://musicbrainz.org/artist/{uuid}"); - let url: Url = url_str.as_str().try_into().unwrap(); - let mb: MusicBrainz = url.try_into().unwrap(); - assert_eq!(url_str, mb.as_ref()); - assert_eq!(uuid, mb.mbid()); - - let url = "not a url at all".to_string(); - let expected_error: Error = url::ParseError::RelativeUrlWithoutBase.into(); - let actual_error = MusicBrainz::from_str(&url).unwrap_err(); - assert_eq!(actual_error, expected_error); - assert_eq!(actual_error.to_string(), expected_error.to_string()); - - let url = "https://musicbrainz.org/artist/i-am-not-a-uuid".to_string(); - let expected_error: Error = Uuid::try_parse("i-am-not-a-uuid").unwrap_err().into(); - let actual_error = MusicBrainz::from_str(&url).unwrap_err(); - assert_eq!(actual_error, expected_error); - assert_eq!(actual_error.to_string(), expected_error.to_string()); - - let url = "https://musicbrainz.org/artist".to_string(); - let expected_error = Error::UrlError(format!("invalid MusicBrainz URL: {url}")); - let actual_error = MusicBrainz::from_str(&url).unwrap_err(); - assert_eq!(actual_error, expected_error); - assert_eq!(actual_error.to_string(), expected_error.to_string()); - } - - #[test] - fn urls() { - assert!(MusicBrainz::from_str(MUSICBRAINZ).is_ok()); - assert!(MusicBrainz::from_str(MUSICBUTLER).is_err()); - } - #[test] fn artist_sort_set_clear() { let artist_id = ArtistId::new("an artist"); @@ -336,15 +212,6 @@ mod tests { assert!(artist < Artist::new(sort_id_2.clone())); } - #[test] - fn musicbrainz_url() { - let result: Result = MUSICBUTLER.try_into(); - assert!(result.is_err()); - - let result: Result = MUSICBRAINZ.try_into(); - assert!(result.is_ok()); - } - #[test] fn set_clear_musicbrainz_url() { let mut artist = Artist::new(ArtistId::new("an artist")); @@ -353,15 +220,15 @@ mod tests { assert_eq!(artist.musicbrainz, expected); // Setting a URL on an artist. - artist.set_musicbrainz_url(MUSICBRAINZ.try_into().unwrap()); - _ = expected.insert(MUSICBRAINZ.try_into().unwrap()); + artist.set_musicbrainz_url(MusicBrainz::artist_from_str(MUSICBRAINZ).unwrap()); + _ = expected.insert(MusicBrainz::artist_from_str(MUSICBRAINZ).unwrap()); assert_eq!(artist.musicbrainz, expected); - artist.set_musicbrainz_url(MUSICBRAINZ.try_into().unwrap()); + artist.set_musicbrainz_url(MusicBrainz::artist_from_str(MUSICBRAINZ).unwrap()); assert_eq!(artist.musicbrainz, expected); - artist.set_musicbrainz_url(MUSICBRAINZ_2.try_into().unwrap()); - _ = expected.insert(MUSICBRAINZ_2.try_into().unwrap()); + artist.set_musicbrainz_url(MusicBrainz::artist_from_str(MUSICBRAINZ_2).unwrap()); + _ = expected.insert(MusicBrainz::artist_from_str(MUSICBRAINZ_2).unwrap()); assert_eq!(artist.musicbrainz, expected); // Clearing URLs. diff --git a/src/core/collection/mod.rs b/src/core/collection/mod.rs index ef4386d..f54f5b4 100644 --- a/src/core/collection/mod.rs +++ b/src/core/collection/mod.rs @@ -2,6 +2,7 @@ pub mod album; pub mod artist; +pub mod musicbrainz; pub mod track; mod merge; diff --git a/src/core/collection/musicbrainz.rs b/src/core/collection/musicbrainz.rs new file mode 100644 index 0000000..bb4251b --- /dev/null +++ b/src/core/collection/musicbrainz.rs @@ -0,0 +1,173 @@ +use std::fmt::{Debug, Display}; + +use url::Url; +use uuid::Uuid; + +use crate::core::collection::Error; + +/// An object with the [`IMbid`] trait contains a [MusicBrainz +/// Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). +pub trait IMbid { + fn mbid(&self) -> &str; +} + +/// MusicBrainz reference. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct MusicBrainz(Url); + +impl MusicBrainz { + pub fn artist_from_str>(url: S) -> Result { + Self::artist_from_url(url.as_ref().try_into()?) + } + + pub fn album_from_str>(url: S) -> Result { + Self::album_from_url(url.as_ref().try_into()?) + } + + pub fn artist_from_url(url: Url) -> Result { + Self::new(url, "artist") + } + + pub fn album_from_url(url: Url) -> Result { + Self::new(url, "release-group") + } + + fn new(url: Url, mb_type: &str) -> Result { + if !url + .domain() + .map(|u| u.ends_with("musicbrainz.org")) + .unwrap_or(false) + { + return Err(Self::invalid_url_error(url, mb_type)); + } + + // path_segments only returns an empty iterator if the URL cannot-be-a-base. However, if the + // URL cannot-be-a-base then it will fail the check above already as it won't have a domain. + if url.path_segments().and_then(|mut ps| ps.nth(0)).unwrap() != mb_type { + return Err(Self::invalid_url_error(url, mb_type)); + } + + match url.path_segments().and_then(|mut ps| ps.nth(1)) { + Some(segment) => Uuid::try_parse(segment)?, + None => return Err(Self::invalid_url_error(url, mb_type)), + }; + + Ok(MusicBrainz(url)) + } + + fn invalid_url_error(url: U, mb_type: &str) -> Error { + Error::UrlError(format!("invalid {mb_type} MusicBrainz URL: {url}")) + } +} + +impl AsRef for MusicBrainz { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl IMbid for MusicBrainz { + fn mbid(&self) -> &str { + // The URL is assumed to have been validated. + self.0.path_segments().and_then(|mut ps| ps.nth(1)).unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn artist() { + let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8"; + let url_str = format!("https://musicbrainz.org/artist/{uuid}"); + + let mb = MusicBrainz::artist_from_str(&url_str).unwrap(); + assert_eq!(url_str, mb.as_ref()); + assert_eq!(uuid, mb.mbid()); + + let url: Url = url_str.as_str().try_into().unwrap(); + let mb = MusicBrainz::artist_from_url(url).unwrap(); + assert_eq!(url_str, mb.as_ref()); + assert_eq!(uuid, mb.mbid()); + } + + #[test] + fn album() { + let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8"; + let url_str = format!("https://musicbrainz.org/release-group/{uuid}"); + + let mb = MusicBrainz::album_from_str(&url_str).unwrap(); + assert_eq!(url_str, mb.as_ref()); + assert_eq!(uuid, mb.mbid()); + + let url: Url = url_str.as_str().try_into().unwrap(); + let mb = MusicBrainz::album_from_url(url).unwrap(); + assert_eq!(url_str, mb.as_ref()); + assert_eq!(uuid, mb.mbid()); + } + + #[test] + fn not_a_url() { + let url = "not a url at all"; + let expected_error: Error = url::ParseError::RelativeUrlWithoutBase.into(); + let actual_error = MusicBrainz::artist_from_str(url).unwrap_err(); + assert_eq!(actual_error, expected_error); + assert_eq!(actual_error.to_string(), expected_error.to_string()); + } + + #[test] + fn invalid_url() { + let url = "https://www.musicbutler.io/artist-page/483340948"; + let expected_error = Error::UrlError(format!("invalid artist MusicBrainz URL: {url}")); + let actual_error = MusicBrainz::artist_from_str(url).unwrap_err(); + assert_eq!(actual_error, expected_error); + assert_eq!(actual_error.to_string(), expected_error.to_string()); + } + + #[test] + fn artist_invalid_type() { + let url = "https://musicbrainz.org/release-group/i-am-not-a-uuid"; + let expected_error = Error::UrlError(format!("invalid artist MusicBrainz URL: {url}")); + let actual_error = MusicBrainz::artist_from_str(url).unwrap_err(); + assert_eq!(actual_error, expected_error); + assert_eq!(actual_error.to_string(), expected_error.to_string()); + } + + #[test] + fn album_invalid_type() { + let url = "https://musicbrainz.org/artist/i-am-not-a-uuid"; + let expected_error = + Error::UrlError(format!("invalid release-group MusicBrainz URL: {url}")); + let actual_error = MusicBrainz::album_from_str(url).unwrap_err(); + assert_eq!(actual_error, expected_error); + assert_eq!(actual_error.to_string(), expected_error.to_string()); + } + + #[test] + fn invalid_uuid() { + let url = "https://musicbrainz.org/artist/i-am-not-a-uuid"; + let expected_error: Error = Uuid::try_parse("i-am-not-a-uuid").unwrap_err().into(); + let actual_error = MusicBrainz::artist_from_str(url).unwrap_err(); + assert_eq!(actual_error, expected_error); + assert_eq!(actual_error.to_string(), expected_error.to_string()); + } + + #[test] + fn missing_type() { + let url = "https://musicbrainz.org"; + let expected_error = Error::UrlError(format!("invalid artist MusicBrainz URL: {url}/")); + let actual_error = MusicBrainz::artist_from_str(url).unwrap_err(); + assert_eq!(actual_error, expected_error); + assert_eq!(actual_error.to_string(), expected_error.to_string()); + } + + #[test] + fn missing_uuid() { + let url = "https://musicbrainz.org/artist"; + let expected_error = Error::UrlError(format!("invalid artist MusicBrainz URL: {url}")); + let actual_error = MusicBrainz::artist_from_str(url).unwrap_err(); + assert_eq!(actual_error, expected_error); + assert_eq!(actual_error.to_string(), expected_error.to_string()); + } +} diff --git a/src/core/database/json/testmod.rs b/src/core/database/json/testmod.rs index d328f8e..7bba672 100644 --- a/src/core/database/json/testmod.rs +++ b/src/core/database/json/testmod.rs @@ -1,5 +1,5 @@ pub static DATABASE_JSON: &str = "{\ - \"V20240302\":\ + \"V20240308\":\ [\ {\ \"name\":\"Album_Artist ‘A’\",\ @@ -10,8 +10,11 @@ pub static DATABASE_JSON: &str = "{\ \"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums\"]\ },\ \"albums\":[\ - {\"title\":\"album_title a.a\",\"seq\":1},\ - {\"title\":\"album_title a.b\",\"seq\":1}\ + {\ + \"title\":\"album_title a.a\",\"seq\":1,\ + \"musicbrainz\":\"https://musicbrainz.org/release-group/00000000-0000-0000-0000-000000000000\"\ + },\ + {\"title\":\"album_title a.b\",\"seq\":1,\"musicbrainz\":null}\ ]\ },\ {\ @@ -27,10 +30,16 @@ pub static DATABASE_JSON: &str = "{\ \"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums\"]\ },\ \"albums\":[\ - {\"title\":\"album_title b.a\",\"seq\":1},\ - {\"title\":\"album_title b.b\",\"seq\":3},\ - {\"title\":\"album_title b.c\",\"seq\":2},\ - {\"title\":\"album_title b.d\",\"seq\":4}\ + {\"title\":\"album_title b.a\",\"seq\":1,\"musicbrainz\":null},\ + {\ + \"title\":\"album_title b.b\",\"seq\":3,\ + \"musicbrainz\":\"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111111\"\ + },\ + {\ + \"title\":\"album_title b.c\",\"seq\":2,\ + \"musicbrainz\":\"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111112\"\ + },\ + {\"title\":\"album_title b.d\",\"seq\":4,\"musicbrainz\":null}\ ]\ },\ {\ @@ -39,8 +48,8 @@ pub static DATABASE_JSON: &str = "{\ \"musicbrainz\":\"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111\",\ \"properties\":{},\ \"albums\":[\ - {\"title\":\"album_title c.a\",\"seq\":0},\ - {\"title\":\"album_title c.b\",\"seq\":0}\ + {\"title\":\"album_title c.a\",\"seq\":0,\"musicbrainz\":null},\ + {\"title\":\"album_title c.b\",\"seq\":0,\"musicbrainz\":null}\ ]\ },\ {\ @@ -49,8 +58,8 @@ pub static DATABASE_JSON: &str = "{\ \"musicbrainz\":null,\ \"properties\":{},\ \"albums\":[\ - {\"title\":\"album_title d.a\",\"seq\":0},\ - {\"title\":\"album_title d.b\",\"seq\":0}\ + {\"title\":\"album_title d.a\",\"seq\":0,\"musicbrainz\":null},\ + {\"title\":\"album_title d.b\",\"seq\":0,\"musicbrainz\":null}\ ]\ }\ ]\ diff --git a/src/core/database/serde/deserialize.rs b/src/core/database/serde/deserialize.rs index cc4a64a..718601e 100644 --- a/src/core/database/serde/deserialize.rs +++ b/src/core/database/serde/deserialize.rs @@ -6,6 +6,7 @@ use crate::core::{ collection::{ album::{Album, AlbumDate, AlbumId, AlbumSeq}, artist::{Artist, ArtistId}, + musicbrainz::MusicBrainz, Collection, }, database::LoadError, @@ -13,7 +14,20 @@ use crate::core::{ #[derive(Debug, Deserialize)] pub enum DeserializeDatabase { - V20240302(Vec), + V20240308(Vec), +} + +impl TryFrom for Collection { + type Error = LoadError; + + fn try_from(database: DeserializeDatabase) -> Result { + match database { + DeserializeDatabase::V20240308(collection) => collection + .into_iter() + .map(|artist| artist.try_into()) + .collect(), + } + } } #[derive(Debug, Deserialize)] @@ -29,19 +43,7 @@ pub struct DeserializeArtist { pub struct DeserializeAlbum { title: String, seq: u8, -} - -impl TryFrom for Collection { - type Error = LoadError; - - fn try_from(database: DeserializeDatabase) -> Result { - match database { - DeserializeDatabase::V20240302(collection) => collection - .into_iter() - .map(|artist| artist.try_into()) - .collect(), - } - } + musicbrainz: Option, } impl TryFrom for Artist { @@ -51,20 +53,33 @@ impl TryFrom for Artist { Ok(Artist { id: ArtistId::new(artist.name), sort: artist.sort.map(ArtistId::new), - musicbrainz: artist.musicbrainz.map(TryInto::try_into).transpose()?, + musicbrainz: artist + .musicbrainz + .map(MusicBrainz::artist_from_str) + .transpose()?, properties: artist.properties, - albums: artist.albums.into_iter().map(Into::into).collect(), + albums: artist + .albums + .into_iter() + .map(TryInto::try_into) + .collect::, LoadError>>()?, }) } } -impl From for Album { - fn from(album: DeserializeAlbum) -> Self { - Album { +impl TryFrom for Album { + type Error = LoadError; + + fn try_from(album: DeserializeAlbum) -> Result { + Ok(Album { id: AlbumId { title: album.title }, date: AlbumDate::default(), seq: AlbumSeq(album.seq), + musicbrainz: album + .musicbrainz + .map(MusicBrainz::album_from_str) + .transpose()?, tracks: vec![], - } + }) } } diff --git a/src/core/database/serde/serialize.rs b/src/core/database/serde/serialize.rs index 5276b54..eeb219f 100644 --- a/src/core/database/serde/serialize.rs +++ b/src/core/database/serde/serialize.rs @@ -6,7 +6,13 @@ use crate::core::collection::{album::Album, artist::Artist, Collection}; #[derive(Debug, Serialize)] pub enum SerializeDatabase<'a> { - V20240302(Vec>), + V20240308(Vec>), +} + +impl<'a> From<&'a Collection> for SerializeDatabase<'a> { + fn from(collection: &'a Collection) -> Self { + SerializeDatabase::V20240308(collection.iter().map(Into::into).collect()) + } } #[derive(Debug, Serialize)] @@ -22,12 +28,7 @@ pub struct SerializeArtist<'a> { pub struct SerializeAlbum<'a> { title: &'a str, seq: u8, -} - -impl<'a> From<&'a Collection> for SerializeDatabase<'a> { - fn from(collection: &'a Collection) -> Self { - SerializeDatabase::V20240302(collection.iter().map(Into::into).collect()) - } + musicbrainz: Option<&'a str>, } impl<'a> From<&'a Artist> for SerializeArtist<'a> { @@ -35,7 +36,7 @@ impl<'a> From<&'a Artist> for SerializeArtist<'a> { SerializeArtist { name: &artist.id.name, sort: artist.sort.as_ref().map(|id| id.name.as_ref()), - musicbrainz: artist.musicbrainz.as_ref().map(|mb| mb.as_ref()), + musicbrainz: artist.musicbrainz.as_ref().map(AsRef::as_ref), properties: artist .properties .iter() @@ -51,6 +52,7 @@ impl<'a> From<&'a Album> for SerializeAlbum<'a> { SerializeAlbum { title: &album.id.title, seq: album.seq.0, + musicbrainz: album.musicbrainz.as_ref().map(AsRef::as_ref), } } } diff --git a/src/core/musichoard/musichoard.rs b/src/core/musichoard/musichoard.rs index cad9ec3..86d2ba8 100644 --- a/src/core/musichoard/musichoard.rs +++ b/src/core/musichoard/musichoard.rs @@ -3,7 +3,8 @@ use std::collections::HashMap; use crate::core::{ collection::{ album::{Album, AlbumDate, AlbumId, AlbumSeq}, - artist::{Artist, ArtistId, MusicBrainz}, + artist::{Artist, ArtistId}, + musicbrainz::MusicBrainz, track::{Track, TrackId, TrackNum, TrackQuality}, Collection, MergeCollections, }, @@ -140,12 +141,11 @@ impl MusicHoard { .find(|album| album.id == album_id) { Some(album) => album.tracks.push(track), - None => artist.albums.push(Album { - id: album_id, - date: album_date, - seq: AlbumSeq(0), - tracks: vec![track], - }), + None => { + let mut album = Album::new(album_id, album_date); + album.tracks.push(track); + artist.albums.push(album); + } } } @@ -353,15 +353,12 @@ impl MusicHoard { ) } - pub fn set_artist_musicbrainz, Mb: TryInto, E>( + pub fn set_artist_musicbrainz, S: AsRef>( &mut self, artist_id: Id, - url: Mb, - ) -> Result<(), Error> - where - Error: From, - { - let mb = url.try_into()?; + url: S, + ) -> Result<(), Error> { + let mb = MusicBrainz::artist_from_str(url)?; self.update_artist(artist_id.as_ref(), |artist| artist.set_musicbrainz_url(mb)) } @@ -470,7 +467,7 @@ mod tests { use mockall::{predicate, Sequence}; use crate::core::{ - collection::artist::{ArtistId, MusicBrainz}, + collection::{artist::ArtistId, musicbrainz::MusicBrainz}, database::{self, MockIDatabase}, library::{self, testmod::LIBRARY_ITEMS, MockILibrary}, testmod::{FULL_COLLECTION, LIBRARY_COLLECTION}, @@ -595,7 +592,7 @@ mod tests { .set_artist_musicbrainz(&artist_id, MUSICBUTLER) .unwrap_err(); let expected_err = Error::CollectionError(format!( - "an error occurred when processing a URL: invalid MusicBrainz URL: {MUSICBUTLER}" + "an error occurred when processing a URL: invalid artist MusicBrainz URL: {MUSICBUTLER}" )); assert_eq!(actual_err, expected_err); assert_eq!(actual_err.to_string(), expected_err.to_string()); @@ -626,7 +623,7 @@ mod tests { assert!(music_hoard .set_artist_musicbrainz(&artist_id, MUSICBRAINZ) .is_ok()); - _ = expected.insert(MUSICBRAINZ.try_into().unwrap()); + _ = expected.insert(MusicBrainz::artist_from_str(MUSICBRAINZ).unwrap()); assert_eq!(music_hoard.collection[0].musicbrainz, expected); // Clearing URLs on an artist that does not exist is an error. @@ -747,12 +744,9 @@ mod tests { let album_id_2 = AlbumId::new("another album"); let mut database_result = vec![Artist::new(artist_id.clone())]; - database_result[0].albums.push(Album { - id: album_id.clone(), - date: AlbumDate::default(), - seq: AlbumSeq::default(), - tracks: vec![], - }); + database_result[0] + .albums + .push(Album::new(album_id.clone(), AlbumDate::default())); database .expect_load() diff --git a/src/core/testmod.rs b/src/core/testmod.rs index a0620fe..651ffd9 100644 --- a/src/core/testmod.rs +++ b/src/core/testmod.rs @@ -1,9 +1,10 @@ use once_cell::sync::Lazy; -use std::{collections::HashMap, str::FromStr}; +use std::collections::HashMap; use crate::core::collection::{ album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq}, - artist::{Artist, ArtistId, MusicBrainz}, + artist::{Artist, ArtistId}, + musicbrainz::MusicBrainz, track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, }; use crate::tests::*; diff --git a/src/tests.rs b/src/tests.rs index c54bb6e..0ddae01 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -19,6 +19,7 @@ macro_rules! library_collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -79,6 +80,7 @@ macro_rules! library_collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -124,6 +126,7 @@ macro_rules! library_collection { day: 6, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -162,6 +165,7 @@ macro_rules! library_collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -200,6 +204,7 @@ macro_rules! library_collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -238,6 +243,7 @@ macro_rules! library_collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -288,6 +294,7 @@ macro_rules! library_collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -326,6 +333,7 @@ macro_rules! library_collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -374,6 +382,7 @@ macro_rules! library_collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -412,6 +421,7 @@ macro_rules! library_collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -454,7 +464,7 @@ macro_rules! full_collection { let artist_a = iter.next().unwrap(); assert_eq!(artist_a.id.name, "Album_Artist ‘A’"); - artist_a.musicbrainz = Some(MusicBrainz::from_str( + artist_a.musicbrainz = Some(MusicBrainz::artist_from_str( "https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000", ).unwrap()); @@ -472,10 +482,14 @@ macro_rules! full_collection { artist_a.albums[0].seq = AlbumSeq(1); artist_a.albums[1].seq = AlbumSeq(1); + artist_a.albums[0].musicbrainz = Some(MusicBrainz::album_from_str( + "https://musicbrainz.org/release-group/00000000-0000-0000-0000-000000000000" + ).unwrap()); + let artist_b = iter.next().unwrap(); assert_eq!(artist_b.id.name, "Album_Artist ‘B’"); - artist_b.musicbrainz = Some(MusicBrainz::from_str( + artist_b.musicbrainz = Some(MusicBrainz::artist_from_str( "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111", ).unwrap()); @@ -497,10 +511,18 @@ macro_rules! full_collection { artist_b.albums[2].seq = AlbumSeq(2); artist_b.albums[3].seq = AlbumSeq(4); + artist_b.albums[1].musicbrainz = Some(MusicBrainz::album_from_str( + "https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111111" + ).unwrap()); + + artist_b.albums[2].musicbrainz = Some(MusicBrainz::album_from_str( + "https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111112" + ).unwrap()); + let artist_c = iter.next().unwrap(); assert_eq!(artist_c.id.name, "The Album_Artist ‘C’"); - artist_c.musicbrainz = Some(MusicBrainz::from_str( + artist_c.musicbrainz = Some(MusicBrainz::artist_from_str( "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111", ).unwrap()); diff --git a/src/tui/testmod.rs b/src/tui/testmod.rs index 1d7e506..2b63a8e 100644 --- a/src/tui/testmod.rs +++ b/src/tui/testmod.rs @@ -1,8 +1,9 @@ -use std::{collections::HashMap, str::FromStr}; +use std::collections::HashMap; use musichoard::collection::{ album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq}, - artist::{Artist, ArtistId, MusicBrainz}, + artist::{Artist, ArtistId}, + musicbrainz::MusicBrainz, track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, }; use once_cell::sync::Lazy; diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 3959a03..119c15c 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -744,7 +744,7 @@ impl IUi for Ui { #[cfg(test)] mod tests { - use musichoard::collection::{album::AlbumId, artist::ArtistId}; + use musichoard::collection::artist::ArtistId; use crate::tui::{ app::{AppPublic, AppPublicInner, Delta}, @@ -868,12 +868,9 @@ mod tests { #[test] fn empty_album() { let mut artists: Vec = vec![Artist::new(ArtistId::new("An artist"))]; - artists[0].albums.push(Album { - id: AlbumId::new("An album"), - date: AlbumDate::default(), - seq: AlbumSeq::default(), - tracks: vec![], - }); + artists[0] + .albums + .push(Album::new("An album", AlbumDate::default())); let mut selection = Selection::new(&artists); draw_test_suite(&artists, &mut selection); diff --git a/tests/files/database/database.json b/tests/files/database/database.json index 3515a9b..ae7043e 100644 --- a/tests/files/database/database.json +++ b/tests/files/database/database.json @@ -1 +1 @@ -{"V20240302":[{"name":"Аркона","sort":"Arkona","musicbrainz":"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212","properties":{"Bandcamp":["https://arkonamoscow.bandcamp.com/"],"MusicButler":["https://www.musicbutler.io/artist-page/283448581"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums"]},"albums":[{"title":"Slovo","seq":0}]},{"name":"Eluveitie","sort":null,"musicbrainz":"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/269358403"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums"]},"albums":[{"title":"Vên [re‐recorded]","seq":0},{"title":"Slania","seq":0}]},{"name":"Frontside","sort":null,"musicbrainz":"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/826588800"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums"]},"albums":[{"title":"…nasze jest królestwo, potęga i chwała na wieki…","seq":0}]},{"name":"Heaven’s Basement","sort":"Heaven’s Basement","musicbrainz":"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/291158685"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums"]},"albums":[{"title":"Paper Plague","seq":0},{"title":"Unbreakable","seq":0}]},{"name":"Metallica","sort":null,"musicbrainz":"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/3996865"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums"]},"albums":[{"title":"Ride the Lightning","seq":0},{"title":"S&M","seq":0}]}]} \ No newline at end of file +{"V20240308":[{"name":"Аркона","sort":"Arkona","musicbrainz":"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212","properties":{"Bandcamp":["https://arkonamoscow.bandcamp.com/"],"MusicButler":["https://www.musicbutler.io/artist-page/283448581"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums"]},"albums":[{"title":"Slovo","seq":0,"musicbrainz":null}]},{"name":"Eluveitie","sort":null,"musicbrainz":"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/269358403"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums"]},"albums":[{"title":"Vên [re‐recorded]","seq":0,"musicbrainz":null},{"title":"Slania","seq":0,"musicbrainz":null}]},{"name":"Frontside","sort":null,"musicbrainz":"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/826588800"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums"]},"albums":[{"title":"…nasze jest królestwo, potęga i chwała na wieki…","seq":0,"musicbrainz":null}]},{"name":"Heaven’s Basement","sort":"Heaven’s Basement","musicbrainz":"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/291158685"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums"]},"albums":[{"title":"Paper Plague","seq":0,"musicbrainz":null},{"title":"Unbreakable","seq":0,"musicbrainz":null}]},{"name":"Metallica","sort":null,"musicbrainz":"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/3996865"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums"]},"albums":[{"title":"Ride the Lightning","seq":0,"musicbrainz":null},{"title":"S&M","seq":0,"musicbrainz":null}]}]} \ No newline at end of file diff --git a/tests/testlib.rs b/tests/testlib.rs index d2bc4d3..2ba7f20 100644 --- a/tests/testlib.rs +++ b/tests/testlib.rs @@ -1,9 +1,10 @@ use once_cell::sync::Lazy; -use std::{collections::HashMap, str::FromStr}; +use std::collections::HashMap; use musichoard::collection::{ album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq}, - artist::{Artist, ArtistId, MusicBrainz}, + artist::{Artist, ArtistId}, + musicbrainz::MusicBrainz, track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, Collection, }; @@ -17,7 +18,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { sort: Some(ArtistId{ name: String::from("Arkona") }), - musicbrainz: Some(MusicBrainz::from_str( + musicbrainz: Some(MusicBrainz::artist_from_str( "https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212" ).unwrap()), properties: HashMap::from([ @@ -41,6 +42,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -204,7 +206,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { name: String::from("Eluveitie"), }, sort: None, - musicbrainz: Some(MusicBrainz::from_str( + musicbrainz: Some(MusicBrainz::artist_from_str( "https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38", ).unwrap()), properties: HashMap::from([ @@ -226,6 +228,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -305,6 +308,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -447,7 +451,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { name: String::from("Frontside"), }, sort: None, - musicbrainz: Some(MusicBrainz::from_str( + musicbrainz: Some(MusicBrainz::artist_from_str( "https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490", ).unwrap()), properties: HashMap::from([ @@ -468,6 +472,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -600,7 +605,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { sort: Some(ArtistId { name: String::from("Heaven’s Basement"), }), - musicbrainz: Some(MusicBrainz::from_str( + musicbrainz: Some(MusicBrainz::artist_from_str( "https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc", ).unwrap()), properties: HashMap::from([ @@ -621,6 +626,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -644,6 +650,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -730,7 +737,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { name: String::from("Metallica"), }, sort: None, - musicbrainz: Some(MusicBrainz::from_str( + musicbrainz: Some(MusicBrainz::artist_from_str( "https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab", ).unwrap()), properties: HashMap::from([ @@ -752,6 +759,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId { @@ -853,6 +861,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { day: 0, }, seq: AlbumSeq(0), + musicbrainz: None, tracks: vec![ Track { id: TrackId {