git/intake/main
internal/tags/tags.go
159 lines3 KB last change raw · history
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
}