Integrate properties with artist

This commit is contained in:
Wojciech Kozlowski 2023-05-21 17:00:48 +02:00
parent 97c9080d50
commit 5108ddf7a3
5 changed files with 205 additions and 69 deletions

View File

@ -67,7 +67,22 @@ mod tests {
use super::*; use super::*;
use crate::{tests::COLLECTION, Artist, ArtistId, Collection, Format}; use crate::{tests::COLLECTION, Artist, ArtistId, Collection, Format, IUrl};
fn opt_to_url<U: IUrl>(opt: &Option<U>) -> String {
match opt {
Some(mb) => format!("\"{}\"", mb.url()),
None => String::from("null"),
}
}
fn vec_to_urls<U: IUrl>(vec: &Vec<U>) -> String {
let mut urls: Vec<String> = vec![];
for item in vec.iter() {
urls.push(format!("\"{}\"", item.url()));
}
format!("[{}]", urls.join(","))
}
fn artist_to_json(artist: &Artist) -> String { fn artist_to_json(artist: &Artist) -> String {
let album_artist = &artist.id.name; let album_artist = &artist.id.name;
@ -114,11 +129,25 @@ mod tests {
} }
let albums = albums.join(","); let albums = albums.join(",");
let musicbrainz = opt_to_url(&artist.properties.musicbrainz);
let musicbutler = vec_to_urls(&artist.properties.musicbutler);
let bandcamp = vec_to_urls(&artist.properties.bandcamp);
let qobuz = opt_to_url(&artist.properties.qobuz);
let properties = format!(
"{{\
\"musicbrainz\":{musicbrainz},\
\"musicbutler\":{musicbutler},\
\"bandcamp\":{bandcamp},\
\"qobuz\":{qobuz}\
}}"
);
format!( format!(
"{{\ "{{\
\"id\":{{\ \"id\":{{\"name\":\"{album_artist}\"}},\
\"name\":\"{album_artist}\"\ \"properties\":{properties},\
}},\"albums\":[{albums}]\ \"albums\":[{albums}]\
}}" }}"
) )
} }

View File

