Browse Source

TAG+ writing support, improved header ?reliability?, v2 writing progress

main
Unbewohnte 3 years ago
parent
commit
6c8109c20e
  1. 30
      README.md
  2. 8
      id3ed_test.go
  3. BIN
      testData/testreadv1.mp3
  4. BIN
      testData/testwritev1.mp3
  5. BIN
      testData/testwritev2.mp3
  6. 8
      v1/constants.go
  7. 52
      v1/read.go
  8. 49
      v1/read_test.go
  9. 19
      v1/tag.go
  10. 90
      v1/v1_test.go
  11. 138
      v1/write.go
  12. 24
      v1/write_test.go
  13. 4
      v2/frame_test.go
  14. 180
      v2/header.go
  15. 24
      v2/header_test.go
  16. 8
      v2/read.go
  17. 12
      v2/v2tag.go
  18. 115
      v2/write.go
  19. 20
      v2/write_test.go

30
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)
[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 !

8
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) {

BIN
testData/testreadv1.mp3

Binary file not shown.

BIN
testData/testwritev1.mp3

Binary file not shown.

BIN
testData/testwritev2.mp3

Binary file not shown.

8
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"

52
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
}

49
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)
}
}

19
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

90
v1/v1_test.go

@ -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)
}
}

138
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
}

24
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)
}
}

4
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)
}

180
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)
}
}

24
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]))
}
}

8
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)

12
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

115
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
// }

20
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)
// }
// }

Loading…
Cancel
Save