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:
|
||||
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,10 +2,6 @@
|
|||
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 = []
|
||||
|
@ -19,5 +15,4 @@ 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, Region};
|
||||
pub use model::Genre;
|
||||
|
||||
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
|
||||
/// Accept-Language http headere).
|
||||
pub fn select_lang<'a, S: AsRef<str>>(&self, langs: &'a [S]) -> Option<&'a str> {
|
||||
|
@ -130,64 +125,33 @@ impl GenreDb {
|
|||
GenreEntry::Alias { alias } => self.get_localized(alias, lang),
|
||||
GenreEntry::Meta(genre) => {
|
||||
if let Some(lang) = lang {
|
||||
Some(
|
||||
self.conv_genre(
|
||||
id,
|
||||
self.get_translated_name(id, lang)
|
||||
.unwrap_or_else(|| genre.name.to_owned()),
|
||||
genre,
|
||||
),
|
||||
)
|
||||
Some(conv_genre(
|
||||
id,
|
||||
self.get_translated_name(id, lang)
|
||||
.unwrap_or_else(|| genre.name.to_owned()),
|
||||
genre,
|
||||
))
|
||||
} 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 {
|
||||
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)) {
|
||||
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();
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -213,25 +177,14 @@ 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()];
|
||||
let languages = vec!["es".to_owned(), "de".to_owned(), "en-GB".to_owned()];
|
||||
|
||||
let db = GenreDb::new(genre_db_path()).unwrap();
|
||||
let lang = db.select_lang(&languages);
|
||||
|
@ -246,18 +199,4 @@ 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,9 +4,8 @@ use std::collections::HashMap;
|
|||
use serde::{Deserialize, Serialize};
|
||||
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)]
|
||||
#[non_exhaustive]
|
||||
pub enum Region {
|
||||
/// Africa
|
||||
AF,
|
||||
|
@ -58,7 +57,28 @@ pub enum Region {
|
|||
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)]
|
||||
#[serde(untagged)]
|
||||
|
@ -81,7 +101,7 @@ pub struct GenreMeta {
|
|||
pub rank: Option<u32>,
|
||||
#[cfg(feature = "playlists")]
|
||||
#[serde(default)]
|
||||
pub playlists: HashMap<String, String>,
|
||||
pub playlists: HashMap<PlaylistKind, String>,
|
||||
#[serde(default)]
|
||||
pub deprecated: bool,
|
||||
#[serde(default)]
|
||||
|
@ -90,51 +110,21 @@ 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>,
|
||||
/// 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(default)]
|
||||
#[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(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")]
|
||||
#[serde(default)]
|
||||
pub playlists: HashMap<PlaylistKind, String>,
|
||||
#[serde(default)]
|
||||
pub deprecated: bool,
|
||||
/// 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")]
|
||||
#[serde(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\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",
|
||||
"enum": [
|
||||
"AF",
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
"pattern": "^[A-Z]{2}$"
|
||||
},
|
||||
"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",
|
||||
"enum": [
|
||||
"AF",
|
||||
|
@ -112,7 +112,7 @@
|
|||
"pattern": "^[a-z0-9\\-+: '&]+$"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"metagenre": {
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
"pattern": "^[A-Z]{2}$"
|
||||
},
|
||||
"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",
|
||||
"enum": [
|
||||
"AF",
|
||||
|
|
Loading…
Reference in a new issue