From 6c8109c20e44a251cfe85d889ac6952139fb0e73 Mon Sep 17 00:00:00 2001 From: Unbewohnte Date: Thu, 22 Jul 2021 11:56:22 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20TAG+=20writing=20support,=20improve?= =?UTF-8?q?d=20header=20=3Freliability=3F,=20v2=20writing=20progress=20?= =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 30 +++++-- id3ed_test.go | 8 ++ testData/testreadv1.mp3 | Bin 165 -> 594 bytes testData/testwritev1.mp3 | Bin 167 -> 844 bytes testData/testwritev2.mp3 | Bin 1144 -> 0 bytes v1/constants.go | 8 +- v1/read.go | 52 ++++++----- v1/read_test.go | 49 +++++++++++ v1/tag.go | 19 +++-- v1/v1_test.go | 90 -------------------- v1/write.go | 138 ++++++++++++++++++++++-------- v1/write_test.go | 24 ++++++ v2/frame_test.go | 4 +- v2/header.go | 180 ++++++++++++++++++++++----------------- v2/header_test.go | 24 +++--- v2/read.go | 8 +- v2/v2tag.go | 12 +-- v2/write.go | 115 +++++++++++++++++++++---- v2/write_test.go | 20 +---- 19 files changed, 478 insertions(+), 303 deletions(-) create mode 100644 v1/read_test.go delete mode 100644 v1/v1_test.go create mode 100644 v1/write_test.go diff --git a/README.md b/README.md index ab02d9d..add7d32 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,23 @@ ## ⚬ Library for encoding/decoding ID3 tags --- - -# Under construction ! -# Project status +# Status of the package + +**ID3v1**. can: + +- read +- write + +**ID3v1 Enhanced**. can: + +- read +- write + +**ID3v2**. can: + +- read -Right now it`s capable of reading and writing ID3v1 and ID3v1.1 tags, -reading ID3v2. ID3v2 writing support is still not implemented. --- @@ -198,4 +208,12 @@ to test a specific package # ∙ License -[MIT LICENSE](https://github.com/Unbewohnte/id3ed/blob/main/LICENSE) \ No newline at end of file +[MIT LICENSE](https://github.com/Unbewohnte/id3ed/blob/main/LICENSE) + +# ∙ Note + +This is **NOT** a fully tested and it is **NOT** a flawlessly working and edge-cases-covered package. + +I work on it alone and I am **NOT** a professional who knows what he does. + +Please, use with caution ! \ No newline at end of file diff --git a/id3ed_test.go b/id3ed_test.go index f97be9b..40e04cb 100644 --- a/id3ed_test.go +++ b/id3ed_test.go @@ -5,6 +5,7 @@ import ( "testing" v1 "github.com/Unbewohnte/id3ed/v1" + v2 "github.com/Unbewohnte/id3ed/v2" ) var TESTDATAPATH string = "testData" @@ -19,9 +20,16 @@ func TestOpen(t *testing.T) { t.Error("Open failed: expected testing file to not contain ID3v1") } + // if file.ID3v1Tag + if !file.ContainsID3v2 { t.Error("Open failed: expected testing file to contain ID3v2") } + + if file.ID3v2Tag.Header.Version() != v2.V2_4 { + t.Errorf("Open failed: id3v2tag: header: expected version to be %s; got %s", + v2.V2_4, file.ID3v2Tag.Header.Version()) + } } func TestWriteID3v1(t *testing.T) { diff --git a/testData/testreadv1.mp3 b/testData/testreadv1.mp3 index c70ca6464c5b7bf38d419dcb60341ffad0927d21..222bcc1724129a1d97436210d04f27fed3b52cfd 100644 GIT binary patch literal 594 zcmbu6u?~VT5QZ5^pTO~)b#h^@hM(BsOwjJ93PwyjCEO z+optlUga}wlWnq{>w6@A#)BD8(uI%r&Ig23h_x1zWfSru8BJGg!4bkTxUyKCWI2Cn zesOd!tEy6mT1AB)Kpy>Uo2Ds$NPi>$!_d&^==S}tKy|d0C+#lx{_A01ND_?etCdeD FO&$!+OAP=3 delta 9 Qcmcb_vXpUx%0&A{01_qxf&c&j diff --git a/testData/testwritev1.mp3 b/testData/testwritev1.mp3 index 064e8daeb1ece17c884abb9f08781edeacf30c65..477ef56042669dbb8d94db531746a2c4dca9dafb 100644 GIT binary patch literal 844 zcmXTU&rMZGNi0cJ$Ve?pY6xVr z194(eNoH|Lh@+1aR6$}+QfaQUzppQl&jZ8}5fKc@`MJ5Nc_ksv{(dl3d6~JXK=vpY M4S~@R7=a-G0QX=QQ~&?~ diff --git a/v1/constants.go b/v1/constants.go index 22d9612..a91f075 100644 --- a/v1/constants.go +++ b/v1/constants.go @@ -1,10 +1,10 @@ package v1 -const ID3v1IDENTIFIER string = "TAG" -const ID3v1SIZE int = 128 -const ID3v1INVALIDGENRE int = 255 +const IDENTIFIER string = "TAG" +const TAGSIZE int = 128 +const INVALIDGENRE int = 255 -const ID3v1ENHANCEDIDENTIFIER string = "TAG+" +const ENHANCEDIDENTIFIER string = "TAG+" const ENHANCEDSIZE int = 227 const V1_0 string = "ID3v1.0" diff --git a/v1/read.go b/v1/read.go index 61ac896..c4748f1 100644 --- a/v1/read.go +++ b/v1/read.go @@ -11,9 +11,28 @@ import ( var errDoesNotUseEnhancedID3v1 error = fmt.Errorf("does not use enhanced ID3v1 tag") +// Checks if rs contains a regular ID3v1 TAG +func containsTAG(rs io.ReadSeeker) bool { + _, err := rs.Seek(-int64(TAGSIZE), io.SeekEnd) + if err != nil { + return false + } + + identifier, err := util.Read(rs, 3) + if err != nil { + return false + } + + if string(identifier) != IDENTIFIER { + return false + } + + return true +} + // Checks if enhanced tag is used -func usesEnhancedTag(rs io.ReadSeeker) bool { - _, err := rs.Seek(-int64(ID3v1SIZE+ENHANCEDSIZE), io.SeekEnd) +func containsEnhancedTAG(rs io.ReadSeeker) bool { + _, err := rs.Seek(-int64(TAGSIZE+ENHANCEDSIZE), io.SeekEnd) if err != nil { return false } @@ -21,7 +40,7 @@ func usesEnhancedTag(rs io.ReadSeeker) bool { if err != nil { return false } - if !bytes.Equal(identifier, []byte(ID3v1ENHANCEDIDENTIFIER)) { + if !bytes.Equal(identifier, []byte(ENHANCEDIDENTIFIER)) { return false } @@ -30,15 +49,15 @@ func usesEnhancedTag(rs io.ReadSeeker) bool { // Tries to read enhanced ID3V1 tag from rs func readEnhancedTag(rs io.ReadSeeker) (EnhancedID3v1Tag, error) { - - if !usesEnhancedTag(rs) { + if !containsEnhancedTAG(rs) { + // rs does not contain enhanced TAG, there is nothing to read return EnhancedID3v1Tag{}, errDoesNotUseEnhancedID3v1 } var enhanced EnhancedID3v1Tag // set reader into the position - _, err := rs.Seek(-int64(ID3v1SIZE+ENHANCEDSIZE), io.SeekEnd) + _, err := rs.Seek(-int64(TAGSIZE+ENHANCEDSIZE), io.SeekEnd) if err != nil { return enhanced, fmt.Errorf("could not seek: %s", err) } @@ -111,26 +130,15 @@ func readEnhancedTag(rs io.ReadSeeker) (EnhancedID3v1Tag, error) { func Readv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) { var tag ID3v1Tag - // check if uses enhanced tag - if usesEnhancedTag(rs) { + // check if need to read enhanced tag + if containsEnhancedTAG(rs) { enhanced, _ := readEnhancedTag(rs) + tag.HasEnhancedTag = true tag.EnhancedTag = enhanced } - // set reader to the last 128 bytes - _, err := rs.Seek(-int64(ID3v1SIZE), io.SeekEnd) - if err != nil { - return nil, fmt.Errorf("could not seek: %s", err) - } - - // ID - identifier, err := util.Read(rs, 3) - if err != nil { - return nil, err - } - - if !bytes.Equal(identifier, []byte(ID3v1IDENTIFIER)) { - // no identifier, given file does not use ID3v1 + if !containsTAG(rs) { + // no TAG to read return nil, ErrDoesNotUseID3v1 } diff --git a/v1/read_test.go b/v1/read_test.go new file mode 100644 index 0000000..5aa361d --- /dev/null +++ b/v1/read_test.go @@ -0,0 +1,49 @@ +package v1 + +import ( + "os" + "path/filepath" + "testing" +) + +var TESTv1TAG = &ID3v1Tag{ + SongName: "testsong", + Artist: "testartist", + Album: "testalbum", + Year: 727, + Comment: "testcomment", + Genre: "Blues", + HasEnhancedTag: true, + EnhancedTag: EnhancedID3v1Tag{ + Artist: "ARRRTIST", + Album: "ALLLLBUUUM", + SongName: "NAME", + }, +} + +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 := Readv1Tag(testfile) + if err != nil { + t.Errorf("GetID3v1Tag failed: %s", err) + } + + if tag.version != V1_1 { + t.Errorf("GetID3v1Tag failed: expected version to be %s; got %s", V1_1, tag.version) + } + + if tag.Comment != "testcomment" { + t.Errorf("GetID3v1Tag failed: expected comment to be %s; got %s", "testcomment", tag.Comment) + } + + if tag.Genre != id3v1genres[0] { + t.Errorf("GetID3v1Tag failed: expected genre to be %s; got %s", id3v1genres[0], tag.Genre) + } + + if tag.Track != 8 { + t.Errorf("GetID3v1Tag failed: expected track number to be %d; got %d", 8, tag.Track) + } +} diff --git a/v1/tag.go b/v1/tag.go index e6179b0..39ec676 100644 --- a/v1/tag.go +++ b/v1/tag.go @@ -3,15 +3,16 @@ package v1 // https://id3.org/ID3v1 - documentation type ID3v1Tag struct { - version string - SongName string - Artist string - Album string - Year int - Comment string - Track uint8 // basically a byte, but converted to int for convenience - Genre string - EnhancedTag EnhancedID3v1Tag + version string + SongName string + Artist string + Album string + Year int + Comment string + Track uint8 // basically a byte, but converted to int for convenience + Genre string + HasEnhancedTag bool + EnhancedTag EnhancedID3v1Tag } // from https://en.wikipedia.org/wiki/ID3 diff --git a/v1/v1_test.go b/v1/v1_test.go deleted file mode 100644 index c9317a1..0000000 --- a/v1/v1_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package v1 - -import ( - "os" - "path/filepath" - "testing" -) - -var TESTDATAPATH string = filepath.Join("..", "testData") - -var TESTv1TAG = &ID3v1Tag{ - SongName: "testsong", - Artist: "testartist", - Album: "testalbum", - Year: 727, - Comment: "testcomment", - Genre: "Blues", -} - -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 := Readv1Tag(testfile) - if err != nil { - t.Errorf("GetID3v1Tag failed: %s", err) - } - - if tag.version != V1_1 { - t.Errorf("GetID3v1Tag failed: expected version to be %s; got %s", V1_1, tag.version) - } - - if tag.Comment != "Comment here " { - t.Errorf("GetID3v1Tag failed: expected comment to be %s; got %s", "Comment here ", tag.Comment) - } - - if tag.Genre != "Soundtrack" { - t.Errorf("GetID3v1Tag failed: expected genre to be %s; got %s", "Soundtrack", tag.Genre) - } - - if tag.Track != 8 { - t.Errorf("GetID3v1Tag failed: expected track number to be %d; got %d", 8, tag.Track) - } -} - -// func TestWritev1Tags(t *testing.T) { -// f, err := os.OpenFile(filepath.Join(TESTDATAPATH, "testwritev1.mp3"), os.O_CREATE|os.O_RDWR, os.ModePerm) -// if err != nil { -// t.Errorf("%s", err) -// } -// defer f.Close() - -// tag := TESTv1TAG - -// // writing a tag -// err = tag.write(f) -// if err != nil { -// t.Errorf("WriteID3v1Tag failed: %s", err) -// } - -// // reading a tag -// readTag, err := Readv1Tag(f) -// if err != nil { -// t.Errorf("%s", err) -// } - -// if readTag.Album != "testalbum" { -// t.Errorf("WriteID3v1Tag failed: expected %s; got %s", "testalbum", readTag.Album) -// } - -// if readTag.Year != 727 { -// t.Errorf("WriteID3v1Tag failed: expected %d; got %d", 727, readTag.Year) -// } -// } - -func TestWriteID3v1ToFile(t *testing.T) { - f, err := os.OpenFile(filepath.Join(TESTDATAPATH, "testwritev1.mp3"), os.O_CREATE|os.O_RDWR, os.ModePerm) - if err != nil { - t.Errorf("%s", err) - } - - tag := TESTv1TAG - - err = tag.WriteToFile(f) - if err != nil { - t.Errorf("WriteID3v1ToFile failed: %s", err) - } - -} diff --git a/v1/write.go b/v1/write.go index d04513f..a6d0e44 100644 --- a/v1/write.go +++ b/v1/write.go @@ -1,7 +1,6 @@ package v1 import ( - "bytes" "encoding/binary" "fmt" "io" @@ -19,34 +18,86 @@ func (tag *ID3v1Tag) write(dst io.WriteSeeker) error { return fmt.Errorf("could not seek: %s", err) } + // write enhanced, if uses one + if tag.HasEnhancedTag { + // IDentifier + err = util.WriteToExtent(dst, []byte(ENHANCEDIDENTIFIER), 4) + if err != nil { + return err + } + + // Songname + err = util.WriteToExtent(dst, []byte(tag.EnhancedTag.SongName), 60) + if err != nil { + return err + } + + // Artist + err = util.WriteToExtent(dst, []byte(tag.EnhancedTag.Artist), 60) + if err != nil { + return err + } + + // Album + err = util.WriteToExtent(dst, []byte(tag.EnhancedTag.Album), 60) + if err != nil { + return err + } + + // Speed + _, err = dst.Write([]byte(tag.EnhancedTag.Speed)) + if err != nil { + return err + } + + // Genre + err = util.WriteToExtent(dst, []byte(tag.EnhancedTag.Genre), 30) + if err != nil { + return err + } + + // Time + err = util.WriteToExtent(dst, []byte(tag.EnhancedTag.StartTime), 6) + if err != nil { + return err + } + + err = util.WriteToExtent(dst, []byte(tag.EnhancedTag.EndTime), 6) + if err != nil { + return err + } + } + + // write a regular ID3v1 + // ID - _, err = dst.Write([]byte(ID3v1IDENTIFIER)) + _, err = dst.Write([]byte(IDENTIFIER)) if err != nil { - return fmt.Errorf("could not write to writer: %s", err) + return err } // Song name err = util.WriteToExtent(dst, []byte(tag.SongName), 30) if err != nil { - return fmt.Errorf("could not write to writer: %s", err) + return err } // Artist err = util.WriteToExtent(dst, []byte(tag.Artist), 30) if err != nil { - return fmt.Errorf("could not write to writer: %s", err) + return err } // Album err = util.WriteToExtent(dst, []byte(tag.Album), 30) if err != nil { - return fmt.Errorf("could not write to writer: %s", err) + return err } // Year err = util.WriteToExtent(dst, []byte(fmt.Sprint(tag.Year)), 4) if err != nil { - return fmt.Errorf("could not write to writer: %s", err) + return err } // Comment and Track @@ -57,13 +108,13 @@ func (tag *ID3v1Tag) write(dst io.WriteSeeker) error { // write only 30 bytes long comment without track err = util.WriteToExtent(dst, []byte(tag.Comment), 30) if err != nil { - return fmt.Errorf("could not write to writer: %s", err) + return err } } else { // write 28 bytes long shrinked comment err = util.WriteToExtent(dst, []byte(tag.Comment), 28) if err != nil { - return fmt.Errorf("could not write to writer: %s", err) + return err } // write 0 byte as padding @@ -82,8 +133,8 @@ func (tag *ID3v1Tag) write(dst io.WriteSeeker) error { // Genre genreCode := util.GetKey(id3v1genres, tag.Genre) if genreCode == -1 { - // if no genre found - encode genre code as 255 - genreCode = ID3v1INVALIDGENRE + // if no genre found - set genre code to 255 + genreCode = INVALIDGENRE } genrebyte := make([]byte, 1) binary.PutVarint(genrebyte, int64(genreCode)) @@ -100,40 +151,59 @@ func (tag *ID3v1Tag) write(dst io.WriteSeeker) error { func (tag *ID3v1Tag) WriteToFile(f *os.File) error { defer f.Close() - // check for existing ID3v1 tag - f.Seek(-int64(ID3v1SIZE), io.SeekEnd) - - identifier, err := util.Read(f, 3) + fStats, err := f.Stat() if err != nil { - return err + return fmt.Errorf("cannot get file stats: %s", err) } - if !bytes.Equal(identifier, []byte(ID3v1IDENTIFIER)) { - // no existing identifier, just write given tag + filesize := fStats.Size() + + // process all possible scenarios + switch { + + case containsEnhancedTAG(f) && containsTAG(f): + // remove both + err = f.Truncate(filesize - int64(TAGSIZE) - int64(ENHANCEDSIZE)) + if err != nil { + return fmt.Errorf("could not truncate file %s", err) + } + // write the new one err = tag.write(f) if err != nil { return err } - return nil - } - // does contain ID3v1 tag. Removing it - fStats, err := f.Stat() - if err != nil { - return fmt.Errorf("cannot get file stats: %s", err) - } + case containsEnhancedTAG(f) && !containsTAG(f): + // remove enhanced tag, replace with new + err = f.Truncate(filesize - int64(ENHANCEDSIZE)) + if err != nil { + return fmt.Errorf("could not truncate file %s", err) + } - err = f.Truncate(fStats.Size() - int64(ID3v1SIZE)) - if err != nil { - return fmt.Errorf("could not truncate file %s", err) - } + err = tag.write(f) + if err != nil { + return err + } - // writing a new tag - err = tag.write(f) - if err != nil { - return fmt.Errorf("could not write to writer: %s", err) + case !containsEnhancedTAG(f) && containsTAG(f): + // remove regular one, replace with new + err = f.Truncate(filesize - int64(TAGSIZE)) + if err != nil { + return fmt.Errorf("could not truncate file %s", err) + } + + err = tag.write(f) + if err != nil { + return err + } + + case !containsEnhancedTAG(f) && !containsTAG(f): + // no existing TAGs, simply write what we have + err := tag.write(f) + if err != nil { + return err + } } return nil - } diff --git a/v1/write_test.go b/v1/write_test.go new file mode 100644 index 0000000..8e6c901 --- /dev/null +++ b/v1/write_test.go @@ -0,0 +1,24 @@ +package v1 + +import ( + "os" + "path/filepath" + "testing" +) + +var TESTDATAPATH string = filepath.Join("..", "testData") + +func TestWriteID3v1ToFile(t *testing.T) { + f, err := os.OpenFile(filepath.Join(TESTDATAPATH, "testwritev1.mp3"), os.O_CREATE|os.O_RDWR, os.ModePerm) + if err != nil { + t.Errorf("%s", err) + } + + tag := TESTv1TAG + + err = tag.WriteToFile(f) + if err != nil { + t.Errorf("WriteID3v1ToFile failed: %s", err) + } + +} diff --git a/v2/frame_test.go b/v2/frame_test.go index 0cb65d7..6ba9fd2 100644 --- a/v2/frame_test.go +++ b/v2/frame_test.go @@ -19,7 +19,7 @@ func TestReadNextFrame(t *testing.T) { t.Errorf("%s", err) } - firstFrame, err := readNextFrame(f, header.Version) + firstFrame, err := readNextFrame(f, header.Version()) if err != nil { t.Errorf("ReadFrame failed: %s", err) } @@ -34,7 +34,7 @@ func TestReadNextFrame(t *testing.T) { false, firstFrame.Header.Flags.Encrypted) } - secondFrame, err := readNextFrame(f, header.Version) + secondFrame, err := readNextFrame(f, header.Version()) if err != nil { t.Errorf("ReadFrame failed: %s", err) } diff --git a/v2/header.go b/v2/header.go index 5cb0e69..9c19171 100644 --- a/v2/header.go +++ b/v2/header.go @@ -19,14 +19,13 @@ type HeaderFlags struct { // ID3v2.x`s main header structure type Header struct { - // Identifier string - Flags HeaderFlags - Version string - Size uint32 - ExtendedHeader ExtendedHeader + flags HeaderFlags + version string + size uint32 + extendedHeader ExtendedHeader } -// extended header`s flags +// Extended header`s flags type ExtendedHeaderFlags struct { UpdateTag bool CRCpresent bool @@ -35,18 +34,41 @@ type ExtendedHeaderFlags struct { } type ExtendedHeader struct { - Size uint32 - Flags ExtendedHeaderFlags - PaddingSize uint32 - CRCdata []byte + size uint32 + flags ExtendedHeaderFlags + paddingSize uint32 + crcData []byte +} + +// using ONLY getters on header, because +// header MUST NOT be changed manually +// from the outside of the package + +func (h *Header) Version() string { + return h.version +} + +func (h *Header) Flags() HeaderFlags { + return h.flags +} + +func (h *Header) Size() uint32 { + return h.size +} + +func (h *Header) ExtendedHeader() *ExtendedHeader { + if h.flags.HasExtendedHeader { + return &h.extendedHeader + } + return nil } // Reads and structuralises extended header. Must // be called AFTER the main header has beeen read (does not seek). // ALSO ISN`T TESTED !!! func (h *Header) readExtendedHeader(r io.Reader) error { - h.ExtendedHeader = ExtendedHeader{} - if !h.Flags.HasExtendedHeader { + // h.ExtendedHeader = ExtendedHeader{} + if !h.Flags().HasExtendedHeader { return nil } @@ -58,24 +80,24 @@ func (h *Header) readExtendedHeader(r io.Reader) error { return fmt.Errorf("could not read from reader: %s", err) } - switch h.Version { + switch h.Version() { case V2_3: - extended.Size = util.BytesToInt(extendedSize) + extended.size = util.BytesToInt(extendedSize) case V2_4: - extended.Size = util.BytesToIntSynchsafe(extendedSize) + extended.size = util.BytesToIntSynchsafe(extendedSize) } // extended flags - switch h.Version { + switch h.Version() { case V2_3: extendedFlag, err := util.Read(r, 2) // reading flag byte and a null-byte after if err != nil { return fmt.Errorf("could not read from reader: %s", err) } if util.GetBit(extendedFlag[0], 1) { - extended.Flags.CRCpresent = true + extended.flags.CRCpresent = true } else { - extended.Flags.CRCpresent = false + extended.flags.CRCpresent = false } case V2_4: @@ -90,39 +112,39 @@ func (h *Header) readExtendedHeader(r io.Reader) error { } if util.GetBit(flagByte[0], 2) { - extended.Flags.UpdateTag = true + extended.flags.UpdateTag = true } else { - extended.Flags.UpdateTag = false + extended.flags.UpdateTag = false } if util.GetBit(flagByte[0], 3) { - extended.Flags.CRCpresent = true + extended.flags.CRCpresent = true } else { - extended.Flags.CRCpresent = false + extended.flags.CRCpresent = false } if util.GetBit(flagByte[0], 4) { - extended.Flags.HasRestrictions = true + extended.flags.HasRestrictions = true } else { - extended.Flags.HasRestrictions = false + extended.flags.HasRestrictions = false } } // extracting data given by flags - switch h.Version { + switch h.Version() { case V2_3: - if extended.Flags.CRCpresent { + if extended.flags.CRCpresent { crcData, err := util.Read(r, 4) if err != nil { return fmt.Errorf("could not read from reader: %s", err) } - extended.CRCdata = crcData + extended.crcData = crcData } case V2_4: // `Each flag that is set in the extended header has data attached` - if extended.Flags.UpdateTag { + if extended.flags.UpdateTag { // skipping null-byte length of `UpdateTag` _, err := util.Read(r, 1) if err != nil { @@ -130,7 +152,7 @@ func (h *Header) readExtendedHeader(r io.Reader) error { } } - if extended.Flags.CRCpresent { + if extended.flags.CRCpresent { crclen, err := util.Read(r, 1) if err != nil { return fmt.Errorf("could not read from reader: %s", err) @@ -139,10 +161,10 @@ func (h *Header) readExtendedHeader(r io.Reader) error { if err != nil { return fmt.Errorf("could not read from reader: %s", err) } - extended.CRCdata = crcData + extended.crcData = crcData } - if extended.Flags.HasRestrictions { + if extended.flags.HasRestrictions { // skipping one-byte length of `Restrictions`, because it`s always `1` _, err := util.Read(r, 1) if err != nil { @@ -154,24 +176,24 @@ func (h *Header) readExtendedHeader(r io.Reader) error { return fmt.Errorf("could not read from reader: %s", err) } // a `lazy` approach :), just for now - extended.Flags.Restrictions = restrictionsByte[0] + extended.flags.Restrictions = restrictionsByte[0] } } // extracting other version-dependent header data // padding if V2_3 - if h.Version == V2_3 { + if h.Version() == V2_3 { paddingSizeBytes, err := util.Read(r, 4) if err != nil { return fmt.Errorf("could not read from reader: %s", err) } paddingSize := util.BytesToInt(paddingSizeBytes) - extended.PaddingSize = paddingSize + extended.paddingSize = paddingSize } // finally `attaching` parsed extended header to the main *Header - h.ExtendedHeader = extended + h.extendedHeader = extended return nil } @@ -204,11 +226,11 @@ func readHeader(rs io.ReadSeeker) (Header, error) { switch majorVersion { case 2: - header.Version = V2_2 + header.version = V2_2 case 3: - header.Version = V2_3 + header.version = V2_3 case 4: - header.Version = V2_4 + header.version = V2_4 default: return Header{}, fmt.Errorf("ID3v2.%d.%d is not supported or invalid", majorVersion, revisionNumber) } @@ -217,57 +239,57 @@ func readHeader(rs io.ReadSeeker) (Header, error) { flags := hBytes[5] // v3.0 and v4.0 have different amount of flags - switch header.Version { + switch header.Version() { case V2_2: if util.GetBit(flags, 1) { - header.Flags.Unsynchronised = true + header.flags.Unsynchronised = true } else { - header.Flags.Unsynchronised = false + header.flags.Unsynchronised = false } if util.GetBit(flags, 2) { - header.Flags.Compressed = true + header.flags.Compressed = true } else { - header.Flags.Compressed = false + header.flags.Compressed = false } case V2_3: if util.GetBit(flags, 1) { - header.Flags.Unsynchronised = true + header.flags.Unsynchronised = true } else { - header.Flags.Unsynchronised = false + header.flags.Unsynchronised = false } if util.GetBit(flags, 2) { - header.Flags.HasExtendedHeader = true + header.flags.HasExtendedHeader = true } else { - header.Flags.HasExtendedHeader = false + header.flags.HasExtendedHeader = false } if util.GetBit(flags, 3) { - header.Flags.Experimental = true + header.flags.Experimental = true } else { - header.Flags.Experimental = false + header.flags.Experimental = false } // always false, because ID3v2.3.0 does not support footers - header.Flags.FooterPresent = false + header.flags.FooterPresent = false case V2_4: if util.GetBit(flags, 1) { - header.Flags.Unsynchronised = true + header.flags.Unsynchronised = true } else { - header.Flags.Unsynchronised = false + header.flags.Unsynchronised = false } if util.GetBit(flags, 2) { - header.Flags.HasExtendedHeader = true + header.flags.HasExtendedHeader = true } else { - header.Flags.HasExtendedHeader = false + header.flags.HasExtendedHeader = false } if util.GetBit(flags, 3) { - header.Flags.Experimental = true + header.flags.Experimental = true } else { - header.Flags.Experimental = false + header.flags.Experimental = false } if util.GetBit(flags, 4) { - header.Flags.FooterPresent = true + header.flags.FooterPresent = true } else { - header.Flags.FooterPresent = false + header.flags.FooterPresent = false } } @@ -276,9 +298,9 @@ func readHeader(rs io.ReadSeeker) (Header, error) { size := util.BytesToIntSynchsafe(sizeBytes) - header.Size = size + header.size = size - if header.Flags.HasExtendedHeader { + if header.flags.HasExtendedHeader { err = header.readExtendedHeader(rs) if err != nil { return header, err @@ -339,7 +361,7 @@ func (h *Header) toBytes() []byte { // version version := []byte{0, 0} - switch h.Version { + switch h.Version() { case V2_2: version = []byte{2, 0} case V2_3: @@ -350,44 +372,44 @@ func (h *Header) toBytes() []byte { buff.Write(version) // flags - flagByte := headerFlagsToByte(h.Flags, h.Version) + flagByte := headerFlagsToByte(h.flags, h.version) buff.WriteByte(flagByte) // size - tagSize := util.IntToBytesSynchsafe(h.Size) + tagSize := util.IntToBytesSynchsafe(h.size) buff.Write(tagSize) // extended header - if !h.Flags.HasExtendedHeader { + if !h.flags.HasExtendedHeader { return buff.Bytes() } // double check for possible errors - if h.Version == V2_2 { + if h.Version() == V2_2 { return buff.Bytes() } // size - extSize := util.IntToBytes(h.ExtendedHeader.Size) + extSize := util.IntToBytes(h.extendedHeader.size) buff.Write(extSize) // flags and other version specific fields - switch h.Version { + switch h.Version() { case V2_3: // flags flagBytes := []byte{0, 0} - if h.ExtendedHeader.Flags.CRCpresent { + if h.extendedHeader.flags.CRCpresent { flagBytes[0] = util.SetBit(flagBytes[0], 8) } buff.Write(flagBytes) // crc data - if h.ExtendedHeader.Flags.CRCpresent { - buff.Write(h.ExtendedHeader.CRCdata) + if h.extendedHeader.flags.CRCpresent { + buff.Write(h.extendedHeader.crcData) } // padding size - paddingSize := util.IntToBytes(h.ExtendedHeader.PaddingSize) + paddingSize := util.IntToBytes(h.extendedHeader.paddingSize) buff.Write(paddingSize) case V2_4: @@ -395,36 +417,36 @@ func (h *Header) toBytes() []byte { buff.WriteByte(numberOfFlagBytes) extFlags := byte(0) - if h.ExtendedHeader.Flags.UpdateTag { + if h.extendedHeader.flags.UpdateTag { extFlags = util.SetBit(extFlags, 7) } - if h.ExtendedHeader.Flags.CRCpresent { + if h.extendedHeader.flags.CRCpresent { extFlags = util.SetBit(extFlags, 6) } - if h.ExtendedHeader.Flags.HasRestrictions { + if h.extendedHeader.flags.HasRestrictions { extFlags = util.SetBit(extFlags, 5) } buff.WriteByte(extFlags) // writing data, provided by flags - if h.ExtendedHeader.Flags.UpdateTag { + if h.extendedHeader.flags.UpdateTag { // data len buff.WriteByte(0) } - if h.ExtendedHeader.Flags.CRCpresent { + if h.extendedHeader.flags.CRCpresent { // data len buff.WriteByte(5) // data - buff.Write(h.ExtendedHeader.CRCdata) + buff.Write(h.extendedHeader.crcData) } - if h.ExtendedHeader.Flags.HasRestrictions { + if h.extendedHeader.flags.HasRestrictions { // data len buff.WriteByte(1) // data - buff.WriteByte(h.ExtendedHeader.Flags.Restrictions) + buff.WriteByte(h.extendedHeader.flags.Restrictions) } } diff --git a/v2/header_test.go b/v2/header_test.go index a18966a..36cc0c1 100644 --- a/v2/header_test.go +++ b/v2/header_test.go @@ -21,16 +21,16 @@ func TestReadHeader(t *testing.T) { t.Errorf("GetHeader failed: %s", err) } - if header.Flags.HasExtendedHeader != false { - t.Errorf("GetHeader failed: expected flag %v; got %v", false, header.Flags.HasExtendedHeader) + if header.Flags().HasExtendedHeader != false { + t.Errorf("GetHeader failed: expected flag %v; got %v", false, header.Flags().HasExtendedHeader) } - if header.Flags.Unsynchronised != false { - t.Errorf("GetHeader failed: expected flag %v; got %v", false, header.Flags.Unsynchronised) + if header.Flags().Unsynchronised != false { + t.Errorf("GetHeader failed: expected flag %v; got %v", false, header.Flags().Unsynchronised) } - if header.Size != 1138 { - t.Errorf("GetHeader failed: expected size %v; got %v", 1138, header.Size) + if header.Size() != 1138 { + t.Errorf("GetHeader failed: expected size %v; got %v", 1138, header.Size()) } } @@ -52,10 +52,10 @@ func TestHeaderFlagsToByte(t *testing.T) { func TestHeaderToBytes(t *testing.T) { testHeader := Header{ - Version: V2_4, - Flags: HeaderFlags{}, // all false - Size: 12345, - ExtendedHeader: ExtendedHeader{}, + version: V2_4, + flags: HeaderFlags{}, // all false + size: 12345, + extendedHeader: ExtendedHeader{}, } hBytes := testHeader.toBytes() @@ -71,8 +71,8 @@ func TestHeaderToBytes(t *testing.T) { t.Errorf("expected to get %s, got %s", HEADERIDENTIFIER, string(hBytes[0:3])) } - if util.BytesToIntSynchsafe(hBytes[6:10]) != testHeader.Size { + if util.BytesToIntSynchsafe(hBytes[6:10]) != testHeader.Size() { t.Errorf("toBytes failed: expected size to be %d; got %d", - testHeader.Size, util.BytesToIntSynchsafe(hBytes[7:10])) + testHeader.Size(), util.BytesToIntSynchsafe(hBytes[7:10])) } } diff --git a/v2/read.go b/v2/read.go index c75d3bd..7df9465 100644 --- a/v2/read.go +++ b/v2/read.go @@ -18,15 +18,15 @@ func ReadV2Tag(rs io.ReadSeeker) (*ID3v2Tag, error) { var read uint64 = 0 var frames []Frame for { - if read == uint64(header.Size) { + if read == uint64(header.Size()) { break - } else if read > uint64(header.Size) { + } else if read > uint64(header.Size()) { // read more than required, but did not // encouter padding, something is wrong here return nil, ErrReadMoreThanSize } - frame, err := readNextFrame(rs, header.Version) + frame, err := readNextFrame(rs, header.Version()) switch err { case nil: case ErrGotPadding: @@ -49,7 +49,7 @@ func ReadV2Tag(rs io.ReadSeeker) (*ID3v2Tag, error) { frames = append(frames, frame) // counting how many bytes read - if header.Version == V2_2 { + if header.Version() == V2_2 { read += uint64(V2_2FrameHeaderSize) + uint64(frame.Header.Size) } else { read += uint64(V2_3FrameHeaderSize) + uint64(frame.Header.Size) diff --git a/v2/v2tag.go b/v2/v2tag.go index 56396da..3bae1ee 100644 --- a/v2/v2tag.go +++ b/v2/v2tag.go @@ -30,7 +30,7 @@ func (tag *ID3v2Tag) FrameExists(id string) bool { // Returns the contents for the title frame func (tag *ID3v2Tag) Title() string { - switch tag.Header.Version { + switch tag.Header.Version() { case V2_2: if !tag.FrameExists("TT2") { return "" @@ -46,7 +46,7 @@ func (tag *ID3v2Tag) Title() string { // Returns the contents for the album frame func (tag *ID3v2Tag) Album() string { - switch tag.Header.Version { + switch tag.Header.Version() { case V2_2: if !tag.FrameExists("TAL") { return "" @@ -62,7 +62,7 @@ func (tag *ID3v2Tag) Album() string { // Returns the contents for the artist frame func (tag *ID3v2Tag) Artist() string { - switch tag.Header.Version { + switch tag.Header.Version() { case V2_2: if !tag.FrameExists("TP1") { return "" @@ -78,7 +78,7 @@ func (tag *ID3v2Tag) Artist() string { // Returns the contents for the year frame func (tag *ID3v2Tag) Year() string { - switch tag.Header.Version { + switch tag.Header.Version() { case V2_2: if !tag.FrameExists("TYE") { return "" @@ -94,7 +94,7 @@ func (tag *ID3v2Tag) Year() string { // Returns the contents for the comment frame func (tag *ID3v2Tag) Comment() string { - switch tag.Header.Version { + switch tag.Header.Version() { case V2_2: if !tag.FrameExists("COM") { return "" @@ -110,7 +110,7 @@ func (tag *ID3v2Tag) Comment() string { // Returns raw bytes of embed picture func (tag *ID3v2Tag) Picture() []byte { - switch tag.Header.Version { + switch tag.Header.Version() { case V2_2: if !tag.FrameExists("PIC") { return nil diff --git a/v2/write.go b/v2/write.go index bc00202..4934680 100644 --- a/v2/write.go +++ b/v2/write.go @@ -1,28 +1,107 @@ package v2 -import ( - "fmt" - "io" -) - // Writes ID3v2Tag to ws -func (tag *ID3v2Tag) write(ws io.WriteSeeker) error { - _, err := ws.Seek(0, io.SeekStart) - if err != nil { - return fmt.Errorf("could not seek: %s", err) - } +// func (tag *ID3v2Tag) write(ws io.WriteSeeker) error { +// _, err := ws.Seek(0, io.SeekStart) +// if err != nil { +// return fmt.Errorf("could not seek: %s", err) +// } - // write header - ws.Write(tag.Header.toBytes()) +// // write header +// ws.Write(tag.Header.toBytes()) - // write frames - for _, frame := range tag.Frames { - ws.Write(frame.toBytes(tag.Header.Version)) - } +// // write frames +// for _, frame := range tag.Frames { +// ws.Write(frame.toBytes(tag.Header.Version())) +// } - return nil -} +// return nil +// } +// // Writes ID3v2Tag to file, removing already existing tag if found // func (tag *ID3v2Tag) WriteToFile(f *os.File) error { +// defer f.Close() + +// _, err := f.Seek(0, io.SeekStart) +// if err != nil { +// return fmt.Errorf("could not seek: %s", err) +// } + +// // check for existing tag +// possibleHeaderID, err := util.ReadToString(f, 3) +// if err != nil { +// return err +// } + +// if possibleHeaderID != HEADERIDENTIFIER { +// // No existing tag, just write what we have +// // and exit +// tag.write(f) + +// return nil +// } +// // there is an existing tag, remove it +// // and write a new one + +// // get size of the existing tag +// existingHeader, err := readHeader(f) +// if err != nil { +// return err +// } +// existingHSize := existingHeader.Size() + +// // cannot truncate just the existing tag with f.Truncate(), +// // so we need to improvise and have a temporary copy of the mp3, +// // wipe the original file, write our tag and place the actual +// // music without the old tag from the temporary copy. + +// // create a temporary file +// temporaryDir := os.TempDir() +// tmpF, err := os.CreateTemp(temporaryDir, fmt.Sprintf("%s_TEMP", filepath.Base(f.Name()))) +// if err != nil { +// return err +// } + +// defer tmpF.Close() +// // remove it afterwards +// defer os.Remove(filepath.Join(temporaryDir, tmpF.Name())) + +// tmpFStats, err := tmpF.Stat() +// if err != nil { +// return err +// } + +// // copy contents from the original mp3 to a temporary one +// _, err = io.Copy(tmpF, f) +// if err != nil { +// return err +// } + +// // fully remove contents from the original file +// err = f.Truncate(0) +// if err != nil { +// return err +// } + +// // write our tag +// tag.write(f) + +// // read all contents of the temporary file, except the existing tag +// tmpF.Seek(int64(existingHSize), io.SeekStart) + +// musicDataSize := uint64(tmpFStats.Size() - int64(existingHSize)) + +// musicData, err := util.Read(tmpF, musicDataSize) +// if err != nil { +// return err +// } + +// // and write them into the original file, which +// // contains only the new tag +// _, err = f.Write(musicData) +// if err != nil { +// return err +// } + // return nil // } diff --git a/v2/write_test.go b/v2/write_test.go index 30b86da..dd68092 100644 --- a/v2/write_test.go +++ b/v2/write_test.go @@ -19,23 +19,9 @@ package v2 // } // defer ff.Close() -// err = testTag.write(ff) +// // WRITING +// err = testTag.WriteToFile(ff) // if err != nil { -// t.Errorf("%s", err) -// } - -// wroteTag, err := ReadV2Tag(ff) -// if err != nil { -// t.Errorf("%s", err) -// } - -// // t.Errorf("ORIGINAL: %+v", testTag) -// // t.Errorf("WRITTEN: %+v", wroteTag) -// for _, origfr := range testTag.Frames { -// t.Errorf("ORIG Fr: %+v\n", origfr) -// } - -// for _, wrtfr := range wroteTag.Frames { -// t.Errorf("WRITTEN Fr: %+v\n", wrtfr) +// t.Errorf("WriteToFile failed: %s", err) // } // }