changed genre scanning to parse multiple genres, updated db to accommodate.

This commit is contained in:
2026-02-22 13:59:28 -05:00
parent 3947c2d6aa
commit 9cb1c6674a
24 changed files with 457 additions and 40 deletions
+48
View File
@@ -0,0 +1,48 @@
// Package metadata provides audio file metadata extraction utilities.
package metadata
import (
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// genreSeparators defines the characters treated as genre delimiters.
const genreSeparators = ",;"
// ParseGenres splits a raw genre string on commas and semicolons,
// trims whitespace, normalizes each entry to title case, removes
// duplicates, and returns the unique genre names. An empty or
// whitespace-only input returns nil.
func ParseGenres(raw string) []string {
parts := strings.FieldsFunc(
raw, func(r rune) bool {
return strings.ContainsRune(genreSeparators, r)
},
)
caser := cases.Title(language.English)
seen := make(map[string]struct{}, len(parts))
var genres []string
for _, p := range parts {
name := strings.TrimSpace(p)
if name == "" {
continue
}
name = caser.String(name)
if _, ok := seen[name]; ok {
continue
}
seen[name] = struct{}{}
genres = append(genres, name)
}
return genres
}
+109
View File
@@ -0,0 +1,109 @@
package metadata
import (
"testing"
)
func TestParseGenres(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw string
want []string
}{
{
name: "single genre",
raw: "Rock",
want: []string{"Rock"},
},
{
name: "semicolon separated",
raw: "Rock; Electronic",
want: []string{"Rock", "Electronic"},
},
{
name: "comma separated",
raw: "Rock, Jazz",
want: []string{"Rock", "Jazz"},
},
{
name: "mixed separators",
raw: "Rock; Pop, Jazz",
want: []string{"Rock", "Pop", "Jazz"},
},
{
name: "case normalization deduplicates",
raw: "rock,ROCK,Rock",
want: []string{"Rock"},
},
{
name: "whitespace and empty segments",
raw: " Pop ; ; Jazz , ",
want: []string{"Pop", "Jazz"},
},
{
name: "empty string",
raw: "",
want: nil,
},
{
name: "only separators",
raw: ";;,,;,",
want: nil,
},
{
name: "whitespace only",
raw: " ",
want: nil,
},
{
name: "title case multi-word genre",
raw: "hip hop; drum and bass",
want: []string{"Hip Hop", "Drum And Bass"},
},
{
name: "preserves already correct casing",
raw: "Post-Punk",
want: []string{"Post-Punk"},
},
{
name: "duplicate after title case",
raw: "electronic; Electronic; ELECTRONIC",
want: []string{"Electronic"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := ParseGenres(tt.raw)
if !slicesEqual(got, tt.want) {
t.Errorf(
"ParseGenres(%q) = %v, want %v",
tt.raw, got, tt.want,
)
}
})
}
}
// slicesEqual reports whether two string slices are equal.
func slicesEqual(a, b []string) bool {
if len(a) == 0 && len(b) == 0 {
return true
}
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}