package tags import ( "fmt" "sort" "strings" ) const ( FieldTrackNumber = "TRACKNUMBER" FieldTrackTotal = "TRACKTOTAL" FieldDiscNumber = "DISCNUMBER" FieldDiscTotal = "DISCTOTAL" FieldTitle = "TITLE" FieldAlbum = "ALBUM" FieldArtist = "ARTIST" FieldAlbumArtist = "ALBUMARTIST" FieldDate = "DATE" FieldLegacyTrackTotal = "TOTALTRACKS" FieldLegacyDiscTotal = "TOTALDISCS" ) var Keep = []string{ FieldTrackNumber, // the totals are counted from the files being written rather than copied, so // they can't disagree with the album. both spellings because players disagree // about which they read. FieldTrackTotal, FieldLegacyTrackTotal, FieldDiscNumber, FieldDiscTotal, FieldLegacyDiscTotal, FieldTitle, FieldAlbum, FieldArtist, FieldAlbumArtist, FieldDate, } type Note struct { Field string Value string } func IsKept(field string) bool { f := normalizeKey(field) for _, k := range Keep { if k == f { return true } } return false } func Parse(lines []string) []Note { var out []Note for _, line := range lines { line = strings.TrimRight(line, "\r") if line == "" || strings.HasPrefix(line, "#") { continue } k, v, ok := strings.Cut(line, "=") if !ok { continue } k = strings.ToUpper(strings.TrimSpace(k)) if k == "" { continue } out = append(out, Note{Field: k, Value: v}) } return out } // normalizeKey folds a raw Vorbis comment key into the form the whitelist uses. // // Vorbis keys are free-form and the same field turns up written several ways: // ALBUM ARTIST and ALBUM_ARTIST are both common in the wild. Without folding the // separators away, such a tag matches nothing, gets reported as a tag being // removed, and the value it carried looks like it was never in the file at all. func normalizeKey(k string) string { k = strings.ToUpper(strings.TrimSpace(k)) return strings.NewReplacer(" ", "", "_", "").Replace(k) } func Get(notes []Note, field string) (string, bool) { f := normalizeKey(field) for _, n := range notes { if normalizeKey(n.Field) == f { return n.Value, true } } return "", false } func Dropped(notes []Note) []string { seen := map[string]bool{} var out []string for _, n := range notes { if IsKept(n.Field) || seen[n.Field] { continue } seen[n.Field] = true out = append(out, n.Field) } sort.Strings(out) return out } func NormalizeYear(date string) string { date = strings.TrimSpace(date) if date == "" { return "" } var run strings.Builder flush := func() string { s := run.String() run.Reset() if len(s) == 4 { if s[0] == '1' || s[0] == '2' { return s } } return "" } for _, r := range date { if r >= '0' && r <= '9' { if run.Len() == 4 { run.Reset() continue } run.WriteRune(r) continue } if y := flush(); y != "" { return y } } return flush() } func Write(notes []Note) ([]string, error) { seen := map[string]bool{} var lines []string for _, n := range Keep { if seen[n] { continue } v, ok := Get(notes, n) if !ok || strings.TrimSpace(v) == "" { continue } seen[n] = true lines = append(lines, fmt.Sprintf("%s=%s", n, v)) } return lines, nil }