changed genre scanning to parse multiple genres, updated db to accommodate.
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user