diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml deleted file mode 100644 index 654e6f5..0000000 --- a/.github/workflows/pipeline.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: Build, Test and Deploy - -on: - push: - branches: - - '**' - -jobs: - build-deploy: - runs-on: ubuntu-latest - steps: - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: '1.18' - - name: Checkout Repo - uses: actions/checkout@v3 - - name: Fetch Dependencies - run: go mod download - - name: Golangci-lint - uses: golangci/golangci-lint-action@v6.0.1 - - name: Run Unit tests - run: go test -race -covermode atomic -coverprofile=covprofile ./... - - name: Install goveralls - if: github.ref == 'refs/heads/main' - run: go install github.com/mattn/goveralls@latest - - name: Send coverage - if: github.ref == 'refs/heads/main' - env: - COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }} - run: goveralls -coverprofile=covprofile -service=github \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e6ad9a0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +## [3.0.0] - 2024-11-5 +### audiometa +- Now uses an interface for common tags between file types instead of a struct +- Each format is now under its own project +- Majorly simplifies interacting with metadata + +### MP3 +- New module wrapper +- Concurrency improvements + +### MP4 +- No longer corrupts files +- Smaller memory footprint +- Now works with album art + +### OGG +- CRC32 checksum is now calculated and checked correctly +- Opus and Vorbis now support cover art +- Opus cover art no longer breaks the stream +- Added a large set of vorbis tags +- No longer corrupts files + +### FLAC +- No longer corrupts files +- Added a large set of vorbis tags +- Major memory improvements as the audio stream is now copied directly to the writer instead of reading it all to memory first \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 498bb98..0000000 --- a/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2023, Gage Cottom - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..b6853b0 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Gage Cottom + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 9534967..655560f 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,12 @@ # audiometa +audiometa is the final piece that ties several projects together with a simple interface. While the code looks short for this specific project, the underlying modules are extensive. -MP3/MP4/FLAC/OGG tag reader and writer for go +## File Types +- MP3 (files containing an ID3 header) +- MP4 (files containing an ftyp header) +- OGG (files containing an oggs header, with vorbis or opus encoding) +- FLAC (files containing a fLaC header) - -[![Go Reference](https://pkg.go.dev/badge/github.com/gcottom/audiometa/v2.svg)](https://pkg.go.dev/github.com/gcottom/audiometa/v2) [![Go Report Card](https://goreportcard.com/badge/github.com/gcottom/audiometa/v2)](https://goreportcard.com/report/github.com/gcottom/audiometa/v2) [![Coverage Status](https://coveralls.io/repos/github/gcottom/audiometa/badge.svg?branch=main)](https://coveralls.io/github/gcottom/audiometa?branch=main) - - -This package enables parsing and writing of ID tags for mp3, mp4 (m4a, m4b, m4p), FLAC, and ogg (Vorbis, OPUS) files. - -All fields can be accessed via the provided accessor methods. Only supported fields per file type will return non zero data. The exception to this is that ogg files support a passthrough map. -By setting kv pairs in the passthrough map, non-standard vorbis comment tags can be written to both ogg Vorbis and ogg OPUS files. - - -# Parsable Fields Per FileType - -## MP3 -Artist, AlbumArtist, Album, AlbumArt, BPM, ContentType, Comments, Composer, CopyrightMessage, Date, EncodedBy, FileType, Genre, Language, Length, Lyricist, PartOfSet, Publisher, Title, Year - -## MP4 & MP4 Types (m4a, m4b, m4p, mp4) -Artist, AlbumArtist, Album, AlbumArt, Comments, Composer, CopyrightMessage, EncodedBy, Genre, Title, Year - -## FLAC -Artist, Album, AlbumArt, Date, Genre, Title - -## OGG (Vorbis and OPUS within an ogg container) -Artist, AlbumArtist, Album, AlbumArt, Comment, Composer, Copyright, Date, Genre, Title, Publisher, (extended support for custom fields via passthrough map) - - -# Writable Fields Per FileType - -## MP3 -Artist, AlbumArtist, Album, AlbumArt, BPM, ContentType, Comments, Composer, CopyrightMessage, Date, EncodedBy, FileType, Genre, Language, Length, Lyricist, PartOfSet, Publisher, Title, Year - -## MP4 & MP4 Types (m4a, m4b, m4p, mp4) -Artist, AlbumArtist, Album, Comments, CopyrightMessage, Composer, Genre, Title, Year (AlbumArt is currently not writeable) - -## FLAC -Artist, Album, AlbumArt, Date, Genre, Title - -## OGG (Vorbis and OPUS within an ogg container) -Artist, AlbumArtist, Album, AlbumArt, Comment, Composer, Copyright, Date, Genre, Title, Publisher, (extended support for custom fields via passthrough map) - -# Usage - -## Open Tag For Reading From Path -``` -tag, err := audiometa.OpenTagFromPath("./my-audio-file.mp3") -if err != nil{ - panic(err) -} - -artist := tag.Artist() -album := tag.Album() -title := tag.Title() -``` - -## Open Tag For Reading From io.ReadSeeker -``` -f, err := os.Open("./my-audio-file.mp3") -tag, err := audiometa.Open(f, audiometa.ParseOptions{Format: audiometa.MP3}) //ParseOptions is only required if the file does not have an extension -if err != nil{ - panic(err) -} - -artist := tag.Artist() -album := tag.Album() -title := tag.Title() -``` - -## Update Tag -``` -f, err := os.Open("./my-audio-file.mp3") -if err != nil{ - panic(err) -} -tag, err := audiometa.Open(f) -if err != nil{ - panic(err) -} -tag.SetArtist("Beyonce") -err = tag.Save(f) -if err != nil{ - panic(err) -} -``` - -## Clear All Tags And Save -``` -f, err := os.Open("./my-audio-file.mp3") -if err != nil{ - panic(err) -} -tag, err := audiometa.Open(f) -if err != nil{ - panic(err) -} -tag.ClearAllTags() -err = tag.Save(f) -if err != nil{ - panic(err) -} -``` \ No newline at end of file +## Improvements +This version ties together bug fixes in almost every module. Improvements in performance and memory usage along with tons of optimization and testing in each package. Check the CHANGLOG for a comprehensive list of changes. \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index 4a52672..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,198 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strings" - - "github.com/gcottom/audiometa/v2" -) - -func main() { - args := os.Args[1:] - if len(args) > 1 && args[0] == "." { - args = args[1:] - } - if len(args) >= 1 { - file := "" - if len(args) >= 2 { - file = args[1] - } - mode := strings.ToLower(args[0]) - if len(args)%2 == 0 && len(args) != 1 { - if mode == "p" || mode == "parse" || mode == "r" || mode == "read" || mode == "-p" || mode == "-parse" || mode == "-r" || mode == "-read" { - tag, err := audiometa.OpenTagFromPath(file) - if err != nil { - panic(err) - } - printedTags := 0 - fmt.Printf("File: %s\n", file) - if tag.Artist() != "" { - fmt.Printf("Artist: %s\n", tag.Artist()) - printedTags++ - } - if tag.AlbumArtist() != "" { - fmt.Printf("AlbumArtist: %s\n", tag.AlbumArtist()) - printedTags++ - } - if tag.Album() != "" { - fmt.Printf("Album: %s\n", tag.Album()) - printedTags++ - } - if tag.BPM() != "" { - fmt.Printf("BPM: %s\n", tag.BPM()) - printedTags++ - } - if tag.Comments() != "" { - fmt.Printf("Comment: %s\n", tag.Comments()) - printedTags++ - } - if tag.Composer() != "" { - fmt.Printf("Composer: %s\n", tag.Composer()) - printedTags++ - } - if tag.CopyrightMsg() != "" { - fmt.Printf("Copyright: %s\n", tag.CopyrightMsg()) - printedTags++ - } - if tag.Date() != "" { - fmt.Printf("Date: %s\n", tag.Date()) - printedTags++ - } - if tag.EncodedBy() != "" { - fmt.Printf("EncodedBy: %s\n", tag.EncodedBy()) - printedTags++ - } - if tag.Genre() != "" { - fmt.Printf("Genre: %s\n", tag.Genre()) - printedTags++ - } - if tag.Language() != "" { - fmt.Printf("Language: %s\n", tag.Language()) - printedTags++ - } - if tag.Length() != "" { - fmt.Printf("Length: %s\n", tag.Length()) - printedTags++ - } - if tag.Lyricist() != "" { - fmt.Printf("Lyricist: %s\n", tag.Lyricist()) - printedTags++ - } - if tag.PartOfSet() != "" { - fmt.Printf("PartOfSet: %s\n", tag.PartOfSet()) - printedTags++ - } - if tag.Publisher() != "" { - fmt.Printf("Publisher: %s\n", tag.Publisher()) - printedTags++ - } - if tag.Title() != "" { - fmt.Printf("Title: %s\n", tag.Title()) - printedTags++ - } - if tag.Year() != "" { - fmt.Printf("Year: %s\n", tag.Year()) - printedTags++ - } - if len(tag.AdditionalTags()) > 0 { - fmt.Print("\nUnmapped Tags:\n") - for key, value := range tag.AdditionalTags() { - fmt.Printf("%s: %s\n", key, value) - printedTags++ - } - } - if printedTags == 0 { - fmt.Println("No tags found for this file!") - } - } else if mode == "s" || mode == "w" || mode == "save" || mode == "write" || mode == "-s" || mode == "-w" || mode == "-save" || mode == "-write" { - if len(args) > 2 { - tag, err := audiometa.OpenTagFromPath(file) - if err != nil { - panic(err) - } - for i := 2; i < len(args); i += 2 { - cmdTag := strings.ToLower(args[i]) - writeTag := strings.ToLower(args[i+1]) - if cmdTag == "art" || cmdTag == "-art" || cmdTag == "artist" || cmdTag == "-artist" { - tag.SetArtist(writeTag) - } else if cmdTag == "aa" || cmdTag == "-aa" || cmdTag == "-albumartist" || cmdTag == "albumartist" { - tag.SetAlbumArtist(writeTag) - } else if cmdTag == "alb" || cmdTag == "-alb" || cmdTag == "album" || cmdTag == "-album" { - tag.SetAlbum(writeTag) - } else if cmdTag == "c" || cmdTag == "-c" || cmdTag == "cover" || cmdTag == "-cover" { - if err := tag.SetAlbumArtFromFilePath(writeTag); err != nil { - fmt.Println(err) - } - } else if cmdTag == "comment" || cmdTag == "-comment" || cmdTag == "comments" || cmdTag == "-comments" { - tag.SetComments(writeTag) - } else if cmdTag == "composer" || cmdTag == "-composer" { - tag.SetComposer(writeTag) - } else if cmdTag == "g" || cmdTag == "-g" || cmdTag == "genre" || cmdTag == "-genre" { - tag.SetGenre(writeTag) - } else if cmdTag == "t" || cmdTag == "-t" || cmdTag == "title" || cmdTag == "-title" { - tag.SetTitle(writeTag) - } else if cmdTag == "y" || cmdTag == "-y" || cmdTag == "-year" || cmdTag == "year" { - tag.SetYear(writeTag) - } else if cmdTag == "b" || cmdTag == "-b" || cmdTag == "bpm" || cmdTag == "-bpm" { - tag.SetBPM(writeTag) - } else { - fileType, err := audiometa.GetFileType(args[1]) - if err != nil { - fmt.Println(err) - } - if fileType == "ogg" { - cmdTag = strings.TrimPrefix(cmdTag, "-") - tag.SetAdditionalTag(strings.ToUpper(cmdTag), writeTag) - } else { - fmt.Printf("Unsupported tag: %s\n", cmdTag) - } - } - } - f, err := os.OpenFile(file, os.O_WRONLY, os.ModePerm) - if err != nil { - panic(err) - } - - err = tag.Save(f) - if err != nil { - panic(err) - } - return - } - - } else if mode == "c" || mode == "clear" || mode == "e" || mode == "empty" || mode == "-c" || mode == "-clear" || mode == "-e" || mode == "-empty" { - tag, err := audiometa.OpenTagFromPath(file) - if err != nil { - panic(err) - } - f, err := os.Open(file) - if err != nil { - panic(err) - } - tag.ClearAllTags(false) - - err = tag.Save(f) - if err != nil { - panic(err) - } - - } else if mode == "h" || mode == "-h" || mode == "-help" || mode == "help" { - if args[1] == "r" || args[1] == "p" || args[1] == "parse" || args[1] == "read" { - fmt.Println("mp3-mp4-tag-cmd-help: Read(Parse) mode\nThe parse mode shows all tags that are attached to the file. If the tag is not mapped with this application it will be shown in the \"unmapped tags\" section at the bottom of the output.\nex usage: mp3-mp4-tag parse filepath.mp3") - } else if args[1] == "w" || args[1] == "s" || args[1] == "write" || args[1] == "save" || args[1] == "-w" || args[1] == "-s" || args[1] == "-write" || args[1] == "-save" { - fmt.Println("mp3-mp4-tag-cmd-help: Write(Save) mode\nThe save mode allows you to save specified tags to a file. The following flags can be used:\n\"art\"= artist\n\"aa\"= albumartist\n\"alb\"= album\n\"c\"= cover (this sets the cover art from a filepath)\n\"comment\"= comments\n\"composer\"= composer\n\"g\"= genre\n\"t\"= title\n\"y\"= year\n\"b\"= bpm\n\nIf the filetype is ogg you can specify additional tags by specifying the tag name as a flag\nex usage: mp3-mp4-tag write filepath.ogg mycustomflag value\nAdditionally every tag can be specified by its full name\nex usage: mp3-mp4-tag write filepath.mp3 artist artistname\nstandard ex usage: mp3-mp4-tag write filepath.mp3 art artistname\nYou can specify 1 or more tag pairs as long as they are complete pairs. Make sure to enclose values with spaces in them within \"quotes\"") - } else if args[1] == "c" || args[1] == "e" || args[1] == "clear" || args[1] == "empty" || args[1] == "-c" || args[1] == "-e" || args[1] == "-clear" || args[1] == "-empty" { - fmt.Println("mp3-mp4-tag-cmd-help: Clear(Empty) mode\nThe clear mode clears all of the tags in the file and saves the file with empty tags. You can use this mode to clear all tags of a file and then use the write mode to write all new tags to the file.\nex usage: mp3-mp4-tag clear filepath.mp3") - } - } else { - if len(args) == 1 && args[0] == "h" || args[0] == "-h" || args[0] == "-help" || args[0] == "help" { - fmt.Println("mp3-mp4-tag-cmd-help: The application offers 3 modes: read, write, and clear. Use the command \"help\" followed by a mode to learn more about its usage.") - } else { - fmt.Println("Invalid number of arguments!\nmp3-mp4-tag-cmd-help: The application offers 3 modes: read, write, and clear. Use the command \"help\" followed by a mode to learn more about its usage.") - } - - } - } - } -} diff --git a/doc.go b/doc.go deleted file mode 100644 index cccb695..0000000 --- a/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -/* -Package audiometa is an all in one solution for reading and writing audio meta data in Go. It's the only Go module available that can read and write OGG Vorbis and OGG Opus metadata. -audiometa supports several different filetypes and a wide range of tags. -*/ -package audiometa diff --git a/errors.go b/errors.go deleted file mode 100644 index 5a9dd49..0000000 --- a/errors.go +++ /dev/null @@ -1,25 +0,0 @@ -package audiometa - -import "errors" - -var ( - ErrFLACParse = errors.New("error parsing flac stream") - ErrFLACCmtParse = errors.New("error parsing flac comment") - - ErrMP4AtomOutOfBounds = errors.New("mp4 atom out of bounds") - ErrMP4InvalidAtomSize = errors.New("mp4 atom has invalid size") - ErrMP4InvalidEncoding = errors.New("invalid encoding: got wrong number of bytes") - ErrMP4IlstAtomMissing = errors.New("ilst atom is missing") - ErrMP4InvalidCntntType = errors.New("invalid content type") - - ErrOggInvalidSgmtTblSz = errors.New("invalid segment table size") - ErrOggInvalidHeader = errors.New("invalid ogg header") - ErrOggInvalidCRC = errors.New("invalid CRC") - ErrOggMissingCOP = errors.New("missing ogg COP packet") - ErrOggImgConfigFail = errors.New("failed to get image config") - ErrOggCodecNotSpprtd = errors.New("unsupported codec for ogg") - - ErrMP3ParseFail = errors.New("error parsing mp3") - - ErrNoMethodAvlble = errors.New("no method available for this filetype") -) diff --git a/flac.go b/flac.go deleted file mode 100644 index 82307d5..0000000 --- a/flac.go +++ /dev/null @@ -1,110 +0,0 @@ -package audiometa - -import ( - "bytes" - "image" - "io" - "os" - "path/filepath" - - "github.com/aler9/writerseeker" - "github.com/gcottom/audiometa/v2/flac" -) - -type flacBlock struct { - cmts *flac.MetaDataBlockVorbisComment - pic *image.Image - picIdx int - cmtIdx int -} - -func extractFLACComment(input io.Reader) (*flac.File, *flacBlock, error) { - fb := flacBlock{} - f, err := flac.ParseMetadata(input) - if err != nil { - return nil, nil, ErrFLACParse - } - for idx, meta := range f.Meta { - if meta.Type == flac.VorbisComment { - fb.cmts, err = flac.ParseFromMetaDataBlock(*meta) - fb.cmtIdx = idx - if err != nil { - return nil, nil, ErrFLACCmtParse - } - continue - } else if meta.Type == flac.Picture { - if pic, err := flac.ParsePicFromMetaDataBlock(*meta); err == nil { - if pic != nil { - if img, _, err := image.Decode(bytes.NewReader(pic.ImageData)); err == nil { - fb.pic = &img - fb.picIdx = idx - } - } - continue - } - } - } - - return f, &fb, nil -} - -func removeFLACMetaBlock(slice []*flac.MetaDataBlock, s int) []*flac.MetaDataBlock { - return append(slice[:s], slice[s+1:]...) -} - -func flacSave(r io.Reader, w io.Writer, m []*flac.MetaDataBlock, needsTemp bool) error { - if needsTemp { - //in and out are the same file so we have to temp it - t := &writerseeker.WriterSeeker{} - defer t.Close() - // Write tag in new file. - if _, err := t.Write([]byte("fLaC")); err != nil { - return err - } - for i, meta := range m { - last := i == len(m)-1 - if _, err := t.Write(meta.Marshal(last)); err != nil { - return err - } - } - if _, err := io.Copy(t, r); err != nil { - return err - } - if _, err := t.Seek(0, io.SeekStart); err != nil { - return err - } - - f := w.(*os.File) - path, err := filepath.Abs(f.Name()) - if err != nil { - return err - } - w2, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) - if err != nil { - return err - } - defer w2.Close() - if _, err := io.Copy(w2, bytes.NewReader(t.Bytes())); err != nil { - return err - } - if _, err = f.Seek(0, io.SeekEnd); err != nil { - return err - } - return nil - } - - if _, err := w.Write([]byte("fLaC")); err != nil { - return err - } - for i, meta := range m { - last := i == len(m)-1 - if _, err := w.Write(meta.Marshal(last)); err != nil { - return err - } - } - if _, err := io.Copy(w, r); err != nil { - return err - } - return nil - -} diff --git a/flac/const.go b/flac/const.go deleted file mode 100644 index a00ff0d..0000000 --- a/flac/const.go +++ /dev/null @@ -1,38 +0,0 @@ -package flac - -const ( - // FIELD_TITLE Track/Work name - FIELD_TITLE = "TITLE" - // FIELD_VERSION The version field may be used to differentiate multiple versions of the same track title in a single collection. (e.g. remix info) - FIELD_VERSION = "VERSION" - // FIELD_ALBUM The collection name to which this track belongs - FIELD_ALBUM = "ALBUM" - // FIELD_TRACKNUMBER The track number of this piece if part of a specific larger collection or album - FIELD_TRACKNUMBER = "TRACKNUMBER" - // FIELD_ARTIST The artist generally considered responsible for the work. In popular music this is usually the performing band or singer. For classical music it would be the composer. For an audio book it would be the author of the original text. - FIELD_ARTIST = "ARTIST" - // FIELD_PERFORMER The artist(s) who performed the work. In classical music this would be the conductor, orchestra, soloists. In an audio book it would be the actor who did the reading. In popular music this is typically the same as the ARTIST and is omitted. - FIELD_PERFORMER = "PERFORMER" - // FIELD_COPYRIGHT Copyright attribution, e.g., '2001 Nobody's Band' or '1999 Jack Moffitt' - FIELD_COPYRIGHT = "COPYRIGHT" - // FIELD_LICENSE License information, eg, 'All Rights Reserved', 'Any Use Permitted', a URL to a license such as a Creative Commons license ("www.creativecommons.org/blahblah/license.html") or the EFF Open Audio License ('distributed under the terms of the Open Audio License. see http://www.eff.org/IP/Open_licenses/eff_oal.html for details'), etc. - FIELD_LICENSE = "LICENSE" - // FIELD_ORGANIZATION Name of the organization producing the track (i.e. the 'record label') - FIELD_ORGANIZATION = "ORGANIZATION" - // FIELD_DESCRIPTION A short text description of the contents - FIELD_DESCRIPTION = "DESCRIPTION" - // FIELD_GENRE A short text indication of music genre - FIELD_GENRE = "GENRE" - // FIELD_DATE Date the track was recorded - FIELD_DATE = "DATE" - // FIELD_LOCATION Location where track was recorded - FIELD_LOCATION = "LOCATION" - // FIELD_CONTACT Contact information for the creators or distributors of the track. This could be a URL, an email address, the physical address of the producing label. - FIELD_CONTACT = "CONTACT" - // FIELD_ISRC ISRC number for the track; see the ISRC intro page for more information on ISRC numbers. - FIELD_ISRC = "ISRC" -) -const ( - // MIMEURL is the MIME string indicating that imgData is a URL pointing to the image - MIMEURL = "-->" -) diff --git a/flac/errors.go b/flac/errors.go deleted file mode 100644 index 1b92756..0000000 --- a/flac/errors.go +++ /dev/null @@ -1,22 +0,0 @@ -package flac - -import "errors" - -var ( - ErrorNotVorbisComment = errors.New("not a vorbis comment metadata block") - ErrorUnexpEof = errors.New("unexpected end of stream") - ErrorMalformedComment = errors.New("malformed comment") - ErrorInvalidFieldName = errors.New("malformed field Name") - // ErrorNotPictureMetadataBlock is returned if the metadata provided is not a picture block. - ErrorNotPictureMetadataBlock = errors.New("not a picture metadata block") - // ErrorUnsupportedMIME is returned if the provided image MIME type is unsupported. - ErrorUnsupportedMIME = errors.New("unsupported MIME") - // ErrorNoFLACHeader indicates that "fLaC" marker not found at the beginning of the file - ErrorNoFLACHeader = errors.New("fLaC head incorrect") - // ErrorNoStreamInfo indicates that StreamInfo Metablock not present or is not the first Metablock - ErrorNoStreamInfo = errors.New("stream info not present") - // ErrorStreamInfoEarlyEOF indicates that an unexpected EOF is hit while reading StreamInfo Metablock - ErrorStreamInfoEarlyEOF = errors.New("unexpected end of stream while reading stream info") - // ErrorNoSyncCode indicates that the frames are malformed as the sync code is not present after the last Metablock - ErrorNoSyncCode = errors.New("frames do not begin with sync code") -) diff --git a/flac/flacfile.go b/flac/flacfile.go deleted file mode 100644 index 888c2fc..0000000 --- a/flac/flacfile.go +++ /dev/null @@ -1,73 +0,0 @@ -package flac - -import ( - "bytes" - "io" - "os" -) - -// File represents a handler of FLAC file -type File struct { - Meta []*MetaDataBlock - Frames FrameData -} - -// Marshal encodes all meta tags and returns the content of the resulting whole FLAC file -func (c *File) Marshal() []byte { - res := bytes.NewBuffer([]byte{}) - res.Write([]byte("fLaC")) - for i, meta := range c.Meta { - last := i == len(c.Meta)-1 - res.Write(meta.Marshal(last)) - } - res.Write(c.Frames) - return res.Bytes() -} - -// Save encapsulates Marshal and save the file to the file system -func (c *File) Save(fn string) error { - return os.WriteFile(fn, c.Marshal(), 0644) -} - -// ParseMetadata accepts a reader to a FLAC stream and consumes only FLAC metadata -// Frames is always nil -func ParseMetadata(f io.Reader) (*File, error) { - res := new(File) - - if err := readFLACHead(f); err != nil { - return nil, err - } - meta, err := readMetadataBlocks(f) - if err != nil { - return nil, err - } - - res.Meta = meta - - return res, nil -} - -// ParseBytes accepts a reader to a FLAC stream and returns the final file -func ParseBytes(f io.Reader) (*File, error) { - res, err := ParseMetadata(f) - if err != nil { - return nil, err - } - - res.Frames, err = readFLACStream(f) - if err != nil { - return nil, err - } - - return res, nil -} - -// ParseFile parses a FLAC file -func ParseFile(filename string) (*File, error) { - f, err := os.Open(filename) - if err != nil { - return nil, err - } - defer f.Close() - return ParseBytes(f) -} diff --git a/flac/metablock.go b/flac/metablock.go deleted file mode 100644 index b0784e6..0000000 --- a/flac/metablock.go +++ /dev/null @@ -1,60 +0,0 @@ -package flac - -import ( - "bytes" -) - -// BlockType representation of types of FLAC Metadata Block -type BlockType int - -// BlockData data in a FLAC Metadata Block. Custom Metadata decoders and modifiers should accept/modify whole MetaDataBlock instead. -type BlockData []byte - -const ( - // StreamInfo METADATA_BLOCK_STREAMINFO - // This block has information about the whole stream, like sample rate, number of channels, total number of samples, etc. It must be present as the first metadata block in the stream. Other metadata blocks may follow, and ones that the decoder doesn't understand, it will skip. - StreamInfo BlockType = iota - // Padding METADATA_BLOCK_PADDING - // This block allows for an arbitrary amount of padding. The contents of a PADDING block have no meaning. This block is useful when it is known that metadata will be edited after encoding; the user can instruct the encoder to reserve a PADDING block of sufficient size so that when metadata is added, it will simply overwrite the padding (which is relatively quick) instead of having to insert it into the right place in the existing file (which would normally require rewriting the entire file). - Padding - // Application METADATA_BLOCK_APPLICATION - // This block is for use by third-party applications. The only mandatory field is a 32-bit identifier. This ID is granted upon request to an application by the FLAC maintainers. The remainder is of the block is defined by the registered application. Visit the registration page if you would like to register an ID for your application with FLAC. - Application - // SeekTable METADATA_BLOCK_SEEKTABLE - // This is an optional block for storing seek points. It is possible to seek to any given sample in a FLAC stream without a seek table, but the delay can be unpredictable since the bitrate may vary widely within a stream. By adding seek points to a stream, this delay can be significantly reduced. Each seek point takes 18 bytes, so 1% resolution within a stream adds less than 2k. There can be only one SEEKTABLE in a stream, but the table can have any number of seek points. There is also a special 'placeholder' seekpoint which will be ignored by decoders but which can be used to reserve space for future seek point insertion. - SeekTable - // VorbisComment METADATA_BLOCK_VORBIS_COMMENT - // This block is for storing a list of human-readable name/value pairs. Values are encoded using UTF-8. It is an implementation of the Vorbis comment specification (without the framing bit). This is the only officially supported tagging mechanism in FLAC. There may be only one VORBIS_COMMENT block in a stream. In some external documentation, Vorbis comments are called FLAC tags to lessen confusion. - VorbisComment - // CueSheet METADATA_BLOCK_CUESHEET - // This block is for storing various information that can be used in a cue sheet. It supports track and index points, compatible with Red Book CD digital audio discs, as well as other CD-DA metadata such as media catalog number and track ISRCs. The CUESHEET block is especially useful for backing up CD-DA discs, but it can be used as a general purpose cueing mechanism for playback. - CueSheet - // Picture METADATA_BLOCK_PICTURE - // This block is for storing pictures associated with the file, most commonly cover art from CDs. There may be more than one PICTURE block in a file. The picture format is similar to the APIC frame in ID3v2. The PICTURE block has a type, MIME type, and UTF-8 description like ID3v2, and supports external linking via URL (though this is discouraged). The differences are that there is no uniqueness constraint on the description field, and the MIME type is mandatory. The FLAC PICTURE block also includes the resolution, color depth, and palette size so that the client can search for a suitable picture without having to scan them all. - Picture - // Reserved Reserved Metadata Block Types - Reserved - // Invalid Invalid Metadata Block Type - Invalid BlockType = 127 -) - -// MetaDataBlock is the struct representation of a FLAC Metadata Block -type MetaDataBlock struct { - Type BlockType - Data BlockData -} - -// Marshal encodes this MetaDataBlock without touching block data -// isfinal defines whether this is the last metadata block of the FLAC file -func (c *MetaDataBlock) Marshal(isfinal bool) []byte { - res := bytes.NewBuffer([]byte{}) - if isfinal { - res.WriteByte(byte(c.Type + 1<<7)) - } else { - res.WriteByte(byte(c.Type)) - } - size := encodeUint32(uint32(len(c.Data))) - res.Write(size[len(size)-3:]) - res.Write(c.Data) - return res.Bytes() -} diff --git a/flac/picture.go b/flac/picture.go deleted file mode 100644 index b27ce3d..0000000 --- a/flac/picture.go +++ /dev/null @@ -1,165 +0,0 @@ -package flac - -import ( - "bytes" - "image/jpeg" - "image/png" -) - -// PictureType defines the type of this image -type PictureType uint32 - -const ( - PictureTypeOther PictureType = iota - PictureTypeFileIcon - PictureTypeOtherIcon - PictureTypeFrontCover - PictureTypeBackCover - PictureTypeLeaflet - PictureTypeMedia - PictureTypeLeadArtist - PictureTypeArtist - PictureTypeConductor - PictureTypeBand - PictureTypeComposer - PictureTypeLyricist - PictureTypeRecordingLocation - PictureTypeDuringRecording - PictureTypeDuringPerformance - PictureTypeScreenCapture - PictureTypeBrightColouredFish - PictureTypeIllustration - PictureTypeBandArtistLogotype - PictureTypePublisherStudioLogotype -) - -// MetadataBlockPicture represents a picture metadata block -type MetadataBlockPicture struct { - PictureType PictureType - MIME string - Description string - Width uint32 - Height uint32 - ColorDepth uint32 - IndexedColorCount uint32 - ImageData []byte -} - -// Marshal encodes the PictureBlock to a standard FLAC MetaDataBloc to be accepted by go-flac -func (c *MetadataBlockPicture) Marshal() MetaDataBlock { - res := bytes.NewBuffer([]byte{}) - res.Write(encodeUint32(uint32(c.PictureType))) - res.Write(encodeUint32(uint32(len(c.MIME)))) - res.Write([]byte(c.MIME)) - res.Write(encodeUint32(uint32(len(c.Description)))) - res.Write([]byte(c.Description)) - res.Write(encodeUint32(c.Width)) - res.Write(encodeUint32(c.Height)) - res.Write(encodeUint32(c.ColorDepth)) - res.Write(encodeUint32(c.IndexedColorCount)) - res.Write(encodeUint32(uint32(len(c.ImageData)))) - res.Write(c.ImageData) - return MetaDataBlock{ - Type: Picture, - Data: res.Bytes(), - } -} - -// NewFromImageData generates a MetadataBlockPicture from image data, returns an error if the picture data connot be parsed -func NewFromImageData(pictype PictureType, description string, imgdata []byte, mime string) (*MetadataBlockPicture, error) { - res := new(MetadataBlockPicture) - res.PictureType = pictype - res.Description = description - res.MIME = mime - res.ImageData = imgdata - err := res.ParsePicture() - return res, err -} - -// ParseFromMetaDataBlock parses an existing picture MetaDataBlock -func ParsePicFromMetaDataBlock(meta MetaDataBlock) (*MetadataBlockPicture, error) { - if meta.Type != Picture { - return nil, ErrorNotPictureMetadataBlock - } - res := new(MetadataBlockPicture) - data := bytes.NewBuffer(meta.Data) - - if pictype, err := readUint32(data); err != nil { - return nil, err - } else { - res.PictureType = PictureType(pictype) - } - - if mimebytes, err := readBytesWith32bitSize(data); err != nil { - return nil, err - } else { - res.MIME = string(mimebytes) - } - - if descbytes, err := readBytesWith32bitSize(data); err != nil { - return nil, err - } else { - res.Description = string(descbytes) - } - - if width, err := readUint32(data); err != nil { - return nil, err - } else { - res.Width = width - } - - if height, err := readUint32(data); err != nil { - return nil, err - } else { - res.Height = height - } - - if depth, err := readUint32(data); err != nil { - return nil, err - } else { - res.ColorDepth = depth - } - - if count, err := readUint32(data); err != nil { - return nil, err - } else { - res.IndexedColorCount = count - } - - if pic, err := readBytesWith32bitSize(data); err != nil { - return nil, err - } else { - res.ImageData = pic - } - - return res, nil -} - -// ParsePicture decodes the image and inflated the Width, Height, ColorDepth, IndexedColorCount fields. This is called automatically by NewFromImageData -func (c *MetadataBlockPicture) ParsePicture() error { - switch c.MIME { - case "image/jpeg": - img, err := jpeg.Decode(bytes.NewReader(c.ImageData)) - if err != nil { - return err - } - c.IndexedColorCount = uint32(0) - size := img.Bounds() - c.Width = uint32(size.Max.X) - c.Height = uint32(size.Max.Y) - c.ColorDepth = uint32(24) - case "image/png": - img, err := png.Decode(bytes.NewReader(c.ImageData)) - if err != nil { - return err - } - c.IndexedColorCount = uint32(0) - size := img.Bounds() - c.Width = uint32(size.Max.X) - c.Height = uint32(size.Max.Y) - c.ColorDepth = uint32(32) - default: - return ErrorUnsupportedMIME - } - return nil -} diff --git a/flac/streamdata.go b/flac/streamdata.go deleted file mode 100644 index 91e3201..0000000 --- a/flac/streamdata.go +++ /dev/null @@ -1,130 +0,0 @@ -package flac - -import ( - "bytes" - "io" -) - -type FrameData []byte - -// StreamInfoBlock represents the undecoded data of StreamInfo block -type StreamInfoBlock struct { - // BlockSizeMin The minimum block size (in samples) used in the stream. - BlockSizeMin int - // BlockSizeMax The maximum block size (in samples) used in the stream. (Minimum blocksize == maximum blocksize) implies a fixed-blocksize stream. - BlockSizeMax int - // FrameSizeMin The minimum frame size (in bytes) used in the stream. May be 0 to imply the value is not known. - FrameSizeMin int - // FrameSizeMax The maximum frame size (in bytes) used in the stream. May be 0 to imply the value is not known. - FrameSizeMax int - // SampleRate Sample rate in Hz - SampleRate int - // ChannelCount Number of channels - ChannelCount int - // BitDepth Bits per sample - BitDepth int - // SampleCount Total samples in stream. 'Samples' means inter-channel sample, i.e. one second of 44.1Khz audio will have 44100 samples regardless of the number of channels. A value of zero here means the number of total samples is unknown. - SampleCount int64 - // AudioMD5 MD5 signature of the unencoded audio data - AudioMD5 []byte -} - -// GetStreamInfo parses the first metadata block of the File which should always be StreamInfo and returns a StreamInfoBlock containing the decoded StreamInfo data. -func (c *File) GetStreamInfo() (*StreamInfoBlock, error) { - if c.Meta[0].Type != StreamInfo { - return nil, ErrorNoStreamInfo - } - streamInfo := bytes.NewReader(c.Meta[0].Data) - res := StreamInfoBlock{} - - if buf, err := readUint16(streamInfo); err != nil { - return nil, ErrorStreamInfoEarlyEOF - } else { - res.BlockSizeMin = int(buf) - } - - if buf, err := readUint16(streamInfo); err != nil { - return nil, ErrorStreamInfoEarlyEOF - } else { - res.BlockSizeMax = int(buf) - } - - buf := bytes.NewBuffer([]byte{0}) - buf24 := make([]byte, 3) - if _, err := io.ReadFull(streamInfo, buf24); err != nil { - return nil, ErrorStreamInfoEarlyEOF - } - buf.Write(buf24) - if buf, err := readUint32(buf); err != nil { - return nil, ErrorStreamInfoEarlyEOF - } else { - res.FrameSizeMin = int(buf) - } - buf.Reset() - buf.WriteByte(0) - if _, err := io.ReadFull(streamInfo, buf24); err != nil { - return nil, ErrorStreamInfoEarlyEOF - } - buf.Write(buf24) - if buf, err := readUint32(buf); err != nil { - return nil, ErrorStreamInfoEarlyEOF - } else { - res.FrameSizeMax = int(buf) - } - - buf.Reset() - buf.WriteByte(0) - smpl := make([]byte, 3) - if _, err := io.ReadFull(streamInfo, smpl); err != nil { - return nil, ErrorStreamInfoEarlyEOF - } - buf.Write(smpl) - if smplrate, err := readUint32(buf); err != nil { - return nil, ErrorStreamInfoEarlyEOF - } else { - res.SampleRate = int(smplrate >> 4) - } - if _, err := streamInfo.Seek(-1, io.SeekCurrent); err != nil { - return nil, ErrorStreamInfoEarlyEOF - } - - if channel, err := readUint8(streamInfo); err != nil { - return nil, ErrorStreamInfoEarlyEOF - } else { - res.ChannelCount = int(channel<<4>>5) + 1 - } - buf.Reset() - if _, err := streamInfo.Seek(-1, io.SeekCurrent); err != nil { - return nil, ErrorStreamInfoEarlyEOF - } - - if bitdepth, err := readUint16(streamInfo); err != nil { - return nil, ErrorStreamInfoEarlyEOF - } else { - res.BitDepth = int(bitdepth<<7>>11) + 1 - } - if _, err := streamInfo.Seek(-1, io.SeekCurrent); err != nil { - return nil, ErrorStreamInfoEarlyEOF - } - - var smplcount int64 - if count, err := readUint32(streamInfo); err != nil { - return nil, ErrorStreamInfoEarlyEOF - } else { - smplcount += int64(count<<4>>4) << 8 - } - if count, err := readUint8(streamInfo); err != nil { - return nil, ErrorStreamInfoEarlyEOF - } else { - smplcount += int64(count) - } - res.SampleCount = smplcount - - res.AudioMD5 = make([]byte, 16) - if _, err := io.ReadFull(streamInfo, res.AudioMD5); err != nil { - return nil, ErrorStreamInfoEarlyEOF - } - - return &res, nil - -} diff --git a/flac/utils.go b/flac/utils.go deleted file mode 100644 index 22587d4..0000000 --- a/flac/utils.go +++ /dev/null @@ -1,138 +0,0 @@ -package flac - -import ( - "bytes" - "encoding/binary" - "io" - "io/ioutil" -) - -func encodeUint32(n uint32) []byte { - buf := bytes.NewBuffer([]byte{}) - if err := binary.Write(buf, binary.BigEndian, n); err != nil { - panic(err) - } - return buf.Bytes() -} - -func encodeUint32L(n uint32) []byte { - buf := bytes.NewBuffer([]byte{}) - if err := binary.Write(buf, binary.LittleEndian, n); err != nil { - panic(err) - } - return buf.Bytes() -} - -func readUint32L(r io.Reader) (res uint32, err error) { - err = binary.Read(r, binary.LittleEndian, &res) - return -} - -func readUint8(r io.Reader) (res uint8, err error) { - err = binary.Read(r, binary.BigEndian, &res) - return -} - -func readUint16(r io.Reader) (res uint16, err error) { - err = binary.Read(r, binary.BigEndian, &res) - return -} - -func readUint32(r io.Reader) (res uint32, err error) { - err = binary.Read(r, binary.BigEndian, &res) - return -} - -func readBytesWith32bitSize(r io.Reader) (res []byte, err error) { - var size uint32 - size, err = readUint32(r) - if err != nil { - return - } - bufall := bytes.NewBuffer([]byte{}) - for size > 0 { - var nn int - buf := make([]byte, size) - nn, err = r.Read(buf) - if err != nil { - return - } - bufall.Write(buf) - size -= uint32(nn) - } - res = bufall.Bytes() - return -} - -func readFLACStream(f io.Reader) ([]byte, error) { - result, err := ioutil.ReadAll(f) - if err != nil { - return nil, err - } - if result[0] != 0xFF || result[1]>>2 != 0x3E { - return nil, ErrorNoSyncCode - } - return result, nil -} - -func parseMetadataBlock(f io.Reader) (block *MetaDataBlock, isfinal bool, err error) { - block = new(MetaDataBlock) - header := make([]byte, 4) - _, err = io.ReadFull(f, header) - if err != nil { - return - } - isfinal = header[0]>>7 != 0 - block.Type = BlockType(header[0] << 1 >> 1) - var length uint32 - err = binary.Read(bytes.NewBuffer(header), binary.BigEndian, &length) - if err != nil { - return - } - length = length << 8 >> 8 - - buf := make([]byte, length) - _, err = io.ReadFull(f, buf) - if err != nil { - return - } - block.Data = buf - - return -} - -func readMetadataBlocks(f io.Reader) (blocks []*MetaDataBlock, err error) { - finishMetaData := false - for !finishMetaData { - var block *MetaDataBlock - block, finishMetaData, err = parseMetadataBlock(f) - if err != nil { - return - } - blocks = append(blocks, block) - } - return -} - -func readFLACHead(f io.Reader) error { - buffer := make([]byte, 4) - _, err := io.ReadFull(f, buffer) - if err != nil { - return err - } - if string(buffer) != "fLaC" { - return ErrorNoFLACHeader - } - return nil -} - -func packStr(w io.Writer, s string) error { - data := []byte(s) - if _, err := w.Write(encodeUint32L(uint32(len(data)))); err != nil { - return err - } - if _, err := w.Write(data); err != nil { - return err - } - return nil -} diff --git a/flac/vorbis.go b/flac/vorbis.go deleted file mode 100644 index eaae825..0000000 --- a/flac/vorbis.go +++ /dev/null @@ -1,111 +0,0 @@ -package flac - -import ( - "bytes" - "strings" -) - -type MetaDataBlockVorbisComment struct { - Vendor string - Comments []string -} - -// New creates a new MetaDataBlockVorbisComment -// vendor is set to flacvorbis by default -func New() *MetaDataBlockVorbisComment { - return &MetaDataBlockVorbisComment{ - "audiometa", - []string{}, - } -} - -// Get get all comments with field name specified by the key parameter -// If there is no match, error would still be nil -func (c *MetaDataBlockVorbisComment) Get(key string) ([]string, error) { - res := make([]string, 0) - for _, cmt := range c.Comments { - p := strings.SplitN(cmt, "=", 2) - if len(p) != 2 { - return nil, ErrorMalformedComment - } - if strings.EqualFold(p[0], key) { - res = append(res, p[1]) - } - } - return res, nil -} - -// Add adds a key-val pair to the comments -func (c *MetaDataBlockVorbisComment) Add(key string, val string) error { - for _, char := range key { - if char < 0x20 || char > 0x7d || char == '=' { - return ErrorInvalidFieldName - } - } - c.Comments = append(c.Comments, key+"="+val) - return nil -} - -// Marshal marshals this block back into a flac.MetaDataBlock -func (c MetaDataBlockVorbisComment) Marshal() (MetaDataBlock, error) { - data := bytes.NewBuffer([]byte{}) - if err := packStr(data, c.Vendor); err != nil { - return MetaDataBlock{}, err - } - data.Write(encodeUint32L(uint32(len(c.Comments)))) - for _, cmt := range c.Comments { - if err := packStr(data, cmt); err != nil { - return MetaDataBlock{}, err - } - } - return MetaDataBlock{ - Type: VorbisComment, - Data: data.Bytes(), - }, nil -} - -// ParseFromMetaDataBlock parses an existing picture MetaDataBlock -func ParseFromMetaDataBlock(meta MetaDataBlock) (*MetaDataBlockVorbisComment, error) { - if meta.Type != VorbisComment { - return nil, ErrorNotVorbisComment - } - - reader := bytes.NewReader(meta.Data) - res := new(MetaDataBlockVorbisComment) - - vendorlen, err := readUint32L(reader) - if err != nil { - return nil, err - } - vendorbytes := make([]byte, vendorlen) - nn, err := reader.Read(vendorbytes) - if err != nil { - return nil, err - } - if nn != int(vendorlen) { - return nil, ErrorUnexpEof - } - res.Vendor = string(vendorbytes) - - cmtcount, err := readUint32L(reader) - if err != nil { - return nil, err - } - res.Comments = make([]string, cmtcount) - for i := range res.Comments { - cmtlen, err := readUint32L(reader) - if err != nil { - return nil, err - } - cmtbytes := make([]byte, cmtlen) - nn, err := reader.Read(cmtbytes) - if err != nil { - return nil, err - } - if nn != int(cmtlen) { - return nil, ErrorUnexpEof - } - res.Comments[i] = string(cmtbytes) - } - return res, nil -} diff --git a/flac_test.go b/flac_test.go deleted file mode 100644 index add9592..0000000 --- a/flac_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package audiometa - -import ( - "errors" - "testing" - - "github.com/gcottom/audiometa/v2/flac" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestExtractFLACCommentErrorCases(t *testing.T) { - // Test case where flac.ParseMetadata returns an error - // Mock the input io.Reader to return an error when passed to flac.ParseMetadata - input := &mockReader{} - _, _, err := extractFLACComment(input) - assert.Error(t, err) -} - -func TestRemoveFLACMetaBlock(t *testing.T) { - // Test case where s is out of bounds - // Create a slice with some elements - slice := []*flac.MetaDataBlock{{}, {}, {}} - // Call removeFLACMetaBlock with an index greater than the length of the slice - result := removeFLACMetaBlock(slice, 1) - assert.Len(t, result, 2) - // Add more test cases as needed -} - -func TestFLACSaveErrorCases(t *testing.T) { - // Test case where needsTemp is true and creating temp file fails - // Mock the input io.Reader and io.Writer - input := &mockReader{} - output := &mockWriter{} - // Set needsTemp to true - needsTemp := true - err := flacSave(input, output, []*flac.MetaDataBlock{}, needsTemp) - assert.Error(t, err) - // Add more test cases as needed -} - -func TestFLACSave(t *testing.T) { - - t.Run("Error Writing FLAC Header", func(t *testing.T) { - mockReader := new(mockReader2) - mockWriter := new(mockWriter2) - metaBlocks := []*flac.MetaDataBlock{ - // Add mock MetaDataBlocks as needed - } - mockWriter.On("Write", mock.Anything).Return(0, errors.New("write error")) - - err := flacSave(mockReader, mockWriter, metaBlocks, false) - assert.Error(t, err) - assert.Equal(t, "write error", err.Error()) - mockWriter.AssertExpectations(t) - }) - - t.Run("Error Writing MetaDataBlock", func(t *testing.T) { - mockReader := new(mockReader2) - mockWriter := new(mockWriter2) - metaBlocks := []*flac.MetaDataBlock{ - // Add mock MetaDataBlocks as needed - } - mockReader.On("Read", mock.Anything).Return(8, nil) - mockWriter.On("Write", mock.Anything).Return(0, errors.New("write error")) - - err := flacSave(mockReader, mockWriter, metaBlocks, false) - assert.Error(t, err) - assert.Equal(t, "write error", err.Error()) - mockWriter.AssertExpectations(t) - }) - - t.Run("Error Copying Data", func(t *testing.T) { - mockReader := new(mockReader2) - mockWriter := new(mockWriter2) - metaBlocks := []*flac.MetaDataBlock{ - // Add mock MetaDataBlocks as needed - } - mockWriter.On("Write", mock.Anything).Return(0, nil) - mockReader.On("Read", mock.Anything).Return(0, errors.New("read error")) - - err := flacSave(mockReader, mockWriter, metaBlocks, false) - assert.Error(t, err) - assert.Equal(t, "read error", err.Error()) - mockWriter.AssertExpectations(t) - }) -} - -// Helper structs for mocking io.Reader and io.Writer -type mockReader struct{} - -func (m *mockReader) Read(p []byte) (n int, err error) { - return 0, errors.New("error while mock reading") -} - -type mockWriter struct{} - -func (m *mockWriter) Write(p []byte) (n int, err error) { - return 0, errors.New("mock error writing to writer") -} - -type mockReader2 struct { - mock.Mock -} - -func (m *mockReader2) Read(p []byte) (n int, err error) { - args := m.Called(p) - return args.Int(0), args.Error(1) -} - -type mockWriter2 struct { - mock.Mock -} - -func (m *mockWriter2) Write(p []byte) (n int, err error) { - args := m.Called(p) - return args.Int(0), args.Error(1) -} - -func (m *mockWriter2) Seek(offset int64, whence int) (int64, error) { - args := m.Called(offset, whence) - return args.Get(0).(int64), args.Error(1) -} diff --git a/go.mod b/go.mod deleted file mode 100644 index 29b9fd4..0000000 --- a/go.mod +++ /dev/null @@ -1,20 +0,0 @@ -module github.com/gcottom/audiometa/v2 - -go 1.18 - -require ( - github.com/abema/go-mp4 v1.2.0 - github.com/aler9/writerseeker v1.1.0 - github.com/bogem/id3v2/v2 v2.1.4 - github.com/stretchr/testify v1.9.0 - github.com/sunfish-shogi/bufseekio v0.1.0 -) - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/objx v0.5.2 // indirect - golang.org/x/text v0.14.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/mp4_decoder.go b/mp4_decoder.go deleted file mode 100644 index ccbe93f..0000000 --- a/mp4_decoder.go +++ /dev/null @@ -1,282 +0,0 @@ -package audiometa - -import ( - "bytes" - "encoding/binary" - "image" - "io" - "strconv" - "strings" -) - -var atomTypes = map[int]string{ - 0: "implicit", // automatic based on atom name - 1: "text", - 13: "png", - 14: "jpg", - 21: "uint8", -} - -// NB: atoms does not include "----", this is handled separately -var atoms = atomNames(map[string]string{ - "\xa9alb": "album", - "\xa9art": "artist", - "\xa9ART": "artist", - "aART": "album_artist", - "\xa9day": "year", - "\xa9nam": "title", - "\xa9gen": "genre", - "trkn": "track", - "\xa9wrt": "composer", - "\xa9too": "encoder", - "cprt": "copyright", - "covr": "picture", - "\xa9grp": "grouping", - "keyw": "keyword", - "\xa9lyr": "lyrics", - "\xa9cmt": "comment", - "tmpo": "tempo", - "cpil": "compilation", - "disk": "disc", -}) - -type atomNames map[string]string - -func (f atomNames) name(n string) []string { - res := make([]string, 1) - for k, v := range f { - if v == n { - res = append(res, k) - } - } - return res -} - -// metadataMP4 is the implementation of Metadata for MP4 tag (atom) data. -type metadataMP4 struct { - data map[string]interface{} -} - -func readFromMP4(r io.ReadSeeker) (metadataMP4, error) { - return readAtoms(r) -} - -// ReadAtoms reads MP4 metadata atoms from the io.ReadSeeker into a Metadata, returning -// non-nil error if there was a problem. -func readAtoms(r io.ReadSeeker) (metadataMP4, error) { - m := metadataMP4{ - data: make(map[string]interface{}), - } - err := m.readAtoms(r) - return m, err -} - -func (m metadataMP4) readAtoms(r io.ReadSeeker) error { - for { - name, size, err := readAtomHeader(r) - if err != nil { - if err == io.EOF { - return nil - } - return err - } - - switch name { - case "meta": - _, err := readBytes(r, 4) - if err != nil { - return err - } - fallthrough - - case "moov", "udta", "ilst": - return m.readAtoms(r) - } - - _, ok := atoms[name] - var data []string - - if !ok { - _, err := r.Seek(int64(size-8), io.SeekCurrent) - if err != nil { - return err - } - continue - } - - err = m.readAtomData(r, name, size-8, data) - if err != nil { - return err - } - } -} - -func (m metadataMP4) readAtomData(r io.ReadSeeker, name string, size uint32, processedData []string) error { - var b []byte - var err error - var contentType string - if len(processedData) > 0 { - b = []byte(strings.Join(processedData, ";")) // add delimiter if multiple data fields - contentType = "text" - } else { - // read the data - b, err = readBytes(r, uint(size)) - if err != nil { - return err - } - if len(b) < 8 { - return ErrMP4InvalidEncoding - } - - // "data" + size (4 bytes each) - b = b[8:] - - if len(b) < 4 { - return ErrMP4InvalidEncoding - } - class := getInt(b[1:4]) - var ok bool - contentType, ok = atomTypes[class] - if !ok { - return ErrMP4InvalidCntntType - } - - // 4: atom version (1 byte) + atom flags (3 bytes) - // 4: NULL (usually locale indicator) - if len(b) < 8 { - return ErrMP4InvalidEncoding - } - b = b[8:] - } - - if name == "trkn" || name == "disk" { - if len(b) < 6 { - return ErrMP4InvalidEncoding - } - - m.data[name] = int(b[3]) - m.data[name+"_count"] = int(b[5]) - return nil - } - - if contentType == "implicit" { - if name == "covr" { - contentType = "png" - } - } - - var data interface{} - switch contentType { - case "implicit": - if _, ok := atoms[name]; ok { - return ErrMP4InvalidCntntType - } - return nil - - case "text": - data = string(b) - - case "uint8": - if len(b) < 1 { - return ErrMP4InvalidEncoding - } - data = getInt(b[:1]) - - case "jpeg", "png": - if img, _, err := image.Decode(bytes.NewReader(b)); err == nil { - data = &img - } - } - m.data[name] = data - - return nil -} - -func readAtomHeader(r io.ReadSeeker) (name string, size uint32, err error) { - err = binary.Read(r, binary.BigEndian, &size) - if err != nil { - return - } - name, err = readString(r, 4) - return -} - -func (m metadataMP4) getString(n []string) string { - for _, k := range n { - if x, ok := m.data[k]; ok { - return x.(string) - } - } - return "" -} - -func (m metadataMP4) getInt(n []string) int { - for _, k := range n { - if x, ok := m.data[k]; ok { - return x.(int) - } - } - return 0 -} - -func (m metadataMP4) title() string { - return m.getString(atoms.name("title")) -} - -func (m metadataMP4) artist() string { - return m.getString(atoms.name("artist")) -} - -func (m metadataMP4) album() string { - return m.getString(atoms.name("album")) -} - -func (m metadataMP4) albumArtist() string { - return m.getString(atoms.name("album_artist")) -} - -func (m metadataMP4) composer() string { - return m.getString(atoms.name("composer")) -} - -func (m metadataMP4) genre() string { - return m.getString(atoms.name("genre")) -} - -func (m metadataMP4) year() int { - date := m.getString(atoms.name("year")) - if len(date) >= 4 { - year, _ := strconv.Atoi(date[:4]) - return year - } - return 0 -} - -func (m metadataMP4) comment() string { - t, ok := m.data["\xa9cmt"] - if !ok { - return "" - } - return t.(string) -} - -func (m metadataMP4) picture() *image.Image { - v, ok := m.data["covr"] - if !ok { - return nil - } - return v.(*image.Image) - -} - -func (m metadataMP4) tempo() int { - return m.getInt(atoms.name("tempo")) -} - -func (m metadataMP4) encoder() string { - return m.getString(atoms.name("encoder")) -} - -func (m metadataMP4) copyright() string { - return m.getString(atoms.name("copyright")) -} diff --git a/mp4_encoder.go b/mp4_encoder.go deleted file mode 100644 index b3e5479..0000000 --- a/mp4_encoder.go +++ /dev/null @@ -1,293 +0,0 @@ -package audiometa - -import ( - "bytes" - "image/png" - "io" - "os" - "path/filepath" - "reflect" - - "github.com/abema/go-mp4" - "github.com/aler9/writerseeker" - "github.com/sunfish-shogi/bufseekio" -) - -var atomsMap = map[string]mp4.BoxType{ - "album": {'\251', 'a', 'l', 'b'}, - "albumArtist": {'a', 'A', 'R', 'T'}, - "artist": {'\251', 'A', 'R', 'T'}, - "comments": {'\251', 'c', 'm', 't'}, - "composer": {'\251', 'w', 'r', 't'}, - "copyrightMsg": {'c', 'p', 'r', 't'}, - "albumArt": {'c', 'o', 'v', 'r'}, - "genre": {'\251', 'g', 'e', 'n'}, - "title": {'\251', 'n', 'a', 'm'}, - "year": {'\251', 'd', 'a', 'y'}, -} - -// Make new atoms and write to. -func createAndWrite(w *mp4.Writer, ctx mp4.Context, _tags *IDTag) error { - for tagName, boxType := range atomsMap { - if tagName == "albumArt" { - if _tags.albumArt != nil { - buf := new(bytes.Buffer) - if err := png.Encode(buf, *_tags.albumArt); err == nil { - if _, err := w.StartBox(&mp4.BoxInfo{Type: boxType}); err != nil { - return err - } - if _, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeData()}); err != nil { - return err - } - var boxData = &mp4.Data{ - DataType: mp4.DataTypeBinary, - Data: buf.Bytes(), - } - dataCtx := ctx - dataCtx.UnderIlstMeta = true - if _, err := mp4.Marshal(w, boxData, dataCtx); err != nil { - return err - } - if _, err := w.EndBox(); err != nil { - return err - } - if _, err := w.EndBox(); err != nil { - return err - } - } - } - continue - } - val := reflect.ValueOf(*_tags).FieldByName(tagName).String() - if val == "" { - continue - } - - if _, err := w.StartBox(&mp4.BoxInfo{Type: boxType}); err != nil { - return err - } - if _, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeData()}); err != nil { - return err - } - var boxData = &mp4.Data{ - DataType: mp4.DataTypeStringUTF8, - Data: []byte(val), - } - dataCtx := ctx - dataCtx.UnderIlstMeta = true - if _, err := mp4.Marshal(w, boxData, dataCtx); err != nil { - return err - } - if _, err := w.EndBox(); err != nil { - return err - } - if _, err := w.EndBox(); err != nil { - return err - } - } - return nil - -} - -func containsAtom(boxType mp4.BoxType, boxes []mp4.BoxType) mp4.BoxType { - for _, _boxType := range boxes { - if boxType == _boxType { - return boxType - } - } - return mp4.BoxType{} -} - -func getAtomsList() []mp4.BoxType { - var atomsList []mp4.BoxType - for _, atom := range atomsMap { - atomsList = append(atomsList, atom) - } - return atomsList -} - -func writeMP4(r *bufseekio.ReadSeeker, wo io.Writer, _tags *IDTag, delete MP4Delete) error { - atomsList := getAtomsList() - - ws := &writerseeker.WriterSeeker{} - defer ws.Close() - - w := mp4.NewWriter(ws) - var mdatOffsetDiff int64 - var stcoOffsets []int64 - closedTags := false - - _, err := mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) { - switch h.BoxInfo.Type { - - case containsAtom(h.BoxInfo.Type, atomsList): - return nil, nil - - case mp4.BoxTypeFree(): - if !closedTags { - _, err := w.EndBox() - if err != nil { - return nil, err - } - if err := w.CopyBox(r, &h.BoxInfo); err != nil { - return nil, err - } - _, err = w.EndBox() - if err != nil { - return nil, err - } - _, err = w.EndBox() - if err != nil { - return nil, err - } - _, err = w.EndBox() - if err != nil { - return nil, err - } - closedTags = true - return nil, nil - } - if err := w.CopyBox(r, &h.BoxInfo); err != nil { - return nil, err - } - return nil, nil - - case mp4.BoxTypeMeta(): - _, err := w.StartBox(&h.BoxInfo) - if err != nil { - return nil, err - } - box, _, err := h.ReadPayload() - if err != nil { - return nil, err - } - if _, err = mp4.Marshal(w, box, h.BoxInfo.Context); err != nil { - return nil, err - } - return h.Expand() - - case mp4.BoxTypeMoov(), - mp4.BoxTypeUdta(): - _, err := w.StartBox(&h.BoxInfo) - if err != nil { - return nil, err - } - box, _, err := h.ReadPayload() - if err != nil { - return nil, err - } - if _, err = mp4.Marshal(w, box, h.BoxInfo.Context); err != nil { - return nil, err - } - return h.Expand() - - case mp4.BoxTypeIlst(): - _, err := w.StartBox(&h.BoxInfo) - if err != nil { - return nil, err - } - ctx := h.BoxInfo.Context - if err = createAndWrite(w, ctx, _tags); err != nil { - return nil, err - } - return h.Expand() - - default: - if h.BoxInfo.Type == mp4.BoxTypeStco() { - offset, _ := w.Seek(0, io.SeekCurrent) - stcoOffsets = append(stcoOffsets, offset) - } - if h.BoxInfo.Type == mp4.BoxTypeMdat() { - iOffset := int64(h.BoxInfo.Offset) - oOffset, _ := w.Seek(0, io.SeekCurrent) - mdatOffsetDiff = oOffset - iOffset - } - if err := w.CopyBox(r, &h.BoxInfo); err != nil { - return nil, err - } - } - return nil, nil - }) - if err != nil { - return err - } - - ts := bufseekio.NewReadSeeker(bytes.NewReader(ws.Bytes()), 1024*1024, 3) - - _, err = mp4.ReadBoxStructure(ts, func(h *mp4.ReadHandle) (any, error) { - switch h.BoxInfo.Type { - case mp4.BoxTypeStco(): - stcoOffsets = append(stcoOffsets, int64(h.BoxInfo.Offset)) - default: - return h.Expand() - } - return nil, nil - }) - if err != nil { - return err - } - - if _, err = ws.Seek(0, io.SeekStart); err != nil { - return err - } - // if mdat box is moved, update stco box - if mdatOffsetDiff != 0 { - for _, stcoOffset := range stcoOffsets { - // seek to stco box header - if _, err := ts.Seek(stcoOffset, io.SeekStart); err != nil { - return err - } - // read box header - bi, err := mp4.ReadBoxInfo(ts) - if err != nil { - return err - } - // read stco box payload - var stco mp4.Stco - _, err = mp4.Unmarshal(ts, bi.Size-bi.HeaderSize, &stco, bi.Context) - if err != nil { - return err - } - // update chunk offsets - for i := range stco.ChunkOffset { - stco.ChunkOffset[i] += uint32(mdatOffsetDiff) - } - // seek to stco box payload - _, err = bi.SeekToPayload(ws) - if err != nil { - return err - } - // write stco box payload - if _, err := mp4.Marshal(ws, &stco, bi.Context); err != nil { - return err - } - } - } - if _, err = ws.Seek(0, io.SeekStart); err != nil { - return err - } - if reflect.TypeOf(wo) == reflect.TypeOf(new(os.File)) { - f := wo.(*os.File) - path, err := filepath.Abs(f.Name()) - if err != nil { - return err - } - w2, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) - if err != nil { - return err - } - defer w2.Close() - if _, err = w2.Write(ws.Bytes()); err != nil { - return err - } - if _, err = f.Seek(0, io.SeekEnd); err != nil { - return err - } - return nil - } - if _, err := wo.Write(ws.Bytes()); err != nil { - return err - } - return nil - -} diff --git a/mp4_test.go b/mp4_test.go deleted file mode 100644 index 72c0b1f..0000000 --- a/mp4_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package audiometa - -import ( - "image" - "os" - "path/filepath" - "testing" - - "github.com/abema/go-mp4" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -type MockWriter struct { - mock.Mock -} - -func (m *MockWriter) StartBox(boxInfo *mp4.BoxInfo) (n int, err error) { - args := m.Called(boxInfo) - return args.Int(0), args.Error(1) -} - -func (m *MockWriter) EndBox() (n int, err error) { - args := m.Called() - return args.Int(0), args.Error(1) -} - -func (m *MockWriter) Write(p []byte) (n int, err error) { - args := m.Called(p) - return args.Int(0), args.Error(1) -} - -func TestGetAtomsList(t *testing.T) { - result := getAtomsList() - expected := []mp4.BoxType{ - {'\251', 'a', 'l', 'b'}, - {'a', 'A', 'R', 'T'}, - {'\251', 'A', 'R', 'T'}, - {'\251', 'c', 'm', 't'}, - {'\251', 'w', 'r', 't'}, - {'c', 'p', 'r', 't'}, - {'c', 'o', 'v', 'r'}, - {'\251', 'g', 'e', 'n'}, - {'\251', 'n', 'a', 'm'}, - {'\251', 'd', 'a', 'y'}, - } - assert.ElementsMatch(t, expected, result) -} - -func TestMetadataGetFunctions(t *testing.T) { - imgPath, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - f, err := os.Open(imgPath) - assert.NoError(t, err) - - imgData, _, err := image.Decode(f) - assert.NoError(t, err) - - m := metadataMP4{ - data: map[string]interface{}{"\xa9day": "2008-08-09", "\xa9cmt": "a comment about comments", "covr": &imgData, "intData": 6}, - } - t.Run("test getInt", func(t *testing.T) { - intData := m.getInt([]string{"intData"}) - assert.Equal(t, 6, intData) - }) - t.Run("test getYear", func(t *testing.T) { - year := m.year() - assert.Equal(t, 2008, year) - }) - t.Run("test getComment", func(t *testing.T) { - cmnt := m.comment() - assert.Equal(t, "a comment about comments", cmnt) - }) - t.Run("test getPicture", func(t *testing.T) { - pic := m.picture() - picFile, err := os.Open(imgPath) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*pic) - - assert.True(t, compareImages(img1data, img2data)) - }) -} diff --git a/ogg.go b/ogg.go deleted file mode 100755 index 2e089a3..0000000 --- a/ogg.go +++ /dev/null @@ -1,676 +0,0 @@ -package audiometa - -import ( - "bytes" - "encoding/base64" - "encoding/binary" - "image" - "image/jpeg" - "io" - "os" - "path/filepath" - "reflect" - "strings" - - "github.com/aler9/writerseeker" - "github.com/sunfish-shogi/bufseekio" -) - -var ( - vorbisCommentPrefix = []byte("\x03vorbis") - opusTagsPrefix = []byte("OpusTags") - crcLookup [8][256]uint32 -) - -func init() { - initCRC32Table() -} - -func initCRC32Table() { - var i, j int - var polynomial uint32 = 0x04C11DB7 - var crc uint32 - - for i = 0; i <= 0xFF; i++ { - crc = uint32(i) << 24 - - for j = 0; j < 8; j++ { - if crc&(1<<31) != 0 { - crc = (crc << 1) ^ polynomial - } else { - crc = crc << 1 - } - } - - crcLookup[0][i] = crc - } - - for i = 0; i <= 0xFF; i++ { - for j = 1; j < 8; j++ { - crcLookup[j][i] = crcLookup[0][(crcLookup[j-1][i]>>24)&0xFF] ^ (crcLookup[j-1][i] << 8) - } - } -} - -// _osUpdateCRC updates the CRC with the given buffer and size -func _osUpdateCRC(crc uint32, buffer []byte, size int) uint32 { - i := 0 - for size >= 8 { - crc ^= (uint32(buffer[i]) << 24) | (uint32(buffer[i+1]) << 16) | (uint32(buffer[i+2]) << 8) | uint32(buffer[i+3]) - - crc = crcLookup[7][crc>>24] ^ crcLookup[6][(crc>>16)&0xFF] ^ - crcLookup[5][(crc>>8)&0xFF] ^ crcLookup[4][crc&0xFF] ^ - crcLookup[3][buffer[i+4]] ^ crcLookup[2][buffer[i+5]] ^ - crcLookup[1][buffer[i+6]] ^ crcLookup[0][buffer[i+7]] - - i += 8 - size -= 8 - } - - for size > 0 { - crc = (crc << 8) ^ crcLookup[0][((crc>>24)&0xFF)^uint32(buffer[i])] - i++ - size-- - } - - return crc -} - -// OggPageChecksumSet sets the checksum for the Ogg page -func OggPageChecksumSet(og *oggPage) { - if og != nil { - var crcReg uint32 - buf := make([]byte, 4) - og.Header.CRC = uint32(0) - buf[0] = 0 - buf[1] = 0 - buf[2] = 0 - buf[3] = 0 - - crcReg = _osUpdateCRC(crcReg, og.Header.toBytesSlice(), len(og.Header.toBytesSlice())) - crcReg = _osUpdateCRC(crcReg, og.Body, len(og.Body)) - - buf[0] = byte(crcReg & 0xFF) - buf[1] = byte((crcReg >> 8) & 0xFF) - buf[2] = byte((crcReg >> 16) & 0xFF) - buf[3] = byte((crcReg >> 24) & 0xFF) - - og.Header.CRC = binary.LittleEndian.Uint32(buf) - } -} - -type oggDemuxer struct { - packetBufs map[uint32]*bytes.Buffer -} - -// Read ogg packets, can return empty slice of packets and nil err -// if more data is needed -func (o *oggDemuxer) read(r io.Reader) ([][]byte, error) { - var oh oggPageHeader - if err := binary.Read(r, binary.LittleEndian, &oh); err != nil { - return nil, err - } - - if !bytes.Equal(oh.Magic[:], []byte("OggS")) { - return nil, ErrOggInvalidHeader - } - - segmentTable := make([]byte, oh.Segments) - if _, err := io.ReadFull(r, segmentTable); err != nil { - return nil, err - } - var segmentsSize int64 - for _, s := range segmentTable { - segmentsSize += int64(s) - } - segmentsData := make([]byte, segmentsSize) - if _, err := io.ReadFull(r, segmentsData); err != nil { - return nil, err - } - - if o.packetBufs == nil { - o.packetBufs = map[uint32]*bytes.Buffer{} - } - - var packetBuf *bytes.Buffer - continued := oh.Flags&0x1 != 0 - if continued { - if b, ok := o.packetBufs[oh.SerialNumber]; ok { - packetBuf = b - } else { - return nil, ErrOggMissingCOP - } - } else { - packetBuf = &bytes.Buffer{} - } - - var packets [][]byte - var p int - for _, s := range segmentTable { - packetBuf.Write(segmentsData[p : p+int(s)]) - if s < 255 { - packets = append(packets, packetBuf.Bytes()) - packetBuf = &bytes.Buffer{} - } - p += int(s) - } - - o.packetBufs[oh.SerialNumber] = packetBuf - - return packets, nil -} - -// ReadOggTags reads Ogg metadata from the io.ReadSeeker, returning the resulting -// metadata in a Metadata implementation, or non-nil error if there was a problem. -func readOggTags(r io.Reader) (*IDTag, error) { - od := &oggDemuxer{} - for { - bs, err := od.read(r) - if err != nil { - return nil, err - } - - for _, b := range bs { - switch { - case bytes.HasPrefix(b, vorbisCommentPrefix): - m := &metadataOgg{ - newMetadataVorbis(), - } - resultTag, err := m.readVorbisComment(bytes.NewReader(b[len(vorbisCommentPrefix):])) - resultTag.codec = "vorbis" - return resultTag, err - case bytes.HasPrefix(b, opusTagsPrefix): - m := &metadataOgg{ - newMetadataVorbis(), - } - resultTag, err := m.readVorbisComment(bytes.NewReader(b[len(opusTagsPrefix):])) - resultTag.codec = "opus" - return resultTag, err - } - } - } -} -func newMetadataVorbis() *metadataVorbis { - return &metadataVorbis{ - c: make(map[string]string), - } -} - -type metadataOgg struct { - *metadataVorbis -} - -type metadataVorbis struct { - c map[string]string // the vorbis comments - p []byte -} - -// Read the vorbis comments from an ogg vorbis or ogg opus file -func (m *metadataVorbis) readVorbisComment(r io.Reader) (*IDTag, error) { - var resultTag IDTag - resultTag.PassThrough = make(map[string]string) - vendorLen, err := readUint32LittleEndian(r) - if err != nil { - return nil, err - } - - vendor, err := readString(r, uint(vendorLen)) - if err != nil { - return nil, err - } - m.c["vendor"] = vendor - - commentsLen, err := readUint32LittleEndian(r) - if err != nil { - return nil, err - } - - for i := uint32(0); i < commentsLen; i++ { - l, err := readUint32LittleEndian(r) - if err != nil { - return nil, err - } - cmt, err := readString(r, uint(l)) - if err != nil { - return nil, err - } - split := strings.Split(cmt, "=") - if len(split) == 2 { - temp := strings.ToUpper(split[0]) - if temp != "ALBUM" && temp != "ARTIST" && temp != "ALBUMARTIST" && temp != "DATE" && temp != "TITLE" && temp != "GENRE" && temp != "COMMENT" && temp != "COPYRIGHT" && temp != "PUBLISHER" && temp != "METADATA_BLOCK_PICTURE" && temp != "COMPOSER" { - resultTag.PassThrough[temp] = split[1] - } else { - m.c[temp] = split[1] - } - } - } - resultTag.album = m.c["ALBUM"] - resultTag.artist = m.c["ARTIST"] - resultTag.albumArtist = m.c["ALBUMARTIST"] - resultTag.date = m.c["DATE"] - resultTag.title = m.c["TITLE"] - resultTag.genre = m.c["GENRE"] - resultTag.comments = m.c["COMMENT"] - resultTag.copyrightMsg = m.c["COPYRIGHT"] - resultTag.publisher = m.c["PUBLISHER"] - resultTag.composer = m.c["COMPOSER"] - - if b64data, ok := m.c["METADATA_BLOCK_PICTURE"]; ok { - data, err := base64.StdEncoding.DecodeString(b64data) - if err != nil { - return nil, err - } - if err = m.readPictureBlock(bytes.NewReader(data)); err != nil { - return nil, err - } - } - if len(m.p) > 0 { - if img, _, err := image.Decode(bytes.NewReader(m.p)); err == nil { - resultTag.albumArt = &img - } - } - return &resultTag, nil -} - -// Read the vorbis comment picture block -func (m *metadataVorbis) readPictureBlock(r io.Reader) error { - //skipping picture type - if _, err := readInt(r, 4); err != nil { - return err - } - mimeLen, err := readUint(r, 4) - if err != nil { - return err - } - //skipping mime type - if _, err := readString(r, mimeLen); err != nil { - return err - } - descLen, err := readUint(r, 4) - if err != nil { - return err - } - //skipping description - if _, err := readString(r, descLen); err != nil { - return err - } - - //skip width <32>, height <32>, colorDepth <32>, coloresUsed <32> - - // width - if _, err = readInt(r, 4); err != nil { - return err - } - // height - if _, err = readInt(r, 4); err != nil { - return err - } - // color depth - if _, err = readInt(r, 4); err != nil { - return err - } - // colors used - if _, err = readInt(r, 4); err != nil { - return err - } - - dataLen, err := readInt(r, 4) - if err != nil { - return err - } - data := make([]byte, dataLen) - if _, err = io.ReadFull(r, data); err != nil { - return err - } - - m.p = data - return nil -} - -// Saves the tags for an ogg Opus file -func saveOpusTags(tag *IDTag, w io.Writer) error { - needsTemp := reflect.TypeOf(w) == reflect.TypeOf(new(os.File)) - var t *writerseeker.WriterSeeker - var encoder *oggEncoder - if needsTemp { - //in and out are the same file so we have to temp it - t = &writerseeker.WriterSeeker{} - defer t.Close() - } - readDat, err := io.ReadAll(tag.reader) - if err != nil { - return err - } - r := bytes.NewReader(readDat) - decoder := newOggDecoder(r) - page, err := decoder.decodeOgg() - if err != nil { - return err - } - if needsTemp { - encoder = newOggEncoder(page.Header.SerialNumber, t) - } else { - encoder = newOggEncoder(page.Header.SerialNumber, w) - } - if err = encoder.encodeBOS(page.Header.GranulePosition, page.Packets); err != nil { - return err - } - var vorbisCommentPage *oggPage - for { - page, err := decoder.decodeOgg() - if err != nil { - if err == io.EOF { - break // Reached the end of the input Ogg stream - } - return err - } - - // Find the Vorbis comment page and store it - if hasOpusCommentPrefix(page.Packets) { - vorbisCommentPage = &page - // Step 5: Prepare the new Vorbis comment packet with updated metadata and album art - commentFields := []string{} - if tag.album != "" { - commentFields = append(commentFields, "ALBUM="+tag.album) - } - if tag.artist != "" { - commentFields = append(commentFields, "ARTIST="+tag.artist) - } - if tag.genre != "" { - commentFields = append(commentFields, "GENRE="+tag.genre) - } - if tag.title != "" { - commentFields = append(commentFields, "TITLE="+tag.title) - } - if tag.date != "" { - commentFields = append(commentFields, "DATE="+tag.date) - } - if tag.albumArtist != "" { - commentFields = append(commentFields, "ALBUMARTIST="+tag.albumArtist) - } - if tag.comments != "" { - commentFields = append(commentFields, "COMMENT="+tag.comments) - } - if tag.publisher != "" { - commentFields = append(commentFields, "PUBLISHER="+tag.publisher) - } - if tag.copyrightMsg != "" { - commentFields = append(commentFields, "COPYRIGHT="+tag.copyrightMsg) - } - if tag.composer != "" { - commentFields = append(commentFields, "COMPOSER="+tag.composer) - } - for key, value := range tag.PassThrough { - commentFields = append(commentFields, key+"="+value) - } - img := []byte{} - if tag.albumArt != nil { - // Convert album art image to JPEG format - buf := new(bytes.Buffer) - if err := jpeg.Encode(buf, *tag.albumArt, nil); err == nil { - img, _ = createMetadataBlockPicture(buf.Bytes()) - } - - } - - // Create the new Vorbis comment packet - commentPacket := createOpusCommentPacket(commentFields, img) - - // Replace the Vorbis comment packet in the original page with the new packet - vorbisCommentPage.Packets[0] = commentPacket - - // Step 6: Write the updated Vorbis comment page to the output file - if err = encoder.encode(vorbisCommentPage.Header.GranulePosition, vorbisCommentPage.Packets); err != nil { - return err - } - } else { - // Write non-Vorbis comment pages to the output file - if page.Header.Flags == EOS { - if err = encoder.encodeEOS(page.Header.GranulePosition, page.Packets); err != nil { - return err - } - } else { - if err = encoder.encode(page.Header.GranulePosition, page.Packets); err != nil { - return err - } - } - } - } - // Step 7: Close and rename the files to the original file - if needsTemp { - f := w.(*os.File) - path, err := filepath.Abs(f.Name()) - if err != nil { - return err - } - w2, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) - if err != nil { - return err - } - defer w2.Close() - if _, err := io.Copy(w2, bytes.NewReader(t.Bytes())); err != nil { - return err - } - if _, err = f.Seek(0, io.SeekEnd); err != nil { - return err - } - } - return nil -} - -// Saves the given tag structure to a ogg vorbis audio file -func saveVorbisTags(tag *IDTag, w io.Writer) error { - needsTemp := reflect.TypeOf(w) == reflect.TypeOf(new(os.File)) - var t *writerseeker.WriterSeeker - var encoder *oggEncoder - if needsTemp { - //in and out are the same file so we have to temp it - t = &writerseeker.WriterSeeker{} - defer t.Close() - } - r := bufseekio.NewReadSeeker(tag.reader, 128*1024, 4) - decoder := newOggDecoder(r) - page, err := decoder.decodeOgg() - if err != nil { - return err - } - if needsTemp { - encoder = newOggEncoder(page.Header.SerialNumber, t) - } else { - encoder = newOggEncoder(page.Header.SerialNumber, w) - } - - if err = encoder.encodeBOS(page.Header.GranulePosition, page.Packets); err != nil { - return err - } - var vorbisCommentPage *oggPage - for { - page, err := decoder.decodeOgg() - if err != nil { - if err == io.EOF { - break // Reached the end of the input Ogg stream - } - return err - } - - // Find the Vorbis comment page and store it - if hasVorbisCommentPrefix(page.Packets) { - vorbisCommentPage = &page - commentFields := []string{} - if tag.album != "" { - commentFields = append(commentFields, "ALBUM="+tag.album) - } - if tag.artist != "" { - commentFields = append(commentFields, "ARTIST="+tag.artist) - } - if tag.genre != "" { - commentFields = append(commentFields, "GENRE="+tag.genre) - } - if tag.title != "" { - commentFields = append(commentFields, "TITLE="+tag.title) - } - if tag.date != "" { - commentFields = append(commentFields, "DATE="+tag.date) - } - if tag.albumArtist != "" { - commentFields = append(commentFields, "ALBUMARTIST="+tag.albumArtist) - } - if tag.comments != "" { - commentFields = append(commentFields, "COMMENT="+tag.comments) - } - if tag.publisher != "" { - commentFields = append(commentFields, "PUBLISHER="+tag.publisher) - } - if tag.composer != "" { - commentFields = append(commentFields, "COMPOSER="+tag.composer) - } - if tag.copyrightMsg != "" { - commentFields = append(commentFields, "COPYRIGHT="+tag.copyrightMsg) - } - for key, value := range tag.PassThrough { - commentFields = append(commentFields, key+"="+value) - } - img := []byte{} - if tag.albumArt != nil { - // Convert album art image to JPEG format - buf := new(bytes.Buffer) - if err = jpeg.Encode(buf, *tag.albumArt, nil); err == nil { - img, _ = createMetadataBlockPicture(buf.Bytes()) - } - } - - // Create the new Vorbis comment packet - commentPacket := createVorbisCommentPacket(commentFields, img) - vorbisCommentPage.Packets[0] = commentPacket - if err = encoder.encode(vorbisCommentPage.Header.GranulePosition, vorbisCommentPage.Packets); err != nil { - return nil - } - } else { - if page.Header.Flags == EOS { - if err = encoder.encodeEOS(page.Header.GranulePosition, page.Packets); err != nil { - return nil - } - } else { - if err = encoder.encode(page.Header.GranulePosition, page.Packets); err != nil { - return nil - } - } - } - } - if needsTemp { - f := w.(*os.File) - path, err := filepath.Abs(f.Name()) - if err != nil { - return err - } - w2, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) - if err != nil { - return err - } - defer w2.Close() - if _, err := io.Copy(w2, bytes.NewReader(t.Bytes())); err != nil { - return err - } - if _, err = f.Seek(0, io.SeekEnd); err != nil { - return err - } - } - return nil -} - -func hasOpusCommentPrefix(packets [][]byte) bool { - return len(packets) > 0 && len(packets[0]) >= 8 && string(packets[0][:8]) == "OpusTags" -} - -// Creates the comment packet for the Opus spec from the given commentFields and albumArt. The only difference between vorbis and opus is the "OpusTags" header and the framing bit -func createOpusCommentPacket(commentFields []string, albumArt []byte) []byte { - vendorString := "audiometa" - - buf := make([]byte, 4) - binary.LittleEndian.PutUint32(buf, uint32(len(vendorString))) - vorbisCommentPacket := append(buf, []byte(vendorString)...) - - if len(albumArt) > 0 { - binary.LittleEndian.PutUint32(buf, uint32(len(commentFields)+1)) - } else { - binary.LittleEndian.PutUint32(buf, uint32(len(commentFields))) - } - vorbisCommentPacket = append(vorbisCommentPacket, buf...) - - for _, field := range commentFields { - binary.LittleEndian.PutUint32(buf, uint32(len(field))) - vorbisCommentPacket = append(vorbisCommentPacket, buf...) - vorbisCommentPacket = append(vorbisCommentPacket, []byte(field)...) - } - vorbisCommentPacket = append([]byte("OpusTags"), vorbisCommentPacket...) - if len(albumArt) > 1 { - albumArtBase64 := base64.StdEncoding.EncodeToString(albumArt) - fieldLength := len("METADATA_BLOCK_PICTURE=") + len(albumArtBase64) - binary.LittleEndian.PutUint32(buf, uint32(fieldLength)) - vorbisCommentPacket = append(vorbisCommentPacket, buf...) - vorbisCommentPacket = append(vorbisCommentPacket, []byte("METADATA_BLOCK_PICTURE=")...) - vorbisCommentPacket = append(vorbisCommentPacket, []byte(albumArtBase64)...) - } - return vorbisCommentPacket -} - -// Checks if the vorbis comment header is present -func hasVorbisCommentPrefix(packets [][]byte) bool { - return len(packets) > 0 && len(packets[0]) >= 7 && string(packets[0][:7]) == "\x03vorbis" -} - -// Creates the vorbis comment packet from the given commentFields and albumArt -func createVorbisCommentPacket(commentFields []string, albumArt []byte) []byte { - vendorString := "audiometa" - - buf := make([]byte, 4) - binary.LittleEndian.PutUint32(buf, uint32(len(vendorString))) - vorbisCommentPacket := append(buf, []byte(vendorString)...) - if len(albumArt) > 0 { - binary.LittleEndian.PutUint32(buf, uint32(len(commentFields)+1)) - } else { - binary.LittleEndian.PutUint32(buf, uint32(len(commentFields))) - } - vorbisCommentPacket = append(vorbisCommentPacket, buf...) - - for _, field := range commentFields { - binary.LittleEndian.PutUint32(buf, uint32(len(field))) - vorbisCommentPacket = append(vorbisCommentPacket, buf...) - vorbisCommentPacket = append(vorbisCommentPacket, []byte(field)...) - } - vorbisCommentPacket = append([]byte("\x03vorbis"), vorbisCommentPacket...) - if len(albumArt) > 1 { - albumArtBase64 := base64.StdEncoding.EncodeToString(albumArt) - fieldLength := len("METADATA_BLOCK_PICTURE=") + len(albumArtBase64) - binary.LittleEndian.PutUint32(buf, uint32(fieldLength)) - vorbisCommentPacket = append(vorbisCommentPacket, buf...) - vorbisCommentPacket = append(vorbisCommentPacket, []byte("METADATA_BLOCK_PICTURE=")...) - vorbisCommentPacket = append(vorbisCommentPacket, []byte(albumArtBase64)...) - } - - vorbisCommentPacket = append(vorbisCommentPacket, []byte("\x01")...) - return vorbisCommentPacket -} - -// Creates the picture block which holds the album art in the vorbis comment header -func createMetadataBlockPicture(albumArtData []byte) ([]byte, error) { - mimeType := "image/jpeg" - description := "Cover" - img, _, err := image.DecodeConfig(bytes.NewReader(albumArtData)) - if err != nil { - return nil, ErrOggImgConfigFail - } - res := bytes.NewBuffer([]byte{}) - res.Write(encodeUint32(uint32(3))) - res.Write(encodeUint32(uint32(len(mimeType)))) - res.Write([]byte(mimeType)) - res.Write(encodeUint32(uint32(len(description)))) - res.Write([]byte(description)) - res.Write(encodeUint32(uint32(img.Width))) - res.Write(encodeUint32(uint32(img.Height))) - res.Write(encodeUint32(24)) - res.Write(encodeUint32(0)) - res.Write(encodeUint32(uint32(len(albumArtData)))) - res.Write(albumArtData) - return res.Bytes(), nil -} diff --git a/ogg_decoder.go b/ogg_decoder.go deleted file mode 100644 index ba2e10a..0000000 --- a/ogg_decoder.go +++ /dev/null @@ -1,122 +0,0 @@ -package audiometa - -import ( - "bytes" - "encoding/binary" - "io" -) - -// OggDecoder is a structure that facilitates the page-by-page decoding of an ogg stream. -type oggDecoder struct { - // lenbuf acts as a buffer for packet lengths and helps avoid allocating (maxSegSize is the maximum per page) - // r is an io.Reader used to read the data - // buf is a byte array of maximum page size - lenbuf [maxSegSize]int - r io.Reader - buf [maxPageSize]byte -} - -// NewOggDecoder is a constructor that initializes an ogg Decoder. -func newOggDecoder(r io.Reader) *oggDecoder { - // returns a new instance of Decoder - return &oggDecoder{r: r} -} - -// Page struct represents a logical unit of an ogg page. -type oggPage struct { - Header *oggPageHeader - // Packets are the actual packet data. - // If Type & COP != 0, the first element is - // a continuation of the previous page's last packet. - Packets [][]byte - Body []byte -} - -// errBadSegs error is thrown when an attempt is made to decode a page with a segment table size less than 1. -var errBadSegs = ErrOggInvalidSgmtTblSz - -// oggs is a byte slice representing the sequence 'OggS' -var oggs = []byte{'O', 'g', 'g', 'S'} - -// / Decode reads from the Reader of the Decoder until the next ogg page is found, then decodes and returns the Page or an error. -// An io.EOF error can be returned if that's what the Reader returns. -// -// The memory for the returned Page's Packets' bytes is managed by the Decoder. -// It can be overwritten by subsequent calls to Decode. -// -// It's safe to call Decode concurrently on different Decoders, provided their Readers are distinct. -// Otherwise, the outcome is not defined. -func (d *oggDecoder) decodeOgg() (oggPage, error) { - hbuf := d.buf[0:headerSize] - b := 0 - for { - _, err := io.ReadFull(d.r, hbuf[b:]) - if err != nil { - return oggPage{}, err - } - - i := bytes.Index(hbuf, oggs) - if i == 0 { - break - } - - if i < 0 { - const n = headerSize - if hbuf[n-1] == 'O' { - i = n - 1 - } else if hbuf[n-2] == 'O' && hbuf[n-1] == 'g' { - i = n - 2 - } else if hbuf[n-3] == 'O' && hbuf[n-2] == 'g' && hbuf[n-1] == 'g' { - i = n - 3 - } - } - - if i > 0 { - b = copy(hbuf, hbuf[i:]) - } - } - - var h oggPageHeader - _ = binary.Read(bytes.NewBuffer(hbuf), byteOrder, &h) - - if h.Segments < 1 { - return oggPage{}, errBadSegs - } - - nsegs := int(h.Segments) - segtbl := d.buf[headerSize : headerSize+nsegs] - if _, err := io.ReadFull(d.r, segtbl); err != nil { - return oggPage{}, err - } - - // A page may encompass multiple packets. Hence, we extract their lengths from the table at this stage, - // and subsequently segment the payload after reading it. - - packetlens := d.lenbuf[0:0] - payloadlen := 0 - more := false - for _, l := range segtbl { - if more { - packetlens[len(packetlens)-1] += int(l) - } else { - packetlens = append(packetlens, int(l)) - } - - more = l == maxSegSize - payloadlen += int(l) - } - - payload := d.buf[headerSize+nsegs : headerSize+nsegs+payloadlen] - if _, err := io.ReadFull(d.r, payload); err != nil { - return oggPage{}, err - } - - packets := make([][]byte, len(packetlens)) - s := 0 - for i, l := range packetlens { - packets[i] = payload[s : s+l] - s += l - } - - return oggPage{Header: &h, Packets: packets}, nil -} diff --git a/ogg_encoder.go b/ogg_encoder.go deleted file mode 100644 index 54141b6..0000000 --- a/ogg_encoder.go +++ /dev/null @@ -1,164 +0,0 @@ -package audiometa - -import ( - "bytes" - "io" -) - -// Encoder converts raw bytes into an ogg stream. -type oggEncoder struct { - serial uint32 - page uint32 - dummy [1][]byte - w io.Writer - buf [maxPageSize]byte -} - -// NewEncoder initializes an ogg encoder with a given serial ID. -// When using multiple Encoders for multiplexed logical streams, ensure unique IDs. -// Encode streams as per ogg RFC for Grouping and Chaining. -func newOggEncoder(id uint32, w io.Writer) *oggEncoder { - return &oggEncoder{serial: id, w: w} -} - -// EncodeBOS writes a beginning-of-stream packet to the ogg stream with a given granule position. -// Large packets are split across multiple pages with continuation-of-packet flag set. -// Packets can be empty or nil, resulting in a single segment of size 0. -func (w *oggEncoder) encodeBOS(granule int64, packets [][]byte) error { - if len(packets) == 0 { - packets = w.dummy[:] - } - return w.writePackets(BOS, granule, packets) -} - -// Encode writes a data packet to the ogg stream with a given granule position. -// Large packets are split across multiple pages with continuation-of-packet flag set. -// Packets can be empty or nil, resulting in a single segment of size 0. -func (w *oggEncoder) encode(granule int64, packets [][]byte) error { - if len(packets) == 0 { - packets = w.dummy[:] - } - return w.writePackets(0, granule, packets) -} - -// EncodeEOS writes a end-of-stream packet to the ogg stream. -// Packets can be empty or nil, resulting in a single segment of size 0. -func (w *oggEncoder) encodeEOS(granule int64, packets [][]byte) error { - if len(packets) == 0 { - packets = w.dummy[:] - } - return w.writePackets(EOS, granule, packets) -} - -func (w *oggEncoder) writePackets(kind byte, granule int64, packets [][]byte) error { - h := oggPageHeader{ - Magic: [4]byte{'O', 'g', 'g', 'S'}, - Flags: kind, - SerialNumber: w.serial, - GranulePosition: granule, - } - - segtbl, car, cdr := w.segmentize(payload{packets[0], packets[1:], nil}) - if err := w.writePage(&h, segtbl, car); err != nil { - return err - } - - h.Flags |= COP - for len(cdr.leftover) > 0 { - segtbl, car, cdr = w.segmentize(cdr) - if err := w.writePage(&h, segtbl, car); err != nil { - return err - } - } - - return nil -} - -func (w *oggEncoder) writePage(h *oggPageHeader, segtbl []byte, pay payload) error { - page := &oggPage{} - h.SequenceNumber = w.page - w.page++ - h.Segments = byte(len(segtbl)) - hb := bytes.NewBuffer(w.buf[0:0:cap(w.buf)]) - //_ = binary.Write(hb, byteOrder, h) - - hb.Write(segtbl) - - hb.Write(pay.leftover) - for _, p := range pay.packets { - hb.Write(p) - } - hb.Write(pay.rightover) - - bb := hb.Bytes() - page.Header = h - page.Body = bb - - OggPageChecksumSet(page) - buf := page.Header.toBytesBuffer() - if _, err := buf.WriteTo(w.w); err != nil { - return err - } - - if _, err := hb.WriteTo(w.w); err != nil { - return err - } - return nil -} - -// payload represents a potentially split group of packets. -// ASCII example (each letter run represents one packet): -// Page 1 (left): [aaaabbbbccccd], Page 2 (right): [dddeeeffff] -type payload struct { - leftover []byte - packets [][]byte - rightover []byte -} - -// segmentize calculates the lacing values for the segment table based on given packets. -// Returns the segment table, the payload for the current page and any leftover payload. -func (w *oggEncoder) segmentize(pay payload) ([]byte, payload, payload) { - segtbl := w.buf[headerSize : headerSize+maxSegSize] - i := 0 - - s255s := len(pay.leftover) / maxSegSize - rem := len(pay.leftover) % maxSegSize - for i < len(segtbl) && s255s > 0 { - segtbl[i] = maxSegSize - i++ - s255s-- - } - if i < maxSegSize { - segtbl[i] = byte(rem) - i++ - } else { - leftStart := len(pay.leftover) - (s255s * maxSegSize) - rem - good := payload{pay.leftover[0:leftStart], nil, nil} - bad := payload{pay.leftover[leftStart:], pay.packets, nil} - return segtbl, good, bad - } - - // Now loop through the rest and track if we need to split - for p := 0; p < len(pay.packets); p++ { - s255s := len(pay.packets[p]) / maxSegSize - rem := len(pay.packets[p]) % maxSegSize - for i < len(segtbl) && s255s > 0 { - segtbl[i] = maxSegSize - i++ - s255s-- - } - if i < maxSegSize { - segtbl[i] = byte(rem) - i++ - } else { - right := len(pay.packets[p]) - (s255s * maxSegSize) - rem - good := payload{pay.leftover, pay.packets[0:p], pay.packets[p][0:right]} - bad := payload{pay.packets[p][right:], pay.packets[p+1:], nil} - return segtbl, good, bad - } - } - - good := pay - bad := payload{} - return segtbl[0:i], good, bad -} diff --git a/ogg_header.go b/ogg_header.go deleted file mode 100644 index 41db618..0000000 --- a/ogg_header.go +++ /dev/null @@ -1,68 +0,0 @@ -package audiometa - -import ( - "bytes" - "encoding/binary" -) - -// The MIME type as defined in RFC 3534. -const MIMEType = "application/ogg" - -const headerSize = 27 - -// max segment size -const maxSegSize = 255 - -// max sequence-of-segments size in a page -const mps = maxSegSize * 255 - -// == 65307, per the RFC -const maxPageSize = headerSize + maxSegSize + mps - -// The byte order of integers in ogg page headers. -var byteOrder = binary.LittleEndian - -type oggPageHeader struct { - Magic [4]byte // 0-3, always == "OggS" - Version byte // 4, always == 0 - Flags byte // 5 Flags is a bitmask of COP, BOS, and/or EOS. - GranulePosition int64 // 6-13, codec-specific, GranulePosition represents the granule position, its interpretation depends on the encapsulated codec. - SerialNumber uint32 // 14-17, associated with a logical stream, SerialNumber represents the bitstream serial number. - SequenceNumber uint32 // 18-21, sequence number of page in packet - CRC uint32 // 22-25 - Segments byte // 26 -} - -const ( - // Continuation of packet - COP byte = 1 << iota - // Beginning of stream - BOS = 1 << iota - // End of stream - EOS = 1 << iota -) - -func (o oggPageHeader) toBytesSlice() []byte { - b := new(bytes.Buffer) - _ = binary.Write(b, byteOrder, o.Magic) - _ = binary.Write(b, byteOrder, o.Version) - _ = binary.Write(b, byteOrder, o.Flags) - _ = binary.Write(b, byteOrder, o.GranulePosition) - _ = binary.Write(b, byteOrder, o.SerialNumber) - _ = binary.Write(b, byteOrder, o.SequenceNumber) - _ = binary.Write(b, byteOrder, o.CRC) - _ = binary.Write(b, byteOrder, o.Segments) - return b.Bytes() -} -func (o oggPageHeader) toBytesBuffer() *bytes.Buffer { - b := new(bytes.Buffer) - _ = binary.Write(b, byteOrder, o.Magic) - _ = binary.Write(b, byteOrder, o.Version) - _ = binary.Write(b, byteOrder, o.Flags) - _ = binary.Write(b, byteOrder, o.GranulePosition) - _ = binary.Write(b, byteOrder, o.SerialNumber) - _ = binary.Write(b, byteOrder, o.SequenceNumber) - _ = binary.Write(b, byteOrder, o.CRC) - _ = binary.Write(b, byteOrder, o.Segments) - return b -} diff --git a/ogg_test.go b/ogg_test.go deleted file mode 100644 index dc473e2..0000000 --- a/ogg_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package audiometa - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestOggPageHeaderToBytesSlice(t *testing.T) { - header := oggPageHeader{ - Magic: [4]byte{'O', 'g', 'g', 'S'}, - Version: 0, - Flags: BOS, - GranulePosition: 12345, - SerialNumber: 30, - SequenceNumber: 1, - CRC: 0, - Segments: 2, - } - - expected := []byte{ - 'O', 'g', 'g', 'S', // Magic - 0, // Version - BOS, // Flags - 57, 48, 0, 0, 0, 0, 0, 0, // GranulePosition - 30, 0, 0, 0, // SerialNumber - 1, 0, 0, 0, // SequenceNumber - 0, 0, 0, 0, // CRC - 2, // Segments - } - - result := header.toBytesSlice() - assert.Equal(t, expected, result, "The byte slice representation of the header should match the expected value.") -} - -func TestOggPageHeaderToBytesBuffer(t *testing.T) { - header := oggPageHeader{ - Magic: [4]byte{'O', 'g', 'g', 'S'}, - Version: 0, - Flags: BOS, - GranulePosition: 12345, - SerialNumber: 15, - SequenceNumber: 1, - CRC: 0, - Segments: 2, - } - - expected := []byte{ - 'O', 'g', 'g', 'S', // Magic - 0, // Version - BOS, // Flags - 57, 48, 0, 0, 0, 0, 0, 0, // GranulePosition - 15, 0, 0, 0, // SerialNumber - 1, 0, 0, 0, // SequenceNumber - 0, 0, 0, 0, // CRC - 2, // Segments - } - - result := header.toBytesBuffer() - assert.Equal(t, expected, result.Bytes(), "The byte buffer representation of the header should match the expected value.") -} - -func TestCRCFunctions(t *testing.T) { - - buffer := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} - crc := uint32(0) - - updatedCRC := _osUpdateCRC(crc, buffer, len(buffer)) - expectedCRC := uint32(0x7d0f3681) // Replace with the actual expected CRC value based on the test buffer - - assert.Equal(t, expectedCRC, updatedCRC, "The CRC should match the expected value.") -} - -func TestOggPageChecksumSet(t *testing.T) { - og := &oggPage{ - Header: &oggPageHeader{ - Magic: [4]byte{'O', 'g', 'g', 'S'}, - Version: 0, - Flags: BOS, - GranulePosition: 12345, - SerialNumber: 67890, - SequenceNumber: 1, - CRC: 0, - Segments: 2, - }, - Body: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, - } - - OggPageChecksumSet(og) - - // Verify CRC in the header - crcReg := og.Header.CRC - expectedCRC := uint32(0x9b4396e8) - - assert.Equal(t, expectedCRC, crcReg, "The CRC in the header should match the expected value.") -} - -func TestOggRead(t *testing.T) { - - og := &oggPage{ - Header: &oggPageHeader{ - Magic: [4]byte{'O', 'g', 'g', 'Z'}, - Version: 0, - Flags: BOS, - GranulePosition: 12345, - SerialNumber: 67890, - SequenceNumber: 1, - CRC: 0, - Segments: 2, - }, - Body: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, - } - t.Run("invalid magic", func(t *testing.T) { - r := bytes.NewReader(og.Header.toBytesSlice()) - dem := oggDemuxer{} - res, err := dem.read(r) - assert.Error(t, err) - assert.Empty(t, res) - }) - t.Run("empty dat", func(t *testing.T) { - r := bytes.NewReader((&oggPage{Header: &oggPageHeader{}}).Header.toBytesSlice()) - dem := oggDemuxer{} - res, err := dem.read(r) - assert.Error(t, err) - assert.Empty(t, res) - }) -} diff --git a/parse.go b/parse.go deleted file mode 100755 index 7bd2791..0000000 --- a/parse.go +++ /dev/null @@ -1,137 +0,0 @@ -package audiometa - -import ( - "bytes" - "image" - "io" - "reflect" - "strconv" - "strings" - - mp3TagLib "github.com/bogem/id3v2/v2" - "github.com/sunfish-shogi/bufseekio" -) - -// This operation opens the ID tag for the corresponding file that is passed in the filepath parameter regardless of the filetype as long as it is a supported file type -func parse(input io.ReadSeeker, opts ParseOptions) (*IDTag, error) { - if _, err := input.Seek(0, io.SeekStart); err != nil { - return nil, err - } - format := opts.Format - switch { - case format == MP3: - tag, err := parseMP3(input) - if err != nil { - return nil, err - } - tag.fileType = "mp3" - tag.reader = input - return tag, nil - case fileTypesContains(format, mp4FileTypes): - tag, err := parseMP4(input) - if err != nil { - return nil, err - } - tag.reader = input - tag.fileType = "mp4" - return tag, nil - case format == FLAC: - tag, err := parseFLAC(input) - if err != nil { - return nil, err - } - tag.reader = input - tag.fileType = "flac" - return tag, nil - case format == OGG: - tag, err := parseOGG(input) - if err != nil { - return nil, err - } - tag.fileType = "ogg" - tag.reader = input - return tag, nil - } - return nil, ErrNoMethodAvlble -} - -func parseMP3(input io.Reader) (*IDTag, error) { - resultTag := IDTag{} - tag, err := mp3TagLib.ParseReader(input, mp3TagLib.Options{Parse: true}) - if err != nil { - return nil, ErrMP3ParseFail - } - resultTag = IDTag{artist: tag.Artist(), album: tag.Album(), genre: tag.Genre(), title: tag.Title(), year: tag.Year()} - rtPtr := reflect.ValueOf(&resultTag) - for k, v := range mp3TextFrames { - field := k - if k == "albumArt" { - continue - } - if k == "bpm" { - field = "BPM" - } - if framer := tag.GetLastFrame(v); framer != nil { - if t, ok := framer.(mp3TagLib.TextFrame); ok { - if t.Text == "" { - continue - } - rtPtr.MethodByName("Set" + strings.ToUpper(field[:1]) + field[1:]).Call([]reflect.Value{reflect.ValueOf(t.Text)}) - } - } - } - if pictures := tag.GetFrames("APIC"); len(pictures) > 0 { - pic := pictures[0].(mp3TagLib.PictureFrame) - if img, _, err := image.Decode(bytes.NewReader(pic.Picture)); err == nil { - resultTag.albumArt = &img - } - } - return &resultTag, nil -} - -func parseFLAC(input io.Reader) (*IDTag, error) { - resultTag := IDTag{} - _, fb, err := extractFLACComment(input) - if err != nil { - return nil, err - } - if fb.cmts != nil { - for _, cmt := range fb.cmts.Comments { - if sp := strings.Split(cmt, "="); len(sp) == 2 { - flactag := strings.ToLower(sp[0]) - if flactag == ALBUM { - resultTag.album = sp[1] - } else if flactag == ARTIST { - resultTag.artist = sp[1] - } else if flactag == DATE { - resultTag.date = sp[1] - } else if flactag == TITLE { - resultTag.title = sp[1] - } else if flactag == GENRE { - resultTag.genre = sp[1] - } - } - } - } - resultTag.albumArt = fb.pic - return &resultTag, nil -} - -func parseOGG(input io.Reader) (*IDTag, error) { - return readOggTags(input) -} - -func parseMP4(input io.ReadSeeker) (*IDTag, error) { - resultTag := IDTag{} - r := bufseekio.NewReadSeeker(input, 128*1024, 4) - tag, err := readFromMP4(r) - if err != nil { - return nil, err - } - resultTag = IDTag{artist: tag.artist(), albumArtist: tag.albumArtist(), album: tag.album(), - albumArt: tag.picture(), comments: tag.comment(), composer: tag.composer(), genre: tag.genre(), - title: tag.title(), year: strconv.Itoa(tag.year()), encodedBy: tag.encoder(), - copyrightMsg: tag.copyright(), bpm: strconv.Itoa(tag.tempo())} - - return &resultTag, nil -} diff --git a/parse_test.go b/parse_test.go deleted file mode 100755 index 9b2faa7..0000000 --- a/parse_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package audiometa - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestReadMP3Tags(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-mp3.mp3") - f, err := os.Open(path) - assert.NoError(t, err) - tag, err := parse(f, ParseOptions{MP3}) - assert.NoError(t, err) - assert.NotEmpty(t, tag.Artist()) - assert.NotEmpty(t, tag.Album()) - assert.NotEmpty(t, tag.Title()) - -} -func TestReadM4ATags(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-m4a.m4a") - f, err := os.Open(path) - assert.NoError(t, err) - tag, err := parse(f, ParseOptions{M4A}) - assert.NoError(t, err) - assert.NotEmpty(t, tag.Artist()) - assert.NotEmpty(t, tag.Album()) - assert.NotEmpty(t, tag.Title()) -} -func TestReadFlacTags(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-flac.flac") - f, err := os.Open(path) - assert.NoError(t, err) - tag, err := parse(f, ParseOptions{FLAC}) - assert.NoError(t, err) - assert.NotEmpty(t, tag.Artist()) - assert.NotEmpty(t, tag.Album()) - assert.NotEmpty(t, tag.Title()) -} -func TestReadOggVorbisTags(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-ogg.ogg") - f, err := os.Open(path) - assert.NoError(t, err) - tag, err := parse(f, ParseOptions{OGG}) - assert.NoError(t, err) - assert.NotEmpty(t, tag.Artist()) - assert.NotEmpty(t, tag.Album()) - assert.NotEmpty(t, tag.Title()) -} -func TestReadOggOpusTags(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-opus.ogg") - f, err := os.Open(path) - assert.NoError(t, err) - tag, err := parse(f, ParseOptions{OGG}) - assert.NoError(t, err) - assert.NotEmpty(t, tag.Artist()) - assert.NotEmpty(t, tag.Album()) - assert.NotEmpty(t, tag.Title()) -} diff --git a/save.go b/save.go deleted file mode 100644 index d4c8f46..0000000 --- a/save.go +++ /dev/null @@ -1,229 +0,0 @@ -package audiometa - -import ( - "bytes" - "fmt" - "image/jpeg" - "io" - "os" - "path/filepath" - "reflect" - - "github.com/aler9/writerseeker" - mp3TagLib "github.com/bogem/id3v2/v2" - "github.com/gcottom/audiometa/v2/flac" - "github.com/sunfish-shogi/bufseekio" -) - -// Save writes the full ID Tag and audio to the io.Writer w. -// If w is of type *os.File, Save overwrites the existing file -// and when complete, w points to the end of the file. -func (tag *IDTag) Save(w io.Writer) error { - fileType := FileType(tag.fileType) - if _, err := tag.reader.Seek(0, io.SeekStart); err != nil { - return err - } - if fileType == MP3 { - return saveMP3(tag, w) - } else if fileType == M4A || fileType == M4B || fileType == M4P || fileType == MP4 { - return saveMP4(tag, w) - } else if fileType == FLAC { - return saveFLAC(tag, w) - } else if fileType == OGG { - return saveOGG(tag, w) - } - return ErrNoMethodAvlble -} - -func saveMP3(tag *IDTag, w io.Writer) error { - r := tag.reader - - readerBytes, err := io.ReadAll(r) - if err != nil { - return err - } - r = bytes.NewReader(readerBytes) - fmt.Println(len(readerBytes)) - if _, err = r.Seek(0, 0); err != nil { - return err - } - - mp3Tag, err := mp3TagLib.ParseReader(r, mp3TagLib.Options{Parse: true}) - if err != nil { - return err - } - originalSize := int64(mp3Tag.Size()) - fmt.Println(originalSize) - for k, v := range mp3TextFrames { - if reflect.ValueOf(*tag).FieldByName(k).IsZero() { - mp3Tag.DeleteFrames(v) - continue - } - if k == "albumArt" { - buf := new(bytes.Buffer) - if err := jpeg.Encode(buf, *tag.albumArt, nil); err == nil { - mp3Tag.AddAttachedPicture(mp3TagLib.PictureFrame{ - Encoding: mp3TagLib.EncodingUTF8, - MimeType: "image/jpeg", - PictureType: mp3TagLib.PTFrontCover, - Description: "Front cover", - Picture: buf.Bytes(), - }) - } - continue - } - textFrame := mp3TagLib.TextFrame{ - Encoding: mp3TagLib.EncodingUTF8, - Text: reflect.ValueOf(*tag).FieldByName(k).String(), - } - mp3Tag.AddFrame(v, textFrame) - } - _, err = r.Seek(0, io.SeekStart) - if err != nil { - return err - } - if reflect.TypeOf(w) == reflect.TypeOf(new(os.File)) { - f := w.(*os.File) - path, err := filepath.Abs(f.Name()) - if err != nil { - return err - } - w2, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) - if err != nil { - return err - } - defer w2.Close() - //in and out are the same file so we have to temp it - t := &writerseeker.WriterSeeker{} - defer t.Close() - // Write tag in new file. - if tagbytes, err := mp3Tag.WriteTo(t); err != nil { - return err - } else { - fmt.Println(tagbytes) - } - - // Seek to a music part of original file. - if _, err = r.Seek(originalSize, io.SeekStart); err != nil { - return err - } - // Write to new file the music part. - //musicData, err := io.ReadAll(r) - //if err != nil { - // return err - //} - if _, err := io.Copy(t, r); err != nil { - return err - } - if _, err = t.Seek(0, io.SeekStart); err != nil { - return err - } - if _, err := io.Copy(w2, bytes.NewReader(t.Bytes())); err != nil { - return err - } - if _, err = f.Seek(0, io.SeekEnd); err != nil { - return err - } - return nil - } - - // Write tag in new file. - if _, err = mp3Tag.WriteTo(w); err != nil { - return err - } - // Seek to a music part of original file. - if _, err = r.Seek(originalSize, io.SeekStart); err != nil { - return err - } - - // Write to new file the music part. - if _, err = io.Copy(w, r); err != nil { - return err - } - return nil -} - -func saveMP4(tag *IDTag, w io.Writer) error { - var delete MP4Delete - fields := reflect.VisibleFields(reflect.TypeOf(*tag)) - for _, field := range fields { - fieldName := field.Name - if fieldName == "data" || fieldName == "reader" { - continue - } - if fieldName == "albumArt" && reflect.ValueOf(*tag).FieldByName(fieldName).IsNil() { - delete = append(delete, fieldName) - continue - } - if reflect.ValueOf(*tag).FieldByName(fieldName).String() == "" { - delete = append(delete, fieldName) - } - } - data, err := io.ReadAll(tag.reader) - if err != nil { - return err - } - r := bufseekio.NewReadSeeker(bytes.NewReader(data), 128*1024, 4) - return writeMP4(r, w, tag, delete) - -} - -func saveFLAC(tag *IDTag, w io.Writer) error { - needsTemp := reflect.TypeOf(w) == reflect.TypeOf(new(os.File)) - r := bufseekio.NewReadSeeker(tag.reader, 128*1024, 4) - f, fb, err := extractFLACComment(r) - if err != nil { - return err - } - cmts := flac.New() - if err := cmts.Add(flac.FIELD_TITLE, tag.title); err != nil { - return err - } - if err := cmts.Add(flac.FIELD_ALBUM, tag.album); err != nil { - return err - } - if err := cmts.Add(flac.FIELD_ARTIST, tag.artist); err != nil { - return err - } - if err := cmts.Add(flac.FIELD_GENRE, tag.genre); err != nil { - return err - } - cmtsmeta, err := cmts.Marshal() - if err != nil { - return err - } - if fb.cmtIdx > 0 { - f.Meta = removeFLACMetaBlock(f.Meta, fb.cmtIdx) - f.Meta = append(f.Meta, &cmtsmeta) - } else { - f.Meta = append(f.Meta, &cmtsmeta) - } - if fb.picIdx > 0 { - f.Meta = removeFLACMetaBlock(f.Meta, fb.picIdx) - } - if tag.albumArt != nil { - buf := new(bytes.Buffer) - if err := jpeg.Encode(buf, *tag.albumArt, nil); err == nil { - picture, err := flac.NewFromImageData(flac.PictureTypeFrontCover, "Front cover", buf.Bytes(), "image/jpeg") - if err != nil { - return err - } - picturemeta := picture.Marshal() - f.Meta = append(f.Meta, &picturemeta) - } - - } - if _, err = r.Seek(0, io.SeekStart); err != nil { - return err - } - return flacSave(r, w, f.Meta, needsTemp) -} - -func saveOGG(tag *IDTag, w io.Writer) error { - if tag.codec == "vorbis" { - return saveVorbisTags(tag, w) - } else if tag.codec == "opus" { - return saveOpusTags(tag, w) - } - return ErrOggCodecNotSpprtd -} diff --git a/save_test.go b/save_test.go deleted file mode 100644 index ee88047..0000000 --- a/save_test.go +++ /dev/null @@ -1,1451 +0,0 @@ -package audiometa - -import ( - "bytes" - "errors" - "image" - "io" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func compareImages(src1 [][][3]float32, src2 [][][3]float32) bool { - dif := 0 - for i, dat1 := range src1 { - for j := range dat1 { - if len(src1[i][j]) != len(src2[i][j]) { - dif++ - } - } - } - return dif == 0 -} - -func image_2_array_at(src image.Image) [][][3]float32 { - bounds := src.Bounds() - width, height := bounds.Max.X, bounds.Max.Y - iaa := make([][][3]float32, height) - - for y := 0; y < height; y++ { - row := make([][3]float32, width) - for x := 0; x < width; x++ { - r, g, b, _ := src.At(x, y).RGBA() - // A color's RGBA method returns values in the range [0, 65535]. - // Shifting by 8 reduces this to the range [0, 255]. - row[x] = [3]float32{float32(r >> 8), float32(g >> 8), float32(b >> 8)} - } - iaa[y] = row - } - - return iaa -} - -func TestMP3(t *testing.T) { - t.Run("TestWriteEmptyTagsMP3-buffers", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-mp3-nonEmpty.mp3") - f, err := os.Open(path) - assert.NoError(t, err) - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - tag, err := parse(r, ParseOptions{MP3}) - assert.NoError(t, err) - tag.ClearAllTags() - buffy := new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = parse(r, ParseOptions{MP3}) - assert.NoError(t, err) - assert.Empty(t, tag.Artist()) - assert.Empty(t, tag.Album()) - assert.Empty(t, tag.Title()) - }) - - t.Run("TestWriteEmptyTagsMP3-file", func(t *testing.T) { - err := os.Mkdir("testdata/temp", 0755) - assert.NoError(t, err) - of, err := os.ReadFile("testdata/testdata-mp3-nonEmpty.mp3") - assert.NoError(t, err) - err = os.WriteFile("testdata/temp/testdata-mp3-nonEmpty.mp3", of, 0755) - assert.NoError(t, err) - path, _ := filepath.Abs("testdata/temp/testdata-mp3-nonEmpty.mp3") - f, err := os.OpenFile(path, os.O_RDONLY, 0755) - assert.NoError(t, err) - defer f.Close() - tag, err := Open(f, ParseOptions{MP3}) - assert.NoError(t, err) - tag.ClearAllTags() - err = SaveTag(tag, f) - assert.NoError(t, err) - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{MP3}) - assert.NoError(t, err) - f.Close() - err = os.RemoveAll("testdata/temp") - assert.NoError(t, err) - assert.Empty(t, tag.Artist()) - assert.Empty(t, tag.Album()) - assert.Empty(t, tag.Title()) - }) - - t.Run("TestWriteTagsMP3FromEmpty-buffers", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-mp3-nonEmpty.mp3") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - - tag, err := Open(r, ParseOptions{MP3}) - assert.NoError(t, err) - tag.ClearAllTags() - - buffy := new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{MP3}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - - buffy = new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{MP3}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - - picFile, err := os.Open(p) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*tag.albumArt) - - assert.True(t, compareImages(img1data, img2data)) - }) - - t.Run("TestWriteTagsMP3FromEmpty-file", func(t *testing.T) { - err := os.Mkdir("testdata/temp", 0755) - assert.NoError(t, err) - of, err := os.ReadFile("testdata/testdata-mp3-nonEmpty.mp3") - assert.NoError(t, err) - err = os.WriteFile("testdata/temp/testdata-mp3-nonEmpty.mp3", of, 0755) - assert.NoError(t, err) - path, _ := filepath.Abs("testdata/temp/testdata-mp3-nonEmpty.mp3") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - - tag, err := Open(f, ParseOptions{MP3}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - err = SaveTag(tag, f) - assert.NoError(t, err) - - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{MP3}) - assert.NoError(t, err) - err = os.RemoveAll("testdata/temp") - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - - picFile, err := os.Open(p) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*tag.albumArt) - - assert.True(t, compareImages(img1data, img2data)) - - }) - - t.Run("TestUpdateTagsMP3-buffers", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-mp3-nonEmpty.mp3") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - - tag, err := Open(r, ParseOptions{MP3}) - assert.NoError(t, err) - tag.ClearAllTags() - - buffy := new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{MP3}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - - buffy = new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{MP3}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - - tag.SetArtist("TestArtist2") - - buffy = new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{MP3}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist2") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - picFile, err := os.Open(p) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*tag.albumArt) - - assert.True(t, compareImages(img1data, img2data)) - }) - - t.Run("TestUpdateTagsMP3-file", func(t *testing.T) { - err := os.Mkdir("testdata/temp", 0755) - assert.NoError(t, err) - of, err := os.ReadFile("testdata/testdata-mp3-nonEmpty.mp3") - assert.NoError(t, err) - err = os.WriteFile("testdata/temp/testdata-mp3-nonEmpty.mp3", of, 0755) - assert.NoError(t, err) - path, _ := filepath.Abs("testdata/temp/testdata-mp3-nonEmpty.mp3") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - - tag, err := Open(f, ParseOptions{MP3}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - err = SaveTag(tag, f) - assert.NoError(t, err) - - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{MP3}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - - tag.SetArtist("TestArtist2") - err = SaveTag(tag, f) - assert.NoError(t, err) - - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{MP3}) - assert.NoError(t, err) - f.Close() - err = os.RemoveAll("testdata/temp") - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist2") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - picFile, err := os.Open(p) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*tag.albumArt) - - assert.True(t, compareImages(img1data, img2data)) - }) -} - -func TestM4A(t *testing.T) { - t.Run("TestWriteEmptyTagsM4A-buffers", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-m4a-nonEmpty.m4a") - f, err := os.Open(path) - assert.NoError(t, err) - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - tag, err := parse(r, ParseOptions{M4A}) - assert.NoError(t, err) - tag.ClearAllTags() - buffy := new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = parse(r, ParseOptions{M4A}) - assert.NoError(t, err) - assert.Empty(t, tag.Artist()) - assert.Empty(t, tag.Album()) - assert.Empty(t, tag.Title()) - }) - - t.Run("TestWriteEmptyTagsM4A-file", func(t *testing.T) { - err := os.Mkdir("testdata/temp", 0755) - assert.NoError(t, err) - of, err := os.ReadFile("testdata/testdata-m4a-nonEmpty.m4a") - assert.NoError(t, err) - err = os.WriteFile("testdata/temp/testdata-m4a-nonEmpty.m4a", of, 0755) - assert.NoError(t, err) - path, _ := filepath.Abs("testdata/temp/testdata-m4a-nonEmpty.m4a") - f, err := os.OpenFile(path, os.O_RDONLY, 0755) - assert.NoError(t, err) - defer f.Close() - tag, err := Open(f, ParseOptions{M4A}) - assert.NoError(t, err) - tag.ClearAllTags() - err = SaveTag(tag, f) - assert.NoError(t, err) - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{M4A}) - assert.NoError(t, err) - f.Close() - err = os.RemoveAll("testdata/temp") - assert.NoError(t, err) - assert.Empty(t, tag.Artist()) - assert.Empty(t, tag.Album()) - assert.Empty(t, tag.Title()) - - }) - - t.Run("TestWriteTagsM4AFromEmpty-buffers", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-m4a-nonEmpty.m4a") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - - tag, err := Open(r, ParseOptions{M4A}) - assert.NoError(t, err) - tag.ClearAllTags() - - buffy := new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{M4A}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - - buffy = new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{M4A}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - - }) - - t.Run("TestWriteTagsM4AFromEmpty-file", func(t *testing.T) { - err := os.Mkdir("testdata/temp", 0755) - assert.NoError(t, err) - of, err := os.ReadFile("testdata/testdata-m4a-nonEmpty.m4a") - assert.NoError(t, err) - err = os.WriteFile("testdata/temp/testdata-m4a-nonEmpty.m4a", of, 0755) - assert.NoError(t, err) - path, _ := filepath.Abs("testdata/temp/testdata-m4a-nonEmpty.m4a") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - - tag, err := Open(f, ParseOptions{M4A}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - - err = SaveTag(tag, f) - assert.NoError(t, err) - - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{M4A}) - assert.NoError(t, err) - err = os.RemoveAll("testdata/temp") - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - }) - - t.Run("TestUpdateTagsM4A-buffers", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-m4a-nonEmpty.m4a") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - - tag, err := Open(r, ParseOptions{M4A}) - assert.NoError(t, err) - tag.ClearAllTags() - - buffy := new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{M4A}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - - buffy = new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{M4A}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - - tag.SetArtist("TestArtist2") - - buffy = new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{M4A}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist2") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - }) - - t.Run("TestUpdateTagsM4A-file", func(t *testing.T) { - err := os.Mkdir("testdata/temp", 0755) - assert.NoError(t, err) - of, err := os.ReadFile("testdata/testdata-m4a-nonEmpty.m4a") - assert.NoError(t, err) - err = os.WriteFile("testdata/temp/testdata-m4a-nonEmpty.m4a", of, 0755) - assert.NoError(t, err) - path, _ := filepath.Abs("testdata/temp/testdata-m4a-nonEmpty.m4a") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - - tag, err := Open(f, ParseOptions{M4A}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - err = SaveTag(tag, f) - assert.NoError(t, err) - - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{M4A}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - - tag.SetArtist("TestArtist2") - err = SaveTag(tag, f) - assert.NoError(t, err) - - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{M4A}) - assert.NoError(t, err) - f.Close() - err = os.RemoveAll("testdata/temp") - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist2") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - }) - t.Run("TestNoChangeM4A-file", func(t *testing.T) { - err := os.Mkdir("testdata/temp", 0755) - assert.NoError(t, err) - of, err := os.ReadFile("testdata/withAlbumArt/test1.m4a") - assert.NoError(t, err) - err = os.WriteFile("testdata/temp/test1.m4a", of, 0755) - assert.NoError(t, err) - path, _ := filepath.Abs("testdata/temp/test1.m4a") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - - tag, err := Open(f, ParseOptions{M4A}) - assert.NoError(t, err) - err = SaveTag(tag, f) - assert.NoError(t, err) - - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{M4A}) - assert.NoError(t, err) - err = os.RemoveAll("testdata/temp") - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "test1") - assert.Equal(t, tag.Album(), "test1") - assert.Equal(t, tag.Title(), "test1") - }) -} - -func TestFLAC(t *testing.T) { - t.Run("TestWriteEmptyTagsFLAC-buffers", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-flac-nonEmpty.flac") - f, err := os.Open(path) - assert.NoError(t, err) - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - tag, err := parse(r, ParseOptions{FLAC}) - assert.NoError(t, err) - tag.ClearAllTags() - buffy := new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = parse(r, ParseOptions{FLAC}) - assert.NoError(t, err) - assert.Empty(t, tag.Artist()) - assert.Empty(t, tag.Album()) - assert.Empty(t, tag.Title()) - }) - - t.Run("TestWriteEmptyTagsFLAC-file", func(t *testing.T) { - err := os.Mkdir("testdata/temp", 0755) - assert.NoError(t, err) - of, err := os.ReadFile("testdata/testdata-flac-nonEmpty.flac") - assert.NoError(t, err) - err = os.WriteFile("testdata/temp/testdata-flac-nonEmpty.flac", of, 0755) - assert.NoError(t, err) - path, _ := filepath.Abs("testdata/temp/testdata-flac-nonEmpty.flac") - f, err := os.OpenFile(path, os.O_RDONLY, 0755) - assert.NoError(t, err) - defer f.Close() - tag, err := Open(f, ParseOptions{FLAC}) - assert.NoError(t, err) - tag.ClearAllTags() - err = SaveTag(tag, f) - assert.NoError(t, err) - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{FLAC}) - assert.NoError(t, err) - f.Close() - err = os.RemoveAll("testdata/temp") - assert.NoError(t, err) - assert.Empty(t, tag.Artist()) - assert.Empty(t, tag.Album()) - assert.Empty(t, tag.Title()) - }) - - t.Run("TestWriteTagsFLACFromEmpty-buffers", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-flac-nonEmpty.flac") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - - tag, err := Open(r, ParseOptions{FLAC}) - assert.NoError(t, err) - tag.ClearAllTags() - - buffy := new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{FLAC}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - - buffy = new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{FLAC}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - picFile, err := os.Open(p) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*tag.albumArt) - - assert.True(t, compareImages(img1data, img2data)) - }) - - t.Run("TestWriteTagsFLACFromEmpty-file", func(t *testing.T) { - err := os.Mkdir("testdata/temp", 0755) - assert.NoError(t, err) - of, err := os.ReadFile("testdata/testdata-flac-nonEmpty.flac") - assert.NoError(t, err) - err = os.WriteFile("testdata/temp/testdata-flac-nonEmpty.flac", of, 0755) - assert.NoError(t, err) - path, _ := filepath.Abs("testdata/temp/testdata-flac-nonEmpty.flac") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - - tag, err := Open(f, ParseOptions{FLAC}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - - err = SaveTag(tag, f) - assert.NoError(t, err) - - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{FLAC}) - assert.NoError(t, err) - err = os.RemoveAll("testdata/temp") - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - picFile, err := os.Open(p) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*tag.albumArt) - - assert.True(t, compareImages(img1data, img2data)) - }) - - t.Run("TestUpdateTagsFLAC-buffers", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-flac-nonEmpty.flac") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - - tag, err := Open(r, ParseOptions{FLAC}) - assert.NoError(t, err) - tag.ClearAllTags() - - buffy := new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{FLAC}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - - buffy = new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{FLAC}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - - tag.SetArtist("TestArtist2") - - buffy = new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{FLAC}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist2") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - }) - - t.Run("TestUpdateTagsFLAC-file", func(t *testing.T) { - err := os.Mkdir("testdata/temp", 0755) - assert.NoError(t, err) - of, err := os.ReadFile("testdata/testdata-flac-nonEmpty.flac") - assert.NoError(t, err) - err = os.WriteFile("testdata/temp/testdata-flac-nonEmpty.flac", of, 0755) - assert.NoError(t, err) - path, _ := filepath.Abs("testdata/temp/testdata-flac-nonEmpty.flac") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - - tag, err := Open(f, ParseOptions{FLAC}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - err = SaveTag(tag, f) - assert.NoError(t, err) - - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{FLAC}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - - tag.SetArtist("TestArtist2") - err = SaveTag(tag, f) - assert.NoError(t, err) - - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{FLAC}) - assert.NoError(t, err) - f.Close() - err = os.RemoveAll("testdata/temp") - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist2") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - }) -} - -func TestOggVorbis(t *testing.T) { - t.Run("TestWriteEmptyTagsOggVorbis-buffers", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-ogg-vorbis-nonEmpty.ogg") - f, err := os.Open(path) - assert.NoError(t, err) - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - tag, err := parse(r, ParseOptions{OGG}) - assert.NoError(t, err) - tag.ClearAllTags() - buffy := new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = parse(r, ParseOptions{OGG}) - assert.NoError(t, err) - assert.Empty(t, tag.Artist()) - assert.Empty(t, tag.Album()) - assert.Empty(t, tag.Title()) - }) - - t.Run("TestWriteEmptyTagsOggVorbis-file", func(t *testing.T) { - err := os.Mkdir("testdata/temp", 0755) - assert.NoError(t, err) - of, err := os.ReadFile("testdata/testdata-ogg-vorbis-nonEmpty.ogg") - assert.NoError(t, err) - err = os.WriteFile("testdata/temp/testdata-ogg-vorbis-nonEmpty.ogg", of, 0755) - assert.NoError(t, err) - path, _ := filepath.Abs("testdata/temp/testdata-ogg-vorbis-nonEmpty.ogg") - f, err := os.OpenFile(path, os.O_RDONLY, 0755) - assert.NoError(t, err) - defer f.Close() - tag, err := Open(f, ParseOptions{OGG}) - assert.NoError(t, err) - tag.ClearAllTags() - err = SaveTag(tag, f) - assert.NoError(t, err) - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{OGG}) - assert.NoError(t, err) - f.Close() - err = os.RemoveAll("testdata/temp") - assert.NoError(t, err) - assert.Empty(t, tag.Artist()) - assert.Empty(t, tag.Album()) - assert.Empty(t, tag.Title()) - }) - - t.Run("TestWriteTagsOggVorbisFromEmpty-buffers", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-ogg-vorbis-nonEmpty.ogg") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - - tag, err := Open(r, ParseOptions{OGG}) - assert.NoError(t, err) - tag.ClearAllTags() - - buffy := new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{OGG}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - tag.SetGenre("Trap") - tag.SetDate("2024-06-01") - tag.SetAlbumArtist("a talented guy") - tag.SetComments("I wrote some comments about your song") - tag.SetPublisher("I am the publisher") - tag.SetCopyrightMsg("hey please don't steal") - tag.SetComposer("someone composed I suppose") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - - buffy = new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{OGG}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - assert.Equal(t, tag.Genre(), "Trap") - assert.Equal(t, tag.Date(), "2024-06-01") - assert.Equal(t, tag.AlbumArtist(), "a talented guy") - assert.Equal(t, tag.Comments(), "I wrote some comments about your song") - assert.Equal(t, tag.Publisher(), "I am the publisher") - assert.Equal(t, tag.CopyrightMsg(), "hey please don't steal") - assert.Equal(t, tag.Composer(), "someone composed I suppose") - picFile, err := os.Open(p) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*tag.albumArt) - - assert.True(t, compareImages(img1data, img2data)) - }) - - t.Run("TestWriteTagsOggVorbisFromEmpty-file", func(t *testing.T) { - err := os.Mkdir("testdata/temp", 0755) - assert.NoError(t, err) - of, err := os.ReadFile("testdata/testdata-ogg-vorbis-nonEmpty.ogg") - assert.NoError(t, err) - err = os.WriteFile("testdata/temp/testdata-ogg-vorbis-nonEmpty.ogg", of, 0755) - assert.NoError(t, err) - path, _ := filepath.Abs("testdata/temp/testdata-ogg-vorbis-nonEmpty.ogg") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - - tag, err := Open(f, ParseOptions{OGG}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - - err = SaveTag(tag, f) - assert.NoError(t, err) - - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{OGG}) - assert.NoError(t, err) - err = os.RemoveAll("testdata/temp") - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - picFile, err := os.Open(p) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*tag.albumArt) - - assert.True(t, compareImages(img1data, img2data)) - }) - - t.Run("TestUpdateTagsOggVorbis-buffers", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-ogg-vorbis-nonEmpty.ogg") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - - tag, err := Open(r, ParseOptions{OGG}) - assert.NoError(t, err) - tag.ClearAllTags() - - buffy := new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{OGG}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - - buffy = new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{OGG}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - - tag.SetArtist("TestArtist2") - - buffy = new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{OGG}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist2") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - picFile, err := os.Open(p) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*tag.albumArt) - - assert.True(t, compareImages(img1data, img2data)) - }) - - t.Run("TestUpdateTagsOggVorbis-file", func(t *testing.T) { - err := os.Mkdir("testdata/temp", 0755) - assert.NoError(t, err) - of, err := os.ReadFile("testdata/testdata-ogg-vorbis-nonEmpty.ogg") - assert.NoError(t, err) - err = os.WriteFile("testdata/temp/testdata-ogg-vorbis-nonEmpty.ogg", of, 0755) - assert.NoError(t, err) - path, _ := filepath.Abs("testdata/temp/testdata-ogg-vorbis-nonEmpty.ogg") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - - tag, err := Open(f, ParseOptions{OGG}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - err = SaveTag(tag, f) - assert.NoError(t, err) - - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{OGG}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - - tag.SetArtist("TestArtist2") - err = SaveTag(tag, f) - assert.NoError(t, err) - - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{OGG}) - assert.NoError(t, err) - f.Close() - err = os.RemoveAll("testdata/temp") - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist2") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - picFile, err := os.Open(p) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*tag.albumArt) - - assert.True(t, compareImages(img1data, img2data)) - }) -} - -func TestOggOpus(t *testing.T) { - t.Run("TestWriteEmptyTagsOggOpus-buffers", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-opus-nonEmpty.ogg") - f, err := os.Open(path) - assert.NoError(t, err) - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - tag, err := parse(r, ParseOptions{OGG}) - assert.NoError(t, err) - tag.ClearAllTags() - buffy := new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = parse(r, ParseOptions{OGG}) - assert.NoError(t, err) - assert.Empty(t, tag.Artist()) - assert.Empty(t, tag.Album()) - assert.Empty(t, tag.Title()) - }) - - t.Run("TestWriteEmptyTagsOggOpus-file", func(t *testing.T) { - err := os.Mkdir("testdata/temp", 0755) - assert.NoError(t, err) - of, err := os.ReadFile("testdata/testdata-opus-nonEmpty.ogg") - assert.NoError(t, err) - err = os.WriteFile("testdata/temp/testdata-opus-nonEmpty.ogg", of, 0755) - assert.NoError(t, err) - path, _ := filepath.Abs("testdata/temp/testdata-opus-nonEmpty.ogg") - f, err := os.OpenFile(path, os.O_RDONLY, 0755) - assert.NoError(t, err) - defer f.Close() - tag, err := Open(f, ParseOptions{OGG}) - assert.NoError(t, err) - tag.ClearAllTags() - err = SaveTag(tag, f) - assert.NoError(t, err) - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{OGG}) - assert.NoError(t, err) - f.Close() - err = os.RemoveAll("testdata/temp") - assert.NoError(t, err) - assert.Empty(t, tag.Artist()) - assert.Empty(t, tag.Album()) - assert.Empty(t, tag.Title()) - }) - - t.Run("TestWriteTagsOggOpusFromEmpty-buffers", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-opus-nonEmpty.ogg") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - - tag, err := Open(r, ParseOptions{OGG}) - assert.NoError(t, err) - tag.ClearAllTags() - - buffy := new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{OGG}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - tag.SetGenre("Trap") - tag.SetDate("2024-06-01") - tag.SetAlbumArtist("a talented guy") - tag.SetComments("I wrote some comments about your song") - tag.SetPublisher("I am the publisher") - tag.SetCopyrightMsg("hey please don't steal") - tag.SetComposer("someone composed I suppose") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - buffy = new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{OGG}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - assert.Equal(t, tag.Genre(), "Trap") - assert.Equal(t, tag.Date(), "2024-06-01") - assert.Equal(t, tag.AlbumArtist(), "a talented guy") - assert.Equal(t, tag.Comments(), "I wrote some comments about your song") - assert.Equal(t, tag.Publisher(), "I am the publisher") - assert.Equal(t, tag.CopyrightMsg(), "hey please don't steal") - assert.Equal(t, tag.Composer(), "someone composed I suppose") - picFile, err := os.Open(p) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*tag.albumArt) - - assert.True(t, compareImages(img1data, img2data)) - }) - - t.Run("TestWriteTagsOggOpusFromEmpty-file", func(t *testing.T) { - err := os.Mkdir("testdata/temp", 0755) - assert.NoError(t, err) - of, err := os.ReadFile("testdata/testdata-opus-nonEmpty.ogg") - assert.NoError(t, err) - err = os.WriteFile("testdata/temp/testdata-opus-nonEmpty.ogg", of, 0755) - assert.NoError(t, err) - path, _ := filepath.Abs("testdata/temp/testdata-opus-nonEmpty.ogg") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - - tag, err := Open(f, ParseOptions{OGG}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - - err = SaveTag(tag, f) - assert.NoError(t, err) - - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{OGG}) - assert.NoError(t, err) - err = os.RemoveAll("testdata/temp") - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - picFile, err := os.Open(p) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*tag.albumArt) - - assert.True(t, compareImages(img1data, img2data)) - }) - - t.Run("TestUpdateTagsOggOpus-buffers", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-opus-nonEmpty.ogg") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - - tag, err := Open(r, ParseOptions{OGG}) - assert.NoError(t, err) - tag.ClearAllTags() - - buffy := new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{OGG}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - - buffy = new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{OGG}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - - tag.SetArtist("TestArtist2") - - buffy = new(bytes.Buffer) - err = SaveTag(tag, buffy) - assert.NoError(t, err) - - r = bytes.NewReader(buffy.Bytes()) - tag, err = Open(r, ParseOptions{OGG}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist2") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - picFile, err := os.Open(p) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*tag.albumArt) - - assert.True(t, compareImages(img1data, img2data)) - }) - - t.Run("TestUpdateTagsOggOpus-file", func(t *testing.T) { - err := os.Mkdir("testdata/temp", 0755) - assert.NoError(t, err) - of, err := os.ReadFile("testdata/testdata-opus-nonEmpty.ogg") - assert.NoError(t, err) - err = os.WriteFile("testdata/temp/testdata-opus-nonEmpty.ogg", of, 0755) - assert.NoError(t, err) - path, _ := filepath.Abs("testdata/temp/testdata-opus-nonEmpty.ogg") - f, err := os.Open(path) - assert.NoError(t, err) - defer f.Close() - - tag, err := Open(f, ParseOptions{OGG}) - assert.NoError(t, err) - tag.SetArtist("TestArtist1") - tag.SetTitle("TestTitle1") - tag.SetAlbum("TestAlbum1") - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - err = tag.SetAlbumArtFromFilePath(p) - assert.NoError(t, err) - err = SaveTag(tag, f) - assert.NoError(t, err) - - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{OGG}) - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist1") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - - tag.SetArtist("TestArtist2") - err = SaveTag(tag, f) - assert.NoError(t, err) - - _, err = f.Seek(0, io.SeekStart) - assert.NoError(t, err) - tag, err = Open(f, ParseOptions{OGG}) - assert.NoError(t, err) - f.Close() - err = os.RemoveAll("testdata/temp") - assert.NoError(t, err) - assert.Equal(t, tag.Artist(), "TestArtist2") - assert.Equal(t, tag.Album(), "TestAlbum1") - assert.Equal(t, tag.Title(), "TestTitle1") - picFile, err := os.Open(p) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*tag.albumArt) - - assert.True(t, compareImages(img1data, img2data)) - }) -} - -type saveMockReader struct { - mock.Mock -} - -func (m *saveMockReader) Seek(offset int64, whence int) (int64, error) { - args := m.Called(offset, whence) - return args.Get(0).(int64), args.Error(1) -} - -func (m *saveMockReader) Read(p []byte) (int, error) { - args := m.Called(p) - return args.Int(0), args.Error(1) -} - -type saveMockWriter struct { - mock.Mock -} - -func (m *saveMockWriter) Write(p []byte) (int, error) { - args := m.Called(p) - return args.Int(0), args.Error(1) -} - -func TestSave_SeekError(t *testing.T) { - mockReader := new(saveMockReader) - mockReader.On("Seek", int64(0), io.SeekStart).Return(int64(0), errors.New("seek error")) - - tag := &IDTag{ - reader: mockReader, - } - - mockWriter := new(saveMockWriter) - - err := tag.Save(mockWriter) - assert.EqualError(t, err, "seek error") - mockReader.AssertExpectations(t) -} - -func TestSave_MP3Error(t *testing.T) { - mockReader := new(saveMockReader) - mockReader.On("Read", mock.Anything).Return(100, io.EOF) - mockReader.On("Seek", int64(0), io.SeekStart).Return(int64(0), nil) - - tag := &IDTag{ - fileType: "mp3", - reader: mockReader, - } - - mockWriter := new(saveMockWriter) - mockWriter.On("Write", mock.Anything).Return(0, errors.New("mp3 write error")) - - err := tag.Save(mockWriter) - assert.EqualError(t, err, "mp3 write error") - mockWriter.AssertExpectations(t) -} -func TestSave_MP3ErrorRead(t *testing.T) { - mockReader := new(saveMockReader) - mockReader.On("Seek", int64(0), io.SeekStart).Return(int64(0), nil) - mockReader.On("Read", mock.Anything).Return(0, errors.New("read error")) - - tag := &IDTag{ - fileType: "mp3", - reader: mockReader, - } - - mockWriter := new(saveMockWriter) - - err := tag.Save(mockWriter) - assert.EqualError(t, err, "read error") - mockWriter.AssertExpectations(t) -} - -func TestSave_FLACError(t *testing.T) { - mockReader := new(saveMockReader) - mockReader.On("Read", mock.Anything).Return(400, io.EOF) - mockReader.On("Seek", int64(0), io.SeekStart).Return(int64(0), nil) - - tag := &IDTag{ - fileType: "flac", - reader: mockReader, - } - - mockWriter := new(saveMockWriter) - - err := tag.Save(mockWriter) - assert.EqualError(t, err, "error parsing flac stream") - mockReader.AssertExpectations(t) -} - -func TestSave_OGGError(t *testing.T) { - mockReader := new(saveMockReader) - mockReader.On("Read", mock.Anything).Return(156, io.EOF) - mockReader.On("Seek", int64(0), io.SeekStart).Return(int64(0), nil) - - tag := &IDTag{ - fileType: "ogg", - codec: "vorbis", - reader: mockReader, - } - - mockWriter := new(saveMockWriter) - - err := tag.Save(mockWriter) - assert.Error(t, err) - mockReader.AssertExpectations(t) -} - -func TestSave_UnsupportedFileType(t *testing.T) { - mockReader := new(saveMockReader) - mockReader.On("Seek", int64(0), io.SeekStart).Return(int64(0), nil) - - tag := &IDTag{ - fileType: "UNKNOWN", - reader: mockReader, - } - - mockWriter := new(saveMockWriter) - - err := tag.Save(mockWriter) - assert.EqualError(t, err, ErrNoMethodAvlble.Error()) - mockReader.AssertExpectations(t) -} diff --git a/tag.go b/tag.go deleted file mode 100644 index 1e6dc2c..0000000 --- a/tag.go +++ /dev/null @@ -1,289 +0,0 @@ -package audiometa - -import ( - "bytes" - "image" - "io" - "os" -) - -// OpenTagFromPath Opens the ID tag for the corresponding file as long as it is a supported filetype -// Use the OpenTagFromPath command and you will be able to access all metadata associated with the file -// If you don't pass ParseOptions this function will try to detect the filetype by the extension. If the extension can't be detected an error will occur. -func OpenTagFromPath(filepath string, p ...ParseOptions) (*IDTag, error) { - file, err := os.Open(filepath) - if err != nil { - return nil, err - } - var f FileType - if p == nil { - f, err = GetFileType(filepath) - if err != nil { - return nil, err - } - } else { - f = p[0].Format - } - return parse(file, ParseOptions{f}) -} - -// Open opens the tag for the passed in reader. It does not have to be a file, it can be a bytes.Reader, or any other interface that implements io.ReadSeeker -func Open(r io.ReadSeeker, p ParseOptions) (*IDTag, error) { - return parse(r, p) -} - -// SaveTag saves the corresponding IDTag to the supplied io.Writer. It can be a bytes.Buffer, file, etc. If it's the same file as the input, audiometa creates a temp buffer to prevent a read/write circle. -func SaveTag(tag *IDTag, w io.Writer) error { - return tag.Save(w) -} - -// ClearAllTags clears all tags except the fileUrl tag which is used to reference the file, takes an optional parameter "preserveUnkown": when this is true passThroughMap is not cleared and unknown tags are preserved -func (tag *IDTag) ClearAllTags(preserveUnknown ...bool) { - tag.artist = "" - tag.albumArtist = "" - tag.album = "" - tag.albumArt = nil - tag.comments = "" - tag.composer = "" - tag.genre = "" - tag.title = "" - tag.year = "" - tag.bpm = "" - tag.copyrightMsg = "" - tag.date = "" - tag.encodedBy = "" - tag.lyricist = "" - tag.language = "" - tag.length = "" - tag.partOfSet = "" - tag.publisher = "" - - preserve := false - if len(preserveUnknown) != 0 { - preserve = preserveUnknown[0] - } - if !preserve { - tag.PassThrough = make(map[string]string) - } - -} - -// Artist gets the artist for a tag -func (tag *IDTag) Artist() string { - return tag.artist -} - -// SetArtist sets the artist for a tag -func (tag *IDTag) SetArtist(artist string) { - tag.artist = artist -} - -// AlbumArtist gets the album artist for a tag -func (tag *IDTag) AlbumArtist() string { - return tag.albumArtist -} - -// SetAlbumArtist sets the album artist for a tag -func (tag *IDTag) SetAlbumArtist(albumArtist string) { - tag.albumArtist = albumArtist -} - -// Album gets the album for a tag -func (tag *IDTag) Album() string { - return tag.album -} - -// SetAlbum sets the album for a tag -func (tag *IDTag) SetAlbum(album string) { - tag.album = album -} - -// Comments gets the comments for a tag -func (tag *IDTag) Comments() string { - return tag.comments -} - -// SetComments sets the comments for a tag -func (tag *IDTag) SetComments(comments string) { - tag.comments = comments -} - -// Composer gets the composer for a tag -func (tag *IDTag) Composer() string { - return tag.composer -} - -// SetComposer sets the composer for a tag -func (tag *IDTag) SetComposer(composer string) { - tag.composer = composer -} - -// Genre gets the genre for a tag -func (tag *IDTag) Genre() string { - return tag.genre -} - -// SetGenre sets the genre for a tag -func (tag *IDTag) SetGenre(genre string) { - tag.genre = genre -} - -// Title gets the title for a tag -func (tag *IDTag) Title() string { - return tag.title -} - -// SetTitle sets the title for a tag -func (tag *IDTag) SetTitle(title string) { - tag.title = title -} - -// Year gets the year for a tag as a string -func (tag *IDTag) Year() string { - return tag.year -} - -// SetYear sets the year for a tag -func (tag *IDTag) SetYear(year string) { - tag.year = year -} - -// BPM gets the BPM for a tag as a string -func (tag *IDTag) BPM() string { - return tag.bpm -} - -// SetBPM sets the BPM for a tag -func (tag *IDTag) SetBPM(bpm string) { - tag.bpm = bpm -} - -// CopyrightMs gets the Copyright Messgae for a tag -func (tag *IDTag) CopyrightMsg() string { - return tag.copyrightMsg -} - -// SetCopyrightMsg sets the Copyright Message for a tag -func (tag *IDTag) SetCopyrightMsg(copyrightMsg string) { - tag.copyrightMsg = copyrightMsg -} - -// Date gets the date for a tag as a string -func (tag *IDTag) Date() string { - return tag.date -} - -// SetDate sets the date for a tag -func (tag *IDTag) SetDate(date string) { - tag.date = date -} - -// EncodedBy gets who encoded the tag -func (tag *IDTag) EncodedBy() string { - return tag.encodedBy -} - -// SetEncodedBy sets who encoded the tag -func (tag *IDTag) SetEncodedBy(encodedBy string) { - tag.encodedBy = encodedBy -} - -// Lyricist gets the lyricist for the tag -func (tag *IDTag) Lyricist() string { - return tag.lyricist -} - -// SetLyricist sets the lyricist for the tag -func (tag *IDTag) SetLyricist(lyricist string) { - tag.lyricist = lyricist -} - -// FileType gets the filetype of the tag -func (tag *IDTag) FileType() string { - return tag.fileType -} - -// SetFileType sets the filtype of the tag -func (tag *IDTag) SetFileType(fileType string) { - tag.fileType = fileType -} - -// Language gets the language of the tag -func (tag *IDTag) Language() string { - return tag.language -} - -// SetLanguage sets the lanuguage of the tag -func (tag *IDTag) SetLanguage(language string) { - tag.language = language -} - -// Length gets the length of the audio file -func (tag *IDTag) Length() string { - return tag.length -} - -// SetLength sets the length of the audio file -func (tag *IDTag) SetLength(length string) { - tag.length = length -} - -// PartOfSet gets if the track is part of a set -func (tag *IDTag) PartOfSet() string { - return tag.partOfSet -} - -// SetPartOfSet sets if the track is part of a set -func (tag *IDTag) SetPartOfSet(partOfSet string) { - tag.partOfSet = partOfSet -} - -// Publisher gets the publisher for the tag -func (tag *IDTag) Publisher() string { - return tag.publisher -} - -// SetPublisher sets the publisher for the tag -func (tag *IDTag) SetPublisher(publisher string) { - tag.publisher = publisher -} - -// AdditionalTags gets all additional (unmapped) tags -func (tag *IDTag) AdditionalTags() map[string]string { - return tag.PassThrough -} - -// SetAdditionalTag sets an additional (unmapped) tag taking an id and value (id,value) (ogg only) -func (tag *IDTag) SetAdditionalTag(id string, value string) { - tag.PassThrough[id] = value -} - -// SetAlbumArtFromByteArray sets the album art by passing a byte array for the album art -func (tag *IDTag) SetAlbumArtFromByteArray(albumArt []byte) error { - img, _, err := image.Decode(bytes.NewReader(albumArt)) - if err != nil { - return err - } - tag.albumArt = &img - return nil -} - -// SetAlbumArtFromImage sets the album art by passing an *image.Image as the album art -func (tag *IDTag) SetAlbumArtFromImage(albumArt *image.Image) { - tag.albumArt = albumArt -} - -// SetAlbumArtFromFilePath sets the album art by passing a filepath as a string -func (tag *IDTag) SetAlbumArtFromFilePath(filePath string) error { - f, err := os.Open(filePath) - if err != nil { - return err - } - defer f.Close() - img, _, err := image.Decode(f) - if err != nil { - return err - } - tag.albumArt = &img - return nil -} diff --git a/tag_test.go b/tag_test.go deleted file mode 100644 index 365267e..0000000 --- a/tag_test.go +++ /dev/null @@ -1,355 +0,0 @@ -package audiometa - -import ( - "bytes" - "image" - "image/jpeg" - "io" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestTag(t *testing.T) { - artist := "the talented guy" - albumArtist := "also the talented guy" - album := "I couldn't come up with a name EP" - comments := "some comments that I wrote" - composer := "bob the composer man" - genre := "Heavy Metal" - title := "the title for thou I am" - year := "2024" - bpm := "107" - copyrightMsg := "don't steal things" - date := "05-31-2024" - encodedBy := "me: the encoder" - lyricist := "the lyrics guy" - fileType := "mp3" - language := "english" - length := "3:08" - partOfSet := "false" - publisher := "someone rich" - - path, _ := filepath.Abs("testdata/testdata-mp3.mp3") - f, err := os.Open(path) - assert.NoError(t, err) - tag, err := parse(f, ParseOptions{MP3}) - assert.NoError(t, err) - - t.Run("test set album", func(t *testing.T) { - tag.SetAlbum(album) - alb := tag.album - assert.Equal(t, album, alb) - }) - t.Run("test set/get album", func(t *testing.T) { - tag.SetAlbum(album) - alb := tag.Album() - assert.Equal(t, album, alb) - }) - t.Run("test set/get album artist", func(t *testing.T) { - tag.SetAlbumArtist(albumArtist) - aart := tag.AlbumArtist() - assert.Equal(t, albumArtist, aart) - }) - t.Run("test set album artist", func(t *testing.T) { - tag.SetAlbumArtist(albumArtist) - aart := tag.albumArtist - assert.Equal(t, albumArtist, aart) - }) - t.Run("test set/get artist", func(t *testing.T) { - tag.SetArtist(artist) - art := tag.Artist() - assert.Equal(t, artist, art) - }) - t.Run("test set artist", func(t *testing.T) { - tag.SetArtist(artist) - art := tag.artist - assert.Equal(t, artist, art) - }) - t.Run("test set bpm", func(t *testing.T) { - tag.SetBPM(bpm) - bp := tag.bpm - assert.Equal(t, bpm, bp) - }) - t.Run("test set/get bpm", func(t *testing.T) { - tag.SetBPM(bpm) - bp := tag.BPM() - assert.Equal(t, bpm, bp) - }) - t.Run("test set comments", func(t *testing.T) { - tag.SetComments(comments) - cmts := tag.comments - assert.Equal(t, comments, cmts) - }) - t.Run("test set/get comments", func(t *testing.T) { - tag.SetComments(comments) - cmts := tag.Comments() - assert.Equal(t, comments, cmts) - }) - t.Run("test set composer", func(t *testing.T) { - tag.SetComposer(composer) - cmpsr := tag.composer - assert.Equal(t, composer, cmpsr) - }) - t.Run("test set/get composer", func(t *testing.T) { - tag.SetComposer(composer) - cmpsr := tag.Composer() - assert.Equal(t, composer, cmpsr) - }) - t.Run("test set copyright", func(t *testing.T) { - tag.SetCopyrightMsg(copyrightMsg) - cprt := tag.copyrightMsg - assert.Equal(t, copyrightMsg, cprt) - }) - t.Run("test set/get copyright", func(t *testing.T) { - tag.SetCopyrightMsg(copyrightMsg) - cprt := tag.CopyrightMsg() - assert.Equal(t, copyrightMsg, cprt) - }) - t.Run("test set date", func(t *testing.T) { - tag.SetDate(date) - dte := tag.date - assert.Equal(t, date, dte) - }) - t.Run("test set/get date", func(t *testing.T) { - tag.SetDate(date) - dte := tag.Date() - assert.Equal(t, date, dte) - }) - t.Run("test set/get encoded by", func(t *testing.T) { - tag.SetEncodedBy(encodedBy) - enc := tag.EncodedBy() - assert.Equal(t, encodedBy, enc) - }) - t.Run("test set encoded by", func(t *testing.T) { - tag.SetEncodedBy(encodedBy) - enc := tag.encodedBy - assert.Equal(t, encodedBy, enc) - }) - t.Run("test set filetype", func(t *testing.T) { - tag.SetFileType(fileType) - ft := tag.fileType - assert.Equal(t, fileType, ft) - }) - t.Run("test set/get filetype", func(t *testing.T) { - tag.SetFileType(fileType) - ft := tag.FileType() - assert.Equal(t, fileType, ft) - }) - t.Run("test set language", func(t *testing.T) { - tag.SetLanguage(language) - lang := tag.language - assert.Equal(t, language, lang) - }) - t.Run("test set/get language", func(t *testing.T) { - tag.SetLanguage(language) - lang := tag.Language() - assert.Equal(t, language, lang) - }) - t.Run("test set length", func(t *testing.T) { - tag.SetLength(length) - lgth := tag.length - assert.Equal(t, length, lgth) - }) - t.Run("test set/get length", func(t *testing.T) { - tag.SetLength(length) - lgth := tag.Length() - assert.Equal(t, length, lgth) - }) - t.Run("test set lyricist", func(t *testing.T) { - tag.SetLyricist(lyricist) - lcst := tag.lyricist - assert.Equal(t, lyricist, lcst) - }) - t.Run("test set/get lyricist", func(t *testing.T) { - tag.SetLyricist(lyricist) - lcst := tag.Lyricist() - assert.Equal(t, lyricist, lcst) - }) - t.Run("test set part of set", func(t *testing.T) { - tag.SetPartOfSet(partOfSet) - pos := tag.partOfSet - assert.Equal(t, partOfSet, pos) - }) - t.Run("test set/get part of set", func(t *testing.T) { - tag.SetPartOfSet(partOfSet) - pos := tag.PartOfSet() - assert.Equal(t, partOfSet, pos) - }) - t.Run("test set publisher", func(t *testing.T) { - tag.SetPublisher(publisher) - pub := tag.publisher - assert.Equal(t, publisher, pub) - }) - t.Run("test set/get publisher", func(t *testing.T) { - tag.SetPublisher(publisher) - pub := tag.Publisher() - assert.Equal(t, publisher, pub) - }) - t.Run("test set/get title", func(t *testing.T) { - tag.SetTitle(title) - titl := tag.Title() - assert.Equal(t, title, titl) - }) - t.Run("test set title", func(t *testing.T) { - tag.SetTitle(title) - titl := tag.title - assert.Equal(t, title, titl) - }) - t.Run("test set/get year", func(t *testing.T) { - tag.SetYear(year) - yr := tag.Year() - assert.Equal(t, year, yr) - }) - t.Run("test set year", func(t *testing.T) { - tag.SetYear(year) - yr := tag.year - assert.Equal(t, year, yr) - }) - t.Run("test set genre", func(t *testing.T) { - tag.SetGenre(genre) - gnr := tag.genre - assert.Equal(t, genre, gnr) - }) - t.Run("test set/get genre", func(t *testing.T) { - tag.SetGenre(genre) - gnr := tag.Genre() - assert.Equal(t, genre, gnr) - }) -} -func TestOpenTagFromPath_NoParseOptions(t *testing.T) { - filepath := "testdata-mp3.mp3" - file, err := os.Create(filepath) - assert.NoError(t, err, "Expected no error creating test file") - defer os.Remove(filepath) - file.Close() - - idTag, err := OpenTagFromPath(filepath) - assert.NoError(t, err, "Expected no error opening tag from path") - assert.NotNil(t, idTag, "Expected non-nil IDTag") -} - -func TestOpenTagFromPath_WithParseOptions(t *testing.T) { - filepath := "testdata-mp3.mp3" - file, err := os.Create(filepath) - assert.NoError(t, err, "Expected no error creating test file") - defer os.Remove(filepath) - file.Close() - - parseOptions := ParseOptions{Format: MP3} - idTag, err := OpenTagFromPath(filepath, parseOptions) - assert.NoError(t, err, "Expected no error opening tag from path") - assert.NotNil(t, idTag, "Expected non-nil IDTag") -} - -func TestOpenTagFromPath_WithParseOptionsInvalidFileType(t *testing.T) { - filepath := "testdata-mp3.mp3" - file, err := os.Create(filepath) - assert.NoError(t, err, "Expected no error creating test file") - defer os.Remove(filepath) - file.Close() - - parseOptions := ParseOptions{Format: "noooo"} - idTag, err := OpenTagFromPath(filepath, parseOptions) - assert.Error(t, err) - assert.Nil(t, idTag) -} - -func TestOpenTagFromPath_NoParseOptionsInvalidFileType(t *testing.T) { - filepath := "testdata-text.txt" - file, err := os.Create(filepath) - assert.NoError(t, err, "Expected no error creating test file") - defer os.Remove(filepath) - file.Close() - - idTag, err := OpenTagFromPath(filepath) - assert.Error(t, err) - assert.Nil(t, idTag) -} - -func TestAlbumArtTags(t *testing.T) { - t.Run("TestSetImageFromByteSlice", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-mp3-nonEmpty.mp3") - f, err := os.Open(path) - assert.NoError(t, err) - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - tag, err := parse(r, ParseOptions{MP3}) - assert.NoError(t, err) - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - img, err := os.Open(p) - assert.NoError(t, err) - - decImg, _, err := image.Decode(img) - assert.NoError(t, err) - buf := new(bytes.Buffer) - err = jpeg.Encode(buf, decImg, nil) - assert.NoError(t, err) - - err = tag.SetAlbumArtFromByteArray(buf.Bytes()) - assert.NoError(t, err) - picFile, err := os.Open(p) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*tag.albumArt) - - assert.True(t, compareImages(img1data, img2data)) - }) - - t.Run("TestSetImageFromImage", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-mp3-nonEmpty.mp3") - f, err := os.Open(path) - assert.NoError(t, err) - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - tag, err := parse(r, ParseOptions{MP3}) - assert.NoError(t, err) - p, err := filepath.Abs("./testdata/withAlbumArt/testdata-img-1.jpg") - assert.NoError(t, err) - img, err := os.Open(p) - assert.NoError(t, err) - decImg, _, err := image.Decode(img) - assert.NoError(t, err) - - tag.SetAlbumArtFromImage(&decImg) - picFile, err := os.Open(p) - assert.NoError(t, err) - picData, _, err := image.Decode(picFile) - assert.NoError(t, err) - img1data := image_2_array_at(picData) - img2data := image_2_array_at(*tag.albumArt) - - assert.True(t, compareImages(img1data, img2data)) - }) -} - -func TestPassthroughOGG(t *testing.T) { - t.Run("TestPassThroughMap-OGG", func(t *testing.T) { - path, _ := filepath.Abs("testdata/testdata-ogg-vorbis-nonEmpty.ogg") - f, err := os.Open(path) - assert.NoError(t, err) - b, err := io.ReadAll(f) - assert.NoError(t, err) - r := bytes.NewReader(b) - tag, err := parse(r, ParseOptions{OGG}) - assert.NoError(t, err) - - tag.SetAdditionalTag("testtag", "testvalue") - buffy := new(bytes.Buffer) - err = tag.Save(buffy) - assert.NoError(t, err) - reader := bytes.NewReader(buffy.Bytes()) - tag, err = parse(reader, ParseOptions{OGG}) - assert.NoError(t, err) - - assert.Equal(t, (tag.AdditionalTags())["TESTTAG"], "testvalue") - - }) -} diff --git a/testdata/testdata-flac-nonEmpty.flac b/testdata/testdata-flac-nonEmpty.flac deleted file mode 100644 index 76a6203..0000000 Binary files a/testdata/testdata-flac-nonEmpty.flac and /dev/null differ diff --git a/testdata/testdata-flac.flac b/testdata/testdata-flac.flac deleted file mode 100644 index 76a6203..0000000 Binary files a/testdata/testdata-flac.flac and /dev/null differ diff --git a/testdata/testdata-img-1.jpg b/testdata/testdata-img-1.jpg deleted file mode 100644 index 03a0adc..0000000 Binary files a/testdata/testdata-img-1.jpg and /dev/null differ diff --git a/testdata/testdata-m4a-nonEmpty.m4a b/testdata/testdata-m4a-nonEmpty.m4a deleted file mode 100644 index e08530c..0000000 Binary files a/testdata/testdata-m4a-nonEmpty.m4a and /dev/null differ diff --git a/testdata/testdata-m4a.m4a b/testdata/testdata-m4a.m4a deleted file mode 100644 index 957f549..0000000 Binary files a/testdata/testdata-m4a.m4a and /dev/null differ diff --git a/testdata/testdata-mp3-nonEmpty.mp3 b/testdata/testdata-mp3-nonEmpty.mp3 deleted file mode 100644 index 9cd6587..0000000 Binary files a/testdata/testdata-mp3-nonEmpty.mp3 and /dev/null differ diff --git a/testdata/testdata-mp3.mp3 b/testdata/testdata-mp3.mp3 deleted file mode 100644 index 87c0343..0000000 Binary files a/testdata/testdata-mp3.mp3 and /dev/null differ diff --git a/testdata/testdata-ogg-vorbis-nonEmpty.ogg b/testdata/testdata-ogg-vorbis-nonEmpty.ogg deleted file mode 100755 index 76717eb..0000000 Binary files a/testdata/testdata-ogg-vorbis-nonEmpty.ogg and /dev/null differ diff --git a/testdata/testdata-ogg.ogg b/testdata/testdata-ogg.ogg deleted file mode 100755 index 118c562..0000000 Binary files a/testdata/testdata-ogg.ogg and /dev/null differ diff --git a/testdata/testdata-opus-nonEmpty.ogg b/testdata/testdata-opus-nonEmpty.ogg deleted file mode 100644 index 0cd8d6b..0000000 Binary files a/testdata/testdata-opus-nonEmpty.ogg and /dev/null differ diff --git a/testdata/testdata-opus.ogg b/testdata/testdata-opus.ogg deleted file mode 100644 index 2361b5e..0000000 Binary files a/testdata/testdata-opus.ogg and /dev/null differ diff --git a/testdata/withAlbumArt/test1.m4a b/testdata/withAlbumArt/test1.m4a deleted file mode 100644 index b1336e5..0000000 Binary files a/testdata/withAlbumArt/test1.m4a and /dev/null differ diff --git a/testdata/withAlbumArt/testdata-flac-nonEmpty.flac b/testdata/withAlbumArt/testdata-flac-nonEmpty.flac deleted file mode 100644 index 9a15258..0000000 Binary files a/testdata/withAlbumArt/testdata-flac-nonEmpty.flac and /dev/null differ diff --git a/testdata/withAlbumArt/testdata-img-1.jpg b/testdata/withAlbumArt/testdata-img-1.jpg deleted file mode 100644 index 03a0adc..0000000 Binary files a/testdata/withAlbumArt/testdata-img-1.jpg and /dev/null differ diff --git a/testdata/withAlbumArt/testdata-m4a-nonEmpty.m4a b/testdata/withAlbumArt/testdata-m4a-nonEmpty.m4a deleted file mode 100644 index 569fc4d..0000000 Binary files a/testdata/withAlbumArt/testdata-m4a-nonEmpty.m4a and /dev/null differ diff --git a/testdata/withAlbumArt/testdata-mp3-nonEmpty.mp3 b/testdata/withAlbumArt/testdata-mp3-nonEmpty.mp3 deleted file mode 100644 index ee37f20..0000000 Binary files a/testdata/withAlbumArt/testdata-mp3-nonEmpty.mp3 and /dev/null differ diff --git a/testdata/withAlbumArt/testdata-ogg-vorbis-nonEmpty.ogg b/testdata/withAlbumArt/testdata-ogg-vorbis-nonEmpty.ogg deleted file mode 100644 index 04d41d2..0000000 Binary files a/testdata/withAlbumArt/testdata-ogg-vorbis-nonEmpty.ogg and /dev/null differ diff --git a/testdata/withAlbumArt/testdata-opus-nonEmpty.ogg b/testdata/withAlbumArt/testdata-opus-nonEmpty.ogg deleted file mode 100644 index 67539ec..0000000 Binary files a/testdata/withAlbumArt/testdata-opus-nonEmpty.ogg and /dev/null differ diff --git a/types.go b/types.go deleted file mode 100644 index 73c05fa..0000000 --- a/types.go +++ /dev/null @@ -1,89 +0,0 @@ -package audiometa - -import ( - "image" - "io" -) - -// The IDTag represents all of the metadata that can be retrieved from a file. -// The IDTag contains all tags for all audio types. Some tags may not be applicable to all types. -// Only the valid types are written to the respective data files. -// Although a tag may be set, if the function to write that tag attribute doesn't exist, the tag attribute will be ignored and the save function will not produce an error. -type IDTag struct { - artist string //Artist - albumArtist string //AlbumArtist - album string //Album - albumArt *image.Image //AlbumArt for the work in image format - comments string //Comments - composer string //Composer - genre string //Genre - title string //Title - year string //Year - bpm string //BPM - codec string //The codec of the file (ogg use only) - copyrightMsg string //Copyright Message - date string //Date - encodedBy string //Endcoded By - lyricist string //Lyricist - fileType string //File Type - language string //Language - length string //Length - partOfSet string //Part of a set - publisher string //Publisher - - reader io.ReadSeeker - - PassThrough map[string]string -} - -// ParseOptions is a struct that is passed when parsing a tag. If included, you should set the format to one of the existing FileTypes -type ParseOptions struct { - Format FileType -} - -type MP4Delete []string - -type FileType string - -const ( - MP3 FileType = "mp3" - M4P FileType = "m4p" - M4A FileType = "m4a" - M4B FileType = "m4b" - MP4 FileType = "mp4" - FLAC FileType = "flac" - OGG FileType = "ogg" -) - -const ( - ALBUM string = "album" - ARTIST string = "artist" - DATE string = "date" - TITLE string = "title" - GENRE string = "genre" -) - -var mp3TextFrames = map[string]string{ - "artist": "TPE1", - "title": "TIT2", - "album": "TALB", - "comments": "COMM", - "bpm": "TBPM", - "genre": "TCON", - "year": "TYER", - "albumArtist": "TPE2", - "composer": "TCOM", - "copyrightMsg": "TCOP", - "date": "TDRC", - "encodedBy": "TENC", - "lyricist": "TEXT", - "fileType": "TFLT", - "language": "TLAN", - "length": "TLEN", - "partOfSet": "TPOS", - "publisher": "TPUB", - "albumArt": "APIC", -} - -var supportedFileTypes = []FileType{MP3, M4P, M4A, M4B, MP4, FLAC, OGG} -var mp4FileTypes = []FileType{M4P, M4A, M4B, MP4} diff --git a/util.go b/util.go deleted file mode 100644 index 972e17a..0000000 --- a/util.go +++ /dev/null @@ -1,98 +0,0 @@ -package audiometa - -import ( - "bytes" - "encoding/binary" - "errors" - "io" - "strings" -) - -// GetFileType returns the file type of the file pointed to by filepath. If the filetype is not supported, an error is returned. -func GetFileType(filepath string) (FileType, error) { - sp := strings.Split(filepath, ".") - if len(sp) < 2 { - return "", errors.New("unsupported file extension or no extension") - } - for _, ft := range supportedFileTypes { - if strings.ToLower(sp[len(sp)-1]) == string(ft) { - return ft, nil - } - } - return "", errors.New("unsupported file extension or no extension") -} - -func getInt(b []byte) int { - var n int - for _, x := range b { - n = n << 8 - n |= int(x) - } - return n -} -func readInt(r io.Reader, n uint) (int, error) { - b, err := readBytes(r, n) - if err != nil { - return 0, err - } - return getInt(b), nil -} - -func readUint(r io.Reader, n uint) (uint, error) { - x, err := readInt(r, n) - if err != nil { - return 0, err - } - return uint(x), nil -} - -// readBytesMaxUpfront is the max up-front allocation allowed -const readBytesMaxUpfront = 10 << 20 // 10MB - -func readBytes(r io.Reader, n uint) ([]byte, error) { - if n > readBytesMaxUpfront { - b := &bytes.Buffer{} - if _, err := io.CopyN(b, r, int64(n)); err != nil { - return nil, err - } - return b.Bytes(), nil - } - - b := make([]byte, n) - _, err := io.ReadFull(r, b) - if err != nil { - return nil, err - } - return b, nil -} - -func readString(r io.Reader, n uint) (string, error) { - b, err := readBytes(r, n) - if err != nil { - return "", err - } - return string(b), nil -} -func readUint32LittleEndian(r io.Reader) (uint32, error) { - b, err := readBytes(r, 4) - if err != nil { - return 0, err - } - return binary.LittleEndian.Uint32(b), nil -} -func encodeUint32(n uint32) []byte { - buf := bytes.NewBuffer([]byte{}) - if err := binary.Write(buf, binary.BigEndian, n); err != nil { - panic(err) - } - return buf.Bytes() -} - -func fileTypesContains(v FileType, a []FileType) bool { - for _, i := range a { - if i == v { - return true - } - } - return false -} diff --git a/util_test.go b/util_test.go deleted file mode 100644 index a843d79..0000000 --- a/util_test.go +++ /dev/null @@ -1,210 +0,0 @@ -package audiometa - -import ( - "bytes" - "errors" - "io" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetFileType(t *testing.T) { - tests := []struct { - filepath string - expected FileType - err error - }{ - {"file.mp3", "mp3", nil}, - {"document.m4a", "m4a", nil}, - {"image.ogg", "ogg", nil}, - {"noextensionfile", "", errors.New("unsupported file extension or no extension")}, - {"unsupportedfile.txt", "", errors.New("unsupported file extension or no extension")}, - } - - for _, test := range tests { - ft, err := GetFileType(test.filepath) - if test.err != nil { - assert.EqualError(t, err, test.err.Error(), "GetFileType(%s) should return error %v", test.filepath, test.err) - } else { - assert.NoError(t, err, "GetFileType(%s) should not return error", test.filepath) - assert.Equal(t, test.expected, ft, "GetFileType(%s) should return %v", test.filepath, test.expected) - } - } -} - -func TestGetInt(t *testing.T) { - tests := []struct { - b []byte - expected int - }{ - {[]byte{0x00}, 0}, - {[]byte{0x01}, 1}, - {[]byte{0x01, 0x00}, 256}, - {[]byte{0x01, 0x00, 0x01}, 65537}, - } - - for _, test := range tests { - result := getInt(test.b) - assert.Equal(t, test.expected, result, "getInt(%v) should return %d", test.b, test.expected) - } -} - -func TestReadInt(t *testing.T) { - tests := []struct { - input []byte - n uint - expected int - err error - }{ - {[]byte{0x01}, 1, 1, nil}, - {[]byte{0x01, 0x00}, 2, 256, nil}, - {[]byte{0x01, 0x00, 0x01}, 3, 65537, nil}, - {[]byte{}, 1, 0, io.EOF}, - } - - for _, test := range tests { - r := bytes.NewReader(test.input) - result, err := readInt(r, test.n) - if test.err != nil { - assert.EqualError(t, err, test.err.Error(), "readInt(%v, %d) should return error %v", test.input, test.n, test.err) - } else { - assert.NoError(t, err, "readInt(%v, %d) should not return error", test.input, test.n) - assert.Equal(t, test.expected, result, "readInt(%v, %d) should return %d", test.input, test.n, test.expected) - } - } -} - -func TestReadUint(t *testing.T) { - tests := []struct { - input []byte - n uint - expected uint - err error - }{ - {[]byte{0x01}, 1, 1, nil}, - {[]byte{0x01, 0x00}, 2, 256, nil}, - {[]byte{0x01, 0x00, 0x01}, 3, 65537, nil}, - {[]byte{}, 1, 0, io.EOF}, - } - - for _, test := range tests { - r := bytes.NewReader(test.input) - result, err := readUint(r, test.n) - if test.err != nil { - assert.EqualError(t, err, test.err.Error(), "readUint(%v, %d) should return error %v", test.input, test.n, test.err) - } else { - assert.NoError(t, err, "readUint(%v, %d) should not return error", test.input, test.n) - assert.Equal(t, test.expected, result, "readUint(%v, %d) should return %d", test.input, test.n, test.expected) - } - } -} - -func TestReadBytes(t *testing.T) { - tests := []struct { - input []byte - n uint - expected []byte - err error - }{ - {[]byte("hello"), 5, []byte("hello"), nil}, - {[]byte("hello"), 10, nil, errors.New("unexpected EOF")}, - {make([]byte, readBytesMaxUpfront+1), uint(readBytesMaxUpfront + 1), make([]byte, readBytesMaxUpfront+1), nil}, - } - - for _, test := range tests { - r := bytes.NewReader(test.input) - result, err := readBytes(r, test.n) - if test.err != nil { - assert.EqualError(t, err, test.err.Error(), "readBytes(%v, %d) should return error %v", test.input, test.n, test.err) - } else { - assert.NoError(t, err, "readBytes(%v, %d) should not return error", test.input, test.n) - assert.Equal(t, test.expected, result, "readBytes(%v, %d) should return %v", test.input, test.n, test.expected) - } - } -} - -func TestReadString(t *testing.T) { - tests := []struct { - input []byte - n uint - expected string - err error - }{ - {[]byte("hello"), 5, "hello", nil}, - {[]byte("hello"), 10, "", errors.New("unexpected EOF")}, - } - - for _, test := range tests { - r := bytes.NewReader(test.input) - result, err := readString(r, test.n) - if test.err != nil { - assert.EqualError(t, err, test.err.Error(), "readString(%v, %d) should return error %v", test.input, test.n, test.err) - } else { - assert.NoError(t, err, "readString(%v, %d) should not return error", test.input, test.n) - assert.Equal(t, test.expected, result, "readString(%v, %d) should return %s", test.input, test.n, test.expected) - } - } -} - -func TestReadUint32LittleEndian(t *testing.T) { - tests := []struct { - input []byte - expected uint32 - err error - }{ - {[]byte{0x01, 0x00, 0x00, 0x00}, 1, nil}, - {[]byte{0xff, 0x00, 0x00, 0x00}, 255, nil}, - {[]byte{0x01, 0x00, 0x00}, 0, io.ErrUnexpectedEOF}, - } - - for _, test := range tests { - r := bytes.NewReader(test.input) - result, err := readUint32LittleEndian(r) - if test.err != nil { - assert.EqualError(t, err, test.err.Error(), "readUint32LittleEndian(%v) should return error %v", test.input, test.err) - } else { - assert.NoError(t, err, "readUint32LittleEndian(%v) should not return error", test.input) - assert.Equal(t, test.expected, result, "readUint32LittleEndian(%v) should return %d", test.input, test.expected) - } - } -} - -func TestEncodeUint32(t *testing.T) { - tests := []struct { - input uint32 - expected []byte - }{ - {1, []byte{0x00, 0x00, 0x00, 0x01}}, - {255, []byte{0x00, 0x00, 0x00, 0xff}}, - } - - for _, test := range tests { - result := encodeUint32(test.input) - assert.Equal(t, test.expected, result, "encodeUint32(%d) should return %v", test.input, test.expected) - } -} - -func TestFileTypesContains(t *testing.T) { - tests := []struct { - input FileType - expected bool - }{ - {"txt", false}, - {"pdf", false}, - {"jpg", false}, - {"png", false}, - {"mp3", true}, - {"m4a", true}, - {"m4b", true}, - {"m4p", true}, - {"mp4", true}, - {"flac", true}, - {"ogg", true}, - } - - for _, test := range tests { - result := fileTypesContains(test.input, supportedFileTypes) - assert.Equal(t, test.expected, result, "fileTypesContains(%s, %v) should return %t", test.input, supportedFileTypes, test.expected) - } -} diff --git a/v3/doc.go b/v3/doc.go new file mode 100644 index 0000000..b074528 --- /dev/null +++ b/v3/doc.go @@ -0,0 +1,4 @@ +/* +Package audiometa is an all in one solution for reading and writing audio metadata in Go. It supports MP3, FLAC, Ogg, and MP4 files. +*/ +package audiometa diff --git a/v3/go.mod b/v3/go.mod new file mode 100644 index 0000000..30277f5 --- /dev/null +++ b/v3/go.mod @@ -0,0 +1,19 @@ +module github.com/gcottom/audiometa/v3 + +go 1.18 + +require ( + github.com/gcottom/flacmeta v0.0.3 + github.com/gcottom/mp3meta v0.0.1 + github.com/gcottom/mp4meta v0.0.2 + github.com/gcottom/oggmeta v0.0.6 +) + +require ( + github.com/abema/go-mp4 v1.2.0 // indirect + github.com/aler9/writerseeker v1.1.0 // indirect + github.com/bogem/id3v2/v2 v2.1.4 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/sunfish-shogi/bufseekio v0.1.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/v3/go.sum similarity index 88% rename from go.sum rename to v3/go.sum index 2e51f96..28253db 100644 --- a/go.sum +++ b/v3/go.sum @@ -8,14 +8,19 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gcottom/flacmeta v0.0.3 h1:rH2J2J1hBeorsMCKvBulnEl34quFYBOb2l87YnkcneU= +github.com/gcottom/flacmeta v0.0.3/go.mod h1:epsVDI52yCmttAfQ2kLxOORX1Lgfspn6a14WRflLVzI= +github.com/gcottom/mp3meta v0.0.1 h1:wM4iemx1RZCmUG3ptSbNWGxhU0tyWtW8PPsRxYfitEs= +github.com/gcottom/mp3meta v0.0.1/go.mod h1:mYnE1j3llRiBHrEbLE57NVGIia4qfXoCShXuFkWv5+s= +github.com/gcottom/mp4meta v0.0.2 h1:RW8p6zyMetPz6KIhzHD1EHJu+KN0GnrHxoLH9p6Z9eA= +github.com/gcottom/mp4meta v0.0.2/go.mod h1:qstBsqczbFF1e90C828qv966Xjx/LFPjWQITARCpyfA= +github.com/gcottom/oggmeta v0.0.6 h1:eN6WpfZTFj8ca6YSzmrT7HTSGE3zF5CKBN/UDDbQ/8Q= +github.com/gcottom/oggmeta v0.0.6/go.mod h1:ifdINhphaEW587BIgA+m5TJwCoVk88JuZMCHwXc/gpM= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw= github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0= @@ -23,10 +28,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I= github.com/sunfish-shogi/bufseekio v0.1.0 h1:zu38kFbv0KuuiwZQeuYeS02U9AM14j0pVA9xkHOCJ2A= github.com/sunfish-shogi/bufseekio v0.1.0/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I= @@ -58,11 +61,9 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v3/tag.go b/v3/tag.go new file mode 100644 index 0000000..f19a1a3 --- /dev/null +++ b/v3/tag.go @@ -0,0 +1,71 @@ +package audiometa + +import ( + "errors" + "fmt" + "image" + "io" + + "github.com/gcottom/flacmeta" + "github.com/gcottom/mp3meta" + "github.com/gcottom/mp4meta" + "github.com/gcottom/oggmeta" +) + +type Tag interface { + Save(io.Writer) error + + GetAlbum() string + GetAlbumArtist() string + GetArtist() string + GetBPM() int + GetComposer() string + GetCopyright() string + GetCoverArt() *image.Image + GetDiscNumber() int + GetDiscTotal() int + GetEncoder() string + GetGenre() string + GetTitle() string + GetTrackNumber() int + GetTrackTotal() int + + SetAlbum(string) + SetAlbumArtist(string) + SetArtist(string) + SetBPM(int) + SetComposer(string) + SetCoverArt(*image.Image) + SetDiscNumber(int) + SetDiscTotal(int) + SetEncoder(string) + SetGenre(string) + SetTitle(string) + SetTrackNumber(int) + SetTrackTotal(int) +} + +func OpenTag(r io.ReadSeeker) (Tag, error) { + b, err := readBytes(r, 8) + if err != nil { + return nil, err + } + + if _, err = r.Seek(-8, io.SeekCurrent); err != nil { + return nil, fmt.Errorf("error seeking back to original position: %v", err) + } + + switch { + case string(b[0:3]) == "ID3": + return mp3meta.ParseMP3(r) + case string(b[4:8]) == "ftyp": + return mp4meta.ReadMP4(r) + case string(b[0:4]) == "fLaC": + return flacmeta.ReadFLAC(r) + case string(b[0:4]) == "OggS": + return oggmeta.ReadOGG(r) + default: + return nil, errors.New("unsupported file type") + } + +} diff --git a/v3/util.go b/v3/util.go new file mode 100644 index 0000000..5ee6cca --- /dev/null +++ b/v3/util.go @@ -0,0 +1,26 @@ +package audiometa + +import ( + "bytes" + "io" +) + +// readBytesMaxUpfront is the max up-front allocation allowed +const readBytesMaxUpfront = 10 << 20 // 10MB + +func readBytes(r io.Reader, n uint) ([]byte, error) { + if n > readBytesMaxUpfront { + b := &bytes.Buffer{} + if _, err := io.CopyN(b, r, int64(n)); err != nil { + return nil, err + } + return b.Bytes(), nil + } + + b := make([]byte, n) + _, err := io.ReadFull(r, b) + if err != nil { + return nil, err + } + return b, nil +}