package files import ( "fmt" "strings" id3v2 "github.com/bogem/id3v2/v2" ) // applyMP3Tags writes only the provided non-empty tags to the MP3 file at // realPath. It preserves existing frames when not overwritten. func applyMP3Tags(realPath string, tags map[string]string) error { // Open tag for read/write tag, err := id3v2.Open(realPath, id3v2.Options{Parse: true}) if err != nil { return fmt.Errorf("open id3v2: %w", err) } defer tag.Close() // Helper to set a text frame (replace existing) setFrame := func(frameID, value string) { if strings.TrimSpace(value) == "" { return } tag.DeleteAllFrames(frameID) tag.AddTextFrame(frameID, tag.DefaultEncoding(), value) } for k, v := range tags { if strings.TrimSpace(v) == "" { continue } switch strings.ToLower(k) { case "title": setFrame("TIT2", v) case "artist": setFrame("TPE1", v) case "album": setFrame("TALB", v) case "albumartist", "album artist": setFrame("TPE2", v) case "composer": setFrame("TCOM", v) case "year", "date": setFrame("TDRC", v) case "track": setFrame("TRCK", v) case "disc": setFrame("TPOS", v) case "genre": setFrame("TCON", v) default: // write custom text frame under TXXX with description = key // Use description to store the original key so we don't collide // with standard frames. // Note: bogem/id3v2 uses AddTextFrame for TXXX as well. tag.AddTextFrame("TXXX", tag.DefaultEncoding(), v) } } if err := tag.Save(); err != nil { return fmt.Errorf("save id3v2: %w", err) } return nil }