From 5de469247a5814ab9893804cae502fe5b2b252ad Mon Sep 17 00:00:00 2001 From: Unbewohnte Date: Fri, 9 Jul 2021 16:15:10 +0300 Subject: [PATCH] =?UTF-8?q?=E2=81=9C=20^Cnimal=20ID3v2=20reading=20support?= =?UTF-8?q?=20!=20=E2=81=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- testData/testwritev1.mp3 | Bin 1312 -> 1825 bytes util/conversion.go | 2 +- util/conversion_test.go | 2 +- util/read.go | 2 +- v1/read.go | 6 +- v1/v1_test.go | 6 +- v2/frame.go | 223 ++++++++++++++++++------------------- v2/frame_identifiers.go | 234 +++++++++++++++++++++++++++++++++++++++ v2/frame_test.go | 85 +++++++------- v2/header.go | 54 ++++----- v2/header_test.go | 2 +- v2/read.go | 47 ++++++++ v2/read_test.go | 19 ++++ v2/v2tag.go | 2 +- 15 files changed, 481 insertions(+), 207 deletions(-) create mode 100644 v2/frame_identifiers.go create mode 100644 v2/read.go create mode 100644 v2/read_test.go diff --git a/README.md b/README.md index ed90472..d042dcf 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ 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 ``` diff --git a/testData/testwritev1.mp3 b/testData/testwritev1.mp3 index 0f1adcb42eb507e31e9033097ea2f012c8f412b3..69dfb02b143326261b393e5fa2ae28a7621269d2 100644 GIT binary patch delta 20 ZcmZ3$wUBSZT*lIkbDLQfE3hzvFaS>*24VmJ delta 10 RcmZ3;w}5NH+>P^^SpXO@1YH0C diff --git a/util/conversion.go b/util/conversion.go index a047bc9..c7a71d6 100644 --- a/util/conversion.go +++ b/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. // (LOSSY, if given bytes contain some nasty ones) -func ToString(gBytes []byte) string { +func ToStringLossy(gBytes []byte) string { var filteredBytes []byte for _, b := range gBytes { if b <= 31 { diff --git a/util/conversion_test.go b/util/conversion_test.go index 3772765..38b11b5 100644 --- a/util/conversion_test.go +++ b/util/conversion_test.go @@ -5,7 +5,7 @@ import "testing" func TestToString(t *testing.T) { 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" { t.Errorf("ToString failed: expected output: %s; got %s", "222", gString) diff --git a/util/read.go b/util/read.go index 474471e..3a648c5 100644 --- a/util/read.go +++ b/util/read.go @@ -24,5 +24,5 @@ func ReadToString(rs io.Reader, n uint64) (string, error) { if err != nil { return "", fmt.Errorf("could not read from reader: %s", err) } - return ToString(read), nil + return ToStringLossy(read), nil } diff --git a/v1/read.go b/v1/read.go index 03efe41..10685a0 100644 --- a/v1/read.go +++ b/v1/read.go @@ -10,7 +10,7 @@ import ( ) // 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 // set reader to the last 128 bytes @@ -67,7 +67,7 @@ func Getv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) { if err != nil { return nil, err } - tag.Comment = util.ToString(comment) + tag.Comment = util.ToStringLossy(comment) tag.Track = 0 var track int = 0 @@ -81,7 +81,7 @@ func Getv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) { tag.Track = uint8(track) comment = comment[0:28] - tag.Comment = util.ToString(comment) + tag.Comment = util.ToStringLossy(comment) } // Genre diff --git a/v1/v1_test.go b/v1/v1_test.go index e8beee9..ae13047 100644 --- a/v1/v1_test.go +++ b/v1/v1_test.go @@ -17,12 +17,12 @@ var TESTv1TAG = &ID3v1Tag{ 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) if err != nil { t.Errorf("could not open file for testing: %s", err) } - tag, err := Getv1Tag(testfile) + tag, err := Readv1Tag(testfile) if err != nil { t.Errorf("GetID3v1Tag failed: %s", err) } @@ -60,7 +60,7 @@ func TestWritev1Tags(t *testing.T) { } // reading a tag - readTag, err := Getv1Tag(f) + readTag, err := Readv1Tag(f) if err != nil { t.Errorf("%s", err) } diff --git a/v2/frame.go b/v2/frame.go index 3ff9f39..f091cc7 100644 --- a/v2/frame.go +++ b/v2/frame.go @@ -3,11 +3,14 @@ package v2 import ( "fmt" "io" - "strings" "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 { TagAlterPreservation bool FileAlterPreservation bool @@ -17,150 +20,136 @@ type FrameFlags struct { InGroup bool } -type Frame struct { - ID string - Size int64 - Flags FrameFlags - GroupByte byte - Contents []byte +type FrameHeader struct { + ID string + Size int64 + Flags FrameFlags } -var ErrGotPadding error = fmt.Errorf("got padding") - -// Reads next ID3v2.3.0 or ID3v2.4.0 frame. -// Returns a blank Frame struct if encountered an error -func ReadFrame(rs io.Reader) (Frame, error) { - var frame Frame +type Frame struct { + Header FrameHeader + Contents []byte +} - // ID - identifier, err := util.ReadToString(rs, 4) - if err != nil { - return Frame{}, err +// Structuralises frame header from given bytes. For versions see: constants. +func getFrameHeader(fHeaderbytes []byte, version string) (FrameHeader, error) { + // validation check + 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 - // implementation it`s the only way I can see that will somewhat work + var header FrameHeader - return Frame{}, ErrGotPadding - } - frame.ID = identifier + switch version { + case V2_2: + header.ID = string(fHeaderbytes[0:3]) - // Size - framesizeBytes, err := util.Read(rs, 4) - if err != nil { - return Frame{}, err - } + framesizeBytes, err := util.BytesToIntIgnoreFirstBit(fHeaderbytes[3:6]) + if err != nil { + return FrameHeader{}, err + } + header.Size = framesizeBytes - framesize, err := util.BytesToIntIgnoreFirstBit(framesizeBytes) - if err != nil { - return Frame{}, err - } + case V2_3: + fallthrough - frame.Size = framesize + case V2_4: + fallthrough - // Flags + default: + // ID + header.ID = string(fHeaderbytes[0:4]) - frameFlagsByte1, err := util.Read(rs, 1) - if err != nil { - return Frame{}, err - } + // Size + framesizeBytes := fHeaderbytes[4:8] - frameFlagsByte2, err := util.Read(rs, 1) - if err != nil { - return Frame{}, err - } + framesize, err := util.BytesToIntIgnoreFirstBit(framesizeBytes) + if err != nil { + return FrameHeader{}, err + } - // I don`t have enough knowledge to handle this more elegantly - // Any pointers ? + header.Size = framesize - flagsByte1Bits := fmt.Sprintf("%08b", frameFlagsByte1) - flagsByte2Bits := fmt.Sprintf("%08b", frameFlagsByte2) - var flags FrameFlags + // Flags + frameFlagsByte1 := fHeaderbytes[8] + frameFlagsByte2 := fHeaderbytes[9] - if flagsByte1Bits[0] == 1 { - 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 - } + // I don`t have enough knowledge to handle this more elegantly - frame.Flags = flags + flagsByte1Bits := fmt.Sprintf("%08b", frameFlagsByte1) + flagsByte2Bits := fmt.Sprintf("%08b", frameFlagsByte2) + var flags FrameFlags - if flags.InGroup { - groupByte, err := util.Read(rs, 1) - if err != nil { - return Frame{}, err + if flagsByte1Bits[0] == 1 { + 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.GroupByte = groupByte[0] - } - // Body - frameContents, err := util.Read(rs, uint64(framesize)) - if err != nil { - return Frame{}, err + header.Flags = flags } - frame.Contents = frameContents - - return frame, nil + return header, nil } -// Reads all ID3v2 frames from rs. -// Returns a nil as []Frame if encountered an error -func GetFrames(rs io.ReadSeeker) ([]Frame, error) { - // skip header - _, err := rs.Seek(10, io.SeekStart) +// Reads ID3v2.3.0 or ID3v2.4.0 frame from given frame bytes. +// Returns a blank Frame struct if encountered an error, amount of +// bytes read from io.Reader. +func ReadNextFrame(r io.Reader, h Header) (Frame, uint64, error) { + var frame Frame + var read uint64 = 0 + + // Frame header + headerBytes, err := util.Read(r, uint64(HEADERSIZE)) if err != nil { - return nil, fmt.Errorf("could not skip header: %s", err) + return Frame{}, 0, err } - var frames []Frame - for { - frame, err := ReadFrame(rs) - if err == ErrGotPadding { - return frames, nil - } - - if err != nil { - return nil, fmt.Errorf("could not read frame: %s", err) - } + read += uint64(HEADERSIZE) - 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 -func GetFrame(id string, frames []Frame) Frame { - for _, frame := range frames { - if strings.Contains(frame.ID, id) { - return frame - } + frame.Header = frameHeader + + // Contents + contents, err := util.Read(r, uint64(frameHeader.Size)) + if err != nil { + return Frame{}, read, err } - return Frame{} + + frame.Contents = contents + + read += uint64(frameHeader.Size) + + return frame, read, err } diff --git a/v2/frame_identifiers.go b/v2/frame_identifiers.go new file mode 100644 index 0000000..204b6ed --- /dev/null +++ b/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", +} diff --git a/v2/frame_test.go b/v2/frame_test.go index e6bab29..3243b4c 100644 --- a/v2/frame_test.go +++ b/v2/frame_test.go @@ -1,7 +1,6 @@ package v2 import ( - "io" "os" "path/filepath" "testing" @@ -9,71 +8,65 @@ import ( "github.com/Unbewohnte/id3ed/util" ) -func TestReadFrame(t *testing.T) { +func TestReadNextFrame(t *testing.T) { f, err := os.Open(filepath.Join(TESTDATAPATH, "testreadv2.mp3")) if err != nil { t.Errorf("%s", err) } - // read right after header`s bytes - f.Seek(int64(HEADERSIZE), io.SeekStart) - - firstFrame, err := ReadFrame(f) + header, err := ReadHeader(f) if err != nil { - t.Errorf("ReadFrame failed: %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) + t.Errorf("%s", err) } - secondFrame, err := ReadFrame(f) + firstFrame, _, err := ReadNextFrame(f, header) if err != nil { t.Errorf("ReadFrame failed: %s", err) } - if secondFrame.ID != "TDRC" { - t.Errorf("ReadFrame failed: expected ID %s; got %s", "TDRC", secondFrame.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) + if firstFrame.Header.ID != "TRCK" { + t.Errorf("GetFrame failed: expected ID %s; got %s", + "TRCK", firstFrame.Header.ID) } -} -func TestGetFrame(t *testing.T) { - f, err := os.Open(filepath.Join(TESTDATAPATH, "testreadv2.mp3")) - if err != nil { - t.Errorf("%s", err) + if firstFrame.Header.Flags.Encrypted != false { + t.Errorf("ReadFrame failed: expected compressed flag to be %v; got %v", + false, firstFrame.Header.Flags.Encrypted) } - frames, err := GetFrames(f) + secondFrame, _, err := ReadNextFrame(f, header) if err != nil { - t.Errorf("GetFrames failed: %s", err) + t.Errorf("ReadFrame failed: %s", err) } - frame := GetFrame("TIT2", frames) - if frame.ID == "" { - t.Errorf("GetFrame failed: expected to find %s; got nothing", "TIT1") + if secondFrame.Header.ID != "TDRC" { + t.Errorf("ReadFrame failed: expected ID %s; got %s", + "TDRC", secondFrame.Header.ID) } - if util.ToString(frame.Contents) != "title" { - t.Errorf("GetFrame failed: expected contents to be %s; got %s", "title", util.ToString(frame.Contents)) + if util.ToStringLossy(secondFrame.Contents) != "2006" { + 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) +// } +// } diff --git a/v2/header.go b/v2/header.go index 7217b02..c8feb34 100644 --- a/v2/header.go +++ b/v2/header.go @@ -23,62 +23,57 @@ type Header struct { 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 -func GetHeader(rs io.ReadSeeker) (Header, error) { - var header Header - - rs.Seek(0, io.SeekStart) +func ReadHeader(rs io.ReadSeeker) (Header, error) { + _, err := rs.Seek(0, io.SeekStart) + if err != nil { + 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 { - 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) { return Header{}, fmt.Errorf("no ID3v2 identifier found") } header.Identifier = string(identifier) // version - VersionBytes, err := util.Read(rs, 2) - if err != nil { - return Header{}, err - } - - majorVersion, err := util.ByteToInt(VersionBytes[0]) + majorVersion, err := util.ByteToInt(hBytes[3]) if err != nil { return Header{}, err } - revisionNumber, err := util.ByteToInt(VersionBytes[1]) + revisionNumber, err := util.ByteToInt(hBytes[4]) if err != nil { return Header{}, err } - var version string switch majorVersion { case 2: - version = V2_2 + header.Version = V2_2 case 3: - version = V2_3 + header.Version = V2_3 case 4: - version = V2_4 + header.Version = V2_4 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, err := util.Read(rs, 1) - if err != nil { - return Header{}, err - } + flags := hBytes[5] flagBits := fmt.Sprintf("%08b", flags) // 1 byte is 8 bits // v3.0 and v4.0 have different amount of flags - switch version { + switch header.Version { case V2_3: if flagBits[0] == 1 { header.Flags.Unsynchronisated = true @@ -122,10 +117,7 @@ func GetHeader(rs io.ReadSeeker) (Header, error) { } // size - sizeBytes, err := util.Read(rs, 4) - if err != nil { - return Header{}, err - } + sizeBytes := hBytes[6:] size, err := util.BytesToIntIgnoreFirstBit(sizeBytes) if err != nil { diff --git a/v2/header_test.go b/v2/header_test.go index 4aa788c..e35fe59 100644 --- a/v2/header_test.go +++ b/v2/header_test.go @@ -14,7 +14,7 @@ func TestGetHeader(t *testing.T) { t.Errorf("%s", err) } - header, err := GetHeader(f) + header, err := ReadHeader(f) if err != nil { t.Errorf("GetHeader failed: %s", err) } diff --git a/v2/read.go b/v2/read.go new file mode 100644 index 0000000..5673d1f --- /dev/null +++ b/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 +} diff --git a/v2/read_test.go b/v2/read_test.go new file mode 100644 index 0000000..f880b4c --- /dev/null +++ b/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) + } +} diff --git a/v2/v2tag.go b/v2/v2tag.go index de8fc43..c859d7d 100644 --- a/v2/v2tag.go +++ b/v2/v2tag.go @@ -2,5 +2,5 @@ package v2 type ID3v2Tag struct { Header Header - Frames []Frame + Frames map[string]Frame }