Browse Source

⁜ ^Cnimal ID3v2 reading support ! ⁜

main
Unbewohnte 3 years ago
parent
commit
5de469247a
  1. 4
      README.md
  2. BIN
      testData/testwritev1.mp3
  3. 2
      util/conversion.go
  4. 2
      util/conversion_test.go
  5. 2
      util/read.go
  6. 6
      v1/read.go
  7. 6
      v1/v1_test.go
  8. 223
      v2/frame.go
  9. 234
      v2/frame_identifiers.go
  10. 85
      v2/frame_test.go
  11. 54
      v2/header.go
  12. 2
      v2/header_test.go
  13. 47
      v2/read.go
  14. 19
      v2/read_test.go
  15. 2
      v2/v2tag.go

4
README.md

@ -9,7 +9,7 @@
Right now it`s capable of reading and writing ID3v1 and ID3v1.1 tags. Right now it`s capable of reading and writing ID3v1 and ID3v1.1 tags.
ID3v2.x support is still in making, but it can read header and v.3~v.4 frames ID3v2 support is still in making, but it can read header and frames
--- ---
@ -28,7 +28,7 @@ go install github.com/Unbewohnte/id3ed/...
--- ---
# ∙ Usage # ∙ Examples
## ⚬ Decoding ID3v1 ## ⚬ Decoding ID3v1
``` ```

BIN
testData/testwritev1.mp3

Binary file not shown.

2
util/conversion.go

