diff --git a/id3ed.go b/id3ed.go index 738317b..372fd3d 100644 --- a/id3ed.go +++ b/id3ed.go @@ -58,7 +58,7 @@ func Open(path string) (*File, error) { 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) + return fmt.Errorf("could not open a file: %s", err) } defer fhandler.Close() @@ -70,7 +70,18 @@ func (f *File) WriteID3v1(tag *v1.ID3v1Tag) error { return nil } -// still not implemented -// func (f *File) WriteID3v2(tag *v2.ID3v2Tag) error { -// return nil -// } +// Writes given ID3v2 tag to file +func (f *File) WriteID3v2(tag *v2.ID3v2Tag) error { + fhandler, err := os.OpenFile(f.path, os.O_RDWR, os.ModePerm) + if err != nil { + return fmt.Errorf("could not open a file: %s", err) + } + defer fhandler.Close() + + err = tag.WriteToFile(fhandler) + if err != nil { + return fmt.Errorf("could not write ID3v2 to file: %s", err) + } + + return nil +} diff --git a/id3ed_test.go b/id3ed_test.go index 40e04cb..fb72180 100644 --- a/id3ed_test.go +++ b/id3ed_test.go @@ -51,3 +51,20 @@ func TestWriteID3v1(t *testing.T) { t.Errorf("WriteID3v1 failed: %s", err) } } + +func TestWriteID3v2(t *testing.T) { + file, err := Open(filepath.Join(TESTDATAPATH, "testwritev2.mp3")) + if err != nil { + t.Errorf("Open failed: %s", err) + } + + frame1, _ := v2.NewFrame("COMM", []byte("Very Cool Song"), true) + frame2, _ := v2.NewFrame("TXXX", []byte("\n\n\n\n\n\n\nF\n\n\n\n"), true) + + v2tag := v2.NewTAG([]v2.Frame{*frame1, *frame2}) + + err = file.WriteID3v2(v2tag) + if err != nil { + t.Errorf("WriteID3v2 failed: %s", err) + } +} diff --git a/testData/testreadv2.mp3 b/testData/testreadv2.mp3 index 3499f38..5e89a33 100644 Binary files a/testData/testreadv2.mp3 and b/testData/testreadv2.mp3 differ diff --git a/testData/testwritev1.mp3 b/testData/testwritev1.mp3 index 2831d26..c90551a 100644 Binary files a/testData/testwritev1.mp3 and b/testData/testwritev1.mp3 differ diff --git a/testData/testwritev2.mp3 b/testData/testwritev2.mp3 index e69de29..a8f1039 100755 Binary files a/testData/testwritev2.mp3 and b/testData/testwritev2.mp3 differ diff --git a/v1/read.go b/v1/read.go index 0d3bfdb..3c0a016 100644 --- a/v1/read.go +++ b/v1/read.go @@ -41,7 +41,6 @@ func containsEnhancedTAG(rs io.ReadSeeker) bool { return false } if !bytes.Equal(identifier, []byte(ENHANCEDIDENTIFIER)) { - fmt.Printf("UWAH: %s ---- %s\n", identifier, ENHANCEDIDENTIFIER) return false } diff --git a/v1/write.go b/v1/write.go index 9de8fad..9a30c8f 100644 --- a/v1/write.go +++ b/v1/write.go @@ -163,8 +163,6 @@ func (tag *ID3v1Tag) WriteToFile(f *os.File) error { switch { case containsEnhancedTAG(f) && containsTAG(f): - fmt.Println("HEA1") - // remove both err = f.Truncate(filesize - int64(TAGSIZE+ENHANCEDSIZE)) if err != nil { @@ -177,8 +175,6 @@ func (tag *ID3v1Tag) WriteToFile(f *os.File) error { } case containsEnhancedTAG(f) && !containsTAG(f): - fmt.Println("HEA2") - // remove enhanced tag, replace with new err = f.Truncate(filesize - int64(ENHANCEDSIZE)) if err != nil { @@ -191,8 +187,6 @@ func (tag *ID3v1Tag) WriteToFile(f *os.File) error { } case !containsEnhancedTAG(f) && containsTAG(f): - fmt.Println("HEA3") - // remove regular one, replace with new err = f.Truncate(filesize - int64(TAGSIZE)) if err != nil { @@ -205,8 +199,6 @@ func (tag *ID3v1Tag) WriteToFile(f *os.File) error { } case !containsEnhancedTAG(f) && !containsTAG(f): - fmt.Println("HEA4") - // no existing TAGs, simply write what we have err := tag.write(f) if err != nil { diff --git a/v2/read.go b/v2/read.go index 82f95f2..c2f3c16 100644 --- a/v2/read.go +++ b/v2/read.go @@ -17,26 +17,27 @@ func ReadV2Tag(rs io.ReadSeeker) (*ID3v2Tag, error) { var read uint64 = 0 var frames []Frame + var padding uint32 = 0 for { if read == uint64(header.Size()) { break - } 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()) switch err { case nil: case ErrGotPadding: - // expected error, just return what we`ve collected + // take a note how many padding bytes are left and + // return collected frames + padding += header.Size() - uint32(read) return &ID3v2Tag{ - Header: header, - Frames: frames, + Header: header, + Frames: frames, + Padding: padding, }, nil + case ErrInvalidID: - // expected error, just return what we`ve collected + // return what has been collected return &ID3v2Tag{ Header: header, Frames: frames, @@ -57,7 +58,8 @@ func ReadV2Tag(rs io.ReadSeeker) (*ID3v2Tag, error) { } return &ID3v2Tag{ - Header: header, - Frames: frames, + Header: header, + Frames: frames, + Padding: padding, }, nil } diff --git a/v2/read_test.go b/v2/read_test.go index b0e473f..284e388 100644 --- a/v2/read_test.go +++ b/v2/read_test.go @@ -17,6 +17,10 @@ func TestReadV2Tag(t *testing.T) { t.Errorf("GetV2Tag failed: %s", err) } + if tag.Padding != 1024 { + t.Errorf("GetV2Tag failed: expected to have %d padding bytes: got %d", 1024, tag.Padding) + } + titleFrame := tag.GetFrame("TIT2") if titleFrame.Text() != "title" { @@ -34,4 +38,9 @@ func TestReadV2Tag(t *testing.T) { if picture != nil { t.Errorf("ReadV2Tag failed: expected file not to have a picture") } + + genre := tag.GetFrame("TCON") + if genre == nil { + t.Errorf("ReadV2Tag failed: expected genre to be %s; got %v", "anime", genre) + } } diff --git a/v2/v2tag.go b/v2/v2tag.go index 300ca11..6080b73 100644 --- a/v2/v2tag.go +++ b/v2/v2tag.go @@ -3,8 +3,9 @@ package v2 import "strings" type ID3v2Tag struct { - Header Header - Frames []Frame + Header Header + Frames []Frame + Padding uint32 } // Creates a new v2 tag from given created frames diff --git a/v2/write.go b/v2/write.go index 652c5de..a59685f 100644 --- a/v2/write.go +++ b/v2/write.go @@ -1,113 +1,221 @@ package v2 -// // 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) -// } - -// // write header -// _, err = ws.Write(tag.Header.toBytes()) -// if err != nil { -// return fmt.Errorf("could not write to writer: %s", err) -// } - -// // write frames -// for _, frame := range tag.Frames { -// _, err = ws.Write(frame.toBytes()) -// if err != nil { -// return fmt.Errorf("could not write to writer: %s", err) -// } -// } - -// 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 -// } +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/Unbewohnte/id3ed/util" +) + +// 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) + } + + // write header + _, err = ws.Write(tag.Header.toBytes()) + if err != nil { + return fmt.Errorf("could not write to writer: %s", err) + } + + // write frames + for _, frame := range tag.Frames { + _, err = ws.Write(frame.toBytes()) + if err != nil { + return fmt.Errorf("could not write to writer: %s", err) + } + } + + // write padding if has any + if tag.Padding != 0 { + util.WriteToExtent(ws, []byte{0}, int(tag.Padding)) + } + + 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 if there`s content at all + fStats, err := f.Stat() + if err != nil { + return err + } + + if fStats.Size() < 3 { + // there`s no way that the file can contain TAG, + // just write and exit + + // `write` for some reason removes all contents if there`s no tag, so + // we need forcefully store already existing data and + // write it again afterwards + + _, err := f.Seek(0, io.SeekStart) + if err != nil { + return fmt.Errorf("could not seek: %s", err) + } + + contents, err := util.Read(f, uint64(fStats.Size())) + if err != nil { + return err + } + + err = tag.write(f) + if err != nil { + return err + } + + _, err = f.Write(contents) + if err != nil { + return err + } + + // apparently, there are 3 zerobytes + // that appear after writing the contents for some + // alien-like reason so we need to remove them. + fStats, err = f.Stat() + if err != nil { + return err + } + + err = f.Truncate(fStats.Size() - 3) + if err != nil { + return err + } + + return nil + } + + // check for an 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 + + // `write` for some reason removes all contents if there`s no tag, so + // we need forcefully store already existing data and + // write it again afterwards + + _, err := f.Seek(0, io.SeekStart) + if err != nil { + return fmt.Errorf("could not seek: %s", err) + } + + contents, err := util.Read(f, uint64(fStats.Size())) + if err != nil { + return err + } + + err = tag.write(f) + if err != nil { + return err + } + + _, err = f.Write(contents) + if err != nil { + return err + } + + // apparently, there are 3 zerobytes + // that appear after writing the contents for some + // alien-like reason so we need to remove them. + fStats, err = f.Stat() + if err != nil { + return err + } + + err = f.Truncate(fStats.Size() - 3) + if err != nil { + return err + } + + 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 + } + existingHeaderSize := 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())) + + // copy contents of the original mp3 to a temporary one + _, err = io.Copy(tmpF, f) + if err != nil { + return err + } + + // fully remove contents of the original file + err = f.Truncate(0) + if err != nil { + return err + } + + // write our tag to the original file, which is at that moment is + // empty + err = tag.write(f) + if err != nil { + return err + } + + tmpFStats, err := tmpF.Stat() + if err != nil { + return err + } + + // read all contents of the temporary file, except the existing tag + + musicDataSize := int64(tmpFStats.Size() - int64(existingHeaderSize)) + + _, err = tmpF.Seek(int64(existingHeaderSize), io.SeekStart) + if err != nil { + return fmt.Errorf("could not seek: %s", err) + } + + musicData, err := util.Read(tmpF, uint64(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 dd68092..f53f53c 100644 --- a/v2/write_test.go +++ b/v2/write_test.go @@ -1,27 +1,33 @@ package v2 -// func TestWrite(t *testing.T) { -// f, err := os.Open(filepath.Join(TESTDATAPATH, "testreadv2.mp3")) -// if err != nil { -// t.Errorf("%s", err) -// } -// defer f.Close() +import ( + "os" + "path/filepath" + "testing" +) -// testTag, err := ReadV2Tag(f) -// if err != nil { -// t.Errorf("%s", err) -// } +func TestWrite(t *testing.T) { + f, err := os.Open(filepath.Join(TESTDATAPATH, "testreadv2.mp3")) + if err != nil { + t.Errorf("%s", err) + } + defer f.Close() -// ff, err := os.OpenFile(filepath.Join(TESTDATAPATH, "testwritev2.mp3"), -// os.O_CREATE|os.O_RDWR, os.ModePerm) -// if err != nil { -// t.Errorf("%s", err) -// } -// defer ff.Close() + testTag, err := ReadV2Tag(f) + if err != nil { + t.Errorf("%s", err) + } -// // WRITING -// err = testTag.WriteToFile(ff) -// if err != nil { -// t.Errorf("WriteToFile failed: %s", err) -// } -// } + ff, err := os.OpenFile(filepath.Join(TESTDATAPATH, "testwritev2.mp3"), + os.O_CREATE|os.O_RDWR, os.ModePerm) + if err != nil { + t.Errorf("%s", err) + } + defer ff.Close() + + // write testTag to the ff + err = testTag.WriteToFile(ff) + if err != nil { + t.Errorf("WriteToFile failed: %s", err) + } +}