Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support CHAP frames #62

Merged
merged 24 commits into from
Nov 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions chapter_frame.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package id3v2

import (
"encoding/binary"
"io"
"time"
)

const (
nanosInMillis = 1000000
IgnoredOffset = 0xFFFFFFFF
)

// ChapterFrame is used to work with CHAP frames
// according to spec from http://id3.org/id3v2-chapters-1.0
// This implementation only supports single TIT2 subframe (Title field).
// All other subframes are ignored.
// If StartOffset or EndOffset == id3v2.IgnoredOffset, then it should be ignored
// and StartTime or EndTime should be utilized
type ChapterFrame struct {
ElementID string
StartTime time.Duration
EndTime time.Duration
StartOffset uint32
EndOffset uint32
Title *TextFrame
Description *TextFrame
}

func (cf ChapterFrame) Size() int {
size := encodedSize(cf.ElementID, EncodingISO) +
1 + // trailing zero after ElementID
4 + 4 + 4 + 4 // (Start, End) (Time, Offset)
if cf.Title != nil {
size = size +
frameHeaderSize + // Title frame header size
cf.Title.Size()
}
if cf.Description != nil {
size = size +
frameHeaderSize + // Description frame header size
cf.Description.Size()
}
return size
}

func (cf ChapterFrame) UniqueIdentifier() string {
return cf.ElementID
}

func (cf ChapterFrame) WriteTo(w io.Writer) (n int64, err error) {
return useBufWriter(w, func(bw *bufWriter) {
bw.EncodeAndWriteText(cf.ElementID, EncodingISO)
bw.WriteByte(0)
binary.Write(bw, binary.BigEndian, int32(cf.StartTime/nanosInMillis))
n10v marked this conversation as resolved.
Show resolved Hide resolved
binary.Write(bw, binary.BigEndian, int32(cf.EndTime/nanosInMillis))

binary.Write(bw, binary.BigEndian, cf.StartOffset)
binary.Write(bw, binary.BigEndian, cf.EndOffset)

if cf.Title != nil {
writeFrame(bw, "TIT2", *cf.Title, true)
}

if cf.Description != nil {
writeFrame(bw, "TIT3", *cf.Description, true)
}
})
}

func parseChapterFrame(br *bufReader, version byte) (Framer, error) {
elementID := br.ReadText(EncodingISO)
synchSafe := version == 4
var startTime uint32
var startOffset uint32
var endTime uint32
var endOffset uint32

if err := binary.Read(br, binary.BigEndian, &startTime); err != nil {
return nil, err
}
if err := binary.Read(br, binary.BigEndian, &endTime); err != nil {
return nil, err
}
if err := binary.Read(br, binary.BigEndian, &startOffset); err != nil {
return nil, err
}
if err := binary.Read(br, binary.BigEndian, &endOffset); err != nil {
return nil, err
}

var title TextFrame
var description TextFrame

// borrowed from parse.go
buf := getByteSlice(32 * 1024)
defer putByteSlice(buf)

for {
header, err := parseFrameHeader(buf, br, synchSafe)
if err == io.EOF || err == errBlankFrame || err == ErrInvalidSizeFormat {
break
}
if err != nil {
return nil, err
}
id, bodySize := header.ID, header.BodySize
if id == "TIT2" || id == "TIT3" {
bodyRd := getLimitedReader(br, bodySize)
br := newBufReader(bodyRd)
frame, err := parseTextFrame(br)
if err != nil {
putLimitedReader(bodyRd)
return nil, err
}
if id == "TIT2" {
title = frame.(TextFrame)
} else if id == "TIT3" {
description = frame.(TextFrame)
}

putLimitedReader(bodyRd)
}
n10v marked this conversation as resolved.
Show resolved Hide resolved
}

cf := ChapterFrame{
ElementID: string(elementID),
// StartTime is given in milliseconds, so we should convert it to nanoseconds
// for time.Duration
StartTime: time.Duration(int64(startTime) * nanosInMillis),
EndTime: time.Duration(int64(endTime) * nanosInMillis),
StartOffset: startOffset,
EndOffset: endOffset,
Title: &title,
Description: &description,
}
return cf, nil
}
169 changes: 169 additions & 0 deletions chapter_frame_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package id3v2

