From 0040a5599eef0205366a692cee30a6dc31e0444b Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 21 May 2023 00:33:22 +0200 Subject: [PATCH] Add ArtistProperties --- Cargo.lock | 68 +++++++ Cargo.toml | 1 + src/database/json/mod.rs | 37 +++- src/lib.rs | 296 ++++++++++++++++++++++++++++- src/testlib.rs | 41 ++++ tests/files/database/database.json | 2 +- tests/lib.rs | 69 ++++++- 7 files changed, 500 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d4320f6..491d365 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,6 +184,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fragile" version = "2.0.0" @@ -225,6 +234,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "instant" version = "0.1.12" @@ -356,6 +375,7 @@ dependencies = [ "structopt", "tempfile", "tokio", + "url", "uuid", ] @@ -436,6 +456,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -801,6 +827,21 @@ dependencies = [ "syn 2.0.11", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.27.0" @@ -859,12 +900,27 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.10.1" @@ -877,6 +933,18 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + [[package]] name = "uuid" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 4ef252b..33dc5d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ serde = { version = "1.0.159", features = ["derive"] } serde_json = { version = "1.0.95", optional = true} structopt = { version = "0.3.26", optional = true} tokio = { version = "1.27.0", features = ["rt"], optional = true} +url = { version = "2.3.1", features = ["serde"] } uuid = { version = "1.3.0", features = ["serde"] } [dev-dependencies] diff --git a/src/database/json/mod.rs b/src/database/json/mod.rs index 14099d8..af200ca 100644 --- a/src/database/json/mod.rs +++ b/src/database/json/mod.rs @@ -67,7 +67,22 @@ mod tests { use super::*; - use crate::{tests::COLLECTION, Artist, ArtistId, Collection, Format}; + use crate::{tests::COLLECTION, Artist, ArtistId, Collection, Format, IUrl}; + + fn opt_to_url(opt: &Option) -> String { + match opt { + Some(mb) => format!("\"{}\"", mb.url()), + None => String::from("null"), + } + } + + fn vec_to_urls(vec: &Vec) -> String { + let mut urls: Vec = vec![]; + for item in vec.iter() { + urls.push(format!("\"{}\"", item.url())); + } + format!("[{}]", urls.join(",")) + } fn artist_to_json(artist: &Artist) -> String { let album_artist = &artist.id.name; @@ -114,11 +129,25 @@ mod tests { } 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!( "{{\ - \"id\":{{\ - \"name\":\"{album_artist}\"\ - }},\"albums\":[{albums}]\ + \"id\":{{\"name\":\"{album_artist}\"}},\ + \"properties\":{properties},\ + \"albums\":[{albums}]\ }}" ) } diff --git a/src/lib.rs b/src/lib.rs index 4dcdda4..035d134 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,10 +14,182 @@ use std::{ use database::IDatabase; use library::{ILibrary, Item, Query}; use serde::{Deserialize, Serialize}; +use url::Url; use uuid::Uuid; -/// [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). -pub type Mbid = Uuid; +/// Object with trait contains a valid URL. +pub trait IUrl { + fn url(&self) -> &str; +} + +/// Object with trait contains a [MusicBrainz +/// Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). +pub trait IMbid { + fn mbid(&self) -> &str; +} + +#[derive(Debug)] +enum UrlType { + MusicBrainz, + MusicButler, + Bandcamp, + Qobuz, +} + +struct InvalidUrlError { + url_type: UrlType, + url: String, +} + +impl fmt::Display for InvalidUrlError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "invalid url of type {:?}: {}", self.url_type, self.url) + } +} + +/// MusicBrainz reference. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct MusicBrainz(Url); + +impl MusicBrainz { + pub fn new>(url: S) -> Result { + let url = Url::parse(url.as_ref())?; + + if !url + .domain() + .map(|u| u.ends_with("musicbrainz.org")) + .unwrap_or(false) + { + return Err(Self::invalid_url_error(url).into()); + } + + 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).into()), + }; + + Ok(MusicBrainz(url)) + } + + fn invalid_url_error>(url: S) -> InvalidUrlError { + InvalidUrlError { + url_type: UrlType::MusicBrainz, + url: url.into(), + } + } +} + +impl IUrl for MusicBrainz { + fn url(&self) -> &str { + self.0.as_str() + } +} + +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() + } +} + +/// MusicButler reference. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct MusicButler(Url); + +impl MusicButler { + pub fn new>(url: S) -> Result { + let url = Url::parse(url.as_ref())?; + + if !url + .domain() + .map(|u| u.ends_with("musicbutler.io")) + .unwrap_or(false) + { + return Err(Self::invalid_url_error(url).into()); + } + + Ok(MusicButler(url)) + } + + fn invalid_url_error>(url: S) -> InvalidUrlError { + InvalidUrlError { + url_type: UrlType::MusicButler, + url: url.into(), + } + } +} + +impl IUrl for MusicButler { + fn url(&self) -> &str { + self.0.as_str() + } +} + +/// Bandcamp reference. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct Bandcamp(Url); + +impl Bandcamp { + pub fn new>(url: S) -> Result { + let url = Url::parse(url.as_ref())?; + + if !url + .domain() + .map(|u| u.ends_with("bandcamp.com")) + .unwrap_or(false) + { + return Err(Self::invalid_url_error(url).into()); + } + + Ok(Bandcamp(url)) + } + + fn invalid_url_error>(url: S) -> InvalidUrlError { + InvalidUrlError { + url_type: UrlType::Bandcamp, + url: url.into(), + } + } +} + +impl IUrl for Bandcamp { + fn url(&self) -> &str { + self.0.as_str() + } +} + +/// Qobuz reference. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct Qobuz(Url); + +impl Qobuz { + pub fn new>(url: S) -> Result { + let url = Url::parse(url.as_ref())?; + + if !url + .domain() + .map(|u| u.ends_with("qobuz.com")) + .unwrap_or(false) + { + return Err(Self::invalid_url_error(url).into()); + } + + Ok(Qobuz(url)) + } + + fn invalid_url_error>(url: S) -> InvalidUrlError { + InvalidUrlError { + url_type: UrlType::Qobuz, + url: url.into(), + } + } +} + +impl IUrl for Qobuz { + fn url(&self) -> &str { + self.0.as_str() + } +} /// The track file format. #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] @@ -107,10 +279,20 @@ pub struct ArtistId { pub name: String, } +/// The artist properties. +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +pub struct ArtistProperties { + pub musicbrainz: Option, + pub musicbutler: Vec, + pub bandcamp: Vec, + pub qobuz: Option, +} + /// An artist. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct Artist { pub id: ArtistId, + pub properties: ArtistProperties, pub albums: Vec, } @@ -194,6 +376,10 @@ pub enum Error { LibraryError(String), /// The [`MusicHoard`] failed to read/write from/to the database. DatabaseError(String), + /// The [`MusicHoard`] failed to parse a user-provided URL. + UrlParseError(String), + /// The user-provided URL is not valid. + InvalidUrlError(String), } impl fmt::Display for Error { @@ -203,6 +389,8 @@ impl fmt::Display for Error { Self::DatabaseError(ref s) => { write!(f, "failed to read/write from/to the database: {s}") } + Self::UrlParseError(ref s) => write!(f, "failed to parse a user-provided URL: {s}"), + Self::InvalidUrlError(ref s) => write!(f, "user-provided URL is invalid: {s}"), } } } @@ -225,6 +413,24 @@ impl From for Error { } } +impl From for Error { + fn from(err: url::ParseError) -> Error { + Error::UrlParseError(err.to_string()) + } +} + +impl From for Error { + fn from(err: uuid::Error) -> Error { + Error::UrlParseError(err.to_string()) + } +} + +impl From for Error { + fn from(err: InvalidUrlError) -> Error { + Error::InvalidUrlError(err.to_string()) + } +} + /// The Music Hoard. It is responsible for pulling information from both the library and the /// database, ensuring its consistent and writing back any changes. pub struct MusicHoard { @@ -327,6 +533,7 @@ impl MusicHoard { album_ids.insert(artist_id.clone(), HashSet::::new()); artists.push(Artist { id: artist_id.clone(), + properties: ArtistProperties::default(), albums: vec![], }); artists.last_mut().unwrap() @@ -403,6 +610,72 @@ mod tests { items } + fn clean_collection(mut collection: Collection) -> Collection { + for artist in collection.iter_mut() { + artist.properties = ArtistProperties::default(); + } + collection + } + + #[test] + fn musicbrainz() { + let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8"; + let url = format!("https://musicbrainz.org/artist/{uuid}"); + let mb = MusicBrainz::new(&url).unwrap(); + assert_eq!(url, mb.url()); + assert_eq!(uuid, mb.mbid()); + + let url = format!("not a url at all"); + let expected_error: Error = url::ParseError::RelativeUrlWithoutBase.into(); + let actual_error = MusicBrainz::new(&url).unwrap_err(); + assert_eq!(actual_error, expected_error); + assert_eq!(actual_error.to_string(), expected_error.to_string()); + + let url = format!("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::new(&url).unwrap_err(); + assert_eq!(actual_error, expected_error); + assert_eq!(actual_error.to_string(), expected_error.to_string()); + + let url = format!("https://musicbrainz.org/artist"); + let expected_error: Error = InvalidUrlError { + url_type: UrlType::MusicBrainz, + url: url.clone(), + } + .into(); + let actual_error = MusicBrainz::new(&url).unwrap_err(); + assert_eq!(actual_error, expected_error); + assert_eq!(actual_error.to_string(), expected_error.to_string()); + } + + #[test] + fn urls() { + let musicbrainz = "https://musicbrainz.org/artist/d368baa8-21ca-4759-9731-0b2753071ad8"; + let musicbutler = "https://www.musicbutler.io/artist-page/483340948"; + let bandcamp = "https://thelasthangmen.bandcamp.com/"; + let qobuz = "https://www.qobuz.com/nl-nl/interpreter/the-last-hangmen/1244413"; + + assert!(MusicBrainz::new(&musicbrainz).is_ok()); + assert!(MusicBrainz::new(&musicbutler).is_err()); + assert!(MusicBrainz::new(&bandcamp).is_err()); + assert!(MusicBrainz::new(&qobuz).is_err()); + + assert!(MusicButler::new(&musicbrainz).is_err()); + assert!(MusicButler::new(&musicbutler).is_ok()); + assert!(MusicButler::new(&bandcamp).is_err()); + assert!(MusicButler::new(&qobuz).is_err()); + + assert!(Bandcamp::new(&musicbrainz).is_err()); + assert!(Bandcamp::new(&musicbutler).is_err()); + assert!(Bandcamp::new(&bandcamp).is_ok()); + assert!(Bandcamp::new(&qobuz).is_err()); + + assert!(Qobuz::new(&musicbrainz).is_err()); + assert!(Qobuz::new(&musicbutler).is_err()); + assert!(Qobuz::new(&bandcamp).is_err()); + assert!(Qobuz::new(&qobuz).is_ok()); + } + #[test] fn merge_track() { let left = Track { @@ -536,7 +809,10 @@ mod tests { let mut music_hoard = MusicHoard::new(library, database); music_hoard.rescan_library().unwrap(); - assert_eq!(music_hoard.get_collection(), &*COLLECTION); + assert_eq!( + music_hoard.get_collection(), + &clean_collection(COLLECTION.to_owned()) + ); } #[test] @@ -560,7 +836,10 @@ mod tests { let mut music_hoard = MusicHoard::new(library, database); music_hoard.rescan_library().unwrap(); - assert_eq!(music_hoard.get_collection(), &*COLLECTION); + assert_eq!( + music_hoard.get_collection(), + &clean_collection(COLLECTION.to_owned()) + ); } #[test] @@ -568,7 +847,7 @@ mod tests { let mut library = MockILibrary::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.title = expected[1].albums[0].id.title.clone(); @@ -614,7 +893,7 @@ mod tests { let library_input = Query::new(); 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(()); library @@ -632,7 +911,10 @@ mod tests { let mut music_hoard = MusicHoard::new(library, database); 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(); } diff --git a/src/testlib.rs b/src/testlib.rs index a790b10..0a4c306 100644 --- a/src/testlib.rs +++ b/src/testlib.rs @@ -5,6 +5,20 @@ macro_rules! collection { id: ArtistId { 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![ Album { id: AlbumId { @@ -86,6 +100,25 @@ macro_rules! collection { id: ArtistId { 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![ Album { id: AlbumId { @@ -159,6 +192,14 @@ macro_rules! collection { id: ArtistId { 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![ Album { id: AlbumId { diff --git a/tests/files/database/database.json b/tests/files/database/database.json index 1c2d62c..de471d8 100644 --- a/tests/files/database/database.json +++ b/tests/files/database/database.json @@ -1 +1 @@ -[{"id":{"name":"Аркона"},"albums":[{"id":{"year":2011,"title":"Slovo"},"tracks":[{"id":{"number":1,"title":"Az’"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":992}},{"id":{"number":2,"title":"Arkaim"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1061}},{"id":{"number":3,"title":"Bol’no mne"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1004}},{"id":{"number":4,"title":"Leshiy"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1077}},{"id":{"number":5,"title":"Zakliatie"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1041}},{"id":{"number":6,"title":"Predok"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":756}},{"id":{"number":7,"title":"Nikogda"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1059}},{"id":{"number":8,"title":"Tam za tumanami"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1023}},{"id":{"number":9,"title":"Potomok"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":838}},{"id":{"number":10,"title":"Slovo"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1028}},{"id":{"number":11,"title":"Odna"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":991}},{"id":{"number":12,"title":"Vo moiom sadochke…"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":919}},{"id":{"number":13,"title":"Stenka na stenku"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1039}},{"id":{"number":14,"title":"Zimushka"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":974}}]}]},{"id":{"name":"Eluveitie"},"albums":[{"id":{"year":2008,"title":"Slania"},"tracks":[{"id":{"number":1,"title":"Samon"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":953}},{"id":{"number":2,"title":"Primordial Breath"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1103}},{"id":{"number":3,"title":"Inis Mona"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1117}},{"id":{"number":4,"title":"Gray Sublime Archon"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1092}},{"id":{"number":5,"title":"Anagantios"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":923}},{"id":{"number":6,"title":"Bloodstained Ground"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1098}},{"id":{"number":7,"title":"The Somber Lay"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1068}},{"id":{"number":8,"title":"Slanias Song"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1098}},{"id":{"number":9,"title":"Giamonios"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":825}},{"id":{"number":10,"title":"Tarvos"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1115}},{"id":{"number":11,"title":"Calling the Rain"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1096}},{"id":{"number":12,"title":"Elembivos"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1059}}]},{"id":{"year":2004,"title":"Vên [re‐recorded]"},"tracks":[{"id":{"number":1,"title":"Verja Urit an Bitus"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":961}},{"id":{"number":2,"title":"Uis Elveti"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1067}},{"id":{"number":3,"title":"Ôrô"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":933}},{"id":{"number":4,"title":"Lament"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1083}},{"id":{"number":5,"title":"Druid"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1073}},{"id":{"number":6,"title":"Jêzaïg"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1002}}]}]},{"id":{"name":"Frontside"},"albums":[{"id":{"year":2001,"title":"…nasze jest królestwo, potęga i chwała na wieki…"},"tracks":[{"id":{"number":1,"title":"Intro = Chaos"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1024}},{"id":{"number":2,"title":"Modlitwa"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1073}},{"id":{"number":3,"title":"Długa droga z piekła"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1058}},{"id":{"number":4,"title":"Synowie ognia"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1066}},{"id":{"number":5,"title":"1902"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1074}},{"id":{"number":6,"title":"Krew za krew"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1080}},{"id":{"number":7,"title":"Kulminacja"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":992}},{"id":{"number":8,"title":"Judasz"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1018}},{"id":{"number":9,"title":"Więzy"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1077}},{"id":{"number":10,"title":"Zagubione dusze"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1033}},{"id":{"number":11,"title":"Linia życia"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":987}}]}]},{"id":{"name":"Heaven’s Basement"},"albums":[{"id":{"year":2011,"title":"Unbreakable"},"tracks":[{"id":{"number":1,"title":"Unbreakable"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":208}},{"id":{"number":2,"title":"Guilt Trips and Sins"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":205}},{"id":{"number":3,"title":"The Long Goodbye"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":227}},{"id":{"number":4,"title":"Close Encounters"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":213}},{"id":{"number":5,"title":"Paranoia"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":218}},{"id":{"number":6,"title":"Let Me Out of Here"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":207}},{"id":{"number":7,"title":"Leeches"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":225}}]}]},{"id":{"name":"Metallica"},"albums":[{"id":{"year":1984,"title":"Ride the Lightning"},"tracks":[{"id":{"number":1,"title":"Fight Fire with Fire"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":954}},{"id":{"number":2,"title":"Ride the Lightning"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":951}},{"id":{"number":3,"title":"For Whom the Bell Tolls"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":889}},{"id":{"number":4,"title":"Fade to Black"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":939}},{"id":{"number":5,"title":"Trapped under Ice"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":955}},{"id":{"number":6,"title":"Escape"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":941}},{"id":{"number":7,"title":"Creeping Death"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":958}},{"id":{"number":8,"title":"The Call of Ktulu"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":888}}]},{"id":{"year":1999,"title":"S&M"},"tracks":[{"id":{"number":1,"title":"The Ecstasy of Gold"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":875}},{"id":{"number":2,"title":"The Call of Ktulu"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1030}},{"id":{"number":3,"title":"Master of Puppets"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1082}},{"id":{"number":4,"title":"Of Wolf and Man"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1115}},{"id":{"number":5,"title":"The Thing That Should Not Be"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1029}},{"id":{"number":6,"title":"Fuel"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1057}},{"id":{"number":7,"title":"The Memory Remains"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1080}},{"id":{"number":8,"title":"No Leaf Clover"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1004}},{"id":{"number":9,"title":"Hero of the Day"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":962}},{"id":{"number":10,"title":"Devil’s Dance"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1076}},{"id":{"number":11,"title":"Bleeding Me"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":993}},{"id":{"number":12,"title":"Nothing Else Matters"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":875}},{"id":{"number":13,"title":"Until It Sleeps"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1038}},{"id":{"number":14,"title":"For Whom the Bell Tolls"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1072}},{"id":{"number":15,"title":"−Human"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1029}},{"id":{"number":16,"title":"Wherever I May Roam"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1035}},{"id":{"number":17,"title":"Outlaw Torn"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1042}},{"id":{"number":18,"title":"Sad but True"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1082}},{"id":{"number":19,"title":"One"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1017}},{"id":{"number":20,"title":"Enter Sandman"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":993}},{"id":{"number":21,"title":"Battery"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":967}}]}]}] \ No newline at end of file +[{"id":{"name":"Аркона"},"properties":{"musicbrainz":"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212","musicbutler":["https://www.musicbutler.io/artist-page/283448581"],"bandcamp":["https://arkonamoscow.bandcamp.com/"],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums"},"albums":[{"id":{"year":2011,"title":"Slovo"},"tracks":[{"id":{"number":1,"title":"Az’"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":992}},{"id":{"number":2,"title":"Arkaim"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1061}},{"id":{"number":3,"title":"Bol’no mne"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1004}},{"id":{"number":4,"title":"Leshiy"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1077}},{"id":{"number":5,"title":"Zakliatie"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1041}},{"id":{"number":6,"title":"Predok"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":756}},{"id":{"number":7,"title":"Nikogda"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1059}},{"id":{"number":8,"title":"Tam za tumanami"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1023}},{"id":{"number":9,"title":"Potomok"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":838}},{"id":{"number":10,"title":"Slovo"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1028}},{"id":{"number":11,"title":"Odna"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":991}},{"id":{"number":12,"title":"Vo moiom sadochke…"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":919}},{"id":{"number":13,"title":"Stenka na stenku"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1039}},{"id":{"number":14,"title":"Zimushka"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":974}}]}]},{"id":{"name":"Eluveitie"},"properties":{"musicbrainz":"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38","musicbutler":["https://www.musicbutler.io/artist-page/269358403"],"bandcamp":[],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums"},"albums":[{"id":{"year":2008,"title":"Slania"},"tracks":[{"id":{"number":1,"title":"Samon"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":953}},{"id":{"number":2,"title":"Primordial Breath"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1103}},{"id":{"number":3,"title":"Inis Mona"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1117}},{"id":{"number":4,"title":"Gray Sublime Archon"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1092}},{"id":{"number":5,"title":"Anagantios"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":923}},{"id":{"number":6,"title":"Bloodstained Ground"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1098}},{"id":{"number":7,"title":"The Somber Lay"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1068}},{"id":{"number":8,"title":"Slanias Song"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1098}},{"id":{"number":9,"title":"Giamonios"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":825}},{"id":{"number":10,"title":"Tarvos"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1115}},{"id":{"number":11,"title":"Calling the Rain"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1096}},{"id":{"number":12,"title":"Elembivos"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1059}}]},{"id":{"year":2004,"title":"Vên [re‐recorded]"},"tracks":[{"id":{"number":1,"title":"Verja Urit an Bitus"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":961}},{"id":{"number":2,"title":"Uis Elveti"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1067}},{"id":{"number":3,"title":"Ôrô"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":933}},{"id":{"number":4,"title":"Lament"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1083}},{"id":{"number":5,"title":"Druid"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1073}},{"id":{"number":6,"title":"Jêzaïg"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1002}}]}]},{"id":{"name":"Frontside"},"properties":{"musicbrainz":"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490","musicbutler":["https://www.musicbutler.io/artist-page/826588800"],"bandcamp":[],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums"},"albums":[{"id":{"year":2001,"title":"…nasze jest królestwo, potęga i chwała na wieki…"},"tracks":[{"id":{"number":1,"title":"Intro = Chaos"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1024}},{"id":{"number":2,"title":"Modlitwa"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1073}},{"id":{"number":3,"title":"Długa droga z piekła"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1058}},{"id":{"number":4,"title":"Synowie ognia"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1066}},{"id":{"number":5,"title":"1902"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1074}},{"id":{"number":6,"title":"Krew za krew"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1080}},{"id":{"number":7,"title":"Kulminacja"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":992}},{"id":{"number":8,"title":"Judasz"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1018}},{"id":{"number":9,"title":"Więzy"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1077}},{"id":{"number":10,"title":"Zagubione dusze"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1033}},{"id":{"number":11,"title":"Linia życia"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":987}}]}]},{"id":{"name":"Heaven’s Basement"},"properties":{"musicbrainz":"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc","musicbutler":["https://www.musicbutler.io/artist-page/291158685"],"bandcamp":[],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums"},"albums":[{"id":{"year":2011,"title":"Unbreakable"},"tracks":[{"id":{"number":1,"title":"Unbreakable"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":208}},{"id":{"number":2,"title":"Guilt Trips and Sins"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":205}},{"id":{"number":3,"title":"The Long Goodbye"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":227}},{"id":{"number":4,"title":"Close Encounters"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":213}},{"id":{"number":5,"title":"Paranoia"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":218}},{"id":{"number":6,"title":"Let Me Out of Here"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":207}},{"id":{"number":7,"title":"Leeches"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":225}}]}]},{"id":{"name":"Metallica"},"properties":{"musicbrainz":"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab","musicbutler":["https://www.musicbutler.io/artist-page/3996865"],"bandcamp":[],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums"},"albums":[{"id":{"year":1984,"title":"Ride the Lightning"},"tracks":[{"id":{"number":1,"title":"Fight Fire with Fire"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":954}},{"id":{"number":2,"title":"Ride the Lightning"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":951}},{"id":{"number":3,"title":"For Whom the Bell Tolls"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":889}},{"id":{"number":4,"title":"Fade to Black"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":939}},{"id":{"number":5,"title":"Trapped under Ice"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":955}},{"id":{"number":6,"title":"Escape"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":941}},{"id":{"number":7,"title":"Creeping Death"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":958}},{"id":{"number":8,"title":"The Call of Ktulu"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":888}}]},{"id":{"year":1999,"title":"S&M"},"tracks":[{"id":{"number":1,"title":"The Ecstasy of Gold"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":875}},{"id":{"number":2,"title":"The Call of Ktulu"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1030}},{"id":{"number":3,"title":"Master of Puppets"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1082}},{"id":{"number":4,"title":"Of Wolf and Man"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1115}},{"id":{"number":5,"title":"The Thing That Should Not Be"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1029}},{"id":{"number":6,"title":"Fuel"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1057}},{"id":{"number":7,"title":"The Memory Remains"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1080}},{"id":{"number":8,"title":"No Leaf Clover"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1004}},{"id":{"number":9,"title":"Hero of the Day"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":962}},{"id":{"number":10,"title":"Devil’s Dance"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1076}},{"id":{"number":11,"title":"Bleeding Me"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":993}},{"id":{"number":12,"title":"Nothing Else Matters"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":875}},{"id":{"number":13,"title":"Until It Sleeps"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1038}},{"id":{"number":14,"title":"For Whom the Bell Tolls"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1072}},{"id":{"number":15,"title":"−Human"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1029}},{"id":{"number":16,"title":"Wherever I May Roam"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1035}},{"id":{"number":17,"title":"Outlaw Torn"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1042}},{"id":{"number":18,"title":"Sad but True"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1082}},{"id":{"number":19,"title":"One"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1017}},{"id":{"number":20,"title":"Enter Sandman"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":993}},{"id":{"number":21,"title":"Battery"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":967}}]}]}] \ No newline at end of file diff --git a/tests/lib.rs b/tests/lib.rs index 4291501..2e44778 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,15 +1,32 @@ mod database; 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; -static COLLECTION: Lazy> = Lazy::new(|| { +static COLLECTION: Lazy> = Lazy::new(|| -> Collection { vec![ Artist { id: ArtistId { 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 { id: AlbumId { year: 2011, @@ -177,6 +194,18 @@ static COLLECTION: Lazy> = Lazy::new(|| { id: ArtistId { 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![ Album { id: AlbumId { @@ -398,6 +427,18 @@ static COLLECTION: Lazy> = Lazy::new(|| { id: ArtistId { 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 { id: AlbumId { year: 2001, @@ -532,6 +573,18 @@ static COLLECTION: Lazy> = Lazy::new(|| { id: ArtistId { name: String::from("Heaven’s Basement"), }, + properties: ArtistProperties { + musicbrainz: Some(MusicBrainz::new( + "https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc", + ).unwrap()), + musicbutler: vec![ + MusicButler::new("https://www.musicbutler.io/artist-page/291158685").unwrap(), + ], + bandcamp: vec![], + qobuz: Some(Qobuz::new( + "https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums", + ).unwrap()), + }, albums: vec![Album { id: AlbumId { year: 2011, @@ -622,6 +675,18 @@ static COLLECTION: Lazy> = Lazy::new(|| { id: ArtistId { 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![ Album { id: AlbumId {