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 }