Web-based Winamp controller for CarPC � Go backend, mobile-first UI
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

128 行
3.4KB

  1. // Package rating reads and writes ID3v2 POPM (Popularimeter) star ratings
  2. // for MP3 files using the scale shared by Winamp and Windows Explorer:
  3. //
  4. // 0 = unrated
  5. // 1 = ★☆☆☆☆ (POPM byte 1)
  6. // 2 = ★★☆☆☆ (POPM byte 64)
  7. // 3 = ★★★☆☆ (POPM byte 128)
  8. // 4 = ★★★★☆ (POPM byte 196)
  9. // 5 = ★★★★★ (POPM byte 255)
  10. //
  11. // The email field is set to "rating@winamp.com" (Winamp's standard identifier).
  12. // Windows Explorer reads all POPM frames regardless of the email field.
  13. package rating
  14. import (
  15. "bytes"
  16. "fmt"
  17. "math/big"
  18. "os"
  19. "path/filepath"
  20. "strings"
  21. id3 "github.com/bogem/id3v2/v2"
  22. )
  23. const emailKey = "rating@winamp.com"
  24. // Get returns the 0–5 star rating stored in the POPM frame of path.
  25. // Returns 0 (unrated) if the file has no POPM frame or is not an MP3.
  26. func Get(path string) (int, error) {
  27. if !isMP3(path) {
  28. return 0, nil
  29. }
  30. tag, err := id3.Open(path, id3.Options{Parse: true, ParseFrames: []string{"POPM"}})
  31. if err != nil {
  32. return 0, fmt.Errorf("rating.Get: %w", err)
  33. }
  34. defer tag.Close()
  35. for _, f := range tag.GetFrames("POPM") {
  36. pf, ok := f.(id3.PopularimeterFrame)
  37. if !ok {
  38. continue
  39. }
  40. return popmToStars(pf.Rating), nil
  41. }
  42. return 0, nil
  43. }
  44. // Set writes a 0–5 star rating into the POPM frame of path.
  45. // stars=0 removes any existing POPM frame (unrated).
  46. //
  47. // bogem/id3v2's Save() writes a temp file and renames it over the original,
  48. // which Windows denies when another process (Winamp) holds the file open
  49. // without FILE_SHARE_DELETE. Instead we use WriteTo to stream the modified
  50. // tag+audio into an in-memory buffer, close our read handle, then overwrite
  51. // the original file in-place — which succeeds because Winamp opens files
  52. // with FILE_SHARE_WRITE.
  53. func Set(path string, stars int) error {
  54. if !isMP3(path) {
  55. return fmt.Errorf("rating.Set: not an MP3 file: %s", filepath.Base(path))
  56. }
  57. if stars < 0 || stars > 5 {
  58. return fmt.Errorf("rating.Set: stars must be 0–5, got %d", stars)
  59. }
  60. tag, err := id3.Open(path, id3.Options{Parse: true})
  61. if err != nil {
  62. return fmt.Errorf("rating.Set: open: %w", err)
  63. }
  64. tag.DeleteFrames("POPM")
  65. if stars > 0 {
  66. tag.AddFrame("POPM", id3.PopularimeterFrame{
  67. Email: emailKey,
  68. Rating: starsToPOPM(stars),
  69. Counter: big.NewInt(0),
  70. })
  71. }
  72. // Stream modified tag + audio into memory buffer while file is still open.
  73. var buf bytes.Buffer
  74. if _, err := tag.WriteTo(&buf); err != nil {
  75. tag.Close()
  76. return fmt.Errorf("rating.Set: encode: %w", err)
  77. }
  78. tag.Close() // release read handle before we open for writing
  79. // Overwrite the original file in-place (no rename needed).
  80. f, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0644)
  81. if err != nil {
  82. return fmt.Errorf("rating.Set: open for write: %w", err)
  83. }
  84. defer f.Close()
  85. if _, err := f.Write(buf.Bytes()); err != nil {
  86. return fmt.Errorf("rating.Set: write: %w", err)
  87. }
  88. return nil
  89. }
  90. func isMP3(path string) bool {
  91. return strings.EqualFold(filepath.Ext(path), ".mp3")
  92. }
  93. // popmToStars maps a raw POPM byte to a 0–5 star rating using the
  94. // Windows Explorer / Winamp read ranges.
  95. func popmToStars(r uint8) int {
  96. switch {
  97. case r == 0:
  98. return 0
  99. case r < 32:
  100. return 1
  101. case r < 96:
  102. return 2
  103. case r < 160:
  104. return 3
  105. case r < 224:
  106. return 4
  107. default:
  108. return 5
  109. }
  110. }
  111. // starsToPOPM returns the canonical POPM byte for a given star count.
  112. func starsToPOPM(stars int) uint8 {
  113. return [6]uint8{0, 1, 64, 128, 196, 255}[stars]
  114. }