import (
"io"
"io/ioutil"
"log"
"os"
"testing"
"time"
)

func prepareTestFile() (*os.File, error) {
src, err := os.Open("./testdata/test.mp3")
if err != nil {
return nil, err
}
defer src.Close()

tmpFile, err := ioutil.TempFile("", "chapter_test")
if err != nil {
return nil, err
}

_, err = io.Copy(tmpFile, src)
if err != nil {
return nil, err
}
return tmpFile, nil
}

func TestAddChapterFrame(t *testing.T) {
type fields struct {
ElementID string
StartTime time.Duration
EndTime time.Duration
StartOffset uint32
EndOffset uint32
Title *TextFrame
Description *TextFrame
}
tests := []struct {
name string
fields fields
}{
{
name: "element id only",
fields: fields{
ElementID: "chap0",
StartTime: 0,
EndTime: time.Duration(1000 * nanosInMillis),
StartOffset: 0,
EndOffset: 0,
},
},
{
name: "with title",
fields: fields{
ElementID: "chap0",
StartTime: 0,
EndTime: time.Duration(1000 * nanosInMillis),
StartOffset: 0,
EndOffset: 0,
Title: &TextFrame{
Encoding: EncodingUTF8,
Text: "chapter 0",
},
},
},
{
name: "with description",
fields: fields{
ElementID: "chap0",
StartTime: 0,
EndTime: time.Duration(1000 * nanosInMillis),
StartOffset: 0,
EndOffset: 0,
Description: &TextFrame{
Encoding: EncodingUTF8,
Text: "chapter 0",
},
},
},
{
name: "with title and description",
fields: fields{
ElementID: "chap0",
StartTime: 0,
EndTime: time.Duration(1000 * nanosInMillis),
StartOffset: 0,
EndOffset: 0,
Title: &TextFrame{
Encoding: EncodingUTF8,
Text: "chapter 0 title",
},
Description: &TextFrame{
Encoding: EncodingUTF8,
Text: "chapter 0 description",
},
},
},
{
name: "non-zero time and offset",
fields: fields{
ElementID: "chap0",
StartTime: time.Duration(1000 * nanosInMillis),
EndTime: time.Duration(1000 * nanosInMillis),
StartOffset: 10,
EndOffset: 10,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpFile, err := prepareTestFile()
if err != nil {
t.Error(err)
}
defer os.Remove(tmpFile.Name())

tag, err := Open(tmpFile.Name(), Options{Parse: true})
if tag == nil || err != nil {
log.Fatal("Error while opening mp3 file: ", err)
}

cf := ChapterFrame{
ElementID: tt.fields.ElementID,
StartTime: tt.fields.StartTime,
EndTime: tt.fields.EndTime,
StartOffset: tt.fields.StartOffset,
EndOffset: tt.fields.EndOffset,
Title: tt.fields.Title,
Description: tt.fields.Description,
}
tag.AddChapterFrame(cf)

if err := tag.Save(); err != nil {
t.Error(err)
}
tag.Close()

tag, err = Open(tmpFile.Name(), Options{Parse: true})
if tag == nil || err != nil {
log.Fatal("Error while opening mp3 file: ", err)
}
frame := tag.GetLastFrame("CHAP").(ChapterFrame)
if frame.ElementID != tt.fields.ElementID {
t.Errorf("Expected element ID: %s, but got %s", tt.fields.ElementID, frame.ElementID)
}
if tt.fields.Title != nil && frame.Title.Text != tt.fields.Title.Text {
t.Errorf("Expected title: %s, but got %s", tt.fields.Title.Text, frame.Title)
}
if tt.fields.Description != nil && frame.Description.Text != tt.fields.Description.Text {
t.Errorf("Expected description: %s, but got %s", tt.fields.Description.Text, frame.Description.Text)
}
if frame.StartTime != tt.fields.StartTime {
t.Errorf("Expected start time: %s, but got %s", tt.fields.StartTime, frame.StartTime)
}
if frame.EndTime != tt.fields.EndTime {
t.Errorf("Expected end time: %s, but got %s", tt.fields.EndTime, frame.EndTime)
}
if frame.StartOffset != tt.fields.StartOffset {
t.Errorf("Expected start offset: %d, but got %d", tt.fields.StartOffset, frame.StartOffset)
}
if frame.EndOffset != tt.fields.EndOffset {
t.Errorf("Expected end offset: %d, but got %d", tt.fields.EndOffset, frame.EndOffset)
}
})
}
}
n10v marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion comment_frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (cf CommentFrame) WriteTo(w io.Writer) (n int64, err error) {
})
}

