From d678ca17f3e661f1145854b57c2003edd27d1aae Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 15 Nov 2023 12:30:11 +0100 Subject: [PATCH 1/4] feat: add tree_path method --- libs/rust/src/lib.rs | 24 +++++++++++++++++++++++- libs/rust/src/model.rs | 25 +------------------------ 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/libs/rust/src/lib.rs b/libs/rust/src/lib.rs index 76cfa5a..5e37c85 100644 --- a/libs/rust/src/lib.rs +++ b/libs/rust/src/lib.rs @@ -137,6 +137,14 @@ impl GenreDb { } } } + + /// Get the path of the genre tree + pub fn tree_path(&self, lang: Option<&str>) -> PathBuf { + match lang.filter(|l| self.translations.contains_key(*l)) { + Some(lang) => path!(self.path / "tree" / format!("tree.{lang}.json")), + None => path!(self.path / "tree" / "tree.en.json"), + } + } } fn conv_genre<'a>(id: &str, name: String, gm: &GenreMeta) -> Genre { @@ -184,7 +192,7 @@ mod tests { #[test] fn select_lang() { - let languages = vec!["es".to_owned(), "de".to_owned(), "en-GB".to_owned()]; + let languages = vec!["xy".to_owned(), "de".to_owned(), "en-GB".to_owned()]; let db = GenreDb::new(genre_db_path()).unwrap(); let lang = db.select_lang(&languages); @@ -199,4 +207,18 @@ mod tests { let lang = db.select_lang(&languages); assert_eq!(lang, None); } + + #[test] + fn tree_path() { + let db_path = genre_db_path(); + let db = GenreDb::new(&db_path).unwrap(); + + let expect_en = path!(&db_path / "tree" / "tree.en.json"); + let expect_de = path!(&db_path / "tree" / "tree.de.json"); + + assert_eq!(db.tree_path(None), expect_en); + assert_eq!(db.tree_path(Some("en")), expect_en); + assert_eq!(db.tree_path(Some("xy")), expect_en); + assert_eq!(db.tree_path(Some("de")), expect_de); + } } diff --git a/libs/rust/src/model.rs b/libs/rust/src/model.rs index fb5e1aa..d0120f8 100644 --- a/libs/rust/src/model.rs +++ b/libs/rust/src/model.rs @@ -57,29 +57,6 @@ pub enum Region { WF, } -#[cfg(feature = "playlists")] -#[serde_as] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum PlaylistKind { - Sound, - Intro, - Pulse, - Edge, - #[serde(rename = "2018")] - Y2018, - #[serde(rename = "2019")] - Y2019, - #[serde(rename = "2020")] - Y2020, - #[serde(rename = "2021")] - Y2021, - #[serde(rename = "2022")] - Y2022, - #[serde(rename = "2023")] - Y2023, -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum GenreEntry { @@ -101,7 +78,7 @@ pub struct GenreMeta { pub rank: Option, #[cfg(feature = "playlists")] #[serde(default)] - pub playlists: HashMap, + pub playlists: HashMap, #[serde(default)] pub deprecated: bool, #[serde(default)] From c35f03c3c9bc67d7f859e304bf692a69e523f166 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 15 Nov 2023 13:09:53 +0100 Subject: [PATCH 2/4] fix: inherit parent genre parameters --- genres/genres_to_meta.py | 17 +++++++++ libs/rust/src/lib.rs | 74 +++++++++++++++++++++++++++++----------- 2 files changed, 71 insertions(+), 20 deletions(-) diff --git a/genres/genres_to_meta.py b/genres/genres_to_meta.py index 474fac5..85ebce7 100644 --- a/genres/genres_to_meta.py +++ b/genres/genres_to_meta.py @@ -453,6 +453,23 @@ def package(out_dir: Path): with open(util.TRANSLATION_FILE_EN) as f: tl_en = json.load(f) + # Remove redundant tags + def remove_redundant_tags(gid: str, parent: str): + genre = metadata[gid] + pg = metadata[parent] + if pg.language and pg.language == genre.language: + metadata[gid].language = None + if pg.country and pg.country == genre.country: + metadata[gid].country = None + if pg.region and pg.region == genre.region: + metadata[gid].region = None + if pg.parent: + remove_redundant_tags(gid, pg.parent) + + for genre_id, genre in metadata.items(): + if genre.parent: + remove_redundant_tags(genre_id, genre.parent) + # Genre database db = { g_id: model.GenreMetadataDB.conv(genre, tl_en.get(g_id)) diff --git a/libs/rust/src/lib.rs b/libs/rust/src/lib.rs index 5e37c85..f6ce123 100644 --- a/libs/rust/src/lib.rs +++ b/libs/rust/src/lib.rs @@ -125,19 +125,46 @@ impl GenreDb { GenreEntry::Alias { alias } => self.get_localized(alias, lang), GenreEntry::Meta(genre) => { if let Some(lang) = lang { - Some(conv_genre( - id, - self.get_translated_name(id, lang) - .unwrap_or_else(|| genre.name.to_owned()), - genre, - )) + Some( + self.conv_genre( + id, + self.get_translated_name(id, lang) + .unwrap_or_else(|| genre.name.to_owned()), + genre, + ), + ) } else { - Some(conv_genre(id, genre.name.to_owned(), genre)) + Some(self.conv_genre(id, genre.name.to_owned(), genre)) } } } } + fn conv_genre<'a>(&self, id: &str, name: String, gm: &GenreMeta) -> Genre { + let mut genre = Genre { + id: id.to_owned(), + name: name, + parent: gm.parent.to_owned(), + language: gm.language.to_owned(), + country: gm.country.to_owned(), + region: gm.region, + rank: gm.rank, + deprecated: gm.deprecated, + metagenre: gm.metagenre, + #[cfg(feature = "playlists")] + playlists: gm.playlists.clone(), + }; + self.inherit_parent(&mut genre, gm.parent.as_deref()); + genre + } + + fn inherit_parent(&self, genre: &mut Genre, parent: Option<&str>) { + if let Some(GenreEntry::Meta(g2)) = parent.and_then(|p| self.genres.get(p)) { + merge_genre_data(genre, g2); + self.inherit_parent(genre, g2.parent.as_deref()); + } + } + /// Get the path of the genre tree pub fn tree_path(&self, lang: Option<&str>) -> PathBuf { match lang.filter(|l| self.translations.contains_key(*l)) { @@ -147,19 +174,15 @@ impl GenreDb { } } -fn conv_genre<'a>(id: &str, name: String, gm: &GenreMeta) -> Genre { - Genre { - id: id.to_owned(), - name: name, - parent: gm.parent.to_owned(), - language: gm.language.to_owned(), - country: gm.country.to_owned(), - region: gm.region, - rank: gm.rank, - deprecated: gm.deprecated, - metagenre: gm.metagenre, - #[cfg(feature = "playlists")] - playlists: gm.playlists.clone(), +fn merge_genre_data(genre: &mut Genre, g2: &GenreMeta) { + if genre.language.is_none() { + genre.language = g2.language.clone(); + } + if genre.country.is_none() { + genre.country = g2.country.clone(); + } + if genre.region.is_none() { + genre.region = g2.region.clone(); } } @@ -185,11 +208,22 @@ mod tests { let genre = db.get_localized(id, lang).unwrap(); assert_eq!(genre.id, id); assert_eq!(genre.name, expect); + assert_eq!(genre.language.unwrap(), "sqi"); + assert_eq!(genre.country.unwrap(), "AL"); let localized_name = db.get_localized_name(id, lang).unwrap(); assert_eq!(localized_name, expect) } + #[test] + /// Genres should inherit parent parameters + fn get_subgenre() { + let db = GenreDb::new(genre_db_path()).unwrap(); + let genre = db.get("k-pop girl group").unwrap(); + assert_eq!(genre.language.unwrap(), "kor"); + assert_eq!(genre.country.unwrap(), "KR"); + } + #[test] fn select_lang() { let languages = vec!["xy".to_owned(), "de".to_owned(), "en-GB".to_owned()]; From e1937268d803234bd13ce3504e996855d9a02d04 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 15 Nov 2023 18:10:12 +0100 Subject: [PATCH 3/4] feat: add languages method, fixes to model --- libs/rust/Cargo.toml | 1 + libs/rust/src/lib.rs | 7 +++++- libs/rust/src/model.rs | 45 +++++++++++++++++++++++++++++++++----- schema/genre_db.json | 2 +- schema/genre_metadata.json | 4 ++-- schema/genre_tree.json | 2 +- 6 files changed, 50 insertions(+), 11 deletions(-) diff --git a/libs/rust/Cargo.toml b/libs/rust/Cargo.toml index 0081e66..0ab21f1 100644 --- a/libs/rust/Cargo.toml +++ b/libs/rust/Cargo.toml @@ -15,4 +15,5 @@ serde_with = { version = "3.0", default-features = false, features = [ "macros", ] } serde_json = "1.0" +serde_plain = "1.0" thiserror = "1.0" diff --git a/libs/rust/src/lib.rs b/libs/rust/src/lib.rs index f6ce123..b3e7176 100644 --- a/libs/rust/src/lib.rs +++ b/libs/rust/src/lib.rs @@ -6,7 +6,7 @@ use path_macro::path; use model::{GenreEntry, GenreMeta}; -pub use model::Genre; +pub use model::{Genre, Region}; type Translation = HashMap; @@ -55,6 +55,11 @@ impl GenreDb { }) } + /// Return a list of supported languages + pub fn languages(&self) -> Vec<&str> { + self.translations.keys().map(String::as_str).collect() + } + /// Select a supported language from a list of given languages (for example from the /// Accept-Language http headere). pub fn select_lang<'a, S: AsRef>(&self, langs: &'a [S]) -> Option<&'a str> { diff --git a/libs/rust/src/model.rs b/libs/rust/src/model.rs index d0120f8..86cf295 100644 --- a/libs/rust/src/model.rs +++ b/libs/rust/src/model.rs @@ -4,8 +4,9 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DefaultOnError}; -/// Region for describing genres from multiple countries +/// Region for describing genres from multiple countrie #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[non_exhaustive] pub enum Region { /// Africa AF, @@ -57,6 +58,8 @@ pub enum Region { WF, } +serde_plain::derive_display_from_serialize!(Region); + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum GenreEntry { @@ -87,21 +90,51 @@ pub struct GenreMeta { #[serde_as] #[derive(Debug, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub struct Genre { + /// Spotify genre ID pub id: String, + /// Localized genre name pub name: String, + /// ID of the parent genre if this is a subgenre + #[serde(skip_serializing_if = "Option::is_none")] pub parent: Option, + /// ISO-639-3 language code if the genre implies lyrics in a specific language + #[serde(skip_serializing_if = "Option::is_none")] pub language: Option, + /// ISO-3166-1 country code if the genre is dominant in a specific country + #[serde(skip_serializing_if = "Option::is_none")] pub country: Option, - #[serde(default)] + /// Region code if the genre is dominant in a specific region + /// + /// Region codes are not standardized, refer to the documentation for a definition. + #[serde(default, skip_serializing_if = "Option::is_none")] #[serde_as(deserialize_as = "DefaultOnError")] pub region: Option, + /// Position in the popularity ranking (1: most popular) + #[serde(skip_serializing_if = "Option::is_none")] pub rank: Option, + /// Spotify playlist IDs #[cfg(feature = "playlists")] - #[serde(default)] - pub playlists: HashMap, - #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub playlists: Option>, + /// True if the genre is no longer part of Spotify's catalog + #[serde(default, skip_serializing_if = "is_default")] pub deprecated: bool, - #[serde(default)] + /// True if the genre is a metagenre + /// + /// Metagenres do not exist at Spotify but are used to group other genres together. + #[serde(default, skip_serializing_if = "is_default")] pub metagenre: bool, } + +impl PartialEq for Genre { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} +impl Eq for Genre {} + +fn is_default(t: &T) -> bool { + t == &T::default() +} diff --git a/schema/genre_db.json b/schema/genre_db.json index 78c2c0c..6b5744d 100644 --- a/schema/genre_db.json +++ b/schema/genre_db.json @@ -34,7 +34,7 @@ "pattern": "^[A-Z]{2}$" }, "region": { - "description": "Region code if the genre is dominant in a specific region", + "description": "Region code if the genre is dominant in a specific region\nRegion codes are not standardized, refer to the documentation for a definition.", "type": "string", "enum": [ "AF", diff --git a/schema/genre_metadata.json b/schema/genre_metadata.json index f0e7ccf..eab53d9 100644 --- a/schema/genre_metadata.json +++ b/schema/genre_metadata.json @@ -36,7 +36,7 @@ "pattern": "^[A-Z]{2}$" }, "region": { - "description": "Region code if the genre is dominant in a specific region", + "description": "Region code if the genre is dominant in a specific region\nRegion codes are not standardized, refer to the documentation for a definition.", "type": "string", "enum": [ "AF", @@ -112,7 +112,7 @@ "pattern": "^[a-z0-9\\-+: '&]+$" }, "deprecated": { - "description": "True if the genre is no longer part of Spotify's catalogue", + "description": "True if the genre is no longer part of Spotify's catalog", "type": "boolean" }, "metagenre": { diff --git a/schema/genre_tree.json b/schema/genre_tree.json index a3f8891..8b90fb5 100644 --- a/schema/genre_tree.json +++ b/schema/genre_tree.json @@ -32,7 +32,7 @@ "pattern": "^[A-Z]{2}$" }, "region": { - "description": "Region code if the genre is dominant in a specific region", + "description": "Region code if the genre is dominant in a specific region\nRegion codes are not standardized, refer to the documentation for a definition.", "type": "string", "enum": [ "AF", From 4560a5ba113feed24e17ba435c4effbb52babea6 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 15 Nov 2023 18:16:35 +0100 Subject: [PATCH 4/4] chore: add cargo metadata --- libs/rust/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/rust/Cargo.toml b/libs/rust/Cargo.toml index 0ab21f1..b3ed6a6 100644 --- a/libs/rust/Cargo.toml +++ b/libs/rust/Cargo.toml @@ -2,6 +2,10 @@ name = "spotify-genrebase" version = "0.1.0" edition = "2021" +authors = ["ThetaDev "] +license = "MIT" +description = "Lookup Spotify genre metadata" +repository = "https://code.thetadev.de/Tiraya/spotify-genres" [features] playlists = []