Browse Source

▩ Now easier to use ! ▩

main
Unbewohnte 3 years ago
parent
commit
135aad7d34
  1. 79
      id3ed.go
  2. 45
      id3ed_test.go
  3. BIN
      testData/testwritev1.mp3
  4. 8
      v1/read.go
  5. 2
      v1/tag.go
  6. 62
      v1/v1_test.go
  7. 7
      v1/write.go
  8. 26
      v2/frame.go
  9. 27
      v2/frame_test.go
  10. 6
      v2/header.go
  11. 4
      v2/header_test.go
  12. 13
      v2/read.go

79
id3ed.go

@ -1,11 +1,76 @@
package id3ed
// type File struct {
// ContainsV1 bool
// ContainsV2 bool
// ?
// }
import (
"fmt"
"os"
v1 "github.com/Unbewohnte/id3ed/v1"
v2 "github.com/Unbewohnte/id3ed/v2"
)
type File struct {
path string
ContainsID3v1 bool
ContainsID3v2 bool
ID3v1Tag *v1.ID3v1Tag
ID3v2Tag *v2.ID3v2Tag
}
// Opens file specified by `path`, reads all ID3 tags that
// it can find and returns a *File
func Open(path string) (*File, error) {
fhandler, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("could not open: %s", err)
}
defer fhandler.Close()
var file File
file.path = path // saving path to the file for future writing
v1tag, err := v1.Readv1Tag(fhandler)
switch err {
case nil:
file.ContainsID3v1 = true
case v1.ErrDoesNotUseID3v1:
file.ContainsID3v1 = false
default:
return nil, fmt.Errorf("could not read ID3v1 tag from file: %s", err)
}
file.ID3v1Tag = v1tag
v2tag, err := v2.ReadV2Tag(fhandler)
switch err {
case nil:
file.ContainsID3v2 = true
case v2.ErrDoesNotUseID3v2:
file.ContainsID3v2 = false
default:
return nil, fmt.Errorf("could not read ID3v2 tag from file: %s", err)
}
file.ID3v2Tag = v2tag
return &file, nil
}
// Writes given ID3v1 tag to file
func (f *File) WriteID3v1(tag *v1.ID3v1Tag) error {
fhandler, err := os.OpenFile(f.path, os.O_RDWR, os.ModePerm)
if err != nil {
return fmt.Errorf("could not read a file: %s", err)
}
defer fhandler.Close()
err = tag.WriteToFile(fhandler)
if err != nil {
return fmt.Errorf("could not write ID3v1 to file: %s", err)
}
return nil
}
// func Open(path string) (*File, error) {
// return &File{}, nil
// still not implemented
// func (f *File) WriteID3v2(tag *v2.ID3v2Tag) error {
// return nil
// }

45
id3ed_test.go

@ -0,0 +1,45 @@
package id3ed
import (
"path/filepath"
"testing"
v1 "github.com/Unbewohnte/id3ed/v1"
)
var TESTDATAPATH string = "testData"
func TestOpen(t *testing.T) {
file, err := Open(filepath.Join(TESTDATAPATH, "testreadv2.mp3"))
if err != nil {
t.Errorf("Open failed: %s", err)
}
if file.ContainsID3v1 {
t.Error("Open failed: expected testing file to not contain ID3v1")
}
if !file.ContainsID3v2 {
t.Error("Open failed: expected testing file to contain ID3v2")
}
}
func TestWriteID3v1(t *testing.T) {
file, err := Open(filepath.Join(TESTDATAPATH, "testwritev1.mp3"))
if err != nil {
t.Errorf("Open failed: %s", err)
}
v1tag := &v1.ID3v1Tag{
SongName: "testsong",
Artist: "testartist",
Album: "testalbum",
Year: 727,
Comment: "testcomment",
Genre: "Blues",
}
err = file.WriteID3v1(v1tag)
if err != nil {
t.Errorf("WriteID3v1 failed: %s", err)
}
}

BIN
testData/testwritev1.mp3

