From 8403eb1ef13ff7da9c23004cfc2fd8b9c6f6c7b0 Mon Sep 17 00:00:00 2001 From: Dave Hulihan Date: Fri, 29 May 2020 16:20:58 -0600 Subject: [PATCH] add popularimeter frame --- common_ids.go | 3 ++ example_test.go | 28 +++++++++++++++ parse_test.go | 21 ++++++++++++ popularimeter_frame.go | 68 +++++++++++++++++++++++++++++++++++++ popularimeter_frame_test.go | 34 +++++++++++++++++++ tag_test.go | 15 ++++++-- 6 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 popularimeter_frame.go create mode 100644 popularimeter_frame_test.go diff --git a/common_ids.go b/common_ids.go index 35c69de..33f6922 100644 --- a/common_ids.go +++ b/common_ids.go @@ -34,6 +34,7 @@ var ( "Original lyricist/text writer": "TOLY", "Original artist/performer": "TOPE", "Original release year": "TORY", + "Popularimeter": "POPM", "File owner/licensee": "TOWN", "Lead artist/Lead performer/Soloist/Performing group": "TPE1", "Band/Orchestra/Accompaniment": "TPE2", @@ -90,6 +91,7 @@ var ( "Original filename": "TOFN", "Original lyricist/text writer": "TOLY", "Original artist/performer": "TOPE", + "Popularimeter": "POPM", "File owner/licensee": "TOWN", "Lead artist/Lead performer/Soloist/Performing group": "TPE1", "Band/Orchestra/Accompaniment": "TPE2", @@ -136,6 +138,7 @@ var ( var parsers = map[string]func(*bufReader) (Framer, error){ "APIC": parsePictureFrame, "COMM": parseCommentFrame, + "POPM": parsePopularimeterFrame, "TXXX": parseUserDefinedTextFrame, "UFID": parseUFIDFrame, "USLT": parseUnsynchronisedLyricsFrame, diff --git a/example_test.go b/example_test.go index 591943e..7c44afd 100644 --- a/example_test.go +++ b/example_test.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "log" + "math/big" "os" "sync" @@ -268,3 +269,30 @@ func ExampleUnsynchronisedLyricsFrame_add() { } tag.AddUnsynchronisedLyricsFrame(uslt) } + +func ExamplePopularimeterFrame_add() { + tag := id3v2.NewEmptyTag() + + popmFrame := id3v2.PopularimeterFrame{ + Email: "foo@bar.com", + Rating: 128, + Counter: big.NewInt(10000000000000000), + } + tag.AddFrame(tag.CommonID("Popularimeter"), popmFrame) +} + +func ExamplePopularimeterFrame_get() { + tag, err := id3v2.Open("file.mp3", id3v2.Options{Parse: true}) + if tag == nil || err != nil { + log.Fatal("Error while opening mp3 file: ", err) + } + + f := tag.GetLastFrame(tag.CommonID("Popularimeter")) + popm, ok := f.(id3v2.PopularimeterFrame) + if !ok { + log.Fatal("Couldn't assert POPM frame") + } + + // do something with POPM Frame + fmt.Printf("Email: %s, Rating: %d, Counter: %d", popm.Email, popm.Rating, popm.Counter) +} diff --git a/parse_test.go b/parse_test.go index 6fb6482..5ca7785 100644 --- a/parse_test.go +++ b/parse_test.go @@ -25,6 +25,7 @@ func TestParse(t *testing.T) { defer tag.Close() testTextFrames(t, tag) + testPopularimeterFrame(t, tag) testPictureFrames(t, tag) testUSLTFrames(t, tag) testTXXXFrames(t, tag) @@ -186,6 +187,26 @@ func compareTXXXFrames(actual, expected UserDefinedTextFrame) error { return nil } +func testPopularimeterFrame(t *testing.T, tag *Tag) { + actual := tag.GetLastFrame(tag.CommonID("Popularimeter")).(PopularimeterFrame) + + if actual.Size() != popmFrame.Size() { + t.Errorf("Expected size: %d, got: %d", popmFrame.Size(), actual.Size()) + } + + if actual.Email != popmFrame.Email { + t.Errorf("Expected email: %v, got: %v", popmFrame.Email, actual.Email) + } + + if actual.Rating != popmFrame.Rating { + t.Errorf("Expected rating: %v, got: %v", popmFrame.Rating, actual.Rating) + } + + if actual.Counter.Text(16) != popmFrame.Counter.Text(16) { + t.Errorf("Expected counter: %s, got: %s", popmFrame.Counter.Text(16), actual.Counter.Text(16)) + } +} + func testUFIDFrames(t *testing.T, tag *Tag) { ufidFrames := tag.GetFrames("UFID") if len(ufidFrames) != 1 { diff --git a/popularimeter_frame.go b/popularimeter_frame.go new file mode 100644 index 0000000..c61d1a7 --- /dev/null +++ b/popularimeter_frame.go @@ -0,0 +1,68 @@ +package id3v2 + +import ( + "io" + "math/big" +) + +// PopularimeterFrame structure is used for Popularimeter (POPM). +// https://id3.org/id3v2.3.0#Popularimeter +type PopularimeterFrame struct { + // Email is the identifier for a POPM frame. + Email string + + // The rating is 1-255 where 1 is worst and 255 is best. 0 is unknown. + Rating uint8 + + // Counter is the number of times this file has been played by this email. + Counter *big.Int +} + +func (pf PopularimeterFrame) UniqueIdentifier() string { + return pf.Email +} + +func (pf PopularimeterFrame) Size() int { + ratingSize := 1 + return len(pf.Email) + 1 + ratingSize + len(pf.counterBytes()) +} + +// counterBytes returns a byte slice that represents the counter. +func (pf PopularimeterFrame) counterBytes() []byte { + bytes := pf.Counter.Bytes() + + // Specification requires at least 4 bytes for counter, pad if necessary. + bytesNeeded := 4 - len(bytes) + if bytesNeeded > 0 { + padding := make([]byte, bytesNeeded) + bytes = append(padding, bytes...) + } + + return bytes +} + +func (pf PopularimeterFrame) WriteTo(w io.Writer) (n int64, err error) { + return useBufWriter(w, func(bw *bufWriter) { + bw.WriteString(pf.Email) + bw.WriteByte(0) + bw.WriteByte(pf.Rating) + bw.Write(pf.counterBytes()) + }) +} + +func parsePopularimeterFrame(br *bufReader) (Framer, error) { + email := br.ReadText(EncodingISO) + rating := br.ReadByte() + + counter := big.NewInt(0) + remainingBytes := br.ReadAll() + counter = counter.SetBytes(remainingBytes) + + pf := PopularimeterFrame{ + Email: string(email), + Rating: rating, + Counter: counter, + } + + return pf, nil +} diff --git a/popularimeter_frame_test.go b/popularimeter_frame_test.go new file mode 100644 index 0000000..3022840 --- /dev/null +++ b/popularimeter_frame_test.go @@ -0,0 +1,34 @@ +package id3v2 + +import ( + "bytes" + "math/big" + "testing" +) + +// Make sure that counter of popularimeter is at least 4 bytes even if it's small number. +func TestPopularimeterFrameSmallCounter(t *testing.T) { + popmFrame := PopularimeterFrame{ + Email: "foo@bar.com", + Rating: 1, + Counter: big.NewInt(1), + } + + expectedBodyLength := len(popmFrame.Email) + 1 + 1 + 4 + + buf := new(bytes.Buffer) + written, err := popmFrame.WriteTo(buf) + if err != nil { + t.Fatalf("Error by writing: %v", err) + } + + if written != int64(expectedBodyLength) { + t.Fatalf("Expected popularimeter frame body length: %v, got: %v", expectedBodyLength, written) + } + + expectedCounter := []byte{0, 0, 0, 1} + gotCounter := buf.Bytes()[expectedBodyLength-4:] + if !bytes.Equal(expectedCounter, gotCounter) { + t.Fatalf("Expected popularimeter counter: %v, got: %v", expectedCounter, gotCounter) + } +} diff --git a/tag_test.go b/tag_test.go index c5c41a8..218317c 100644 --- a/tag_test.go +++ b/tag_test.go @@ -9,6 +9,7 @@ import ( "bytes" "fmt" "io/ioutil" + "math/big" "os" "strings" "sync" @@ -20,10 +21,10 @@ const ( frontCoverPath = "testdata/front_cover.jpg" backCoverPath = "testdata/back_cover.jpg" - framesSize = 211948 + framesSize = 211978 tagSize = tagHeaderSize + framesSize musicSize = 3840834 - countOfFrames = 14 + countOfFrames = 15 ) var ( @@ -79,6 +80,12 @@ var ( Text: "Der eigentliche Text", } + popmFrame = PopularimeterFrame{ + Email: "foo@bar.com", + Rating: 128, + Counter: big.NewInt(10000000000000000), + } + unknownFrameID = "WPUB" unknownFrame = UnknownFrame{ Body: []byte("https://soundcloud.com/suicidepart2"), @@ -117,6 +124,8 @@ func resetMP3Tag() error { tag.AddUserDefinedTextFrame(musicBrainzUDTF) tag.AddUFIDFrame(musicBrainzUF) + tag.AddFrame(tag.CommonID("Popularimeter"), popmFrame) + tag.AddCommentFrame(engComm) tag.AddCommentFrame(gerComm) @@ -146,7 +155,7 @@ func TestCountLenSize(t *testing.T) { } // Check len of tag.AllFrames(). - if len(tag.AllFrames()) != 11 { + if len(tag.AllFrames()) != 12 { t.Errorf("Expected: %v, got: %v", 11, len(tag.AllFrames())) }