From 67a10faefe9e245f3e0495e25ede25bb2719106e Mon Sep 17 00:00:00 2001 From: Unbewohnte Date: Fri, 25 Jun 2021 14:32:52 +0300 Subject: [PATCH] ID3V1 and ID3v1.1 tag writing support --- README.md | 22 ++++++ id3v10.go | 60 +++++++++++++- id3v10_test.go | 39 +++++++++- id3v11.go | 82 ++++++++++++++++++-- id3v11_test.go | 52 +++++++++++-- testData/{testmp3id3v1.mp3 => testread.mp3} | Bin testData/testwrite.mp3 | Bin 0 -> 128 bytes util.go | 41 ++++++++++ 8 files changed, 281 insertions(+), 15 deletions(-) rename testData/{testmp3id3v1.mp3 => testread.mp3} (100%) create mode 100644 testData/testwrite.mp3 diff --git a/README.md b/README.md index 829e04f..5559dd7 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,28 @@ func main() { ## Encoding ID3v1.1 ``` + f, err := os.OpenFile("/path/to/file/myfile.mp3",os.O_WRONLY, os.ModePerm) + if err != nil { + panic(err) + } + defer f.Close() + + // create your tags struct + tags := ID3v11Tags{ + SongName: "mysong", + Artist: "me", + Album: "my album", + Year: 2021, + Comment: "Cool song", + Track: 1, + Genre: "Christian Gangsta Rap", // list of genres see "id3v1genres.go" + } + + // write given tags in file + err = WriteID3v11Tags(f, tags) + if err != nil { + panic(err) + } ``` # Testing diff --git a/id3v10.go b/id3v10.go index 75cb448..483b4f7 100644 --- a/id3v10.go +++ b/id3v10.go @@ -2,6 +2,7 @@ package id3ed import ( "bytes" + "encoding/binary" "fmt" "io" "strconv" @@ -71,8 +72,11 @@ func GetID3v1Tags(rs io.ReadSeeker) (*ID3v1Tags, error) { if err != nil { return nil, err } - // genre is one byte by specification - genre, exists := id3v1genres[int(genreByte[0])] + genreInt, err := binary.ReadVarint(bytes.NewBuffer(genreByte)) + if err != nil { + return nil, err + } + genre, exists := id3v1genres[int(genreInt)] if !exists { genre = "" } @@ -88,8 +92,58 @@ func GetID3v1Tags(rs io.ReadSeeker) (*ID3v1Tags, error) { } // Writes given ID3v1.0 tags to dst -func SetID3v1Tags(dst io.WriteSeeker, tags ID3v11Tags) error { +func WriteID3v1Tags(dst io.WriteSeeker, tags ID3v1Tags) error { dst.Seek(0, io.SeekEnd) + // TAG + _, err := dst.Write([]byte("TAG")) + if err != nil { + return err + } + + // Song name + err = writeToExtend(dst, []byte(tags.SongName), 30) + if err != nil { + return err + } + + // Artist + err = writeToExtend(dst, []byte(tags.Artist), 30) + if err != nil { + return err + } + + // Album + err = writeToExtend(dst, []byte(tags.Album), 30) + if err != nil { + return err + } + + // Year + err = writeToExtend(dst, []byte(fmt.Sprint(tags.Year)), 4) + if err != nil { + return err + } + + // Comment + err = writeToExtend(dst, []byte(tags.Comment), 30) + if err != nil { + return err + } + + // Genre + genreCode := getKey(id3v1genres, tags.Genre) + if genreCode == -1 { + // if no genre found - encode genre code as 255 + genreCode = 255 + } + genrebyte := make([]byte, 1) + binary.PutVarint(genrebyte, int64(genreCode)) + + _, err = dst.Write(genrebyte) + if err != nil { + return err + } + return nil } diff --git a/id3v10_test.go b/id3v10_test.go index 895e818..ea8ebe6 100644 --- a/id3v10_test.go +++ b/id3v10_test.go @@ -6,7 +6,7 @@ import ( ) func TestGetID3v1Tags(t *testing.T) { - testfile, err := os.Open("./testData/testmp3id3v1.mp3") + testfile, err := os.Open("./testData/testread.mp3") if err != nil { t.Errorf("could not open file for testing: %s", err) } @@ -19,3 +19,40 @@ func TestGetID3v1Tags(t *testing.T) { t.Errorf("GetID3v1Tags failed: expected %s; got %s", "Comment here ", tags.Comment) } } + +func TestWriteID3v1Tags(t *testing.T) { + os.Remove("./testData/testwrite.mp3") + + f, err := os.Create("./testData/testwrite.mp3") + if err != nil { + t.Errorf("%s", err) + } + defer f.Close() + + tags := ID3v1Tags{ + SongName: "testsong", + Artist: "testartist", + Album: "testalbum", + Year: 727, + Comment: "testcomment", + Genre: "Blues", + } + + err = WriteID3v1Tags(f, tags) + if err != nil { + t.Errorf("WriteID3v1Tags failed: %s", err) + } + + readTags, err := GetID3v1Tags(f) + if err != nil { + t.Errorf("%s", err) + } + + if readTags.Album != "testalbum" { + t.Errorf("WriteID3v1Tags failed: expected %s; got %s", "testalbum", readTags.Album) + } + + if readTags.Year != 727 { + t.Errorf("WriteID3v1Tags failed: expected %d; got %d", 727, readTags.Year) + } +} diff --git a/id3v11.go b/id3v11.go index d60d6a0..901445e 100644 --- a/id3v11.go +++ b/id3v11.go @@ -2,6 +2,7 @@ package id3ed import ( "bytes" + "encoding/binary" "fmt" "io" "strconv" @@ -75,15 +76,20 @@ func GetID3v11Tags(rs io.ReadSeeker) (*ID3v11Tags, error) { return nil, err } - // track is one byte by specification - track := int(trackByte[0]) + track, err := binary.ReadVarint(bytes.NewBuffer(trackByte)) + if err != nil { + return nil, err + } genreByte, err := read(rs, 1) if err != nil { return nil, err } - // genre is one byte by specification - genre, exists := id3v1genres[int(genreByte[0])] + genreInt, err := binary.ReadVarint(bytes.NewBuffer(genreByte)) + if err != nil { + return nil, err + } + genre, exists := id3v1genres[int(genreInt)] if !exists { genre = "" } @@ -94,14 +100,78 @@ func GetID3v11Tags(rs io.ReadSeeker) (*ID3v11Tags, error) { Album: album, Year: year, Comment: comment, - Track: track, + Track: int(track), Genre: genre, }, nil } // Writes given ID3v1.1 tags to dst -func SetID3v11Tags(dst io.WriteSeeker, tags ID3v11Tags) error { +func WriteID3v11Tags(dst io.WriteSeeker, tags ID3v11Tags) error { dst.Seek(0, io.SeekEnd) + // TAG + _, err := dst.Write([]byte("TAG")) + if err != nil { + return fmt.Errorf("could not write to dst: %s", err) + } + + // Song name + err = writeToExtend(dst, []byte(tags.SongName), 30) + if err != nil { + return fmt.Errorf("could not write to dst: %s", err) + } + + // Artist + err = writeToExtend(dst, []byte(tags.Artist), 30) + if err != nil { + return fmt.Errorf("could not write to dst: %s", err) + } + + // Album + err = writeToExtend(dst, []byte(tags.Album), 30) + if err != nil { + return fmt.Errorf("could not write to dst: %s", err) + } + + // Year + err = writeToExtend(dst, []byte(fmt.Sprint(tags.Year)), 4) + if err != nil { + return fmt.Errorf("could not write to dst: %s", err) + } + + // Comment + err = writeToExtend(dst, []byte(tags.Comment), 28) + if err != nil { + return fmt.Errorf("could not write to dst: %s", err) + } + + _, err = dst.Write([]byte{0}) + if err != nil { + return fmt.Errorf("could not write to dst: %s", err) + } + + // Track + trackBytes := make([]byte, 1) + binary.PutVarint(trackBytes, int64(tags.Track)) + // binary.BigEndian.PutUint16(trackBytes, uint16(tags.Track)) + _, err = dst.Write(trackBytes) + if err != nil { + return fmt.Errorf("could not write to dst: %s", err) + } + + //Genre + genreCode := getKey(id3v1genres, tags.Genre) + if genreCode == -1 { + // if no genre found - encode genre code as 255 + genreCode = 255 + } + genrebyte := make([]byte, 1) + binary.PutVarint(genrebyte, int64(genreCode)) + + err = writeToExtend(dst, genrebyte, 1) + if err != nil { + return fmt.Errorf("could not write to dst: %s", err) + } + return nil } diff --git a/id3v11_test.go b/id3v11_test.go index 022ce35..23ba797 100644 --- a/id3v11_test.go +++ b/id3v11_test.go @@ -7,21 +7,63 @@ import ( ) func TestGetID3v11Tags(t *testing.T) { - testfile, err := os.Open("./testData/testmp3id3v1.mp3") + testfile, err := os.Open("./testData/testread.mp3") if err != nil { t.Errorf("could not open file for testing: %s", err) } mp3tags, err := GetID3v11Tags(testfile) if err != nil { - t.Errorf("GetID3v1Tags failed: %s", err) + t.Errorf("GetID3v11Tags failed: %s", err) } if mp3tags.Artist != "Artist" { fmt.Printf("%v", mp3tags.Artist) - t.Errorf("GetID3v1Tags has failed: expected %v; got %v", "Artist", mp3tags.Artist) + t.Errorf("GetID3v11Tags failed: expected %s; got %s", "Artist", mp3tags.Artist) } - if mp3tags.Track != 8 { - t.Errorf("GetID3v1Tags has failed: expected %v; got %v", 8, mp3tags.Track) + if mp3tags.Track != 4 { + t.Errorf("GetID3v11Tags failed: expected %d; got %d", 4, mp3tags.Track) + } +} + +func TestWriteID3v11Tags(t *testing.T) { + os.Remove("./testData/testwrite.mp3") + + f, err := os.Create("./testData/testwrite.mp3") + if err != nil { + t.Errorf("%s", err) + } + defer f.Close() + + tags := ID3v11Tags{ + SongName: "testsong", + Artist: "testartist", + Album: "testalbum", + Year: 727, + Comment: "testcomment", + Track: 5, + Genre: "Blues", + } + + err = WriteID3v11Tags(f, tags) + if err != nil { + t.Errorf("WriteID3v1Tags failed: %s", err) + } + + readTags, err := GetID3v11Tags(f) + if err != nil { + t.Errorf("%s", err) + } + + if readTags.Album != "testalbum" { + t.Errorf("WriteID3v11Tags failed: expected %s; got %s", "testalbum", readTags.Album) + } + + if readTags.Year != 727 { + t.Errorf("WriteID3v11Tags failed: expected %d; got %d", 727, readTags.Year) + } + + if readTags.Track != 5 { + t.Errorf("WriteID3v11Tags failed: expected %d; got %d", 5, readTags.Track) } } diff --git a/testData/testmp3id3v1.mp3 b/testData/testread.mp3 similarity index 100% rename from testData/testmp3id3v1.mp3 rename to testData/testread.mp3 diff --git a/testData/testwrite.mp3 b/testData/testwrite.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..6ed27e6ed24cadb72ba25bb14b54f4c5e2dddfed GIT binary patch literal 128 zcmWG>bT3IQE-B8>OJ~3eK(dKNC7Hz~*rdU-IZ36t*d!Uujm#N9Dw6Yab5rxM7{J8< E02Cw<*#H0l literal 0 HcmV?d00001 diff --git a/util.go b/util.go index e23c1cc..0743b59 100644 --- a/util.go +++ b/util.go @@ -1,6 +1,7 @@ package id3ed import ( + "bytes" "fmt" "io" ) @@ -35,3 +36,43 @@ func readToString(rs io.Reader, n int) (string, error) { return readString, nil } + +// Writes data to wr, if len(data) is less than lenNeeded - adds null bytes until written lenNeeded bytes +func writeToExtend(wr io.Writer, data []byte, lenNeeded int) error { + if len(data) > lenNeeded { + return fmt.Errorf("length of given data bytes is bigger than length needed") + } + + buff := new(bytes.Buffer) + for i := 0; i < lenNeeded; i++ { + if i < len(data) { + err := buff.WriteByte(data[i]) + if err != nil { + return err + } + } else { + err := buff.WriteByte(0) + if err != nil { + return err + } + } + } + + _, err := wr.Write(buff.Bytes()) + if err != nil { + return err + } + + return nil +} + +// Returns found key (int) in provided map by value (string); +// If key does not exist in map - returns -1 +func getKey(mp map[int]string, givenValue string) int { + for key, value := range mp { + if value == givenValue { + return key + } + } + return -1 +}