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 } 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.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 } 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, "[") { if strings.Index(trimmedLine, ":") > 0 { validLines = append(validLines, line) continue } closeBracket := strings.Index(trimmedLine, "]") if closeBracket > 0 { timestampStr := trimmedLine[1:closeBracket] ms := parseLRCTimestamp(timestampStr) if ms >= 0 && 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) } } 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 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) } 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 }