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
}