Web-based Winamp controller for CarPC � Go backend, mobile-first UI
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

105 рядки
2.5KB

  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. "fmt"
  16. "math/big"
  17. "path/filepath"
  18. "strings"
  19. id3 "github.com/bogem/id3v2/v2"
  20. )
  21. const emailKey = "rating@winamp.com"
  22. // Get returns the 0–5 star rating stored in the POPM frame of path.
  23. // Returns 0 (unrated) if the file has no POPM frame or is not an MP3.
  24. func Get(path string) (int, error) {
  25. if !isMP3(path) {
  26. return 0, nil
  27. }
  28. tag, err := id3.Open(path, id3.Options{Parse: true, ParseFrames: []string{"POPM"}})
  29. if err != nil {
  30. return 0, fmt.Errorf("rating.Get: %w", err)
  31. }
  32. defer tag.Close()
  33. for _, f := range tag.GetFrames("POPM") {
  34. pf, ok := f.(id3.PopularimeterFrame)
  35. if !ok {
  36. continue
  37. }
  38. return popmToStars(pf.Rating), nil
  39. }
  40. return 0, nil
  41. }
  42. // Set writes a 0–5 star rating into the POPM frame of path.
  43. // stars=0 removes any existing POPM frame (unrated).
  44. func Set(path string, stars int) error {
  45. if !isMP3(path) {
  46. return fmt.Errorf("rating.Set: not an MP3 file: %s", filepath.Base(path))
  47. }
  48. if stars < 0 || stars > 5 {
  49. return fmt.Errorf("rating.Set: stars must be 0–5, got %d", stars)
  50. }
  51. tag, err := id3.Open(path, id3.Options{Parse: true})
  52. if err != nil {
  53. return fmt.Errorf("rating.Set: %w", err)
  54. }
  55. defer tag.Close()
  56. tag.DeleteFrames("POPM")
  57. if stars > 0 {
  58. tag.AddFrame("POPM", id3.PopularimeterFrame{
  59. Email: emailKey,
  60. Rating: starsToPOPM(stars),
  61. Counter: big.NewInt(0),
  62. })
  63. }
  64. if err := tag.Save(); err != nil {
  65. return fmt.Errorf("rating.Set: save %s: %w", filepath.Base(path), err)
  66. }
  67. return nil
  68. }
  69. func isMP3(path string) bool {
  70. return strings.EqualFold(filepath.Ext(path), ".mp3")
  71. }
  72. // popmToStars maps a raw POPM byte to a 0–5 star rating using the
  73. // Windows Explorer / Winamp read ranges.
  74. func popmToStars(r uint8) int {
  75. switch {
  76. case r == 0:
  77. return 0
  78. case r < 32:
  79. return 1
  80. case r < 96:
  81. return 2
  82. case r < 160:
  83. return 3
  84. case r < 224:
  85. return 4
  86. default:
  87. return 5
  88. }
  89. }
  90. // starsToPOPM returns the canonical POPM byte for a given star count.
  91. func starsToPOPM(stars int) uint8 {
  92. return [6]uint8{0, 1, 64, 128, 196, 255}[stars]
  93. }