|
|
|
@@ -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 |
|
|
|
} |
|
|
|
|