@ -38,7 +38,7 @@ func BytesToIntIgnoreFirstBit(gBytes []byte) (int64, error) {
// Converts given bytes into string, ignoring the first 31 non-printable ASCII characters. // Converts given bytes into string, ignoring the first 31 non-printable ASCII characters.
// (LOSSY, if given bytes contain some nasty ones) // (LOSSY, if given bytes contain some nasty ones)
func ToString(gBytes []byte) string { func ToStringLossy(gBytes []byte) string {
var filteredBytes []byte var filteredBytes []byte
for _, b := range gBytes { for _, b := range gBytes {
if b <= 31 { if b <= 31 {

2
util/conversion_test.go

@ -5,7 +5,7 @@ import "testing"
func TestToString(t *testing.T) { func TestToString(t *testing.T) {
someVeryNastyBytes := []byte{0, 1, 2, 3, 4, 5, 6, 50, 7, 8, 9, 10, 11, 50, 50} someVeryNastyBytes := []byte{0, 1, 2, 3, 4, 5, 6, 50, 7, 8, 9, 10, 11, 50, 50}
gString := ToString(someVeryNastyBytes) gString := ToStringLossy(someVeryNastyBytes)
if gString != "222" { if gString != "222" {
t.Errorf("ToString failed: expected output: %s; got %s", "222", gString) t.Errorf("ToString failed: expected output: %s; got %s", "222", gString)

2
util/read.go

@ -24,5 +24,5 @@ func ReadToString(rs io.Reader, n uint64) (string, error) {
if err != nil { if err != nil {
return "", fmt.Errorf("could not read from reader: %s", err) return "", fmt.Errorf("could not read from reader: %s", err)
} }
return ToString(read), nil return ToStringLossy(read), nil
} }

6
v1/read.go

@ -10,7 +10,7 @@ import (
) )
// Retrieves ID3v1 field values of provided io.ReadSeeker (usually a file) // Retrieves ID3v1 field values of provided io.ReadSeeker (usually a file)
func Getv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) { func Readv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) {
var tag ID3v1Tag var tag ID3v1Tag
// set reader to the last 128 bytes // set reader to the last 128 bytes
@ -67,7 +67,7 @@ func Getv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
tag.Comment = util.ToString(comment) tag.Comment = util.ToStringLossy(comment)
tag.Track = 0 tag.Track = 0
var track int = 0 var track int = 0
@ -81,7 +81,7 @@ func Getv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) {
tag.Track = uint8(track) tag.Track = uint8(track)
comment = comment[0:28] comment = comment[0:28]
tag.Comment = util.ToString(comment) tag.Comment = util.ToStringLossy(comment)
} }
// Genre // Genre

6
v1/v1_test.go

@ -17,12 +17,12 @@ var TESTv1TAG = &ID3v1Tag{
Genre: "Blues", Genre: "Blues",
} }
func TestGetv1Tags(t *testing.T) { func TestReadv1Tag(t *testing.T) {
testfile, err := os.OpenFile(filepath.Join(TESTDATAPATH, "testreadv1.mp3"), os.O_CREATE|os.O_RDONLY, os.ModePerm) testfile, err := os.OpenFile(filepath.Join(TESTDATAPATH, "testreadv1.mp3"), os.O_CREATE|os.O_RDONLY, os.ModePerm)
if err != nil { if err != nil {
t.Errorf("could not open file for testing: %s", err) t.Errorf("could not open file for testing: %s", err)
} }
tag, err := Getv1Tag(testfile) tag, err := Readv1Tag(testfile)
if err != nil { if err != nil {
t.Errorf("GetID3v1Tag failed: %s", err) t.Errorf("GetID3v1Tag failed: %s", err)
} }
@ -60,7 +60,7 @@ func TestWritev1Tags(t *testing.T) {
} }
// reading a tag // reading a tag
readTag, err := Getv1Tag(f) readTag, err := Readv1Tag(f)
if err != nil { if err != nil {
t.Errorf("%s", err) t.Errorf("%s", err)
} }

223
v2/frame.go

@ -3,11 +3,14 @@ package v2
import ( import (
"fmt" "fmt"
"io" "io"
"strings"
"github.com/Unbewohnte/id3ed/util" "github.com/Unbewohnte/id3ed/util"
) )
var ErrGotPadding error = fmt.Errorf("got padding")
var ErrBiggerThanSize error = fmt.Errorf("frame size is bigger than size of the whole tag")
var ErrInvalidFHeaderSize error = fmt.Errorf("frame header must be 6 or 10 bytes long")
type FrameFlags struct { type FrameFlags struct {
TagAlterPreservation bool TagAlterPreservation bool
FileAlterPreservation bool FileAlterPreservation bool
@ -17,150 +20,136 @@ type FrameFlags struct {
InGroup bool InGroup bool
} }
type Frame struct { type FrameHeader struct {
ID string ID string
Size int64 Size int64
Flags FrameFlags Flags FrameFlags
GroupByte byte
Contents []byte
} }
var ErrGotPadding error = fmt.Errorf("got padding") type Frame struct {
Header FrameHeader
// Reads next ID3v2.3.0 or ID3v2.4.0 frame. Contents []byte
// Returns a blank Frame struct if encountered an error }
func ReadFrame(rs io.Reader) (Frame, error) {
var frame Frame
// ID // Structuralises frame header from given bytes. For versions see: constants.
identifier, err := util.ReadToString(rs, 4) func getFrameHeader(fHeaderbytes []byte, version string) (FrameHeader, error) {
if err != nil { // validation check
return Frame{}, err if int(len(fHeaderbytes)) != int(10) && int(len(fHeaderbytes)) != int(6) {
return FrameHeader{}, ErrInvalidFHeaderSize
} }
if len(identifier) < 1 {
// probably read all frames and got padding as identifier
// I know that it`s a terrible desicion, but with my current var header FrameHeader
// implementation it`s the only way I can see that will somewhat work
return Frame{}, ErrGotPadding switch version {
} case V2_2:
frame.ID = identifier header.ID = string(fHeaderbytes[0:3])
// Size framesizeBytes, err := util.BytesToIntIgnoreFirstBit(fHeaderbytes[3:6])
framesizeBytes, err := util.Read(rs, 4) if err != nil {
if err != nil { return FrameHeader{}, err
return Frame{}, err }
} header.Size = framesizeBytes
framesize, err := util.BytesToIntIgnoreFirstBit(framesizeBytes) case V2_3:
if err != nil { fallthrough
return Frame{}, err
}
frame.Size = framesize case V2_4:
fallthrough
// Flags default:
// ID
header.ID = string(fHeaderbytes[0:4])
frameFlagsByte1, err := util.Read(rs, 1) // Size
if err != nil { framesizeBytes := fHeaderbytes[4:8]
return Frame{}, err
}
frameFlagsByte2, err := util.Read(rs, 1) framesize, err := util.BytesToIntIgnoreFirstBit(framesizeBytes)
if err != nil { if err != nil {
return Frame{}, err return FrameHeader{}, err
} }
// I don`t have enough knowledge to handle this more elegantly header.Size = framesize
// Any pointers ?
flagsByte1Bits := fmt.Sprintf("%08b", frameFlagsByte1) // Flags
flagsByte2Bits := fmt.Sprintf("%08b", frameFlagsByte2) frameFlagsByte1 := fHeaderbytes[8]
var flags FrameFlags frameFlagsByte2 := fHeaderbytes[9]
if flagsByte1Bits[0] == 1 { // I don`t have enough knowledge to handle this more elegantly
flags.TagAlterPreservation = true
} else {
flags.TagAlterPreservation = false
}
if flagsByte1Bits[1] == 1 {
flags.FileAlterPreservation = true
} else {
flags.FileAlterPreservation = false
}
if flagsByte1Bits[2] == 1 {
flags.ReadOnly = true
} else {
flags.ReadOnly = false
}
if flagsByte2Bits[0] == 1 {
flags.Compressed = true
} else {
flags.Compressed = false
}
if flagsByte2Bits[1] == 1 {
flags.Encrypted = true
} else {
flags.Encrypted = false
}
if flagsByte2Bits[2] == 1 {
flags.InGroup = true
} else {
flags.InGroup = false
}
frame.Flags = flags flagsByte1Bits := fmt.Sprintf("%08b", frameFlagsByte1)
flagsByte2Bits := fmt.Sprintf("%08b", frameFlagsByte2)
var flags FrameFlags
if flags.InGroup { if flagsByte1Bits[0] == 1 {
groupByte, err := util.Read(rs, 1) flags.TagAlterPreservation = true
if err != nil { } else {
return Frame{}, err flags.TagAlterPreservation = false
}
if flagsByte1Bits[1] == 1 {
flags.FileAlterPreservation = true
} else {
flags.FileAlterPreservation = false
}
if flagsByte1Bits[2] == 1 {
flags.ReadOnly = true
} else {
flags.ReadOnly = false
}
if flagsByte2Bits[0] == 1 {
flags.Compressed = true
} else {
flags.Compressed = false
}
if flagsByte2Bits[1] == 1 {
flags.Encrypted = true
} else {
flags.Encrypted = false
}
if flagsByte2Bits[2] == 1 {
flags.InGroup = true
} else {
flags.InGroup = false
} }
frame.GroupByte = groupByte[0]
}
// Body header.Flags = flags
frameContents, err := util.Read(rs, uint64(framesize))
if err != nil {
return Frame{}, err
} }
frame.Contents = frameContents return header, nil
return frame, nil
} }
// Reads all ID3v2 frames from rs. // Reads ID3v2.3.0 or ID3v2.4.0 frame from given frame bytes.
// Returns a nil as []Frame if encountered an error // Returns a blank Frame struct if encountered an error, amount of
func GetFrames(rs io.ReadSeeker) ([]Frame, error) { // bytes read from io.Reader.
// skip header func ReadNextFrame(r io.Reader, h Header) (Frame, uint64, error) {
_, err := rs.Seek(10, io.SeekStart) var frame Frame
var read uint64 = 0
// Frame header
headerBytes, err := util.Read(r, uint64(HEADERSIZE))
if err != nil { if err != nil {
return nil, fmt.Errorf("could not skip header: %s", err) return Frame{}, 0, err
} }
var frames []Frame read += uint64(HEADERSIZE)
for {
frame, err := ReadFrame(rs)
if err == ErrGotPadding {
return frames, nil
}
if err != nil {
return nil, fmt.Errorf("could not read frame: %s", err)
}
frames = append(frames, frame) frameHeader, err := getFrameHeader(headerBytes, h.Version)
if err == ErrGotPadding {
return Frame{}, read, err
} else if err != nil {
return Frame{}, read, fmt.Errorf("could not get header of a frame: %s", err)
} }
}
// Looks for a certain identificator in given frames and returns frame if found frame.Header = frameHeader
func GetFrame(id string, frames []Frame) Frame {
for _, frame := range frames { // Contents
if strings.Contains(frame.ID, id) { contents, err := util.Read(r, uint64(frameHeader.Size))
return frame if err != nil {
} return Frame{}, read, err
} }
return Frame{}
frame.Contents = contents
read += uint64(frameHeader.Size)
return frame, read, err
} }

234
v2/frame_identifiers.go

@ -0,0 +1,234 @@
package v2
// from [https://id3.org/]
var V2_2FrameIdentifiers = map[string]string{
"BUF": "Recommended buffer size",
"CNT": "Play counter",
"COM": "Comments",
"CRA": "Audio encryption",
"CRM": "Encrypted meta frame",
"ETC": "Event timing codes",
"EQU": "Equalization",
"GEO": "General encapsulated object",
"IPL": "Involved people list",
"LNK": "Linked information",
"MCI": "Music CD Identifier",
"MLL": "MPEG location lookup table",
"PIC": "Attached picture",
"POP": "Popularimeter",
"REV": "Reverb",
"RVA": "Relative volume adjustment",
"SLT": "Synchronized lyric/text",
"STC": "Synced tempo codes",
"TAL": "Album/Movie/Show title",
"TBP": "BPM (Beats Per Minute)",
"TCM": "Composer",
"TCO": "Content type",
"TCR": "Copyright message",
"TDA": "Date",
"TDY": "Playlist delay",
"TEN": "Encoded by",
"TFT": "File type",
"TIM": "Time",
"TKE": "Initial key",
"TLA": "Language(s)",
"TLE": "Length",
"TMT": "Media type",
"TOA": "Original artist(s)/performer(s)",
"TOF": "Original filename",
"TOL": "Original Lyricist(s)/text writer(s)",
"TOR": "Original release year",
"TOT": "Original album/Movie/Show title",
"TP1": "Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group",
"TP2": "Band/Orchestra/Accompaniment",
"TP3": "Conductor/Performer refinement",
"TP4": "Interpreted, remixed, or otherwise modified by",
"TPA": "Part of a set",
"TPB": "Publisher",
"TRC": "ISRC (International Standard Recording Code)",
"TRD": "Recording dates",
"TRK": "Track number/Position in set",
"TSI": "Size",
"TSS": "Software/hardware and settings used for encoding",
"TT1": "Content group description",
"TT2": "Title/Songname/Content description",
"TT3": "Subtitle/Description refinement",
"TXT": "Lyricist/text writer",
"TXX": "User defined text information frame",
"TYE": "Year",
"UFI": "Unique file identifier",
"ULT": "Unsychronized lyric/text transcription",
"WAF": "Official audio file webpage",
"WAR": "Official artist/performer webpage",
"WAS": "Official audio source webpage",
"WCM": "Commercial information",
"WCP": "Copyright/Legal information",
"WPB": "Publishers official webpage",
"WXX": "User defined URL link frame",
}
var V2_3FrameIdentifiers = map[string]string{
"AENC": "Audio encryption",
"APIC": "Attached picture",
"COMM": "Comments",
"COMR": "Commercial frame",
"ENCR": "Encryption method registration",
"EQUA": "Equalization",
"ETCO": "Event timing codes",
"GEOB": "General encapsulated object",
"GRID": "Group identification registration",
"IPLS": "Involved people list",
"LINK": "Linked information",
"MCDI": "Music CD identifier",
"MLLT": "MPEG location lookup table",
"OWNE": "Ownership frame",
"PRIV": "Private frame",
"PCNT": "Play counter",
"POPM": "Popularimeter",
"POSS": "Position synchronisation frame",
"RBUF": "Recommended buffer size",
"RVAD": "Relative volume adjustment",
"RVRB": "Reverb",
"SYLT": "Synchronized lyric/text",
"SYTC": "Synchronized tempo codes",
"TALB": "Album/Movie/Show title",
"TBPM": "BPM (beats per minute)",
"TCOM": "Composer",
"TCON": "Content type",
"TCOP": "Copyright message",
"TDAT": "Date",
"TDLY": "Playlist delay",
"TENC": "Encoded by",
"TEXT": "Lyricist/Text writer",
"TFLT": "File type",
"TIME": "Time",
"TIT1": "Content group description",
"TIT2": "Title/songname/content description",
"TIT3": "Subtitle/Description refinement",
"TKEY": "Initial key",
"TLAN": "Language(s)",
"TLEN": "Length",
"TMED": "Media type",
"TOAL": "Original album/movie/show title",
"TOFN": "Original filename",
"TOLY": "Original lyricist(s)/text writer(s)",
"TOPE": "Original artist(s)/performer(s)",
"TORY": "Original release year",
"TOWN": "File owner/licensee",
"TPE1": "Lead performer(s)/Soloist(s)",
"TPE2": "Band/orchestra/accompaniment",
"TPE3": "Conductor/performer refinement",
"TPE4": "Interpreted, remixed, or otherwise modified by",
"TPOS": "Part of a set",
"TPUB": "Publisher",
"TRCK": "Track number/Position in set",
"TRDA": "Recording dates",
"TRSN": "Internet radio station name",
"TRSO": "Internet radio station owner",
"TSIZ": "Size",
"TSRC": "ISRC (international standard recording code)",
"TSSE": "Software/Hardware and settings used for encoding",
"TYER": "Year",
"TXXX": "User defined text information frame",
"UFID": "Unique file identifier",
"USER": "Terms of use",
"USLT": "Unsychronized lyric/text transcription",
"WCOM": "Commercial information",
"WCOP": "Copyright/Legal information",
"WOAF": "Official audio file webpage",
"WOAR": "Official artist/performer webpage",
"WOAS": "Official audio source webpage",
"WORS": "Official internet radio station homepage",
"WPAY": "Payment",
"WPUB": "Publishers official webpage",
"WXXX": "User defined URL link frame",
}
var V2_4FrameIdentifiers = map[string]string{
// new frames
"ASPI": "Audio seek point index",
"EQU2": "Equalisation",
"RVA2": "Relative volume adjustment",
"SEEK": "Seek frame",
"SIGN": "Signature frame",
"TDEN": "Encoding time",
"TDOR": "Original release time",
"TDRC": "Recording time",
"TDRL": "Release time",
"TDTG": "Tagging time",
"TIPL": "Involved people list",
"TMCL": "Musician credits list",
"TMOO": "Mood",
"TPRO": "Produced notice",
"TSOA": "Album sort order",
"TSOP": "Performer sort order",
"TSOT": "Title sort order",
"TSST": "Set subtitle",
// old frames without depricated IDs
"AENC": "Audio encryption",
"APIC": "Attached picture",
"COMM": "Comments",
"COMR": "Commercial frame",
"ENCR": "Encryption method registration",
"ETCO": "Event timing codes",
"GEOB": "General encapsulated object",
"GRID": "Group identification registration",
"LINK": "Linked information",
"MCDI": "Music CD identifier",
"MLLT": "MPEG location lookup table",
"OWNE": "Ownership frame",
"PRIV": "Private frame",
"PCNT": "Play counter",
"POPM": "Popularimeter",
"POSS": "Position synchronisation frame",
"RBUF": "Recommended buffer size",
"RVRB": "Reverb",
"SYLT": "Synchronized lyric/text",
"SYTC": "Synchronized tempo codes",
"TALB": "Album/Movie/Show title",
"TBPM": "BPM (beats per minute)",
"TCOM": "Composer",
"TCON": "Content type",
"TCOP": "Copyright message",
"TDLY": "Playlist delay",
"TENC": "Encoded by",
"TEXT": "Lyricist/Text writer",
"TFLT": "File type",
"TIT1": "Content group description",
"TIT2": "Title/songname/content description",
"TIT3": "Subtitle/Description refinement",
"TKEY": "Initial key",
"TLAN": "Language(s)",
"TLEN": "Length",
"TMED": "Media type",
"TOAL": "Original album/movie/show title",
"TOFN": "Original filename",
"TOLY": "Original lyricist(s)/text writer(s)",
"TOPE": "Original artist(s)/performer(s)",
"TOWN": "File owner/licensee",
"TPE1": "Lead performer(s)/Soloist(s)",
"TPE2": "Band/orchestra/accompaniment",
"TPE3": "Conductor/performer refinement",
"TPE4": "Interpreted, remixed, or otherwise modified by",
"TPOS": "Part of a set",
"TPUB": "Publisher",
"TRCK": "Track number/Position in set",
"TRSN": "Internet radio station name",
"TRSO": "Internet radio station owner",
"TSRC": "ISRC (international standard recording code)",
"TSSE": "Software/Hardware and settings used for encoding",
"TXXX": "User defined text information frame",
"UFID": "Unique file identifier",
"USER": "Terms of use",
"USLT": "Unsychronized lyric/text transcription",
"WCOM": "Commercial information",
"WCOP": "Copyright/Legal information",
"WOAF": "Official audio file webpage",
"WOAR": "Official artist/performer webpage",
"WOAS": "Official audio source webpage",
"WORS": "Official internet radio station homepage",
"WPAY": "Payment",
"WPUB": "Publishers official webpage",
"WXXX": "User defined URL link frame",
}

85
v2/frame_test.go

@ -1,7 +1,6 @@
package v2 package v2
import ( import (
"io"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -9,71 +8,65 @@ import (
"github.com/Unbewohnte/id3ed/util" "github.com/Unbewohnte/id3ed/util"
) )
func TestReadFrame(t *testing.T) { func TestReadNextFrame(t *testing.T) {
f, err := os.Open(filepath.Join(TESTDATAPATH, "testreadv2.mp3")) f, err := os.Open(filepath.Join(TESTDATAPATH, "testreadv2.mp3"))
if err != nil { if err != nil {
t.Errorf("%s", err) t.Errorf("%s", err)
} }
// read right after header`s bytes header, err := ReadHeader(f)
f.Seek(int64(HEADERSIZE), io.SeekStart)
firstFrame, err := ReadFrame(f)
if err != nil { if err != nil {
t.Errorf("ReadFrame failed: %s", err) t.Errorf("%s", err)
}
if firstFrame.ID != "TRCK" {
t.Errorf("ReadFrame failed: expected ID %s; got %s", "TRCK", firstFrame.ID)
}
if firstFrame.Flags.Encrypted != false {
t.Errorf("ReadFrame failed: expected compressed flag to be %v; got %v", false, firstFrame.Flags.Encrypted)
} }
secondFrame, err := ReadFrame(f) firstFrame, _, err := ReadNextFrame(f, header)
if err != nil { if err != nil {
t.Errorf("ReadFrame failed: %s", err) t.Errorf("ReadFrame failed: %s", err)
} }
if secondFrame.ID != "TDRC" { if firstFrame.Header.ID != "TRCK" {
t.Errorf("ReadFrame failed: expected ID %s; got %s", "TDRC", secondFrame.ID) t.Errorf("GetFrame failed: expected ID %s; got %s",
} "TRCK", firstFrame.Header.ID)
if util.ToString(secondFrame.Contents) != "2006" {
t.Errorf("ReadFrame failed: expected contents to be %s; got %s", "2006", util.ToString(secondFrame.Contents))
}
}
func TestGetFrames(t *testing.T) {
f, err := os.Open(filepath.Join(TESTDATAPATH, "testreadv2.mp3"))
if err != nil {
t.Errorf("%s", err)
}
_, err = GetFrames(f)
if err != nil {
t.Errorf("GetFrames failed: %s", err)
} }
}
func TestGetFrame(t *testing.T) { if firstFrame.Header.Flags.Encrypted != false {
f, err := os.Open(filepath.Join(TESTDATAPATH, "testreadv2.mp3")) t.Errorf("ReadFrame failed: expected compressed flag to be %v; got %v",
if err != nil { false, firstFrame.Header.Flags.Encrypted)
t.Errorf("%s", err)
} }
frames, err := GetFrames(f) secondFrame, _, err := ReadNextFrame(f, header)
if err != nil { if err != nil {
t.Errorf("GetFrames failed: %s", err) t.Errorf("ReadFrame failed: %s", err)
} }
frame := GetFrame("TIT2", frames) if secondFrame.Header.ID != "TDRC" {
if frame.ID == "" { t.Errorf("ReadFrame failed: expected ID %s; got %s",
t.Errorf("GetFrame failed: expected to find %s; got nothing", "TIT1") "TDRC", secondFrame.Header.ID)
} }
if util.ToString(frame.Contents) != "title" { if util.ToStringLossy(secondFrame.Contents) != "2006" {
t.Errorf("GetFrame failed: expected contents to be %s; got %s", "title", util.ToString(frame.Contents)) t.Errorf("ReadFrame failed: expected contents to be %s; got %s",
"2006", secondFrame.Contents)
} }
} }
// func TestGetFrames(t *testing.T) {
// f, err := os.Open(filepath.Join(TESTDATAPATH, "testreadv2.mp3"))
// if err != nil {
// t.Errorf("%s", err)
// }
// frames, err := GetFrames(f)
// if err != nil {
// t.Errorf("GetFrames failed: %s", err)
// }
// titleFrame, ok := frames["TIT2"]
// if !ok {
// t.Errorf("GetFrames failed: no %s in frames", "TIT2")
// }
// if util.ToStringLossy(titleFrame.Contents) != "title" {
// t.Errorf("GetFrames failed: expected title to be %s; got %s", "title", titleFrame.Contents)
// }
// }

54
v2/header.go

@ -23,62 +23,57 @@ type Header struct {
Size int64 // size of the whole tag - 10 header bytes Size int64 // size of the whole tag - 10 header bytes
} }
// Reads and structuralises ID3v2.3.0 or ID3v2.4.0 header. // Reads and structuralises ID3v2 header from given bytes.
// Returns a blank header struct if encountered an error // Returns a blank header struct if encountered an error
func GetHeader(rs io.ReadSeeker) (Header, error) { func ReadHeader(rs io.ReadSeeker) (Header, error) {
var header Header _, err := rs.Seek(0, io.SeekStart)
if err != nil {
rs.Seek(0, io.SeekStart) return Header{}, fmt.Errorf("could not seek: %s", err)
}
identifier, err := util.Read(rs, 3) hBytes, err := util.Read(rs, uint64(HEADERSIZE))
if err != nil { if err != nil {
return Header{}, err return Header{}, fmt.Errorf("could not read from reader: %s", err)
} }
// check if ID3v2 is used
var header Header
identifier := hBytes[0:3]
// check if has identifier ID3v2
if !bytes.Equal([]byte(HEADERIDENTIFIER), identifier) { if !bytes.Equal([]byte(HEADERIDENTIFIER), identifier) {
return Header{}, fmt.Errorf("no ID3v2 identifier found") return Header{}, fmt.Errorf("no ID3v2 identifier found")
} }
header.Identifier = string(identifier) header.Identifier = string(identifier)
// version // version
VersionBytes, err := util.Read(rs, 2) majorVersion, err := util.ByteToInt(hBytes[3])
if err != nil {
return Header{}, err
}
majorVersion, err := util.ByteToInt(VersionBytes[0])
if err != nil { if err != nil {
return Header{}, err return Header{}, err
} }
revisionNumber, err := util.ByteToInt(VersionBytes[1]) revisionNumber, err := util.ByteToInt(hBytes[4])
if err != nil { if err != nil {
return Header{}, err return Header{}, err
} }
var version string
switch majorVersion { switch majorVersion {
case 2: case 2:
version = V2_2 header.Version = V2_2
case 3: case 3:
version = V2_3 header.Version = V2_3
case 4: case 4:
version = V2_4 header.Version = V2_4
default: default:
return Header{}, fmt.Errorf("ID3v2.%d.%d is not supported", majorVersion, revisionNumber) return Header{}, fmt.Errorf("ID3v2.%d.%d is not supported or invalid", majorVersion, revisionNumber)
} }
header.Version = version
// flags // flags
flags, err := util.Read(rs, 1) flags := hBytes[5]
if err != nil {
return Header{}, err
}
flagBits := fmt.Sprintf("%08b", flags) // 1 byte is 8 bits flagBits := fmt.Sprintf("%08b", flags) // 1 byte is 8 bits
// v3.0 and v4.0 have different amount of flags // v3.0 and v4.0 have different amount of flags
switch version { switch header.Version {
case V2_3: case V2_3:
if flagBits[0] == 1 { if flagBits[0] == 1 {
header.Flags.Unsynchronisated = true header.Flags.Unsynchronisated = true
@ -122,10 +117,7 @@ func GetHeader(rs io.ReadSeeker) (Header, error) {
} }
// size // size
sizeBytes, err := util.Read(rs, 4) sizeBytes := hBytes[6:]
if err != nil {
return Header{}, err
}
size, err := util.BytesToIntIgnoreFirstBit(sizeBytes) size, err := util.BytesToIntIgnoreFirstBit(sizeBytes)
if err != nil { if err != nil {

2
v2/header_test.go

@ -14,7 +14,7 @@ func TestGetHeader(t *testing.T) {
t.Errorf("%s", err) t.Errorf("%s", err)
} }
header, err := GetHeader(f) header, err := ReadHeader(f)
if err != nil { if err != nil {
t.Errorf("GetHeader failed: %s", err) t.Errorf("GetHeader failed: %s", err)
} }

47
v2/read.go

@ -0,0 +1,47 @@
package v2
import (
"fmt"
"io"
)
// Reads the whole ID3v2 tag from rs
func ReadV2Tag(rs io.ReadSeeker) (*ID3v2Tag, error) {
header, err := ReadHeader(rs)
if err != nil {
return nil, fmt.Errorf("could not get header: %s", err)
}
// collect frames
var read uint64 = 10 // because already read header
var frames []Frame
for {
if read > uint64(header.Size) {
break
}
frame, r, err := ReadNextFrame(rs, header)
if err == ErrGotPadding || err == ErrBiggerThanSize {
break
}
if err != nil {
return nil, fmt.Errorf("could not read frame: %s", err)
}
read += r
frames = append(frames, frame)
}
// create a map from collected frames
framesMp := make(map[string]Frame)
for _, frame := range frames {
framesMp[frame.Header.ID] = frame
}
return &ID3v2Tag{
Header: header,
Frames: framesMp,
}, nil
}

19
v2/read_test.go

@ -0,0 +1,19 @@
package v2
import (
"os"
"path/filepath"
"testing"
)
func TestReadV2Tag(t *testing.T) {
f, err := os.Open(filepath.Join(TESTDATAPATH, "testreadv2.mp3"))
if err != nil {
t.Errorf("%s", err)
}
_, err = ReadV2Tag(f)
if err != nil {
t.Errorf("GetV2Tag failed: %s", err)
}
}

2
v2/v2tag.go

@ -2,5 +2,5 @@ package v2
type ID3v2Tag struct { type ID3v2Tag struct {
Header Header Header Header
Frames []Frame Frames map[string]Frame
} }

Loading…
Cancel
Save