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

add ChapterFrame type for CHAP frames #40

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
116 changes: 116 additions & 0 deletions chapter_frame.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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 string
}

func (cf ChapterFrame) Size() int {
titleFrame := TextFrame{
Encoding: EncodingUTF8,
Text: cf.Title,
}
return encodedSize(cf.ElementID, EncodingISO) +
1 + // trailing zero after ElementID
4 + 4 + 4 + 4 + // (Start, End) (Time, Offset)
frameHeaderSize + // Title frame header size
titleFrame.Size()
}

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)
// nanoseconds => milliseconds
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)

titleFrame := TextFrame{
Encoding: EncodingUTF8,
Text: cf.Title,
}
writeFrame(bw, "TIT2", titleFrame, true)
})
}

func parseChapterFrame(br *bufReader) (Framer, error) {
ElementID := br.ReadText(EncodingISO)
chapterTime := make([]int32, 2)
for i := range chapterTime {
if err := binary.Read(br, binary.BigEndian, &chapterTime[i]); err != nil {
return nil, err
}
}
chapterOffset := make([]uint32, 2)
for i := range chapterOffset {
if err := binary.Read(br, binary.BigEndian, &chapterOffset[i]); err != nil {
return nil, err
}
}
var title string

// borrowed from parse.go
buf := getByteSlice(32 * 1024)
defer putByteSlice(buf)
for {
// no way to determine whether this should be true or not
// this is likely should be fixed
header, err := parseFrameHeader(buf, br, true)
if err == io.EOF || err == errBlankFrame || err == ErrInvalidSizeFormat {
break
}
if err != nil {
return nil, err
}
id, bodySize := header.ID, header.BodySize
if id == "TIT2" {
bodyRd := getLimitedReader(br, bodySize)
br2 := newBufReader(bodyRd)
frame, err := parseTextFrame(br2)
if err != nil {
putLimitedReader(bodyRd)
return nil, err
}
title = frame.(TextFrame).Text

putLimitedReader(bodyRd)
break
}
}

cf := ChapterFrame{
ElementID: string(ElementID),
// StartTime is given in milliseconds, so we should convert it to nanoseconds
// for time.Duration
StartTime: time.Duration(chapterTime[0] * nanosInMillis),
EndTime: time.Duration(chapterTime[1] * nanosInMillis),
StartOffset: chapterOffset[0],
EndOffset: chapterOffset[1],
Title: title,
}
return cf, nil
}
5 changes: 4 additions & 1 deletion common_ids.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package id3v2
var (
V23CommonIDs = map[string]string{
"Attached picture": "APIC",
"Chapters": "CHAP",
"Comments": "COMM",
"Album/Movie/Show title": "TALB",
"BPM": "TBPM",
Expand Down Expand Up @@ -59,6 +60,7 @@ var (

V24CommonIDs = map[string]string{
"Attached picture": "APIC",
"Chapters": "CHAP",
"Comments": "COMM",
"Album/Movie/Show title": "TALB",
"BPM": "TBPM",
Expand Down Expand Up @@ -133,6 +135,7 @@ var (
// }
var parsers = map[string]func(*bufReader) (Framer, error){
"APIC": parsePictureFrame,
"CHAP": parseChapterFrame,
"COMM": parseCommentFrame,
"TXXX": parseUserDefinedTextFrame,
"UFID": parseUFIDFrame,
Expand All @@ -143,7 +146,7 @@ var parsers = map[string]func(*bufReader) (Framer, error){
// be added to sequence.
func mustFrameBeInSequence(id string) bool {
switch id {
case "APIC", "COMM", "TXXX", "USLT":
case "APIC", "CHAP", "COMM", "TXXX", "USLT":
return true
}
return false
Expand Down
2 changes: 2 additions & 0 deletions sequence.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ func (s *sequence) AddFrame(f Framer) {
id = uslf.Language + uslf.ContentDescriptor
} else if udtf, ok := f.(UserDefinedTextFrame); ok {
id = udtf.Description
} else if cf, ok := f.(ChapterFrame); ok {
id = cf.ElementID
} else {
panic("sequence: unknown type of Framer")
}
Expand Down
5 changes: 5 additions & 0 deletions tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ func (tag *Tag) AddAttachedPicture(pf PictureFrame) {
tag.AddFrame(tag.CommonID("Attached picture"), pf)
}

// AddChapterFrame adds the chapter frame to tag.
func (tag *Tag) AddChapterFrame(cf ChapterFrame) {
tag.AddFrame(tag.CommonID("Chapters"), cf)
}

// AddCommentFrame adds the comment frame to tag.
func (tag *Tag) AddCommentFrame(cf CommentFrame) {
tag.AddFrame(tag.CommonID("Comments"), cf)
Expand Down