-
Notifications
You must be signed in to change notification settings - Fork 60
support CHAP frames #62
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
Merged
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
5cdc82a
add ChapterFrame type for CHAP frames
cnt0 40442e1
Merge remote-tracking branch 'origin/master' into chapter-frame
takaishi c41e764
fix to parse CHAP frame
takaishi 47bf0f8
update logic
takaishi 1bd15a3
support old golang
takaishi 478dc81
use `Description` called in spec
takaishi cc723a7
refactor: put together identical code
takaishi 046dfba
fix: should write title and description
takaishi 8dad358
refine test
takaishi edc860f
if tag version is 4, syncSafe is enable
takaishi a72682d
need test that has non-zero time and offset
takaishi 10c8d21
use `else if`
takaishi c1ed23b
add name
takaishi a9af757
make more simple
takaishi 4930cb2
better to use lowercase letter
takaishi 3805151
remove comment
takaishi 0867a7f
Update chapter_frame_test.go
takaishi 8ba656f
Update chapter_frame_test.go
takaishi f06deed
Update chapter_frame_test.go
takaishi 1cddd58
Update chapter_frame_test.go
takaishi 8449f13
Update chapter_frame_test.go
takaishi ac67233
Update chapter_frame_test.go
takaishi 531ce04
Update chapter_frame_test.go
takaishi bbcbdfe
use tt.fields.* as StartTime or EndTime
takaishi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)) | ||
| 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 | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.