Compare commits
No commits in common. "4560a5ba113feed24e17ba435c4effbb52babea6" and "bf4ab15a6f03e259117eed1660b873f61c7384fd" have entirely different histories.
4560a5ba11
...
bf4ab15a6f
7 changed files with 55 additions and 148 deletions
|
@ -453,23 +453,6 @@ def package(out_dir: Path):
|
||||||
with open(util.TRANSLATION_FILE_EN) as f:
|
with open(util.TRANSLATION_FILE_EN) as f:
|
||||||
tl_en = json.load(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
|
# Genre database
|
||||||
db = {
|
db = {
|
||||||
g_id: model.GenreMetadataDB.conv(genre, tl_en.get(g_id))
|
g_id: model.GenreMetadataDB.conv(genre, tl_en.get(g_id))
|
||||||
|
|
|
@ -2,10 +2,6 @@
|
||||||
name = "spotify-genrebase"
|
name = "spotify-genrebase"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["ThetaDev <thetadev@magenta.de>"]
|
|
||||||
license = "MIT"
|
|
||||||
description = "Lookup Spotify genre metadata"
|
|
||||||
repository = "https://code.thetadev.de/Tiraya/spotify-genres"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
playlists = []
|
playlists = []
|
||||||
|
@ -19,5 +15,4 @@ serde_with = { version = "3.0", default-features = false, features = [
|
||||||
"macros",
|
"macros",
|
||||||
] }
|
] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_plain = "1.0"
|
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
|
|
|
@ -6,7 +6,7 @@ use path_macro::path;
|
||||||
|
|
||||||
use model::{GenreEntry, GenreMeta};
|
use model::{GenreEntry, GenreMeta};
|
||||||
|
|
||||||
pub use model::{Genre, Region};
|
pub use model::Genre;
|
||||||
|
|
||||||
type Translation = HashMap<String, String>;
|
type Translation = HashMap<String, String>;
|
||||||
|
|
||||||
|
@ -55,11 +55,6 @@ 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
|
/// Select a supported language from a list of given languages (for example from the
|
||||||
/// Accept-Language http headere).
|
/// Accept-Language http headere).
|
||||||
pub fn select_lang<'a, S: AsRef<str>>(&self, langs: &'a [S]) -> Option<&'a str> {
|
pub fn select_lang<'a, S: AsRef<str>>(&self, langs: &'a [S]) -> Option<&'a str> {
|
||||||
|
@ -130,23 +125,22 @@ impl GenreDb {
|
||||||
GenreEntry::Alias { alias } => self.get_localized(alias, lang),
|
GenreEntry::Alias { alias } => self.get_localized(alias, lang),
|
||||||
GenreEntry::Meta(genre) => {
|
GenreEntry::Meta(genre) => {
|
||||||
if let Some(lang) = lang {
|
if let Some(lang) = lang {
|
||||||
Some(
|
Some(conv_genre(
|
||||||
self.conv_genre(
|
|
||||||
id,
|
id,
|
||||||
self.get_translated_name(id, lang)
|
self.get_translated_name(id, lang)
|
||||||
.unwrap_or_else(|| genre.name.to_owned()),
|
.unwrap_or_else(|| genre.name.to_owned()),
|
||||||
genre,
|
genre,
|
||||||
),
|
))
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
Some(self.conv_genre(id, genre.name.to_owned(), genre))
|
Some(conv_genre(id, genre.name.to_owned(), genre))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn conv_genre<'a>(&self, id: &str, name: String, gm: &GenreMeta) -> Genre {
|
fn conv_genre<'a>(id: &str, name: String, gm: &GenreMeta) -> Genre {
|
||||||
let mut genre = Genre {
|
Genre {
|
||||||
id: id.to_owned(),
|
id: id.to_owned(),
|
||||||
name: name,
|
name: name,
|
||||||
parent: gm.parent.to_owned(),
|
parent: gm.parent.to_owned(),
|
||||||
|
@ -158,36 +152,6 @@ impl GenreDb {
|
||||||
metagenre: gm.metagenre,
|
metagenre: gm.metagenre,
|
||||||
#[cfg(feature = "playlists")]
|
#[cfg(feature = "playlists")]
|
||||||
playlists: gm.playlists.clone(),
|
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,25 +177,14 @@ mod tests {
|
||||||
let genre = db.get_localized(id, lang).unwrap();
|
let genre = db.get_localized(id, lang).unwrap();
|
||||||
assert_eq!(genre.id, id);
|
assert_eq!(genre.id, id);
|
||||||
assert_eq!(genre.name, expect);
|
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();
|
let localized_name = db.get_localized_name(id, lang).unwrap();
|
||||||
assert_eq!(localized_name, expect)
|
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]
|
#[test]
|
||||||
fn select_lang() {
|
fn select_lang() {
|
||||||
let languages = vec!["xy".to_owned(), "de".to_owned(), "en-GB".to_owned()];
|
let languages = vec!["es".to_owned(), "de".to_owned(), "en-GB".to_owned()];
|
||||||
|
|
||||||
let db = GenreDb::new(genre_db_path()).unwrap();
|
let db = GenreDb::new(genre_db_path()).unwrap();
|
||||||
let lang = db.select_lang(&languages);
|
let lang = db.select_lang(&languages);
|
||||||
|
@ -246,18 +199,4 @@ mod tests {
|
||||||
let lang = db.select_lang(&languages);
|
let lang = db.select_lang(&languages);
|
||||||
assert_eq!(lang, None);
|
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,9 +4,8 @@ use std::collections::HashMap;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::{serde_as, DefaultOnError};
|
use serde_with::{serde_as, DefaultOnError};
|
||||||
|
|
||||||
/// Region for describing genres from multiple countrie
|
/// Region for describing genres from multiple countries
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum Region {
|
pub enum Region {
|
||||||
/// Africa
|
/// Africa
|
||||||
AF,
|
AF,
|
||||||
|
@ -58,7 +57,28 @@ pub enum Region {
|
||||||
WF,
|
WF,
|
||||||
}
|
}
|
||||||
|
|
||||||
serde_plain::derive_display_from_serialize!(Region);
|
#[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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
|
@ -81,7 +101,7 @@ pub struct GenreMeta {
|
||||||
pub rank: Option<u32>,
|
pub rank: Option<u32>,
|
||||||
#[cfg(feature = "playlists")]
|
#[cfg(feature = "playlists")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub playlists: HashMap<String, String>,
|
pub playlists: HashMap<PlaylistKind, String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub deprecated: bool,
|
pub deprecated: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -90,51 +110,21 @@ pub struct GenreMeta {
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[non_exhaustive]
|
|
||||||
pub struct Genre {
|
pub struct Genre {
|
||||||
/// Spotify genre ID
|
|
||||||
pub id: String,
|
pub id: String,
|
||||||
/// Localized genre name
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// ID of the parent genre if this is a subgenre
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub parent: Option<String>,
|
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>,
|
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>,
|
pub country: Option<String>,
|
||||||
/// Region code if the genre is dominant in a specific region
|
#[serde(default)]
|
||||||
///
|
|
||||||
/// 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")]
|
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||||
pub region: Option<Region>,
|
pub region: Option<Region>,
|
||||||
/// Position in the popularity ranking (1: most popular)
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub rank: Option<u32>,
|
pub rank: Option<u32>,
|
||||||
/// Spotify playlist IDs
|
|
||||||
#[cfg(feature = "playlists")]
|
#[cfg(feature = "playlists")]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(default)]
|
||||||
pub playlists: Option<HashMap<String, String>>,
|
pub playlists: HashMap<PlaylistKind, String>,
|
||||||
/// True if the genre is no longer part of Spotify's catalog
|
#[serde(default)]
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
|
||||||
pub deprecated: bool,
|
pub deprecated: bool,
|
||||||
/// True if the genre is a metagenre
|
#[serde(default)]
|
||||||
///
|
|
||||||
/// Metagenres do not exist at Spotify but are used to group other genres together.
|
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
|
||||||
pub metagenre: bool,
|
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}$"
|
"pattern": "^[A-Z]{2}$"
|
||||||
},
|
},
|
||||||
"region": {
|
"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.",
|
"description": "Region code if the genre is dominant in a specific region",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"AF",
|
"AF",
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
"pattern": "^[A-Z]{2}$"
|
"pattern": "^[A-Z]{2}$"
|
||||||
},
|
},
|
||||||
"region": {
|
"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.",
|
"description": "Region code if the genre is dominant in a specific region",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"AF",
|
"AF",
|
||||||
|
@ -112,7 +112,7 @@
|
||||||
"pattern": "^[a-z0-9\\-+: '&]+$"
|
"pattern": "^[a-z0-9\\-+: '&]+$"
|
||||||
},
|
},
|
||||||
"deprecated": {
|
"deprecated": {
|
||||||
"description": "True if the genre is no longer part of Spotify's catalog",
|
"description": "True if the genre is no longer part of Spotify's catalogue",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"metagenre": {
|
"metagenre": {
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
"pattern": "^[A-Z]{2}$"
|
"pattern": "^[A-Z]{2}$"
|
||||||
},
|
},
|
||||||
"region": {
|
"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.",
|
"description": "Region code if the genre is dominant in a specific region",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"AF",
|
"AF",
|
||||||
|
|
Loading…
Reference in a new issue