258 lines
7.3 KiB
Go
258 lines
7.3 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",
|
|
"Album": "album",
|
|
"AlbumArtist": "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",
|
|
}
|
|
|
|
// ffmpegMultiKey maps multi-valued canonical names to ffmpeg/Vorbis keys.
|
|
// For FLAC/Vorbis, use uppercase ARTISTS/ALBUMARTISTS for proper multi-value support.
|
|
var ffmpegMultiKey = map[string]string{
|
|
"Artists": "ARTISTS",
|
|
"AlbumArtists": "ALBUMARTISTS",
|
|
}
|
|
|
|
// allowedCanonicals is the set of canonical tags we accept for writes.
|
|
var allowedCanonicals = func() map[string]struct{} {
|
|
m := make(map[string]struct{}, len(ffmpegKey)+len(ffmpegMultiKey))
|
|
for k := range ffmpegKey {
|
|
m[k] = struct{}{}
|
|
}
|
|
for k := range ffmpegMultiKey {
|
|
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.
|
|
// Uses ffmpegMultiKey for proper Vorbis comment names (ARTISTS, ALBUMARTISTS).
|
|
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
|
|
}
|
|
}
|
|
// First check if this is a multi-valued tag
|
|
ffk, ok := ffmpegMultiKey[canonical]
|
|
if !ok {
|
|
// Fall back to regular ffmpeg key
|
|
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
|
|
}
|