Binary file not shown.

8
v1/read.go

@ -9,6 +9,8 @@ import (
"github.com/Unbewohnte/id3ed/util"
)
var ErrDoesNotUseID3v1 error = fmt.Errorf("does not use ID3v1")
// Retrieves ID3v1 field values of provided io.ReadSeeker (usually a file)
func Readv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) {
var tag ID3v1Tag
@ -27,7 +29,7 @@ func Readv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) {
if !bytes.Equal(identifier, []byte(ID3v1IDENTIFIER)) {
// no identifier, given file does not use ID3v1
return nil, fmt.Errorf("does not use ID3v1: expected %s; got %s", ID3v1IDENTIFIER, identifier)
return nil, ErrDoesNotUseID3v1
}
// Songname
@ -100,9 +102,9 @@ func Readv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) {
tag.Genre = genre
if track == 0 {
tag.Version = V1_0
tag.version = V1_0
} else {
tag.Version = V1_1
tag.version = V1_1
}
return &tag, nil

2
v1/tag.go

@ -3,7 +3,7 @@ package v1
// https://id3.org/ID3v1 - documentation
type ID3v1Tag struct {
Version string
version string
SongName string
Artist string
Album string

62
v1/v1_test.go

@ -27,8 +27,8 @@ func TestReadv1Tag(t *testing.T) {
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.version != V1_1 {
t.Errorf("GetID3v1Tag failed: expected version to be %s; got %s", V1_1, tag.version)
}
if tag.Comment != "Comment here " {
@ -44,35 +44,35 @@ func TestReadv1Tag(t *testing.T) {
}
}
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 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)

7
v1/write.go

@ -14,10 +14,13 @@ import (
// NOTE: will not remove already existing ID3v1 tag if it`s present,
// use ⁕WriteToFile⁕ method if you`re working with REAL mp3 files !!!
func (tag *ID3v1Tag) write(dst io.WriteSeeker) error {
dst.Seek(0, io.SeekEnd)
_, err := dst.Seek(0, io.SeekEnd)
if err != nil {
return fmt.Errorf("could not seek: %s", err)
}
// ID
_, err := dst.Write([]byte(ID3v1IDENTIFIER))
_, err = dst.Write([]byte(ID3v1IDENTIFIER))
if err != nil {
return fmt.Errorf("could not write to writer: %s", err)
}

26
v2/frame.go

@ -3,6 +3,7 @@ package v2
import (
"fmt"
"io"
"strings"
"github.com/Unbewohnte/id3ed/util"
)
@ -10,6 +11,7 @@ import (
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")
var ErrInvalidID error = fmt.Errorf("invalid identifier")
type FrameFlags struct {
TagAlterPreservation bool
@ -31,6 +33,20 @@ type Frame struct {
Contents []byte
}
// Checks if provided frame identifier is valid by
// its length and presence of invalid characters.
func isValidFrameID(frameID []byte) bool {
if len(frameID) != 3 && len(frameID) != 4 {
return false
}
str := strings.ToValidUTF8(string(frameID), "invalidChar")
if len(str) != 3 && len(str) != 4 {
return false
}
return true
}
// Structuralises frame header from given bytes. For versions see: constants.
func getFrameHeader(fHeaderbytes []byte, version string) (FrameHeader, error) {
// validation check
@ -42,6 +58,9 @@ func getFrameHeader(fHeaderbytes []byte, version string) (FrameHeader, error) {
switch version {
case V2_2:
if !isValidFrameID(fHeaderbytes[0:3]) {
return FrameHeader{}, ErrInvalidID
}
header.ID = string(fHeaderbytes[0:3])
framesizeBytes, err := util.BytesToIntIgnoreFirstBit(fHeaderbytes[3:6])
@ -58,6 +77,9 @@ func getFrameHeader(fHeaderbytes []byte, version string) (FrameHeader, error) {
default:
// ID
if !isValidFrameID(fHeaderbytes[0:4]) {
return FrameHeader{}, ErrInvalidID
}
header.ID = string(fHeaderbytes[0:4])
// Size
@ -120,7 +142,7 @@ func getFrameHeader(fHeaderbytes []byte, version string) (FrameHeader, error) {
// 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) {
func readNextFrame(r io.Reader, h Header) (Frame, uint64, error) {
var frame Frame
var read uint64 = 0
@ -133,7 +155,7 @@ func ReadNextFrame(r io.Reader, h Header) (Frame, uint64, error) {
read += uint64(HEADERSIZE)
frameHeader, err := getFrameHeader(headerBytes, h.Version)
if err == ErrGotPadding {
if err == ErrGotPadding || err == ErrInvalidID {
return Frame{}, read, err
} else if err != nil {
return Frame{}, read, fmt.Errorf("could not get header of a frame: %s", err)

27
v2/frame_test.go

@ -14,12 +14,12 @@ func TestReadNextFrame(t *testing.T) {
t.Errorf("%s", err)
}
header, err := ReadHeader(f)
header, err := readHeader(f)
if err != nil {
t.Errorf("%s", err)
}
firstFrame, _, err := ReadNextFrame(f, header)
firstFrame, _, err := readNextFrame(f, header)
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)
secondFrame, _, err := readNextFrame(f, header)
if err != nil {
t.Errorf("ReadFrame failed: %s", err)
}
@ -49,24 +49,3 @@ func TestReadNextFrame(t *testing.T) {
"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)
// }
// }

6
v2/header.go

@ -8,6 +8,8 @@ import (
"github.com/Unbewohnte/id3ed/util"
)
var ErrDoesNotUseID3v2 error = fmt.Errorf("does not use ID3v2")
type HeaderFlags struct {
Unsynchronisated bool
HasExtendedHeader bool
@ -25,7 +27,7 @@ type Header struct {
// Reads and structuralises ID3v2 header from given bytes.
// Returns a blank header struct if encountered an error
func ReadHeader(rs io.ReadSeeker) (Header, error) {
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)
@ -42,7 +44,7 @@ func ReadHeader(rs io.ReadSeeker) (Header, error) {
// check if has identifier ID3v2
if !bytes.Equal([]byte(HEADERIDENTIFIER), identifier) {
return Header{}, fmt.Errorf("no ID3v2 identifier found")
return Header{}, ErrDoesNotUseID3v2
}
header.Identifier = string(identifier)

4
v2/header_test.go

@ -8,13 +8,13 @@ import (
var TESTDATAPATH string = filepath.Join("..", "testData")
func TestGetHeader(t *testing.T) {
func TestReadHeader(t *testing.T) {
f, err := os.Open(filepath.Join(TESTDATAPATH, "testreadv2.mp3"))
if err != nil {
t.Errorf("%s", err)
}
header, err := ReadHeader(f)
header, err := readHeader(f)
if err != nil {
t.Errorf("GetHeader failed: %s", err)
}

13
v2/read.go

@ -7,9 +7,12 @@ import (
// Reads the whole ID3v2 tag from rs
func ReadV2Tag(rs io.ReadSeeker) (*ID3v2Tag, error) {
header, err := ReadHeader(rs)
if err != nil {
header, err := readHeader(rs)
if err == ErrDoesNotUseID3v2 {
return nil, err
} else if err != nil {
return nil, fmt.Errorf("could not get header: %s", err)
}
// collect frames
@ -20,8 +23,8 @@ func ReadV2Tag(rs io.ReadSeeker) (*ID3v2Tag, error) {
break
}
frame, r, err := ReadNextFrame(rs, header)
if err == ErrGotPadding || err == ErrBiggerThanSize {
frame, r, err := readNextFrame(rs, header)
if err == ErrGotPadding || err == ErrBiggerThanSize || err == ErrInvalidID {
break
}
@ -38,4 +41,4 @@ func ReadV2Tag(rs io.ReadSeeker) (*ID3v2Tag, error) {
Header: header,
Frames: frames,
}, nil
}
}

Loading…
Cancel
Save