Compare commits
4 commits
bf4ab15a6f
...
4560a5ba11
Author | SHA1 | Date | |
---|---|---|---|
4560a5ba11 | |||
e1937268d8 | |||
c35f03c3c9 | |||
d678ca17f3 |
7 changed files with 148 additions and 55 deletions
|
@ -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))
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
name = "spotify-genrebase"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["ThetaDev <thetadev@magenta.de>"]
|
||||
license = "MIT"
|
||||
description = "Lookup Spotify genre metadata"
|
||||
repository = "https://code.thetadev.de/Tiraya/spotify-genres"
|
||||
|
||||
[features]
|
||||
playlists = []
|
||||
|
@ -15,4 +19,5 @@ serde_with = { version = "3.0", default-features = false, features = [
|
|||
"macros",
|
||||
] }
|
||||
serde_json = "1.0"
|
||||
serde_plain = "1.0"
|
||||
thiserror = "1.0"
|
||||
|
|
|
@ -6,7 +6,7 @@ use path_macro::path;
|
|||
|
||||
use model::{GenreEntry, GenreMeta};
|
||||
|
||||
pub use model::Genre;
|
||||
pub use model::{Genre, Region};
|
||||
|
||||
type Translation = HashMap<String, String>;
|
||||
|
||||
|
@ -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<str>>(&self, langs: &'a [S]) -> Option<&'a str> {
|
||||
|
@ -125,22 +130,23 @@ impl GenreDb {
|
|||
GenreEntry::Alias { alias } => self.get_localized(alias, lang),
|
||||
GenreEntry::Meta(genre) => {
|
||||
if let Some(lang) = lang {
|
||||
Some(conv_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>(id: &str, name: String, gm: &GenreMeta) -> Genre {
|
||||
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(),
|
||||
|
@ -152,6 +158,36 @@ fn conv_genre<'a>(id: &str, name: String, gm: &GenreMeta) -> Genre {
|
|||
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)) {
|
||||
Some(lang) => path!(self.path / "tree" / format!("tree.{lang}.json")),
|
||||
None => path!(self.path / "tree" / "tree.en.json"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -177,14 +213,25 @@ 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!["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 +246,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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,28 +58,7 @@ 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,
|
||||
}
|
||||
serde_plain::derive_display_from_serialize!(Region);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
|
@ -101,7 +81,7 @@ pub struct GenreMeta {
|
|||
pub rank: Option<u32>,
|
||||
#[cfg(feature = "playlists")]
|
||||
#[serde(default)]
|
||||
pub playlists: HashMap<PlaylistKind, String>,
|
||||
pub playlists: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub deprecated: bool,
|
||||
#[serde(default)]
|
||||
|
@ -110,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<String>,
|
||||
/// ISO-639-3 language code if the genre implies lyrics in a specific language
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub language: Option<String>,
|
||||
/// ISO-3166-1 country code if the genre is dominant in a specific country
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub country: Option<String>,
|
||||
#[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<Region>,
|
||||
/// Position in the popularity ranking (1: most popular)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rank: Option<u32>,
|
||||
/// Spotify playlist IDs
|
||||
#[cfg(feature = "playlists")]
|
||||
#[serde(default)]
|
||||
pub playlists: HashMap<PlaylistKind, String>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub playlists: Option<HashMap<String, String>>,
|
||||
/// 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: Default + PartialEq>(t: &T) -> bool {
|
||||
t == &T::default()
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue