From 8ffad2d0abee54ca255da4dc8b6e9ac70ae6ed8e Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Wed, 27 May 2026 09:13:15 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20rating=20write=20=E2=80=94=20avoid=20ren?= =?UTF-8?q?ame,=20overwrite=20file=20in-place?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bogem/id3v2 Save() renames a temp file over the original, which Windows denies when Winamp holds the file open (no FILE_SHARE_DELETE). Fix: - Use tag.WriteTo(&buf) to encode tag+audio into memory while the read handle is still open - Close the read handle - Reopen the original file with O_WRONLY|O_TRUNC and write the buffer Winamp opens MP3s with FILE_SHARE_WRITE so the in-place overwrite succeeds. Co-Authored-By: Claude Sonnet 4.6 --- internal/rating/rating.go | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/internal/rating/rating.go b/internal/rating/rating.go index e1fed5d..a7b17b2 100644 --- a/internal/rating/rating.go +++ b/internal/rating/rating.go @@ -13,8 +13,10 @@ package rating import ( + "bytes" "fmt" "math/big" + "os" "path/filepath" "strings" @@ -47,6 +49,13 @@ func Get(path string) (int, error) { // Set writes a 0–5 star rating into the POPM frame of path. // stars=0 removes any existing POPM frame (unrated). +// +// bogem/id3v2's Save() writes a temp file and renames it over the original, +// which Windows denies when another process (Winamp) holds the file open +// without FILE_SHARE_DELETE. Instead we use WriteTo to stream the modified +// tag+audio into an in-memory buffer, close our read handle, then overwrite +// the original file in-place — which succeeds because Winamp opens files +// with FILE_SHARE_WRITE. func Set(path string, stars int) error { if !isMP3(path) { return fmt.Errorf("rating.Set: not an MP3 file: %s", filepath.Base(path)) @@ -57,9 +66,8 @@ func Set(path string, stars int) error { tag, err := id3.Open(path, id3.Options{Parse: true}) if err != nil { - return fmt.Errorf("rating.Set: %w", err) + return fmt.Errorf("rating.Set: open: %w", err) } - defer tag.Close() tag.DeleteFrames("POPM") if stars > 0 { @@ -69,8 +77,23 @@ func Set(path string, stars int) error { Counter: big.NewInt(0), }) } - if err := tag.Save(); err != nil { - return fmt.Errorf("rating.Set: save %s: %w", filepath.Base(path), err) + + // Stream modified tag + audio into memory buffer while file is still open. + var buf bytes.Buffer + if _, err := tag.WriteTo(&buf); err != nil { + tag.Close() + return fmt.Errorf("rating.Set: encode: %w", err) + } + tag.Close() // release read handle before we open for writing + + // Overwrite the original file in-place (no rename needed). + f, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("rating.Set: open for write: %w", err) + } + defer f.Close() + if _, err := f.Write(buf.Bytes()); err != nil { + return fmt.Errorf("rating.Set: write: %w", err) } return nil }