Add worldwide genre style selector

This commit is contained in:
diyaa
2026-09-15 18:49:34 +02:00
parent 03bfd3ef75
commit 297b791a3e
10 changed files with 607 additions and 15 deletions
@@ -594,6 +594,35 @@ public struct GenreStyle: Codable, Equatable, Identifiable, Sendable {
}
}
public struct GenreStyleCatalogItem: Codable, Equatable, Identifiable, Sendable {
public let id: String
public var name: String
public var familyCategory: String
public var regionOrigin: String?
public var aliases: [String]
public var searchTerms: [String]
public init(
id: String,
name: String,
familyCategory: String,
regionOrigin: String? = nil,
aliases: [String] = [],
searchTerms: [String] = []
) {
self.id = id
self.name = name
self.familyCategory = familyCategory
self.regionOrigin = regionOrigin
self.aliases = aliases
self.searchTerms = searchTerms
}
public var genreStyle: GenreStyle {
GenreStyle(id: id, name: name, aliases: aliases)
}
}
public struct MoodTag: Codable, Equatable, Identifiable, Sendable {
public let id: String
public var name: String
@@ -0,0 +1,338 @@
import Foundation
public struct LocalGenreStyleCatalog: Sendable {
public let items: [GenreStyleCatalogItem]
public init(items: [GenreStyleCatalogItem] = LocalGenreStyleCatalog.worldwideItems) {
self.items = items
}
public var familyCategories: [String] {
Array(Set(items.map(\.familyCategory)))
.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
}
public var regionsOfOrigin: [String] {
Array(Set(items.compactMap(\.regionOrigin)))
.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
}
public func styles(
matching query: String,
familyCategory: String? = nil,
regionOrigin: String? = nil
) -> [GenreStyleCatalogItem] {
let searchTokens = Self.searchTokens(for: query)
let selectedFamilyCategory = familyCategory?.trimmingCharacters(in: .whitespacesAndNewlines)
let selectedRegionOrigin = regionOrigin?.trimmingCharacters(in: .whitespacesAndNewlines)
return items
.filter { item in
let matchesSearch = searchTokens.isEmpty || searchTokens.allSatisfy { token in
Self.searchableTerms(for: item).contains { $0.contains(token) }
}
let matchesFamily = selectedFamilyCategory?.isEmpty != false || item.familyCategory == selectedFamilyCategory
let matchesRegion = selectedRegionOrigin?.isEmpty != false || item.regionOrigin == selectedRegionOrigin
return matchesSearch && matchesFamily && matchesRegion
}
.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
}
public static let worldwideItems: [GenreStyleCatalogItem] =
popAndContemporary + rockAndAlternative + electronicAndDance + urbanAndSoul + jazzAndBlues +
countryFolkAndRoots + latinAndCaribbean + middleEasternAndNorthAfrican + southAsian + eastAsian +
southeastAsianAndOceania + african + reggaeAndCaribbean + classicalAndCinematic
private static let popAndContemporary: [GenreStyleCatalogItem] = [
item("pop", "Pop", "Pop & Contemporary", "Global"),
item("adult-contemporary", "Adult Contemporary", "Pop & Contemporary", "Western"),
item("art-pop", "Art Pop", "Pop & Contemporary", "Western"),
item("baroque-pop", "Baroque Pop", "Pop & Contemporary", "Western"),
item("chamber-pop", "Chamber Pop", "Pop & Contemporary", "Western"),
item("dance-pop", "Dance Pop", "Pop & Contemporary", "Western"),
item("dream-pop", "Dream Pop", "Pop & Contemporary", "Western"),
item("electropop", "Electropop", "Pop & Contemporary", "Western"),
item("hyperpop", "Hyperpop", "Pop & Contemporary", "Western"),
item("indie-pop", "Indie Pop", "Pop & Contemporary", "Western"),
item("power-pop", "Power Pop", "Pop & Contemporary", "Western"),
item("synth-pop", "Synth Pop", "Pop & Contemporary", "Western"),
item("teen-pop", "Teen Pop", "Pop & Contemporary", "Western"),
item("soft-pop", "Soft Pop", "Pop & Contemporary", "Western"),
item("singer-songwriter", "Singer-Songwriter", "Pop & Contemporary", "Western")
]
private static let rockAndAlternative: [GenreStyleCatalogItem] = [
item("rock", "Rock", "Rock & Alternative", "Western"),
item("alternative-rock", "Alternative Rock", "Rock & Alternative", "Western", aliases: ["alt rock"]),
item("classic-rock", "Classic Rock", "Rock & Alternative", "Western"),
item("hard-rock", "Hard Rock", "Rock & Alternative", "Western"),
item("indie-rock", "Indie Rock", "Rock & Alternative", "Western"),
item("progressive-rock", "Progressive Rock", "Rock & Alternative", "Western", aliases: ["prog rock"]),
item("psychedelic-rock", "Psychedelic Rock", "Rock & Alternative", "Western", aliases: ["psych rock"]),
item("garage-rock", "Garage Rock", "Rock & Alternative", "Western"),
item("surf-rock", "Surf Rock", "Rock & Alternative", "Western"),
item("post-rock", "Post-Rock", "Rock & Alternative", "Western"),
item("math-rock", "Math Rock", "Rock & Alternative", "Western"),
item("grunge", "Grunge", "Rock & Alternative", "Western"),
item("shoegaze", "Shoegaze", "Rock & Alternative", "Western"),
item("emo", "Emo", "Rock & Alternative", "Western"),
item("punk", "Punk", "Rock & Alternative", "Western"),
item("pop-punk", "Pop Punk", "Rock & Alternative", "Western"),
item("post-punk", "Post-Punk", "Rock & Alternative", "Western"),
item("metal", "Metal", "Rock & Alternative", "Western"),
item("heavy-metal", "Heavy Metal", "Rock & Alternative", "Western"),
item("power-metal", "Power Metal", "Rock & Alternative", "Western"),
item("progressive-metal", "Progressive Metal", "Rock & Alternative", "Western"),
item("thrash-metal", "Thrash Metal", "Rock & Alternative", "Western"),
item("death-metal", "Death Metal", "Rock & Alternative", "Western"),
item("black-metal", "Black Metal", "Rock & Alternative", "Western"),
item("doom-metal", "Doom Metal", "Rock & Alternative", "Western"),
item("folk-rock", "Folk Rock", "Rock & Alternative", "Western"),
item("blues-rock", "Blues Rock", "Rock & Alternative", "Western")
]
private static let electronicAndDance: [GenreStyleCatalogItem] = [
item("electronic", "Electronic", "Electronic & Dance", "Global"),
item("edm", "EDM", "Electronic & Dance", "Global", aliases: ["electronic dance music"]),
item("ambient", "Ambient", "Electronic & Dance", "Western"),
item("downtempo", "Downtempo", "Electronic & Dance", "Western"),
item("chillout", "Chillout", "Electronic & Dance", "Western"),
item("lofi", "Lo-Fi", "Electronic & Dance", "Western", aliases: ["lofi"]),
item("synthwave", "Synthwave", "Electronic & Dance", "Western"),
item("vaporwave", "Vaporwave", "Electronic & Dance", "Western"),
item("electro", "Electro", "Electronic & Dance", "Western"),
item("industrial", "Industrial", "Electronic & Dance", "Western"),
item("house", "House", "Electronic & Dance", "Western"),
item("deep-house", "Deep House", "Electronic & Dance", "Western"),
item("tech-house", "Tech House", "Electronic & Dance", "Western"),
item("progressive-house", "Progressive House", "Electronic & Dance", "Western"),
item("future-house", "Future House", "Electronic & Dance", "Western"),
item("techno", "Techno", "Electronic & Dance", "Western"),
item("minimal-techno", "Minimal Techno", "Electronic & Dance", "Western"),
item("trance", "Trance", "Electronic & Dance", "Western"),
item("progressive-trance", "Progressive Trance", "Electronic & Dance", "Western"),
item("psytrance", "Psytrance", "Electronic & Dance", "Western"),
item("drum-and-bass", "Drum and Bass", "Electronic & Dance", "UK", aliases: ["dnb"]),
item("jungle", "Jungle", "Electronic & Dance", "UK"),
item("dubstep", "Dubstep", "Electronic & Dance", "UK"),
item("uk-garage", "UK Garage", "Electronic & Dance", "UK"),
item("breakbeat", "Breakbeat", "Electronic & Dance", "Western"),
item("hardstyle", "Hardstyle", "Electronic & Dance", "Europe"),
item("moombahton", "Moombahton", "Electronic & Dance", "Latin America")
]
private static let urbanAndSoul: [GenreStyleCatalogItem] = [
item("hip-hop", "Hip Hop", "Hip Hop, R&B & Soul", "Global", aliases: ["hiphop"]),
item("rap", "Rap", "Hip Hop, R&B & Soul", "Global"),
item("boom-bap", "Boom Bap", "Hip Hop, R&B & Soul", "US"),
item("trap", "Trap", "Hip Hop, R&B & Soul", "US"),
item("drill", "Drill", "Hip Hop, R&B & Soul", "US/UK"),
item("conscious-hip-hop", "Conscious Hip Hop", "Hip Hop, R&B & Soul", "US"),
item("cloud-rap", "Cloud Rap", "Hip Hop, R&B & Soul", "US"),
item("lofi-hip-hop", "Lo-Fi Hip Hop", "Hip Hop, R&B & Soul", "Global"),
item("r-and-b", "R&B", "Hip Hop, R&B & Soul", "US", aliases: ["rhythm and blues"]),
item("contemporary-r-and-b", "Contemporary R&B", "Hip Hop, R&B & Soul", "US"),
item("soul", "Soul", "Hip Hop, R&B & Soul", "US"),
item("neo-soul", "Neo Soul", "Hip Hop, R&B & Soul", "US"),
item("funk", "Funk", "Hip Hop, R&B & Soul", "US"),
item("disco", "Disco", "Hip Hop, R&B & Soul", "US"),
item("nu-disco", "Nu Disco", "Hip Hop, R&B & Soul", "Western")
]
private static let jazzAndBlues: [GenreStyleCatalogItem] = [
item("jazz", "Jazz", "Jazz & Blues", "US"),
item("swing", "Swing", "Jazz & Blues", "US"),
item("bebop", "Bebop", "Jazz & Blues", "US"),
item("cool-jazz", "Cool Jazz", "Jazz & Blues", "US"),
item("hard-bop", "Hard Bop", "Jazz & Blues", "US"),
item("modal-jazz", "Modal Jazz", "Jazz & Blues", "US"),
item("jazz-fusion", "Jazz Fusion", "Jazz & Blues", "US"),
item("smooth-jazz", "Smooth Jazz", "Jazz & Blues", "US"),
item("vocal-jazz", "Vocal Jazz", "Jazz & Blues", "US"),
item("big-band", "Big Band", "Jazz & Blues", "US"),
item("blues", "Blues", "Jazz & Blues", "US"),
item("delta-blues", "Delta Blues", "Jazz & Blues", "US"),
item("chicago-blues", "Chicago Blues", "Jazz & Blues", "US"),
item("gospel", "Gospel", "Jazz & Blues", "US")
]
private static let countryFolkAndRoots: [GenreStyleCatalogItem] = [
item("country", "Country", "Country, Folk & Roots", "US"),
item("modern-country", "Modern Country", "Country, Folk & Roots", "US"),
item("americana", "Americana", "Country, Folk & Roots", "US"),
item("bluegrass", "Bluegrass", "Country, Folk & Roots", "US"),
item("folk", "Folk", "Country, Folk & Roots", "Global"),
item("indie-folk", "Indie Folk", "Country, Folk & Roots", "Western"),
item("celtic", "Celtic", "Country, Folk & Roots", "Ireland/Scotland"),
item("roots", "Roots", "Country, Folk & Roots", "Global"),
item("traditional", "Traditional", "Country, Folk & Roots", "Global"),
item("world", "World", "Country, Folk & Roots", "Global", aliases: ["world music"]),
item("skiffle", "Skiffle", "Country, Folk & Roots", "UK")
]
private static let latinAndCaribbean: [GenreStyleCatalogItem] = [
item("latin-pop", "Latin Pop", "Latin & Caribbean", "Latin America"),
item("reggaeton", "Reggaeton", "Latin & Caribbean", "Caribbean"),
item("salsa", "Salsa", "Latin & Caribbean", "Caribbean"),
item("bachata", "Bachata", "Latin & Caribbean", "Caribbean"),
item("merengue", "Merengue", "Latin & Caribbean", "Caribbean"),
item("cumbia", "Cumbia", "Latin & Caribbean", "Latin America"),
item("bossa-nova", "Bossa Nova", "Latin & Caribbean", "Brazil"),
item("samba", "Samba", "Latin & Caribbean", "Brazil"),
item("tango", "Tango", "Latin & Caribbean", "Argentina/Uruguay"),
item("bolero", "Bolero", "Latin & Caribbean", "Latin America"),
item("mariachi", "Mariachi", "Latin & Caribbean", "Mexico"),
item("norteno", "Norteno", "Latin & Caribbean", "Mexico"),
item("banda", "Banda", "Latin & Caribbean", "Mexico"),
item("corrido", "Corrido", "Latin & Caribbean", "Mexico"),
item("baile-funk", "Baile Funk", "Latin & Caribbean", "Brazil"),
item("dembow", "Dembow", "Latin & Caribbean", "Caribbean")
]
private static let middleEasternAndNorthAfrican: [GenreStyleCatalogItem] = [
item("arabic-pop", "Arabic Pop", "Middle East & North Africa", "Arab World", aliases: ["arab pop"], terms: ["بوب عربي"]),
item("arabic-traditional", "Arabic Traditional", "Middle East & North Africa", "Arab World", terms: ["موسيقى عربية", "طرب"]),
item("khaleeji", "Khaleeji", "Middle East & North Africa", "Gulf", aliases: ["gulf pop"], terms: ["خليجي"]),
item("levantine-pop", "Levantine Pop", "Middle East & North Africa", "Levant", terms: ["شامي"]),
item("egyptian-pop", "Egyptian Pop", "Middle East & North Africa", "Egypt", terms: ["مصري"]),
item("iraqi-pop", "Iraqi Pop", "Middle East & North Africa", "Iraq", terms: ["عراقي"]),
item("maghrebi-pop", "Maghrebi Pop", "Middle East & North Africa", "North Africa", terms: ["مغاربي"]),
item("rai", "Rai", "Middle East & North Africa", "Algeria/Morocco"),
item("chaabi", "Chaabi", "Middle East & North Africa", "North Africa"),
item("gnawa", "Gnawa", "Middle East & North Africa", "Morocco"),
item("andalusian", "Andalusian", "Middle East & North Africa", "North Africa"),
item("turkish-pop", "Turkish Pop", "Middle East & North Africa", "Turkey"),
item("turkish-folk", "Turkish Folk", "Middle East & North Africa", "Turkey"),
item("persian-pop", "Persian Pop", "Middle East & North Africa", "Iran"),
item("persian-traditional", "Persian Traditional", "Middle East & North Africa", "Iran"),
item("kurdish", "Kurdish", "Middle East & North Africa", "Kurdistan"),
item("armenian", "Armenian", "Middle East & North Africa", "Armenia")
]
private static let southAsian: [GenreStyleCatalogItem] = [
item("bollywood", "Bollywood", "South Asian", "India", aliases: ["filmi"]),
item("hindustani-classical", "Hindustani Classical", "South Asian", "India"),
item("carnatic", "Carnatic", "South Asian", "South India"),
item("bhangra", "Bhangra", "South Asian", "Punjab"),
item("punjabi-pop", "Punjabi Pop", "South Asian", "Punjab"),
item("ghazal", "Ghazal", "South Asian", "South Asia"),
item("qawwali", "Qawwali", "South Asian", "South Asia"),
item("bhajan", "Bhajan", "South Asian", "India"),
item("sufi", "Sufi", "South Asian", "South Asia"),
item("pakistani-pop", "Pakistani Pop", "South Asian", "Pakistan"),
item("bangladeshi-folk", "Bangladeshi Folk", "South Asian", "Bangladesh")
]
private static let eastAsian: [GenreStyleCatalogItem] = [
item("k-pop", "K-Pop", "East Asian", "Korea", aliases: ["kpop"]),
item("korean-traditional", "Korean Traditional", "East Asian", "Korea"),
item("j-pop", "J-Pop", "East Asian", "Japan", aliases: ["jpop"]),
item("japanese-city-pop", "Japanese City Pop", "East Asian", "Japan", aliases: ["city pop"]),
item("enka", "Enka", "East Asian", "Japan"),
item("anime-music", "Anime Music", "East Asian", "Japan"),
item("c-pop", "C-Pop", "East Asian", "China", aliases: ["cpop"]),
item("mandopop", "Mandopop", "East Asian", "China/Taiwan"),
item("cantopop", "Cantopop", "East Asian", "Hong Kong"),
item("chinese-traditional", "Chinese Traditional", "East Asian", "China"),
item("taiwanese-pop", "Taiwanese Pop", "East Asian", "Taiwan")
]
private static let southeastAsianAndOceania: [GenreStyleCatalogItem] = [
item("indonesian-pop", "Indonesian Pop", "Southeast Asia & Oceania", "Indonesia"),
item("dangdut", "Dangdut", "Southeast Asia & Oceania", "Indonesia"),
item("gamelan", "Gamelan", "Southeast Asia & Oceania", "Indonesia"),
item("malay-pop", "Malay Pop", "Southeast Asia & Oceania", "Malaysia"),
item("thai-pop", "Thai Pop", "Southeast Asia & Oceania", "Thailand"),
item("luk-thung", "Luk Thung", "Southeast Asia & Oceania", "Thailand"),
item("vietnamese-pop", "Vietnamese Pop", "Southeast Asia & Oceania", "Vietnam"),
item("filipino-pop", "Filipino Pop", "Southeast Asia & Oceania", "Philippines"),
item("pinoy-rock", "Pinoy Rock", "Southeast Asia & Oceania", "Philippines"),
item("maori", "Maori", "Southeast Asia & Oceania", "New Zealand"),
item("pacific", "Pacific", "Southeast Asia & Oceania", "Oceania"),
item("australian-indigenous", "Australian Indigenous", "Southeast Asia & Oceania", "Australia")
]
private static let african: [GenreStyleCatalogItem] = [
item("afrobeats", "Afrobeats", "African", "West Africa"),
item("afropop", "Afropop", "African", "Africa"),
item("afro-house", "Afro House", "African", "Africa"),
item("amapiano", "Amapiano", "African", "South Africa"),
item("highlife", "Highlife", "African", "West Africa"),
item("juju", "Juju", "African", "Nigeria"),
item("fuji", "Fuji", "African", "Nigeria"),
item("benga", "Benga", "African", "East Africa"),
item("soukous", "Soukous", "African", "Central Africa"),
item("kwaito", "Kwaito", "African", "South Africa"),
item("mbalax", "Mbalax", "African", "Senegal"),
item("taarab", "Taarab", "African", "East Africa"),
item("ethiopian", "Ethiopian", "African", "Ethiopia"),
item("sudanese", "Sudanese", "African", "Sudan"),
item("desert-blues", "Desert Blues", "African", "Sahara")
]
private static let reggaeAndCaribbean: [GenreStyleCatalogItem] = [
item("reggae", "Reggae", "Reggae & Caribbean", "Jamaica"),
item("roots-reggae", "Roots Reggae", "Reggae & Caribbean", "Jamaica"),
item("dub", "Dub", "Reggae & Caribbean", "Jamaica"),
item("dancehall", "Dancehall", "Reggae & Caribbean", "Jamaica"),
item("ska", "Ska", "Reggae & Caribbean", "Jamaica"),
item("rocksteady", "Rocksteady", "Reggae & Caribbean", "Jamaica"),
item("lovers-rock", "Lovers Rock", "Reggae & Caribbean", "Jamaica"),
item("calypso", "Calypso", "Reggae & Caribbean", "Caribbean"),
item("soca", "Soca", "Reggae & Caribbean", "Caribbean")
]
private static let classicalAndCinematic: [GenreStyleCatalogItem] = [
item("classical", "Classical", "Classical & Cinematic", "Western"),
item("baroque", "Baroque", "Classical & Cinematic", "Europe"),
item("classical-period", "Classical Period", "Classical & Cinematic", "Europe"),
item("romantic", "Romantic", "Classical & Cinematic", "Europe"),
item("contemporary-classical", "Contemporary Classical", "Classical & Cinematic", "Global"),
item("orchestral", "Orchestral", "Classical & Cinematic", "Western"),
item("chamber-music", "Chamber Music", "Classical & Cinematic", "Western"),
item("opera", "Opera", "Classical & Cinematic", "Europe"),
item("choral", "Choral", "Classical & Cinematic", "Global"),
item("cinematic", "Cinematic", "Classical & Cinematic", "Global"),
item("soundtrack", "Soundtrack", "Classical & Cinematic", "Global"),
item("musical-theater", "Musical Theater", "Classical & Cinematic", "Western"),
item("new-age", "New Age", "Classical & Cinematic", "Western"),
item("minimalism", "Minimalism", "Classical & Cinematic", "Western")
]
private static func item(
_ id: String,
_ name: String,
_ familyCategory: String,
_ regionOrigin: String? = nil,
aliases: [String] = [],
terms: [String] = []
) -> GenreStyleCatalogItem {
GenreStyleCatalogItem(
id: id,
name: name,
familyCategory: familyCategory,
regionOrigin: regionOrigin,
aliases: aliases,
searchTerms: terms
)
}
private static func searchableTerms(for item: GenreStyleCatalogItem) -> [String] {
searchTokens(for: [item.name, item.familyCategory, item.regionOrigin ?? ""] + item.aliases + item.searchTerms)
}
private static func searchTokens(for query: String) -> [String] {
searchTokens(for: [query])
}
private static func searchTokens(for values: [String]) -> [String] {
values
.joined(separator: " ")
.folding(
options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive],
locale: Locale(identifier: "en_US_POSIX")
)
.split(whereSeparator: { $0.isWhitespace || $0 == "-" || $0 == "/" })
.map(String.init)
}
}