@ -17,31 +17,15 @@ use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
/// [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). /// Object with trait contains a valid URL.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct Mbid {
string: String,
uuid: Uuid,
}
impl Mbid {
pub fn new<S: Into<String>>(uuid: S) -> Result<Self, Error> {
let string: String = uuid.into();
let uuid = Uuid::try_parse(&string)?;
Ok(Mbid { string, uuid })
}
pub fn as_str(&self) -> &str {
self.string.as_str()
}
}
pub trait IUrl { pub trait IUrl {
fn url(&self) -> &str; fn url(&self) -> &str;
} }
pub trait IUuid { /// Object with trait contains a [MusicBrainz
fn uuid(&self) -> &str; /// Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID).
pub trait IMbid {
fn mbid(&self) -> &str;
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
@ -60,25 +44,26 @@ struct InvalidUrlError {
/// MusicBrainz reference. /// MusicBrainz reference.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct MusicBrainz { pub struct MusicBrainz(Url);
url: Url,
mbid: Mbid,
}
impl MusicBrainz { impl MusicBrainz {
pub fn new<S: AsRef<str>>(url: S) -> Result<Self, Error> { pub fn new<S: AsRef<str>>(url: S) -> Result<Self, Error> {
let url = Url::parse(url.as_ref())?; let url = Url::parse(url.as_ref())?;
if !url.domain().map(|u| u.ends_with("musicbrainz.org")).unwrap_or(false) { if !url
return Err(Self::invalid_url_error(url).into()) .domain()
.map(|u| u.ends_with("musicbrainz.org"))
.unwrap_or(false)
{
return Err(Self::invalid_url_error(url).into());
} }
let mbid: Mbid = match url.path_segments().and_then(|mut ps| ps.nth(1)) { match url.path_segments().and_then(|mut ps| ps.nth(1)) {
Some(segment) => Mbid::new(segment)?, Some(segment) => Uuid::try_parse(&segment)?,
None => return Err(Self::invalid_url_error(url).into()), None => return Err(Self::invalid_url_error(url).into()),
}; };
Ok(MusicBrainz { url, mbid }) Ok(MusicBrainz(url))
} }
fn invalid_url_error<S: Into<String>>(url: S) -> InvalidUrlError { fn invalid_url_error<S: Into<String>>(url: S) -> InvalidUrlError {
@ -91,31 +76,34 @@ impl MusicBrainz {
impl IUrl for MusicBrainz { impl IUrl for MusicBrainz {
fn url(&self) -> &str { fn url(&self) -> &str {
self.url.as_str() self.0.as_str()
} }
} }
impl IUuid for MusicBrainz { impl IMbid for MusicBrainz {
fn uuid(&self) -> &str { fn mbid(&self) -> &str {
self.mbid.as_str() // The URL is assumed to have been validated.
self.0.path_segments().and_then(|mut ps| ps.nth(1)).unwrap()
} }
} }
/// MusicButler reference. /// MusicButler reference.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct MusicButler { pub struct MusicButler(Url);
url: Url,
}
impl MusicButler { impl MusicButler {
pub fn new<S: AsRef<str>>(url: S) -> Result<Self, Error> { pub fn new<S: AsRef<str>>(url: S) -> Result<Self, Error> {
let url = Url::parse(url.as_ref())?; let url = Url::parse(url.as_ref())?;
if !url.domain().map(|u| u.ends_with("musicbutler.io")).unwrap_or(false) { if !url
return Err(Self::invalid_url_error(url).into()) .domain()
.map(|u| u.ends_with("musicbutler.io"))
.unwrap_or(false)
{
return Err(Self::invalid_url_error(url).into());
} }
Ok(MusicButler { url }) Ok(MusicButler(url))
} }
fn invalid_url_error<S: Into<String>>(url: S) -> InvalidUrlError { fn invalid_url_error<S: Into<String>>(url: S) -> InvalidUrlError {
@ -128,25 +116,27 @@ impl MusicButler {
impl IUrl for MusicButler { impl IUrl for MusicButler {
fn url(&self) -> &str { fn url(&self) -> &str {
self.url.as_str() self.0.as_str()
} }
} }
/// Bandcamp reference. /// Bandcamp reference.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct Bandcamp { pub struct Bandcamp(Url);
url: Url,
}
impl Bandcamp { impl Bandcamp {
pub fn new<S: AsRef<str>>(url: S) -> Result<Self, Error> { pub fn new<S: AsRef<str>>(url: S) -> Result<Self, Error> {
let url = Url::parse(url.as_ref())?; let url = Url::parse(url.as_ref())?;
if !url.domain().map(|u| u.ends_with("bandcamp.com")).unwrap_or(false) { if !url
return Err(Self::invalid_url_error(url).into()) .domain()
.map(|u| u.ends_with("bandcamp.com"))
.unwrap_or(false)
{
return Err(Self::invalid_url_error(url).into());
} }
Ok(Bandcamp { url }) Ok(Bandcamp(url))
} }
fn invalid_url_error<S: Into<String>>(url: S) -> InvalidUrlError { fn invalid_url_error<S: Into<String>>(url: S) -> InvalidUrlError {
@ -159,25 +149,27 @@ impl Bandcamp {
impl IUrl for Bandcamp { impl IUrl for Bandcamp {
fn url(&self) -> &str { fn url(&self) -> &str {
self.url.as_str() self.0.as_str()
} }
} }
/// Qobuz reference. /// Qobuz reference.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct Qobuz { pub struct Qobuz(Url);
url: Url,
}
impl Qobuz { impl Qobuz {
pub fn new<S: AsRef<str>>(url: S) -> Result<Self, Error> { pub fn new<S: AsRef<str>>(url: S) -> Result<Self, Error> {
let url = Url::parse(url.as_ref())?; let url = Url::parse(url.as_ref())?;
if !url.domain().map(|u| u.ends_with("qobuz.com")).unwrap_or(false) { if !url
return Err(Self::invalid_url_error(url).into()) .domain()
.map(|u| u.ends_with("qobuz.com"))
.unwrap_or(false)
{
return Err(Self::invalid_url_error(url).into());
} }
Ok(Qobuz { url }) Ok(Qobuz(url))
} }
fn invalid_url_error<S: Into<String>>(url: S) -> InvalidUrlError { fn invalid_url_error<S: Into<String>>(url: S) -> InvalidUrlError {
@ -190,7 +182,7 @@ impl Qobuz {
impl IUrl for Qobuz { impl IUrl for Qobuz {
fn url(&self) -> &str { fn url(&self) -> &str {
self.url.as_str() self.0.as_str()
} }
} }
@ -283,7 +275,7 @@ pub struct ArtistId {
} }
/// The artist properties. /// The artist properties.
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct ArtistProperties { pub struct ArtistProperties {
pub musicbrainz: Option<MusicBrainz>, pub musicbrainz: Option<MusicBrainz>,
pub musicbutler: Vec<MusicButler>, pub musicbutler: Vec<MusicButler>,
@ -295,6 +287,7 @@ pub struct ArtistProperties {
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct Artist { pub struct Artist {
pub id: ArtistId, pub id: ArtistId,
pub properties: ArtistProperties,
pub albums: Vec<Album>, pub albums: Vec<Album>,
} }
@ -535,6 +528,7 @@ impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> {
album_ids.insert(artist_id.clone(), HashSet::<AlbumId>::new()); album_ids.insert(artist_id.clone(), HashSet::<AlbumId>::new());
artists.push(Artist { artists.push(Artist {
id: artist_id.clone(), id: artist_id.clone(),
properties: ArtistProperties::default(),
albums: vec![], albums: vec![],
}); });
artists.last_mut().unwrap() artists.last_mut().unwrap()
@ -611,13 +605,20 @@ mod tests {
items items
} }
fn clean_collection(mut collection: Collection) -> Collection {
for artist in collection.iter_mut() {
artist.properties = ArtistProperties::default();
}
collection
}
#[test] #[test]
fn musicbrainz() { fn musicbrainz() {
let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8"; let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8";
let url = format!("https://musicbrainz.org/artist/{uuid}"); let url = format!("https://musicbrainz.org/artist/{uuid}");
let mb = MusicBrainz::new(&url).unwrap(); let mb = MusicBrainz::new(&url).unwrap();
assert_eq!(url, mb.url()); assert_eq!(url, mb.url());
assert_eq!(uuid, mb.uuid()); assert_eq!(uuid, mb.mbid());
let url = format!("https://musicbrainz.org/artist/i-am-not-a-uuid"); let url = format!("https://musicbrainz.org/artist/i-am-not-a-uuid");
assert!(MusicBrainz::new(&url).is_err()); assert!(MusicBrainz::new(&url).is_err());
@ -787,7 +788,7 @@ mod tests {
let mut music_hoard = MusicHoard::new(library, database); let mut music_hoard = MusicHoard::new(library, database);
music_hoard.rescan_library().unwrap(); music_hoard.rescan_library().unwrap();
assert_eq!(music_hoard.get_collection(), &*COLLECTION); assert_eq!(music_hoard.get_collection(), &clean_collection(COLLECTION.to_owned()));
} }
#[test] #[test]
@ -811,7 +812,7 @@ mod tests {
let mut music_hoard = MusicHoard::new(library, database); let mut music_hoard = MusicHoard::new(library, database);
music_hoard.rescan_library().unwrap(); music_hoard.rescan_library().unwrap();
assert_eq!(music_hoard.get_collection(), &*COLLECTION); assert_eq!(music_hoard.get_collection(), &clean_collection(COLLECTION.to_owned()));
} }
#[test] #[test]
@ -819,7 +820,7 @@ mod tests {
let mut library = MockILibrary::new(); let mut library = MockILibrary::new();
let database = MockIDatabase::new(); let database = MockIDatabase::new();
let mut expected = COLLECTION.to_owned(); let mut expected = clean_collection(COLLECTION.to_owned());
expected[0].albums[0].id.year = expected[1].albums[0].id.year; expected[0].albums[0].id.year = expected[1].albums[0].id.year;
expected[0].albums[0].id.title = expected[1].albums[0].id.title.clone(); expected[0].albums[0].id.title = expected[1].albums[0].id.title.clone();
@ -865,7 +866,7 @@ mod tests {
let library_input = Query::new(); let library_input = Query::new();
let library_result = Ok(artists_to_items(&COLLECTION)); let library_result = Ok(artists_to_items(&COLLECTION));
let database_input = COLLECTION.to_owned(); let database_input = clean_collection(COLLECTION.to_owned());
let database_result = Ok(()); let database_result = Ok(());
library library
@ -883,7 +884,7 @@ mod tests {
let mut music_hoard = MusicHoard::new(library, database); let mut music_hoard = MusicHoard::new(library, database);
music_hoard.rescan_library().unwrap(); music_hoard.rescan_library().unwrap();
assert_eq!(music_hoard.get_collection(), &*COLLECTION); assert_eq!(music_hoard.get_collection(), &clean_collection(COLLECTION.to_owned()));
music_hoard.save_to_database().unwrap(); music_hoard.save_to_database().unwrap();
} }

View File

@ -5,6 +5,20 @@ macro_rules! collection {
id: ArtistId { id: ArtistId {
name: "album_artist a".to_string(), name: "album_artist a".to_string(),
}, },
properties: ArtistProperties {
musicbrainz: Some(MusicBrainz::new(
"https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000",
).unwrap()),
musicbutler: vec![
MusicButler::new(
"https://www.musicbutler.io/artist-page/000000000"
).unwrap(),
],
bandcamp: vec![],
qobuz: Some(Qobuz::new(
"https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums",
).unwrap()),
},
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { id: AlbumId {
@ -86,6 +100,25 @@ macro_rules! collection {
id: ArtistId { id: ArtistId {
name: "album_artist b".to_string(), name: "album_artist b".to_string(),
}, },
properties: ArtistProperties {
musicbrainz: Some(MusicBrainz::new(
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111",
).unwrap()),
musicbutler: vec![
MusicButler::new(
"https://www.musicbutler.io/artist-page/111111111"
).unwrap(),
MusicButler::new(
"https://www.musicbutler.io/artist-page/111111112"
).unwrap(),
],
bandcamp: vec![
Bandcamp::new("https://artist-b.bandcamp.com/").unwrap(),
],
qobuz: Some(Qobuz::new(
"https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums",
).unwrap()),
},
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { id: AlbumId {
@ -159,6 +192,14 @@ macro_rules! collection {
id: ArtistId { id: ArtistId {
name: "album_artist c".to_string(), name: "album_artist c".to_string(),
}, },
properties: ArtistProperties {
musicbrainz: Some(MusicBrainz::new(
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111",
).unwrap()),
musicbutler: vec![],
bandcamp: vec![],
qobuz: None,
},
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { id: AlbumId {

File diff suppressed because one or more lines are too long

View File

@ -1,15 +1,32 @@
mod database; mod database;
mod library; mod library;
use musichoard::{Album, AlbumId, Artist, ArtistId, Format, Quality, Track, TrackId}; use musichoard::{
Album, AlbumId, Artist, ArtistId, ArtistProperties, Bandcamp, Collection, Format, MusicBrainz,
MusicButler, Qobuz, Quality, Track, TrackId,
};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| { static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
vec![ vec![
Artist { Artist {
id: ArtistId { id: ArtistId {
name: String::from("Аркона"), name: String::from("Аркона"),
}, },
properties: ArtistProperties {
musicbrainz: Some(MusicBrainz::new(
"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212",
).unwrap()),
musicbutler: vec![
MusicButler::new("https://www.musicbutler.io/artist-page/283448581").unwrap(),
],
bandcamp: vec![
Bandcamp::new("https://arkonamoscow.bandcamp.com/").unwrap(),
],
qobuz: Some(Qobuz::new(
"https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums",
).unwrap()),
},
albums: vec![Album { albums: vec![Album {
id: AlbumId { id: AlbumId {
year: 2011, year: 2011,
@ -177,6 +194,18 @@ static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| {
id: ArtistId { id: ArtistId {
name: String::from("Eluveitie"), name: String::from("Eluveitie"),
}, },
properties: ArtistProperties {
musicbrainz: Some(MusicBrainz::new(
"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38",
).unwrap()),
musicbutler: vec![
MusicButler::new("https://www.musicbutler.io/artist-page/269358403").unwrap(),
],
bandcamp: vec![],
qobuz: Some(Qobuz::new(
"https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums",
).unwrap()),
},
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { id: AlbumId {
@ -398,6 +427,18 @@ static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| {
id: ArtistId { id: ArtistId {
name: String::from("Frontside"), name: String::from("Frontside"),
}, },
properties: ArtistProperties {
musicbrainz: Some(MusicBrainz::new(
"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490",
).unwrap()),
musicbutler: vec![
MusicButler::new("https://www.musicbutler.io/artist-page/826588800").unwrap(),
],
bandcamp: vec![],
qobuz: Some(Qobuz::new(
"https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums",
).unwrap()),
},
albums: vec![Album { albums: vec![Album {
id: AlbumId { id: AlbumId {
year: 2001, year: 2001,
@ -532,6 +573,18 @@ static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| {
id: ArtistId { id: ArtistId {
name: String::from("Heavens Basement"), name: String::from("Heavens Basement"),
}, },
properties: ArtistProperties {
musicbrainz: Some(MusicBrainz::new(
"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc",
).unwrap()),
musicbutler: vec![
MusicButler::new("https://www.musicbutler.io/artist-page/291158685").unwrap(),
],
bandcamp: vec![],
qobuz: Some(Qobuz::new(
"https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums",
).unwrap()),
},
albums: vec![Album { albums: vec![Album {
id: AlbumId { id: AlbumId {
year: 2011, year: 2011,
@ -622,6 +675,18 @@ static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| {
id: ArtistId { id: ArtistId {
name: String::from("Metallica"), name: String::from("Metallica"),
}, },
properties: ArtistProperties {
musicbrainz: Some(MusicBrainz::new(
"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab",
).unwrap()),
musicbutler: vec![
MusicButler::new("https://www.musicbutler.io/artist-page/3996865").unwrap(),
],
bandcamp: vec![],
qobuz: Some(Qobuz::new(
"https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums",
).unwrap()),
},
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { id: AlbumId {