From f1a02e8e58fefddf2d993e499f164ada0580e390 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Wed, 28 Aug 2024 22:25:47 +0200 Subject: [PATCH 1/3] Serialize side done --- src/external/database/serde/common.rs | 10 +++---- src/external/database/serde/serialize.rs | 33 +++++++++++++++++++++--- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/external/database/serde/common.rs b/src/external/database/serde/common.rs index 28f9c65..b0268d9 100644 --- a/src/external/database/serde/common.rs +++ b/src/external/database/serde/common.rs @@ -4,7 +4,7 @@ use crate::core::collection::album::{AlbumPrimaryType, AlbumSecondaryType}; #[derive(Debug, Deserialize, Serialize)] #[serde(remote = "AlbumPrimaryType")] -pub enum SerdeAlbumPrimaryTypeDef { +pub enum AlbumPrimaryTypeDef { Album, Single, Ep, @@ -13,7 +13,7 @@ pub enum SerdeAlbumPrimaryTypeDef { } #[derive(Debug, Deserialize, Serialize)] -pub struct SerdeAlbumPrimaryType(#[serde(with = "SerdeAlbumPrimaryTypeDef")] AlbumPrimaryType); +pub struct SerdeAlbumPrimaryType(#[serde(with = "AlbumPrimaryTypeDef")] AlbumPrimaryType); impl From for AlbumPrimaryType { fn from(value: SerdeAlbumPrimaryType) -> Self { @@ -29,7 +29,7 @@ impl From for SerdeAlbumPrimaryType { #[derive(Debug, Deserialize, Serialize)] #[serde(remote = "AlbumSecondaryType")] -pub enum SerdeAlbumSecondaryTypeDef { +pub enum AlbumSecondaryTypeDef { Compilation, Soundtrack, Spokenword, @@ -45,9 +45,7 @@ pub enum SerdeAlbumSecondaryTypeDef { } #[derive(Debug, Deserialize, Serialize)] -pub struct SerdeAlbumSecondaryType( - #[serde(with = "SerdeAlbumSecondaryTypeDef")] AlbumSecondaryType, -); +pub struct SerdeAlbumSecondaryType(#[serde(with = "AlbumSecondaryTypeDef")] AlbumSecondaryType); impl From for AlbumSecondaryType { fn from(value: SerdeAlbumSecondaryType) -> Self { diff --git a/src/external/database/serde/serialize.rs b/src/external/database/serde/serialize.rs index acfe155..e54fda0 100644 --- a/src/external/database/serde/serialize.rs +++ b/src/external/database/serde/serialize.rs @@ -3,6 +3,7 @@ use std::collections::BTreeMap; use serde::Serialize; use crate::{ + collection::musicbrainz::{MbAlbumRef, MbArtistRef}, core::collection::{album::Album, artist::Artist, musicbrainz::IMusicBrainzRef, Collection}, external::database::serde::common::{SerdeAlbumPrimaryType, SerdeAlbumSecondaryType}, }; @@ -22,7 +23,7 @@ impl<'a> From<&'a Collection> for SerializeDatabase<'a> { pub struct SerializeArtist<'a> { name: &'a str, sort: Option<&'a str>, - musicbrainz: Option<&'a str>, + musicbrainz: Option>, properties: BTreeMap<&'a str, &'a Vec>, albums: Vec>, } @@ -31,17 +32,41 @@ pub struct SerializeArtist<'a> { pub struct SerializeAlbum<'a> { title: &'a str, seq: u8, - musicbrainz: Option<&'a str>, + musicbrainz: Option>, primary_type: Option, secondary_types: Vec, } +#[derive(Clone, Debug)] +pub struct SerdeMbArtistRef<'a>(&'a MbArtistRef); + +impl<'a> Serialize for SerdeMbArtistRef<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.0.url().as_str()) + } +} + +#[derive(Clone, Debug)] +pub struct SerdeMbAlbumRef<'a>(&'a MbAlbumRef); + +impl<'a> Serialize for SerdeMbAlbumRef<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.0.url().as_str()) + } +} + impl<'a> From<&'a Artist> for SerializeArtist<'a> { fn from(artist: &'a Artist) -> Self { SerializeArtist { name: &artist.id.name, sort: artist.sort.as_ref().map(|id| id.name.as_ref()), - musicbrainz: artist.musicbrainz.as_ref().map(|mb| mb.url().as_str()), + musicbrainz: artist.musicbrainz.as_ref().map(|mb| SerdeMbArtistRef(mb)), properties: artist .properties .iter() @@ -57,7 +82,7 @@ impl<'a> From<&'a Album> for SerializeAlbum<'a> { SerializeAlbum { title: &album.id.title, seq: album.seq.0, - musicbrainz: album.musicbrainz.as_ref().map(|mb| mb.url().as_str()), + musicbrainz: album.musicbrainz.as_ref().map(|mb| SerdeMbAlbumRef(mb)), primary_type: album.primary_type.map(Into::into), secondary_types: album .secondary_types -- 2.45.2 From 0cbfd3627a643c5b2677f900342601e88fa54e34 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Wed, 28 Aug 2024 22:40:30 +0200 Subject: [PATCH 2/3] And deserialize now too --- src/external/database/json/mod.rs | 2 +- src/external/database/serde/deserialize.rs | 141 ++++++++++++++------- src/external/database/serde/serialize.rs | 19 +-- 3 files changed, 110 insertions(+), 52 deletions(-) diff --git a/src/external/database/json/mod.rs b/src/external/database/json/mod.rs index b791203..3347c5c 100644 --- a/src/external/database/json/mod.rs +++ b/src/external/database/json/mod.rs @@ -51,7 +51,7 @@ impl IDatabase for JsonDatabase { fn load(&self) -> Result { let serialized = self.backend.read()?; let database: DeserializeDatabase = serde_json::from_str(&serialized)?; - database.try_into() + Ok(database.into()) } fn save(&mut self, collection: &Collection) -> Result<(), SaveError> { diff --git a/src/external/database/serde/deserialize.rs b/src/external/database/serde/deserialize.rs index 84e742c..6a1f731 100644 --- a/src/external/database/serde/deserialize.rs +++ b/src/external/database/serde/deserialize.rs @@ -1,16 +1,13 @@ -use std::collections::HashMap; +use std::{collections::HashMap, fmt}; -use serde::Deserialize; +use serde::{de::Visitor, Deserialize, Deserializer}; use crate::{ - core::{ - collection::{ - album::{Album, AlbumDate, AlbumId, AlbumSeq}, - artist::{Artist, ArtistId}, - musicbrainz::{MbAlbumRef, MbArtistRef}, - Collection, - }, - interface::database::LoadError, + core::collection::{ + album::{Album, AlbumDate, AlbumId, AlbumSeq}, + artist::{Artist, ArtistId}, + musicbrainz::{MbAlbumRef, MbArtistRef}, + Collection, Error as CollectionError, }, external::database::serde::common::{SerdeAlbumPrimaryType, SerdeAlbumSecondaryType}, }; @@ -20,13 +17,11 @@ pub enum DeserializeDatabase { V20240313(Vec), } -impl TryFrom for Collection { - type Error = LoadError; - - fn try_from(database: DeserializeDatabase) -> Result { +impl From for Collection { + fn from(database: DeserializeDatabase) -> Self { match database { DeserializeDatabase::V20240313(collection) => { - collection.into_iter().map(TryInto::try_into).collect() + collection.into_iter().map(Into::into).collect() } } } @@ -36,7 +31,7 @@ impl TryFrom for Collection { pub struct DeserializeArtist { name: String, sort: Option, - musicbrainz: Option, + musicbrainz: Option, properties: HashMap>, albums: Vec, } @@ -45,47 +40,107 @@ pub struct DeserializeArtist { pub struct DeserializeAlbum { title: String, seq: u8, - musicbrainz: Option, + musicbrainz: Option, primary_type: Option, secondary_types: Vec, } -impl TryFrom for Artist { - type Error = LoadError; +#[derive(Clone, Debug)] +pub struct DeserializeMbArtistRef(MbArtistRef); - fn try_from(artist: DeserializeArtist) -> Result { - Ok(Artist { - id: ArtistId::new(artist.name), - sort: artist.sort.map(ArtistId::new), - musicbrainz: artist - .musicbrainz - .map(MbArtistRef::from_url_str) - .transpose()?, - properties: artist.properties, - albums: artist - .albums - .into_iter() - .map(TryInto::try_into) - .collect::, LoadError>>()?, - }) +impl From for MbArtistRef { + fn from(value: DeserializeMbArtistRef) -> Self { + value.0 } } -impl TryFrom for Album { - type Error = LoadError; +struct DeserializeMbArtistRefVisitor; - fn try_from(album: DeserializeAlbum) -> Result { - Ok(Album { +impl<'de> Visitor<'de> for DeserializeMbArtistRefVisitor { + type Value = DeserializeMbArtistRef; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a valid MusicBrainz identifier") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(DeserializeMbArtistRef( + MbArtistRef::from_url_str(v).map_err(|e: CollectionError| E::custom(e.to_string()))?, + )) + } +} + +impl<'de> Deserialize<'de> for DeserializeMbArtistRef { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(DeserializeMbArtistRefVisitor) + } +} + +#[derive(Clone, Debug)] +pub struct DeserializeMbAlbumRef(MbAlbumRef); + +impl From for MbAlbumRef { + fn from(value: DeserializeMbAlbumRef) -> Self { + value.0 + } +} + +struct DeserializeMbAlbumRefVisitor; + +impl<'de> Visitor<'de> for DeserializeMbAlbumRefVisitor { + type Value = DeserializeMbAlbumRef; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a valid MusicBrainz identifier") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(DeserializeMbAlbumRef( + MbAlbumRef::from_url_str(v).map_err(|e: CollectionError| E::custom(e.to_string()))?, + )) + } +} + +impl<'de> Deserialize<'de> for DeserializeMbAlbumRef { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(DeserializeMbAlbumRefVisitor) + } +} + +impl From for Artist { + fn from(artist: DeserializeArtist) -> Self { + Artist { + id: ArtistId::new(artist.name), + sort: artist.sort.map(ArtistId::new), + musicbrainz: artist.musicbrainz.map(Into::into), + properties: artist.properties, + albums: artist.albums.into_iter().map(Into::into).collect(), + } + } +} + +impl From for Album { + fn from(album: DeserializeAlbum) -> Self { + Album { id: AlbumId { title: album.title }, date: AlbumDate::default(), seq: AlbumSeq(album.seq), - musicbrainz: album - .musicbrainz - .map(MbAlbumRef::from_url_str) - .transpose()?, + musicbrainz: album.musicbrainz.map(Into::into), primary_type: album.primary_type.map(Into::into), secondary_types: album.secondary_types.into_iter().map(Into::into).collect(), tracks: vec![], - }) + } } } diff --git a/src/external/database/serde/serialize.rs b/src/external/database/serde/serialize.rs index e54fda0..1b1d5f8 100644 --- a/src/external/database/serde/serialize.rs +++ b/src/external/database/serde/serialize.rs @@ -23,7 +23,7 @@ impl<'a> From<&'a Collection> for SerializeDatabase<'a> { pub struct SerializeArtist<'a> { name: &'a str, sort: Option<&'a str>, - musicbrainz: Option>, + musicbrainz: Option>, properties: BTreeMap<&'a str, &'a Vec>, albums: Vec>, } @@ -32,15 +32,15 @@ pub struct SerializeArtist<'a> { pub struct SerializeAlbum<'a> { title: &'a str, seq: u8, - musicbrainz: Option>, + musicbrainz: Option>, primary_type: Option, secondary_types: Vec, } #[derive(Clone, Debug)] -pub struct SerdeMbArtistRef<'a>(&'a MbArtistRef); +pub struct SerializeMbArtistRef<'a>(&'a MbArtistRef); -impl<'a> Serialize for SerdeMbArtistRef<'a> { +impl<'a> Serialize for SerializeMbArtistRef<'a> { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -50,9 +50,9 @@ impl<'a> Serialize for SerdeMbArtistRef<'a> { } #[derive(Clone, Debug)] -pub struct SerdeMbAlbumRef<'a>(&'a MbAlbumRef); +pub struct SerializeMbAlbumRef<'a>(&'a MbAlbumRef); -impl<'a> Serialize for SerdeMbAlbumRef<'a> { +impl<'a> Serialize for SerializeMbAlbumRef<'a> { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -66,7 +66,10 @@ 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| SerdeMbArtistRef(mb)), + musicbrainz: artist + .musicbrainz + .as_ref() + .map(|mb| SerializeMbArtistRef(mb)), properties: artist .properties .iter() @@ -82,7 +85,7 @@ impl<'a> From<&'a Album> for SerializeAlbum<'a> { SerializeAlbum { title: &album.id.title, seq: album.seq.0, - musicbrainz: album.musicbrainz.as_ref().map(|mb| SerdeMbAlbumRef(mb)), + musicbrainz: album.musicbrainz.as_ref().map(|mb| SerializeMbAlbumRef(mb)), primary_type: album.primary_type.map(Into::into), secondary_types: album .secondary_types -- 2.45.2 From 87c459f00b8b0097ee559670156b0b8298dc81b9 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Wed, 28 Aug 2024 22:58:01 +0200 Subject: [PATCH 3/3] And adjust --- src/external/database/json/testmod.rs | 14 +++---- src/external/database/serde/deserialize.rs | 49 ++++++++++++++++++++-- src/external/database/serde/serialize.rs | 15 +++---- tests/files/database/database.json | 2 +- 4 files changed, 59 insertions(+), 21 deletions(-) diff --git a/src/external/database/json/testmod.rs b/src/external/database/json/testmod.rs index ec0121f..8bd5dd0 100644 --- a/src/external/database/json/testmod.rs +++ b/src/external/database/json/testmod.rs @@ -1,10 +1,10 @@ pub static DATABASE_JSON: &str = "{\ - \"V20240313\":\ + \"V20240828\":\ [\ {\ \"name\":\"Album_Artist ‘A’\",\ \"sort\":null,\ - \"musicbrainz\":\"https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000\",\ + \"musicbrainz\":\"00000000-0000-0000-0000-000000000000\",\ \"properties\":{\ \"MusicButler\":[\"https://www.musicbutler.io/artist-page/000000000\"],\ \"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums\"]\ @@ -12,7 +12,7 @@ pub static DATABASE_JSON: &str = "{\ \"albums\":[\ {\ \"title\":\"album_title a.a\",\"seq\":1,\ - \"musicbrainz\":\"https://musicbrainz.org/release-group/00000000-0000-0000-0000-000000000000\",\ + \"musicbrainz\":\"00000000-0000-0000-0000-000000000000\",\ \"primary_type\":\"Album\",\"secondary_types\":[]\ },\ {\ @@ -24,7 +24,7 @@ pub static DATABASE_JSON: &str = "{\ {\ \"name\":\"Album_Artist ‘B’\",\ \"sort\":null,\ - \"musicbrainz\":\"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111\",\ + \"musicbrainz\":\"11111111-1111-1111-1111-111111111111\",\ \"properties\":{\ \"Bandcamp\":[\"https://artist-b.bandcamp.com/\"],\ \"MusicButler\":[\ @@ -40,12 +40,12 @@ pub static DATABASE_JSON: &str = "{\ },\ {\ \"title\":\"album_title b.b\",\"seq\":3,\ - \"musicbrainz\":\"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111111\",\ + \"musicbrainz\":\"11111111-1111-1111-1111-111111111111\",\ \"primary_type\":\"Album\",\"secondary_types\":[]\ },\ {\ \"title\":\"album_title b.c\",\"seq\":2,\ - \"musicbrainz\":\"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111112\",\ + \"musicbrainz\":\"11111111-1111-1111-1111-111111111112\",\ \"primary_type\":\"Album\",\"secondary_types\":[]\ },\ {\ @@ -57,7 +57,7 @@ pub static DATABASE_JSON: &str = "{\ {\ \"name\":\"The Album_Artist ‘C’\",\ \"sort\":\"Album_Artist ‘C’, The\",\ - \"musicbrainz\":\"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111\",\ + \"musicbrainz\":\"11111111-1111-1111-1111-111111111111\",\ \"properties\":{},\ \"albums\":[\ {\ diff --git a/src/external/database/serde/deserialize.rs b/src/external/database/serde/deserialize.rs index 6a1f731..e456795 100644 --- a/src/external/database/serde/deserialize.rs +++ b/src/external/database/serde/deserialize.rs @@ -14,13 +14,13 @@ use crate::{ #[derive(Debug, Deserialize)] pub enum DeserializeDatabase { - V20240313(Vec), + V20240828(Vec), } impl From for Collection { fn from(database: DeserializeDatabase) -> Self { match database { - DeserializeDatabase::V20240313(collection) => { + DeserializeDatabase::V20240828(collection) => { collection.into_iter().map(Into::into).collect() } } @@ -68,7 +68,7 @@ impl<'de> Visitor<'de> for DeserializeMbArtistRefVisitor { E: serde::de::Error, { Ok(DeserializeMbArtistRef( - MbArtistRef::from_url_str(v).map_err(|e: CollectionError| E::custom(e.to_string()))?, + MbArtistRef::from_uuid_str(v).map_err(|e: CollectionError| E::custom(e.to_string()))?, )) } } @@ -105,7 +105,7 @@ impl<'de> Visitor<'de> for DeserializeMbAlbumRefVisitor { E: serde::de::Error, { Ok(DeserializeMbAlbumRef( - MbAlbumRef::from_url_str(v).map_err(|e: CollectionError| E::custom(e.to_string()))?, + MbAlbumRef::from_uuid_str(v).map_err(|e: CollectionError| E::custom(e.to_string()))?, )) } } @@ -144,3 +144,44 @@ impl From for Album { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deserialize_mb_artist_ref() { + let mbid = "\"d368baa8-21ca-4759-9731-0b2753071ad8\""; + let mbref: DeserializeMbArtistRef = serde_json::from_str(mbid).unwrap(); + let mbref: MbArtistRef = mbref.into(); + assert_eq!( + mbref, + MbArtistRef::from_uuid_str("d368baa8-21ca-4759-9731-0b2753071ad8").unwrap() + ); + + let mbid = "null"; + let result: Result = serde_json::from_str(mbid); + assert!(result + .unwrap_err() + .to_string() + .contains("a valid MusicBrainz identifier")); + } + + #[test] + fn deserialize_mb_album_ref() { + let mbid = "\"d368baa8-21ca-4759-9731-0b2753071ad8\""; + let mbref: DeserializeMbAlbumRef = serde_json::from_str(mbid).unwrap(); + let mbref: MbAlbumRef = mbref.into(); + assert_eq!( + mbref, + MbAlbumRef::from_uuid_str("d368baa8-21ca-4759-9731-0b2753071ad8").unwrap() + ); + + let mbid = "null"; + let result: Result = serde_json::from_str(mbid); + assert!(result + .unwrap_err() + .to_string() + .contains("a valid MusicBrainz identifier")); + } +} diff --git a/src/external/database/serde/serialize.rs b/src/external/database/serde/serialize.rs index 1b1d5f8..53d5b4b 100644 --- a/src/external/database/serde/serialize.rs +++ b/src/external/database/serde/serialize.rs @@ -10,12 +10,12 @@ use crate::{ #[derive(Debug, Serialize)] pub enum SerializeDatabase<'a> { - V20240313(Vec>), + V20240828(Vec>), } impl<'a> From<&'a Collection> for SerializeDatabase<'a> { fn from(collection: &'a Collection) -> Self { - SerializeDatabase::V20240313(collection.iter().map(Into::into).collect()) + SerializeDatabase::V20240828(collection.iter().map(Into::into).collect()) } } @@ -45,7 +45,7 @@ impl<'a> Serialize for SerializeMbArtistRef<'a> { where S: serde::Serializer, { - serializer.serialize_str(&self.0.url().as_str()) + serializer.serialize_str(&self.0.mbid().uuid().as_hyphenated().to_string()) } } @@ -57,7 +57,7 @@ impl<'a> Serialize for SerializeMbAlbumRef<'a> { where S: serde::Serializer, { - serializer.serialize_str(&self.0.url().as_str()) + serializer.serialize_str(&self.0.mbid().uuid().as_hyphenated().to_string()) } } @@ -66,10 +66,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| SerializeMbArtistRef(mb)), + musicbrainz: artist.musicbrainz.as_ref().map(SerializeMbArtistRef), properties: artist .properties .iter() @@ -85,7 +82,7 @@ impl<'a> From<&'a Album> for SerializeAlbum<'a> { SerializeAlbum { title: &album.id.title, seq: album.seq.0, - musicbrainz: album.musicbrainz.as_ref().map(|mb| SerializeMbAlbumRef(mb)), + musicbrainz: album.musicbrainz.as_ref().map(SerializeMbAlbumRef), primary_type: album.primary_type.map(Into::into), secondary_types: album .secondary_types diff --git a/tests/files/database/database.json b/tests/files/database/database.json index 9a288e0..a7d802c 100644 --- a/tests/files/database/database.json +++ b/tests/files/database/database.json @@ -1 +1 @@ -{"V20240313":[{"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,"primary_type":"Album","secondary_types":[]}]},{"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,"primary_type":"Ep","secondary_types":[]},{"title":"Slania","seq":0,"musicbrainz":null,"primary_type":"Album","secondary_types":[]}]},{"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,"primary_type":"Album","secondary_types":[]}]},{"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,"primary_type":null,"secondary_types":[]},{"title":"Unbreakable","seq":0,"musicbrainz":null,"primary_type":"Album","secondary_types":[]}]},{"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,"primary_type":"Album","secondary_types":[]},{"title":"S&M","seq":0,"musicbrainz":null,"primary_type":"Album","secondary_types":["Live"]}]}]} \ No newline at end of file +{"V20240828":[{"name":"Аркона","sort":"Arkona","musicbrainz":"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,"primary_type":"Album","secondary_types":[]}]},{"name":"Eluveitie","sort":null,"musicbrainz":"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,"primary_type":"Ep","secondary_types":[]},{"title":"Slania","seq":0,"musicbrainz":null,"primary_type":"Album","secondary_types":[]}]},{"name":"Frontside","sort":null,"musicbrainz":"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,"primary_type":"Album","secondary_types":[]}]},{"name":"Heaven’s Basement","sort":"Heaven’s Basement","musicbrainz":"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,"primary_type":null,"secondary_types":[]},{"title":"Unbreakable","seq":0,"musicbrainz":null,"primary_type":"Album","secondary_types":[]}]},{"name":"Metallica","sort":null,"musicbrainz":"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,"primary_type":"Album","secondary_types":[]},{"title":"S&M","seq":0,"musicbrainz":null,"primary_type":"Album","secondary_types":["Live"]}]}]} \ No newline at end of file -- 2.45.2