Files
filebrozer/http/metadata.go

244 lines
6.7 KiB
Go

package fbhttp
import (
"strings"
)
// canonicalMap maps various user-entered or localized keys to canonical tag names.
// Canonical names are English, PascalCase where appropriate (e.g., AlbumArtist).
var canonicalMap = map[string]string{
// title
"title": "Title",
"titre": "Title",
"song": "Title",
// artist (main)
"artist": "Artist",
"artiste": "Artist",
"artists": "Artist",
// album
"album": "Album",
// album artist
"albumartist": "AlbumArtist",
"album artist": "AlbumArtist",
"album_artist": "AlbumArtist",
"artistesdelalbum": "AlbumArtist",
"artistealbum": "AlbumArtist",
// composer / author
"composer": "Composer",
"auteur": "Composer",
// track
"track": "Track",
"tracknumber": "Track",
"trackno": "Track",
"piste": "Track",
// disc
"disc": "Disc",
"discnumber": "Disc",
"disque": "Disc",
// genre
"genre": "Genre",
// year/date
"year": "Year",
"annee": "Year",
"année": "Year",
"date": "Date",
// comment
"comment": "Comment",
"commentaire": "Comment",
// lyrics
"lyrics": "Lyrics",
}
// ffmpegKey maps canonical names to ffmpeg metadata keys.
// FFmpeg expects lower-case keys; AlbumArtist becomes album_artist, Year maps to date.
var ffmpegKey = map[string]string{
"Title": "title",
"Artist": "artist",
"Artists": "artist",
"Album": "album",
"AlbumArtist": "album_artist",
"AlbumArtists": "album_artist",
"Composer": "composer",
"Track": "track",
"TrackNumber": "track",
"Disc": "disc",
"DiscNumber": "disc",
"Genre": "genre",
"Date": "date",
"Year": "date", // prefer date for ffmpeg
"Comment": "comment",
"Lyrics": "lyrics",
}
// allowedCanonicals is the set of canonical tags we accept for writes.
var allowedCanonicals = func() map[string]struct{} {
m := make(map[string]struct{}, len(ffmpegKey))
for k := range ffmpegKey {
m[k] = struct{}{}
}
return m
}()
// normalizeKey reduces a user-provided key to a lookup token: lowercased, trimmed
// and with common separators removed.
func normalizeKey(s string) string {
s = strings.TrimSpace(s)
s = strings.ToLower(s)
// normalize separators so that "Album Artist", "album-artist", "album_artist" match
s = strings.ReplaceAll(s, "_", " ")
s = strings.ReplaceAll(s, "-", " ")
s = strings.Join(strings.Fields(s), " ") // collapse multiple spaces
return s
}
// normalizeAndMapToFFmpeg takes arbitrary incoming tags, applies key normalization
// and synonym mapping, filters to allowed canonical keys, and returns a map of
// ffmpeg-ready metadata keys with their values (empty values are dropped).
func normalizeAndMapToFFmpeg(in map[string]string) map[string]string {
out := map[string]string{}
for k, v := range in {
if strings.TrimSpace(v) == "" {
continue
}
token := normalizeKey(k)
canonical, ok := canonicalMap[token]
if !ok {
// If user already provided a canonical name, accept it
// (e.g., Title, Artist, AlbumArtist)
c2 := strings.TrimSpace(k)
if _, allowed := allowedCanonicals[c2]; allowed {
canonical = c2
} else {
// unrecognized key: skip
continue
}
}
ffk, ok := ffmpegKey[canonical]
if !ok {
continue
}
out[ffk] = v
}
return out
}
// normalizeMultiToFFmpeg maps multi-valued canonical keys to ffmpeg keys, preserving arrays.
func normalizeMultiToFFmpeg(in map[string][]string) map[string][]string {
out := map[string][]string{}
for k, arr := range in {
token := normalizeKey(k)
canonical, ok := canonicalMap[token]
if !ok {
// if already canonical, keep as-is
if _, allowed := allowedCanonicals[k]; allowed {
canonical = k
} else {
continue
}
}
ffk, ok := ffmpegKey[canonical]
if !ok {
continue
}
vals := []string{}
for _, v := range arr {
v = strings.TrimSpace(v)
if v == "" {
continue
}
vals = append(vals, v)
}
if len(vals) > 0 {
out[ffk] = vals
}
}
return out
}
// mapClearCanonicalsToFFmpeg converts a list of canonical names to ffmpeg keys
// intended to be cleared by setting them to empty value.
func mapClearCanonicalsToFFmpeg(keys []string) map[string]struct{} {
out := map[string]struct{}{}
for _, k := range keys {
token := normalizeKey(k)
canonical, ok := canonicalMap[token]
if !ok {
// if already canonical and allowed, keep
if _, allowed := allowedCanonicals[k]; allowed {
canonical = k
} else {
continue
}
}
if ffk, ok := ffmpegKey[canonical]; ok {
out[ffk] = struct{}{}
}
}
return out
}
// mapClearsToID3Frames converts canonical clear keys to ID3v2 frame IDs
func mapClearsToID3Frames(keys []string) []string {
out := []string{}
for _, k := range keys {
token := normalizeKey(k)
canonical, ok := canonicalMap[token]
if !ok {
if _, allowed := allowedCanonicals[k]; allowed {
canonical = k
} else {
continue
}
}
switch canonical {
case "Title":
out = append(out, "TIT2")
case "Artist", "Artists":
out = append(out, "TPE1")
case "Album":
out = append(out, "TALB")
case "AlbumArtist", "AlbumArtists":
out = append(out, "TPE2")
case "Composer":
out = append(out, "TCOM")
case "Track", "TrackNumber":
out = append(out, "TRCK")
case "Disc", "DiscNumber":
out = append(out, "TPOS")
case "Genre":
out = append(out, "TCON")
case "Date", "Year":
// Prefer TDRC; TYER used historically
out = append(out, "TDRC", "TYER")
case "Comment":
out = append(out, "COMM")
case "Lyrics":
out = append(out, "USLT")
}
}
return out
}
func mapClearsToTXXX(keys []string) []string {
out := []string{}
for _, k := range keys {
token := normalizeKey(k)
switch token {
case "artists":
out = append(out, "Artists", "ARTISTS")
case "albumartists":
out = append(out, "AlbumArtists", "ALBUMARTISTS")
}
}
return out
}