func parseCommentFrame(br *bufReader) (Framer, error) {
func parseCommentFrame(br *bufReader, version byte) (Framer, error) {
encoding := getEncoding(br.ReadByte())
language := br.Next(3)
description := br.ReadText(encoding)
Expand Down
5 changes: 4 additions & 1 deletion common_ids.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import "strings"
var (
V23CommonIDs = map[string]string{
"Attached picture": "APIC",
"Chapters": "CHAP",
"Comments": "COMM",
"Album/Movie/Show title": "TALB",
"BPM": "TBPM",
Expand Down Expand Up @@ -62,6 +63,7 @@ var (

V24CommonIDs = map[string]string{
"Attached picture": "APIC",
"Chapters": "CHAP",
"Comments": "COMM",
"Album/Movie/Show title": "TALB",
"BPM": "TBPM",
Expand Down Expand Up @@ -135,8 +137,9 @@ var (
// if strings.HasPrefix(id, "T") {
// ...
// }
var parsers = map[string]func(*bufReader) (Framer, error){
var parsers = map[string]func(*bufReader, byte) (Framer, error){
"APIC": parsePictureFrame,
"CHAP": parseChapterFrame,
"COMM": parseCommentFrame,
"POPM": parsePopularimeterFrame,
"TXXX": parseUserDefinedTextFrame,
Expand Down
2 changes: 1 addition & 1 deletion encoding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TestUnsynchronisedLyricsFrameWithUTF16(t *testing.T) {
t.Fatal(err)
}

parsed, err := parseUnsynchronisedLyricsFrame(newBufReader(buf))
parsed, err := parseUnsynchronisedLyricsFrame(newBufReader(buf), 4)
if err != nil {
t.Fatal(err)
}
Expand Down
6 changes: 3 additions & 3 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (tag *Tag) parseFrames(opts Options) error {
}

br.Reset(bodyRd)
frame, err := parseFrameBody(id, br)
frame, err := parseFrameBody(id, br, tag.version)
if err != nil && err != io.EOF {
return err
}
Expand Down Expand Up @@ -174,13 +174,13 @@ func skipReaderBuf(rd io.Reader, buf []byte) error {
return nil
}

func parseFrameBody(id string, br *bufReader) (Framer, error) {
func parseFrameBody(id string, br *bufReader, version byte) (Framer, error) {
if id[0] == 'T' && id != "TXXX" {
return parseTextFrame(br)
}

if parseFunc, exists := parsers[id]; exists {
return parseFunc(br)
return parseFunc(br, version)
}

return parseUnknownFrame(br)
Expand Down
2 changes: 1 addition & 1 deletion picture_frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (pf PictureFrame) WriteTo(w io.Writer) (n int64, err error) {
})
}

func parsePictureFrame(br *bufReader) (Framer, error) {
func parsePictureFrame(br *bufReader, version byte) (Framer, error) {
encoding := getEncoding(br.ReadByte())
mimeType := br.ReadText(EncodingISO)
pictureType := br.ReadByte()
Expand Down
2 changes: 1 addition & 1 deletion popularimeter_frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (pf PopularimeterFrame) WriteTo(w io.Writer) (n int64, err error) {
})
}

func parsePopularimeterFrame(br *bufReader) (Framer, error) {
func parsePopularimeterFrame(br *bufReader, version byte) (Framer, error) {
email := br.ReadText(EncodingISO)
rating := br.ReadByte()

Expand Down
Loading