package backend import ( "encoding/json" "fmt" "os" "os/exec" pathfilepath "path/filepath" "strconv" "strings" id3v2 "github.com/bogem/id3v2/v2" "github.com/go-flac/flacpicture" "github.com/go-flac/flacvorbis" "github.com/go-flac/go-flac" ) type Metadata struct { Title string Artist string Album string AlbumArtist string Date string ReleaseDate string TrackNumber int TotalTracks int DiscNumber int TotalDiscs int URL string Copyright string Publisher string Lyrics string Description string ISRC string Genre string } func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error { f, err := flac.ParseFile(filepath) if err != nil { return fmt.Errorf("failed to parse FLAC file: %w", err) } var cmtIdx = -1 for idx, block := range f.Meta { if block.Type == flac.VorbisComment { cmtIdx = idx break } } cmt := flacvorbis.New() if metadata.Title != "" { _ = cmt.Add(flacvorbis.FIELD_TITLE, metadata.Title) } if metadata.Artist != "" { _ = cmt.Add(flacvorbis.FIELD_ARTIST, metadata.Artist) } if metadata.Album != "" { _ = cmt.Add(flacvorbis.FIELD_ALBUM, metadata.Album) } if metadata.AlbumArtist != "" { _ = cmt.Add("ALBUMARTIST", metadata.AlbumArtist) } if metadata.Date != "" { _ = cmt.Add(flacvorbis.FIELD_DATE, metadata.Date) } if metadata.TrackNumber > 0 { _ = cmt.Add(flacvorbis.FIELD_TRACKNUMBER, strconv.Itoa(metadata.TrackNumber)) } if metadata.TotalTracks > 0 { _ = cmt.Add("TOTALTRACKS", strconv.Itoa(metadata.TotalTracks)) } if metadata.DiscNumber > 0 { _ = cmt.Add("DISCNUMBER", strconv.Itoa(metadata.DiscNumber)) } if metadata.TotalDiscs > 0 { _ = cmt.Add("TOTALDISCS", strconv.Itoa(metadata.TotalDiscs)) } if metadata.Copyright != "" { _ = cmt.Add("COPYRIGHT", metadata.Copyright) } if metadata.Publisher != "" { _ = cmt.Add("PUBLISHER", metadata.Publisher) } if metadata.Description != "" { _ = cmt.Add("DESCRIPTION", metadata.Description) } if metadata.ISRC != "" { _ = cmt.Add("ISRC", metadata.ISRC) } if metadata.Genre != "" { _ = cmt.Add("GENRE", metadata.Genre) } if metadata.Lyrics != "" { _ = cmt.Add("LYRICS", metadata.Lyrics) } cmtBlock := cmt.Marshal() if cmtIdx < 0 { f.Meta = append(f.Meta, &cmtBlock) } else { f.Meta[cmtIdx] = &cmtBlock } if coverPath != "" && fileExists(coverPath) { if err := embedCoverArt(f, coverPath); err != nil { fmt.Printf("Warning: Failed to embed cover art: %v\n", err) } } if err := f.Save(filepath); err != nil { return fmt.Errorf("failed to save FLAC file: %w", err) } return nil } func embedCoverArt(f *flac.File, coverPath string) error { imgData, err := os.ReadFile(coverPath) if err != nil { return fmt.Errorf("failed to read cover image: %w", err) } picture, err := flacpicture.NewFromImageData( flacpicture.PictureTypeFrontCover, "Cover", imgData, "image/jpeg", ) if err != nil { return fmt.Errorf("failed to create picture block: %w", err) } pictureBlock := picture.Marshal() for i := len(f.Meta) - 1; i >= 0; i-- { if f.Meta[i].Type == flac.Picture { f.Meta = append(f.Meta[:i], f.Meta[i+1:]...) } } f.Meta = append(f.Meta, &pictureBlock) return nil } func fileExists(path string) bool { _, err := os.Stat(path) return err == nil } func extractYear(releaseDate string) string { if releaseDate == "" { return "" } if len(releaseDate) >= 4 { return releaseDate[:4] } return releaseDate } func EmbedLyricsOnly(filepath string, lyrics string) error { if lyrics == "" { return nil } f, err := flac.ParseFile(filepath) if err != nil { return fmt.Errorf("failed to parse FLAC file: %w", err) } var cmtIdx = -1 var existingCmt *flacvorbis.MetaDataBlockVorbisComment for idx, block := range f.Meta { if block.Type == flac.VorbisComment { cmtIdx = idx existingCmt, err = flacvorbis.ParseFromMetaDataBlock(*block) if err != nil { existingCmt = nil } break } } cmt := flacvorbis.New() if existingCmt != nil { for _, comment := range existingCmt.Comments { parts := strings.SplitN(comment, "=", 2) if len(parts) == 2 { fieldName := strings.ToUpper(parts[0]) if fieldName != "LYRICS" && fieldName != "UNSYNCEDLYRICS" && fieldName != "SYNCEDLYRICS" { _ = cmt.Add(parts[0], parts[1]) } } } } _ = cmt.Add("LYRICS", lyrics) cmtBlock := cmt.Marshal() if cmtIdx < 0 { f.Meta = append(f.Meta, &cmtBlock) } else { f.Meta[cmtIdx] = &cmtBlock } if err := f.Save(filepath); err != nil { return fmt.Errorf("failed to save FLAC file: %w", err) } return nil } func ExtractCoverArt(filePath string) (string, error) { ext := strings.ToLower(pathfilepath.Ext(filePath)) switch ext { case ".mp3": return extractCoverFromMp3(filePath) case ".m4a", ".flac": return extractCoverFromM4AOrFlac(filePath) default: return "", fmt.Errorf("unsupported file format: %s", ext) } } func extractCoverFromMp3(filePath string) (string, error) { tag, err := id3v2.Open(filePath, id3v2.Options{Parse: true}) if err != nil { return "", fmt.Errorf("failed to open MP3 file: %w", err) } defer tag.Close() pictures := tag.GetFrames(tag.CommonID("Attached picture")) if len(pictures) == 0 { return "", fmt.Errorf("no cover art found") } pic, ok := pictures[0].(id3v2.PictureFrame) if !ok { return "", fmt.Errorf("invalid picture frame") } tmpFile, err := os.CreateTemp("", "cover-*.jpg") if err != nil { return "", fmt.Errorf("failed to create temp file: %w", err) } defer tmpFile.Close() if _, err := tmpFile.Write(pic.Picture); err != nil { os.Remove(tmpFile.Name()) return "", fmt.Errorf("failed to write cover art: %w", err) } return tmpFile.Name(), nil } func extractCoverFromM4AOrFlac(filePath string) (string, error) { ext := strings.ToLower(pathfilepath.Ext(filePath)) if ext == ".flac" { f, err := flac.ParseFile(filePath) if err != nil { return "", fmt.Errorf("failed to parse FLAC file: %w", err) } for _, block := range f.Meta { if block.Type == flac.Picture { pic, err := flacpicture.ParseFromMetaDataBlock(*block) if err != nil { continue } tmpFile, err := os.CreateTemp("", "cover-*.jpg") if err != nil { return "", fmt.Errorf("failed to create temp file: %w", err) } defer tmpFile.Close() if _, err := tmpFile.Write(pic.ImageData); err != nil { os.Remove(tmpFile.Name()) return "", fmt.Errorf("failed to write cover art: %w", err) } return tmpFile.Name(), nil } } return "", fmt.Errorf("no cover art found") } return "", nil } func ExtractLyrics(filePath string) (string, error) { ext := strings.ToLower(pathfilepath.Ext(filePath)) switch ext { case ".mp3": return extractLyricsFromMp3(filePath) case ".flac": return extractLyricsFromFlac(filePath) case ".m4a": return "", nil default: return "", fmt.Errorf("unsupported file format: %s", ext) } } func extractLyricsFromMp3(filePath string) (string, error) { tag, err := id3v2.Open(filePath, id3v2.Options{Parse: true}) if err != nil { return "", fmt.Errorf("failed to open MP3 file: %w", err) } defer tag.Close() usltFrames := tag.GetFrames(tag.CommonID("Unsynchronised lyrics/text transcription")) if len(usltFrames) == 0 { fmt.Printf("[ExtractLyrics] No USLT frames found in MP3: %s\n", filePath) return "", nil } uslt, ok := usltFrames[0].(id3v2.UnsynchronisedLyricsFrame) if !ok { fmt.Printf("[ExtractLyrics] USLT frame type assertion failed in MP3: %s\n", filePath) return "", nil } if uslt.Lyrics == "" { fmt.Printf("[ExtractLyrics] USLT frame has empty lyrics in MP3: %s\n", filePath) return "", nil } fmt.Printf("[ExtractLyrics] Successfully extracted lyrics from MP3: %s (%d characters)\n", filePath, len(uslt.Lyrics)) return uslt.Lyrics, nil } func extractLyricsFromFlac(filePath string) (string, error) { f, err := flac.ParseFile(filePath) if err != nil { return "", fmt.Errorf("failed to parse FLAC file: %w", err) } for _, block := range f.Meta { if block.Type == flac.VorbisComment { cmt, err := flacvorbis.ParseFromMetaDataBlock(*block) if err != nil { continue } for _, comment := range cmt.Comments { parts := strings.SplitN(comment, "=", 2) if len(parts) == 2 { fieldName := strings.ToUpper(parts[0]) if fieldName == "LYRICS" || fieldName == "UNSYNCEDLYRICS" { lyrics := parts[1] fmt.Printf("[ExtractLyrics] Successfully extracted lyrics from FLAC: %s (%d characters)\n", filePath, len(lyrics)) return lyrics, nil } } } } } fmt.Printf("[ExtractLyrics] No lyrics found in FLAC: %s\n", filePath) return "", nil } func EmbedCoverArtOnly(filePath string, coverPath string) error { if coverPath == "" || !fileExists(coverPath) { return nil } ext := strings.ToLower(pathfilepath.Ext(filePath)) switch ext { case ".mp3": return embedCoverToMp3(filePath, coverPath) case ".m4a": return nil default: return fmt.Errorf("unsupported file format: %s", ext) } } func embedCoverToMp3(filePath string, coverPath string) error { tag, err := id3v2.Open(filePath, id3v2.Options{Parse: true}) if err != nil { return fmt.Errorf("failed to open MP3 file: %w", err) } defer tag.Close() tag.DeleteFrames(tag.CommonID("Attached picture")) artwork, err := os.ReadFile(coverPath) if err != nil { return fmt.Errorf("failed to read cover art: %w", err) } pic := id3v2.PictureFrame{ Encoding: id3v2.EncodingUTF8, MimeType: "image/jpeg", PictureType: id3v2.PTFrontCover, Description: "Front cover", Picture: artwork, } tag.AddAttachedPicture(pic) if err := tag.Save(); err != nil { return fmt.Errorf("failed to save MP3 tags: %w", err) } return nil } func EmbedLyricsOnlyMP3(filepath string, lyrics string) error { if lyrics == "" { return nil } validatedLyrics, err := validateLyricsDuration(lyrics, filepath) if err != nil { fmt.Printf("[EmbedLyricsOnlyMP3] Warning: Failed to validate lyrics duration: %v, using original lyrics\n", err) validatedLyrics = lyrics } lyrics = validatedLyrics tag, err := id3v2.Open(filepath, id3v2.Options{Parse: true}) if err != nil { return fmt.Errorf("failed to open MP3 file: %w", err) } defer tag.Close() tag.DeleteFrames(tag.CommonID("Unsynchronised lyrics/text transcription")) usltFrame := id3v2.UnsynchronisedLyricsFrame{ Encoding: id3v2.EncodingUTF8, Language: "eng", ContentDescriptor: "", Lyrics: lyrics, } tag.AddUnsynchronisedLyricsFrame(usltFrame) if err := tag.Save(); err != nil { return fmt.Errorf("failed to save MP3 tags: %w", err) } return nil } func embedLyricsToM4A(filepath string, lyrics string) error { validatedLyrics, err := validateLyricsDuration(lyrics, filepath) if err != nil { fmt.Printf("[embedLyricsToM4A] Warning: Failed to validate lyrics duration: %v, using original lyrics\n", err) validatedLyrics = lyrics } lyrics = validatedLyrics ffmpegPath, err := GetFFmpegPath() if err != nil { return fmt.Errorf("ffmpeg not found: %w", err) } if err := ValidateExecutable(ffmpegPath); err != nil { return fmt.Errorf("invalid ffmpeg executable: %w", err) } tmpOutputFile := strings.TrimSuffix(filepath, pathfilepath.Ext(filepath)) + ".tmp" + pathfilepath.Ext(filepath) defer func() { if _, err := os.Stat(tmpOutputFile); err == nil { os.Remove(tmpOutputFile) } }() cmd := exec.Command(ffmpegPath, "-i", filepath, "-map", "0", "-map_metadata", "0", "-metadata", "lyrics-eng="+lyrics, "-metadata", "lyrics="+lyrics, "-codec", "copy", "-f", "ipod", "-y", tmpOutputFile, ) setHideWindow(cmd) output, err := cmd.CombinedOutput() if err != nil { fmt.Printf("[FFmpeg] Error embedding lyrics to M4A: %s\n", string(output)) return fmt.Errorf("ffmpeg failed to embed lyrics: %s - %w", string(output), err) } if err := os.Rename(tmpOutputFile, filepath); err != nil { return fmt.Errorf("failed to replace original file: %w", err) } fmt.Printf("[FFmpeg] Lyrics embedded to M4A successfully: %d characters\n", len(lyrics)) return nil } func EmbedLyricsOnlyUniversal(filepath string, lyrics string) error { if lyrics == "" { return nil } validatedLyrics, err := validateLyricsDuration(lyrics, filepath) if err != nil { fmt.Printf("[EmbedLyricsOnlyUniversal] Warning: Failed to validate lyrics duration: %v, using original lyrics\n", err) validatedLyrics = lyrics } lyrics = validatedLyrics ext := strings.ToLower(pathfilepath.Ext(filepath)) switch ext { case ".mp3": return EmbedLyricsOnlyMP3(filepath, lyrics) case ".flac": return EmbedLyricsOnly(filepath, lyrics) case ".m4a": return embedLyricsToM4A(filepath, lyrics) default: return fmt.Errorf("unsupported file format for lyrics embedding: %s", ext) } } func GetAudioDuration(filepath string) (float64, error) { ext := strings.ToLower(pathfilepath.Ext(filepath)) if ext == ".flac" { duration, err := getFlacDuration(filepath) if err == nil && duration > 0 { return duration, nil } } return getDurationWithFFprobe(filepath) } func getFlacDuration(filepath string) (float64, error) { f, err := flac.ParseFile(filepath) if err != nil { return 0, err } if len(f.Meta) > 0 { streamInfo := f.Meta[0] if streamInfo.Type == flac.StreamInfo { data := streamInfo.Data if len(data) >= 18 { sampleRate := uint32(data[10])<<12 | uint32(data[11])<<4 | uint32(data[12])>>4 totalSamples := uint64(data[13]&0x0F)<<32 | uint64(data[14])<<24 | uint64(data[15])<<16 | uint64(data[16])<<8 | uint64(data[17]) if sampleRate > 0 { return float64(totalSamples) / float64(sampleRate), nil } } } } return 0, fmt.Errorf("could not extract duration from FLAC file") } func getDurationWithFFprobe(filepath string) (float64, error) { ffprobePath, err := GetFFprobePath() if err != nil { return 0, err } if err := ValidateExecutable(ffprobePath); err != nil { return 0, fmt.Errorf("invalid ffprobe executable: %w", err) } cmd := exec.Command(ffprobePath, "-v", "quiet", "-print_format", "json", "-show_format", filepath, ) setHideWindow(cmd) output, err := cmd.Output() if err != nil { return 0, err } var result struct { Format struct { Duration string `json:"duration"` } `json:"format"` } if err := json.Unmarshal(output, &result); err != nil { return 0, err } if result.Format.Duration == "" { return 0, fmt.Errorf("duration not found in ffprobe output") } duration, err := strconv.ParseFloat(result.Format.Duration, 64) if err != nil { return 0, err } return duration, nil } func validateLyricsDuration(lyrics string, filepath string) (string, error) { duration, err := GetAudioDuration(filepath) if err != nil { fmt.Printf("[ValidateLyrics] Warning: Could not get audio duration: %v, skipping validation\n", err) return lyrics, nil } if duration <= 0 { fmt.Printf("[ValidateLyrics] Warning: Invalid duration (%f seconds), skipping validation\n", duration) return lyrics, nil } durationMs := int64(duration * 1000) lines := strings.Split(lyrics, "\n") var validLines []string for _, line := range lines { trimmedLine := strings.TrimSpace(line) if trimmedLine == "" { validLines = append(validLines, line) continue } if strings.HasPrefix(trimmedLine, "[") { closeBracket := strings.Index(trimmedLine, "]") if closeBracket > 0 { timestampStr := trimmedLine[1:closeBracket] ms := parseLRCTimestamp(timestampStr) if ms >= 0 { if ms <= durationMs { validLines = append(validLines, line) } else { fmt.Printf("[ValidateLyrics] Filtered out line with timestamp %s (exceeds duration %d ms): %s\n", timestampStr, durationMs, trimmedLine) } } else { validLines = append(validLines, line) } continue } } else { validLines = append(validLines, line) } } return strings.Join(validLines, "\n"), nil } func parseLRCTimestamp(timestamp string) int64 { var minutes, seconds, centiseconds int64 n, _ := fmt.Sscanf(timestamp, "%d:%d.%d", &minutes, &seconds, ¢iseconds) if n >= 2 { return minutes*60*1000 + seconds*1000 + centiseconds*10 } return -1 } func ExtractFullMetadataFromFile(filePath string) (Metadata, error) { var metadata Metadata ffprobePath, err := GetFFprobePath() if err != nil { return metadata, err } if err := ValidateExecutable(ffprobePath); err != nil { return metadata, fmt.Errorf("invalid ffprobe executable: %w", err) } cmd := exec.Command(ffprobePath, "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath, ) setHideWindow(cmd) output, err := cmd.Output() if err != nil { return metadata, err } var result struct { Format struct { Tags map[string]string `json:"tags"` } `json:"format"` Streams []struct { Tags map[string]string `json:"tags"` } `json:"streams"` } if err := json.Unmarshal(output, &result); err != nil { return metadata, err } allTags := make(map[string]string) for _, stream := range result.Streams { for key, value := range stream.Tags { allTags[strings.ToLower(key)] = value } } for key, value := range result.Format.Tags { allTags[strings.ToLower(key)] = value } for key, value := range allTags { switch key { case "title": metadata.Title = value case "artist": metadata.Artist = value case "album": metadata.Album = value case "album_artist", "albumartist": metadata.AlbumArtist = value case "date", "year": if metadata.Date == "" || len(value) > len(metadata.Date) { metadata.Date = value } case "track": parts := strings.Split(value, "/") if len(parts) > 0 { if num, err := strconv.Atoi(parts[0]); err == nil { metadata.TrackNumber = num } } if len(parts) > 1 { if num, err := strconv.Atoi(parts[1]); err == nil { metadata.TotalTracks = num } } case "disc": parts := strings.Split(value, "/") if len(parts) > 0 { if num, err := strconv.Atoi(parts[0]); err == nil { metadata.DiscNumber = num } } if len(parts) > 1 { if num, err := strconv.Atoi(parts[1]); err == nil { metadata.TotalDiscs = num } } case "copyright", "tcop": metadata.Copyright = value case "publisher", "tpub", "label": metadata.Publisher = value case "url": metadata.URL = value case "description", "comment": if metadata.Description == "" { metadata.Description = value } } } return metadata, nil } func EmbedMetadataToConvertedFile(filePath string, metadata Metadata, coverPath string) error { ext := strings.ToLower(pathfilepath.Ext(filePath)) switch ext { case ".flac": return EmbedMetadata(filePath, metadata, coverPath) case ".mp3": return embedMetadataToMP3(filePath, metadata, coverPath) case ".m4a": return embedMetadataToM4A(filePath, metadata, coverPath) default: return fmt.Errorf("unsupported file format: %s", ext) } } func embedMetadataToMP3(filePath string, metadata Metadata, coverPath string) error { tag, err := id3v2.Open(filePath, id3v2.Options{Parse: true}) if err != nil { return fmt.Errorf("failed to open MP3 file: %w", err) } defer tag.Close() tag.DeleteFrames("TXXX") if metadata.Title != "" { tag.SetTitle(metadata.Title) } if metadata.Artist != "" { tag.SetArtist(metadata.Artist) } if metadata.Album != "" { tag.SetAlbum(metadata.Album) } if metadata.Date != "" { year := metadata.Date if len(year) >= 4 { year = year[:4] } tag.SetYear(year) } if metadata.AlbumArtist != "" { tag.DeleteFrames("TPE2") tag.AddTextFrame("TPE2", id3v2.EncodingUTF8, metadata.AlbumArtist) } if metadata.TrackNumber > 0 { tag.DeleteFrames(tag.CommonID("Track number/Position in set")) trackStr := strconv.Itoa(metadata.TrackNumber) if metadata.TotalTracks > 0 { trackStr = fmt.Sprintf("%d/%d", metadata.TrackNumber, metadata.TotalTracks) } tag.AddTextFrame(tag.CommonID("Track number/Position in set"), id3v2.EncodingUTF8, trackStr) } if metadata.DiscNumber > 0 { tag.DeleteFrames(tag.CommonID("Part of a set")) discStr := strconv.Itoa(metadata.DiscNumber) if metadata.TotalDiscs > 0 { discStr = fmt.Sprintf("%d/%d", metadata.DiscNumber, metadata.TotalDiscs) } tag.AddTextFrame(tag.CommonID("Part of a set"), id3v2.EncodingUTF8, discStr) } if metadata.Copyright != "" { tag.DeleteFrames("TCOP") tag.AddTextFrame("TCOP", id3v2.EncodingUTF8, metadata.Copyright) } if metadata.Publisher != "" { tag.DeleteFrames("TPUB") tag.AddTextFrame("TPUB", id3v2.EncodingUTF8, metadata.Publisher) } if metadata.ISRC != "" { tag.DeleteFrames("TSRC") tag.AddTextFrame("TSRC", id3v2.EncodingUTF8, metadata.ISRC) } if coverPath != "" && fileExists(coverPath) { tag.DeleteFrames(tag.CommonID("Attached picture")) artwork, err := os.ReadFile(coverPath) if err == nil { pic := id3v2.PictureFrame{ Encoding: id3v2.EncodingUTF8, MimeType: "image/jpeg", PictureType: id3v2.PTFrontCover, Description: "Cover", Picture: artwork, } tag.AddAttachedPicture(pic) } else { fmt.Printf("[EmbedMetadataToMP3] Warning: Failed to read cover art file: %v\n", err) } } if err := tag.Save(); err != nil { return fmt.Errorf("failed to save MP3 tags: %w", err) } return nil } func embedMetadataToM4A(filePath string, metadata Metadata, coverPath string) error { ffmpegPath, err := GetFFmpegPath() if err != nil { return fmt.Errorf("ffmpeg not found: %w", err) } if err := ValidateExecutable(ffmpegPath); err != nil { return fmt.Errorf("invalid ffmpeg executable: %w", err) } args := []string{ "-i", filePath, "-y", } if coverPath != "" && fileExists(coverPath) { args = append(args, "-i", coverPath) args = append(args, "-map", "0:a", "-map", "1", "-c:a", "copy", "-c:v", "copy", "-disposition:v:0", "attached_pic") } else { args = append(args, "-map", "0", "-codec", "copy") } if metadata.Title != "" { args = append(args, "-metadata", "title="+metadata.Title) } if metadata.Artist != "" { args = append(args, "-metadata", "artist="+metadata.Artist) } if metadata.Album != "" { args = append(args, "-metadata", "album="+metadata.Album) } if metadata.AlbumArtist != "" { args = append(args, "-metadata", "album_artist="+metadata.AlbumArtist) } if metadata.Date != "" { args = append(args, "-metadata", "date="+metadata.Date) } if metadata.TrackNumber > 0 { trackStr := strconv.Itoa(metadata.TrackNumber) if metadata.TotalTracks > 0 { trackStr = fmt.Sprintf("%d/%d", metadata.TrackNumber, metadata.TotalTracks) } args = append(args, "-metadata", "track="+trackStr) } if metadata.DiscNumber > 0 { discStr := strconv.Itoa(metadata.DiscNumber) if metadata.TotalDiscs > 0 { discStr = fmt.Sprintf("%d/%d", metadata.DiscNumber, metadata.TotalDiscs) } args = append(args, "-metadata", "disk="+discStr) } if metadata.Copyright != "" { args = append(args, "-metadata", "copyright="+metadata.Copyright) } if metadata.Publisher != "" { args = append(args, "-metadata", "publisher="+metadata.Publisher) } if metadata.ISRC != "" { args = append(args, "-metadata", "isrc="+metadata.ISRC) } tmpOutputFile := strings.TrimSuffix(filePath, pathfilepath.Ext(filePath)) + ".tmp" + pathfilepath.Ext(filePath) defer func() { if _, err := os.Stat(tmpOutputFile); err == nil { os.Remove(tmpOutputFile) } }() args = append(args, "-f", "ipod", tmpOutputFile) cmd := exec.Command(ffmpegPath, args...) setHideWindow(cmd) output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("ffmpeg failed to embed metadata: %s - %w", string(output), err) } if err := os.Rename(tmpOutputFile, filePath); err != nil { return fmt.Errorf("failed to replace original file: %w", err) } return nil }