From 9efd333e79c1cac0e029a387a5f8969a594e423c Mon Sep 17 00:00:00 2001 From: Marvin Preuss Date: Mon, 25 Oct 2021 09:21:00 +0200 Subject: [PATCH] feat: mp3 tagging --- go.mod | 2 + go.sum | 4 + main.go | 32 +- vendor/github.com/djimenez/iconv-go/LICENSE | 22 + vendor/github.com/djimenez/iconv-go/README.md | 111 ++++ .../github.com/djimenez/iconv-go/converter.go | 168 +++++ vendor/github.com/djimenez/iconv-go/iconv.go | 48 ++ vendor/github.com/djimenez/iconv-go/reader.go | 100 +++ vendor/github.com/djimenez/iconv-go/writer.go | 82 +++ vendor/github.com/mikkyang/id3-go/.travis.yml | 4 + vendor/github.com/mikkyang/id3-go/LICENSE | 22 + vendor/github.com/mikkyang/id3-go/README.md | 85 +++ .../mikkyang/id3-go/encodedbytes/reader.go | 87 +++ .../mikkyang/id3-go/encodedbytes/util.go | 152 +++++ .../mikkyang/id3-go/encodedbytes/writer.go | 63 ++ vendor/github.com/mikkyang/id3-go/id3.go | 116 ++++ vendor/github.com/mikkyang/id3-go/util.go | 59 ++ vendor/github.com/mikkyang/id3-go/v1/id3v1.go | 148 +++++ vendor/github.com/mikkyang/id3-go/v2/frame.go | 585 ++++++++++++++++++ vendor/github.com/mikkyang/id3-go/v2/id3v2.go | 350 +++++++++++ .../github.com/mikkyang/id3-go/v2/id3v22.go | 131 ++++ .../github.com/mikkyang/id3-go/v2/id3v23.go | 161 +++++ vendor/github.com/mikkyang/id3-go/v2/util.go | 8 + vendor/modules.txt | 9 + 24 files changed, 2545 insertions(+), 4 deletions(-) create mode 100644 vendor/github.com/djimenez/iconv-go/LICENSE create mode 100644 vendor/github.com/djimenez/iconv-go/README.md create mode 100644 vendor/github.com/djimenez/iconv-go/converter.go create mode 100644 vendor/github.com/djimenez/iconv-go/iconv.go create mode 100644 vendor/github.com/djimenez/iconv-go/reader.go create mode 100644 vendor/github.com/djimenez/iconv-go/writer.go create mode 100644 vendor/github.com/mikkyang/id3-go/.travis.yml create mode 100644 vendor/github.com/mikkyang/id3-go/LICENSE create mode 100644 vendor/github.com/mikkyang/id3-go/README.md create mode 100644 vendor/github.com/mikkyang/id3-go/encodedbytes/reader.go create mode 100644 vendor/github.com/mikkyang/id3-go/encodedbytes/util.go create mode 100644 vendor/github.com/mikkyang/id3-go/encodedbytes/writer.go create mode 100644 vendor/github.com/mikkyang/id3-go/id3.go create mode 100644 vendor/github.com/mikkyang/id3-go/util.go create mode 100644 vendor/github.com/mikkyang/id3-go/v1/id3v1.go create mode 100644 vendor/github.com/mikkyang/id3-go/v2/frame.go create mode 100644 vendor/github.com/mikkyang/id3-go/v2/id3v2.go create mode 100644 vendor/github.com/mikkyang/id3-go/v2/id3v22.go create mode 100644 vendor/github.com/mikkyang/id3-go/v2/id3v23.go create mode 100644 vendor/github.com/mikkyang/id3-go/v2/util.go diff --git a/go.mod b/go.mod index e547153..c1b9175 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.17 require ( github.com/gocolly/colly/v2 v2.1.0 + github.com/mikkyang/id3-go v0.0.0-20191012064224-2c6ab3bb1fbd go.xsfx.dev/workgroups v0.2.0 ) @@ -13,6 +14,7 @@ require ( github.com/antchfx/htmlquery v1.2.3 // indirect github.com/antchfx/xmlquery v1.2.4 // indirect github.com/antchfx/xpath v1.1.8 // indirect + github.com/djimenez/iconv-go v0.0.0-20160305225143-8960e66bd3da // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect diff --git a/go.sum b/go.sum index 1524bb3..0d34758 100644 --- a/go.sum +++ b/go.sum @@ -249,6 +249,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/djimenez/iconv-go v0.0.0-20160305225143-8960e66bd3da h1:0qwwqQCLOOXPl58ljnq3sTJR7yRuMolM02vjxDh4ZVE= +github.com/djimenez/iconv-go v0.0.0-20160305225143-8960e66bd3da/go.mod h1:ns+zIWBBchgfRdxNgIJWn2x6U95LQchxeqiN5Cgdgts= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= @@ -630,6 +632,8 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mikkyang/id3-go v0.0.0-20191012064224-2c6ab3bb1fbd h1:Cqivkwpk34qJJsi0xbZp2TOhpMsG381iaum8mb+6T/s= +github.com/mikkyang/id3-go v0.0.0-20191012064224-2c6ab3bb1fbd/go.mod h1:6ReX25kzt2D67Dt9vH3kTm8R4luFEfW9W3RDuytp0IA= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.1.2/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4= diff --git a/main.go b/main.go index fb0dfed..6ed557b 100644 --- a/main.go +++ b/main.go @@ -15,11 +15,13 @@ import ( "time" "github.com/gocolly/colly/v2" + id3 "github.com/mikkyang/id3-go" "go.xsfx.dev/workgroups" ) const ( - URL = "https://susallefolgen.netlify.app/" + URL = "https://susallefolgen.netlify.app/" + Artist = "Sanft & Sorgfältig" ) type Episode struct { @@ -62,7 +64,9 @@ func (e *Episode) Download() error { } defer resp.Body.Close() - f, err := os.Create(fmt.Sprintf("%d.mp3", e.Number)) + e.file = fmt.Sprintf("%d.mp3", e.Number) + + f, err := os.Create(e.file) if err != nil { return fmt.Errorf("could not create file: %w", err) } @@ -75,6 +79,22 @@ func (e *Episode) Download() error { return nil } +func (e *Episode) Tag() error { + f, err := id3.Open(e.file) + if err != nil { + return fmt.Errorf("could not open file: %w", err) + } + defer f.Close() + + title := fmt.Sprintf("#%03d %s", e.Number, e.Title) + + f.SetArtist(Artist) + f.SetAlbum(title) + f.SetTitle(title) + + return nil +} + //nolint:funlen func main() { c := colly.NewCollector() @@ -118,12 +138,12 @@ func main() { d, ctx := workgroups.NewDispatcher( context.Background(), runtime.GOMAXPROCS(0), - len(episodes.Episodes), + len(episodes.Episodes[:1]), ) d.Start() - for _, ep := range episodes.Episodes { + for _, ep := range episodes.Episodes[:1] { ep := ep d.Append(workgroups.NewJob(ctx, func(ctx context.Context) error { @@ -137,6 +157,10 @@ func main() { return fmt.Errorf("could not download: %w", err) } + if err := ep.Tag(); err != nil { + return fmt.Errorf("could not tag: %w", err) + } + return nil })) } diff --git a/vendor/github.com/djimenez/iconv-go/LICENSE b/vendor/github.com/djimenez/iconv-go/LICENSE new file mode 100644 index 0000000..51282f3 --- /dev/null +++ b/vendor/github.com/djimenez/iconv-go/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2013, Donovan Jimenez +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * 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. + +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/vendor/github.com/djimenez/iconv-go/README.md b/vendor/github.com/djimenez/iconv-go/README.md new file mode 100644 index 0000000..8c24a1f --- /dev/null +++ b/vendor/github.com/djimenez/iconv-go/README.md @@ -0,0 +1,111 @@ +# Install + +The main method of installation is through "go get" (provided in $GOROOT/bin) + + go get github.com/djimenez/iconv-go + +This both downloads from github and installs the package into your $GOPATH. To just +recompile the package after you've already "get"ed (e.g. for a new go version), +use "go install" instead. + + go install github.com/djimenez/iconv-go + +See documentation for "go get" and "go install" for more information. + +__PLEASE NOTE__ that this package requires the use of cgo, since it is only a wrapper around iconv - either provided by libiconv or glibc on your system. Attempts to build without cgo enabled will fail. + +# Usage + +To use the package, you'll need the appropriate import statement: + + import ( + iconv "github.com/djimenez/iconv-go" + ) + +## Converting string Values + +Converting a string can be done with two methods. First, there's +iconv.ConvertString(input, fromEncoding, toEncoding string) + + output,_ := iconv.ConvertString("Hello World!", "utf-8", "windows-1252") + +Alternatively, you can create a converter and use its ConvertString method. +Reuse of a Converter instance is recommended when doing many string conversions +between the same encodings. + + converter := iconv.NewConverter("utf-8", "windows-1252") + output,_ := converter.ConvertString("Hello World!") + + // converter can then be closed explicitly + // this will also happen when garbage collected + converter.Close() + +ConvertString may return errors for the following reasons: + + * EINVAL - when either the from or to encoding is not supported by iconv + * EILSEQ - when the input string contains an invalid byte sequence for the + given from encoding + +## Converting []byte Values + +Converting a []byte can similarly be done with two methods. First, there's +iconv.Convert(input, output []byte, fromEncoding, toEncoding string). You'll +immediately notice this requires you to give it both the input and output +buffer. Ideally, the output buffer should be sized so that it can hold all +converted bytes from input, but if it cannot, then Convert will put as many +bytes as it can into the buffer without creating an invalid sequence. For +example, if iconv only has a single byte left in the output buffer but needs 2 +or more for the complete character in a multibyte encoding it will stop writing +to the buffer and return with an iconv.E2BIG error. + + in := []byte("Hello World!") + out := make([]byte, len(input)) + + bytesRead, bytesWritten, err := iconv.Convert(in, out, "utf-8", "latin1") + +Just like with ConvertString, there is also a Convert method on Converter that +can be used. + + ... + converter := iconv.NewConverter("utf-8", "windows-1252") + + bytesRead, bytesWritten, error := converter.Convert(input, output) + +Convert may return errors for the following reasons: + + * EINVAL - when either the from or to encoding is not supported by iconv + * EILSEQ - when the input string contains an invalid byte sequence for the + given from encoding + * E2BIG - when the output buffer is not big enough to hold the full + conversion of input + + Note on E2BIG: this is a common error value especially when converting to a + multibyte encoding and should not be considered fatal. Partial conversion + has probably occurred be sure to check bytesRead and bytesWritten. + +### Note on Shift Based Encodings + +When using iconv.Convert convenience method it will automatically try to append +to your output buffer with a nil input so that any end shift sequences are +appropiately written. Using a Converter.Convert method however will not +automatically do this since it can be used to process a full stream in chunks. +So you'll need to remember to pass a nil input buffer at the end yourself, just +like you would with direct iconv usage. + +## Converting an \*io.Reader + +The iconv.Reader allows any other \*io.Reader to be wrapped and have its bytes +transcoded as they are read. + + // We're wrapping stdin for simplicity, but a File or network reader could + // be wrapped as well + reader,_ := iconv.NewReader(os.Stdin, "utf-8", "windows-1252") + +## Converting an \*io.Writer + +The iconv.Writer allows any other \*io.Writer to be wrapped and have its bytes +transcoded as they are written. + + // We're wrapping stdout for simplicity, but a File or network reader could + // be wrapped as well + writer,_ := iconv.NewWriter(os.Stdout, "utf-8", "windows-1252") diff --git a/vendor/github.com/djimenez/iconv-go/converter.go b/vendor/github.com/djimenez/iconv-go/converter.go new file mode 100644 index 0000000..0b404e6 --- /dev/null +++ b/vendor/github.com/djimenez/iconv-go/converter.go @@ -0,0 +1,168 @@ +package iconv + +/* +#cgo darwin LDFLAGS: -liconv +#cgo freebsd LDFLAGS: -liconv +#cgo windows LDFLAGS: -liconv +#include +#include + +// As of GO 1.6 passing a pointer to Go pointer, will lead to panic +// Therofore we use this wrapper function, to avoid passing **char directly from go +size_t call_iconv(iconv_t ctx, char *in, size_t *size_in, char *out, size_t *size_out){ + return iconv(ctx, &in, size_in, &out, size_out); +} + +*/ +import "C" +import "syscall" +import "unsafe" + +type Converter struct { + context C.iconv_t + open bool +} + +// Initialize a new Converter. If fromEncoding or toEncoding are not supported by +// iconv then an EINVAL error will be returned. An ENOMEM error maybe returned if +// there is not enough memory to initialize an iconv descriptor +func NewConverter(fromEncoding string, toEncoding string) (converter *Converter, err error) { + converter = new(Converter) + + // convert to C strings + toEncodingC := C.CString(toEncoding) + fromEncodingC := C.CString(fromEncoding) + + // open an iconv descriptor + converter.context, err = C.iconv_open(toEncodingC, fromEncodingC) + + // free the C Strings + C.free(unsafe.Pointer(toEncodingC)) + C.free(unsafe.Pointer(fromEncodingC)) + + // check err + if err == nil { + // no error, mark the context as open + converter.open = true + } + + return +} + +// destroy is called during garbage collection +func (this *Converter) destroy() { + this.Close() +} + +// Close a Converter's iconv description explicitly +func (this *Converter) Close() (err error) { + if this.open { + _, err = C.iconv_close(this.context) + } + + return +} + +// Convert bytes from an input byte slice into a give output byte slice +// +// As many bytes that can converted and fit into the size of output will be +// processed and the number of bytes read for input as well as the number of +// bytes written to output will be returned. If not all converted bytes can fit +// into output and E2BIG error will also be returned. If input contains an invalid +// sequence of bytes for the Converter's fromEncoding an EILSEQ error will be returned +// +// For shift based output encodings, any end shift byte sequences can be generated by +// passing a 0 length byte slice as input. Also passing a 0 length byte slice for output +// will simply reset the iconv descriptor shift state without writing any bytes. +func (this *Converter) Convert(input []byte, output []byte) (bytesRead int, bytesWritten int, err error) { + // make sure we are still open + if this.open { + inputLeft := C.size_t(len(input)) + outputLeft := C.size_t(len(output)) + + if inputLeft > 0 && outputLeft > 0 { + // we have to give iconv a pointer to a pointer of the underlying + // storage of each byte slice - so far this is the simplest + // way i've found to do that in Go, but it seems ugly + inputPointer := (*C.char)(unsafe.Pointer(&input[0])) + outputPointer := (*C.char)(unsafe.Pointer(&output[0])) + + _, err = C.call_iconv(this.context, inputPointer, &inputLeft, outputPointer, &outputLeft) + + // update byte counters + bytesRead = len(input) - int(inputLeft) + bytesWritten = len(output) - int(outputLeft) + } else if inputLeft == 0 && outputLeft > 0 { + // inputPointer will be nil, outputPointer is generated as above + outputPointer := (*C.char)(unsafe.Pointer(&output[0])) + + _, err = C.call_iconv(this.context, nil, &inputLeft, outputPointer, &outputLeft) + + // update write byte counter + bytesWritten = len(output) - int(outputLeft) + } else { + // both input and output are zero length, do a shift state reset + _, err = C.call_iconv(this.context, nil, &inputLeft, nil, &outputLeft) + } + } else { + err = syscall.EBADF + } + + return bytesRead, bytesWritten, err +} + +// Convert an input string +// +// EILSEQ error may be returned if input contains invalid bytes for the +// Converter's fromEncoding. +func (this *Converter) ConvertString(input string) (output string, err error) { + // make sure we are still open + if this.open { + // construct the buffers + inputBuffer := []byte(input) + outputBuffer := make([]byte, len(inputBuffer)*2) // we use a larger buffer to help avoid resizing later + + // call Convert until all input bytes are read or an error occurs + var bytesRead, totalBytesRead, bytesWritten, totalBytesWritten int + + for totalBytesRead < len(inputBuffer) && err == nil { + // use the totals to create buffer slices + bytesRead, bytesWritten, err = this.Convert(inputBuffer[totalBytesRead:], outputBuffer[totalBytesWritten:]) + + totalBytesRead += bytesRead + totalBytesWritten += bytesWritten + + // check for the E2BIG error specifically, we can add to the output + // buffer to correct for it and then continue + if err == syscall.E2BIG { + // increase the size of the output buffer by another input length + // first, create a new buffer + tempBuffer := make([]byte, len(outputBuffer)+len(inputBuffer)) + + // copy the existing data + copy(tempBuffer, outputBuffer) + + // switch the buffers + outputBuffer = tempBuffer + + // forget the error + err = nil + } + } + + if err == nil { + // perform a final shift state reset + _, bytesWritten, err = this.Convert([]byte{}, outputBuffer[totalBytesWritten:]) + + // update total count + totalBytesWritten += bytesWritten + } + + // construct the final output string + output = string(outputBuffer[:totalBytesWritten]) + } else { + err = syscall.EBADF + } + + return output, err +} diff --git a/vendor/github.com/djimenez/iconv-go/iconv.go b/vendor/github.com/djimenez/iconv-go/iconv.go new file mode 100644 index 0000000..3e0036a --- /dev/null +++ b/vendor/github.com/djimenez/iconv-go/iconv.go @@ -0,0 +1,48 @@ +/* +Wraps the iconv API present on most systems, which allows for conversion +of bytes from one encoding to another. This package additionally provides +some convenient interface implementations like a Reader and Writer. +*/ +package iconv + +// All in one Convert method, rather than requiring the construction of an iconv.Converter +func Convert(input []byte, output []byte, fromEncoding string, toEncoding string) (bytesRead int, bytesWritten int, err error) { + // create a temporary converter + converter, err := NewConverter(fromEncoding, toEncoding) + + if err == nil { + // call converter's Convert + bytesRead, bytesWritten, err = converter.Convert(input, output) + + if err == nil { + var shiftBytesWritten int + + // call Convert with a nil input to generate any end shift sequences + _, shiftBytesWritten, err = converter.Convert(nil, output[bytesWritten:]) + + // add shift bytes to total bytes + bytesWritten += shiftBytesWritten + } + + // close the converter + converter.Close() + } + + return +} + +// All in one ConvertString method, rather than requiring the construction of an iconv.Converter +func ConvertString(input string, fromEncoding string, toEncoding string) (output string, err error) { + // create a temporary converter + converter, err := NewConverter(fromEncoding, toEncoding) + + if err == nil { + // convert the string + output, err = converter.ConvertString(input) + + // close the converter + converter.Close() + } + + return +} diff --git a/vendor/github.com/djimenez/iconv-go/reader.go b/vendor/github.com/djimenez/iconv-go/reader.go new file mode 100644 index 0000000..2835ce6 --- /dev/null +++ b/vendor/github.com/djimenez/iconv-go/reader.go @@ -0,0 +1,100 @@ +package iconv + +import ( + "io" + "syscall" +) + +type Reader struct { + source io.Reader + converter *Converter + buffer []byte + readPos, writePos int + err error +} + +func NewReader(source io.Reader, fromEncoding string, toEncoding string) (*Reader, error) { + // create a converter + converter, err := NewConverter(fromEncoding, toEncoding) + + if err == nil { + return NewReaderFromConverter(source, converter), err + } + + // return the error + return nil, err +} + +func NewReaderFromConverter(source io.Reader, converter *Converter) (reader *Reader) { + reader = new(Reader) + + // copy elements + reader.source = source + reader.converter = converter + + // create 8K buffers + reader.buffer = make([]byte, 8*1024) + + return reader +} + +func (this *Reader) fillBuffer() { + // slide existing data to beginning + if this.readPos > 0 { + // copy current bytes - is this guaranteed safe? + copy(this.buffer, this.buffer[this.readPos:this.writePos]) + + // adjust positions + this.writePos -= this.readPos + this.readPos = 0 + } + + // read new data into buffer at write position + bytesRead, err := this.source.Read(this.buffer[this.writePos:]) + + // adjust write position + this.writePos += bytesRead + + // track any reader error / EOF + if err != nil { + this.err = err + } +} + +// implement the io.Reader interface +func (this *Reader) Read(p []byte) (n int, err error) { + // checks for when we have no data + for this.writePos == 0 || this.readPos == this.writePos { + // if we have an error / EOF, just return it + if this.err != nil { + return n, this.err + } + + // else, fill our buffer + this.fillBuffer() + } + + // TODO: checks for when we have less data than len(p) + + // we should have an appropriate amount of data, convert it into the given buffer + bytesRead, bytesWritten, err := this.converter.Convert(this.buffer[this.readPos:this.writePos], p) + + // adjust byte counters + this.readPos += bytesRead + n += bytesWritten + + // if we experienced an iconv error, check it + if err != nil { + // E2BIG errors can be ignored (we'll get them often) as long + // as at least 1 byte was written. If we experienced an E2BIG + // and no bytes were written then the buffer is too small for + // even the next character + if err != syscall.E2BIG || bytesWritten == 0 { + // track anything else + this.err = err + } + } + + // return our results + return n, this.err +} diff --git a/vendor/github.com/djimenez/iconv-go/writer.go b/vendor/github.com/djimenez/iconv-go/writer.go new file mode 100644 index 0000000..db007eb --- /dev/null +++ b/vendor/github.com/djimenez/iconv-go/writer.go @@ -0,0 +1,82 @@ +package iconv + +import "io" + +type Writer struct { + destination io.Writer + converter *Converter + buffer []byte + readPos, writePos int + err error +} + +func NewWriter(destination io.Writer, fromEncoding string, toEncoding string) (*Writer, error) { + // create a converter + converter, err := NewConverter(fromEncoding, toEncoding) + + if err == nil { + return NewWriterFromConverter(destination, converter), err + } + + // return the error + return nil, err +} + +func NewWriterFromConverter(destination io.Writer, converter *Converter) (writer *Writer) { + writer = new(Writer) + + // copy elements + writer.destination = destination + writer.converter = converter + + // create 8K buffers + writer.buffer = make([]byte, 8*1024) + + return writer +} + +func (this *Writer) emptyBuffer() { + // write new data out of buffer + bytesWritten, err := this.destination.Write(this.buffer[this.readPos:this.writePos]) + + // update read position + this.readPos += bytesWritten + + // slide existing data to beginning + if this.readPos > 0 { + // copy current bytes - is this guaranteed safe? + copy(this.buffer, this.buffer[this.readPos:this.writePos]) + + // adjust positions + this.writePos -= this.readPos + this.readPos = 0 + } + + // track any reader error / EOF + if err != nil { + this.err = err + } +} + +// implement the io.Writer interface +func (this *Writer) Write(p []byte) (n int, err error) { + // write data into our internal buffer + bytesRead, bytesWritten, err := this.converter.Convert(p, this.buffer[this.writePos:]) + + // update bytes written for return + n += bytesRead + this.writePos += bytesWritten + + // checks for when we have a full buffer + for this.writePos > 0 { + // if we have an error, just return it + if this.err != nil { + return + } + + // else empty the buffer + this.emptyBuffer() + } + + return n, err +} diff --git a/vendor/github.com/mikkyang/id3-go/.travis.yml b/vendor/github.com/mikkyang/id3-go/.travis.yml new file mode 100644 index 0000000..a94cb33 --- /dev/null +++ b/vendor/github.com/mikkyang/id3-go/.travis.yml @@ -0,0 +1,4 @@ +language: go +go: + - 1.11 + - 1.12 diff --git a/vendor/github.com/mikkyang/id3-go/LICENSE b/vendor/github.com/mikkyang/id3-go/LICENSE new file mode 100644 index 0000000..95996f5 --- /dev/null +++ b/vendor/github.com/mikkyang/id3-go/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2013 Michael Yang + +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. diff --git a/vendor/github.com/mikkyang/id3-go/README.md b/vendor/github.com/mikkyang/id3-go/README.md new file mode 100644 index 0000000..dbe29e1 --- /dev/null +++ b/vendor/github.com/mikkyang/id3-go/README.md @@ -0,0 +1,85 @@ +# id3 + +[![build status](https://travis-ci.org/mikkyang/id3-go.svg)](https://travis-ci.org/mikkyang/id3-go) + +ID3 library for Go. + +Supported formats: + +* ID3v1 +* ID3v2.2 +* ID3v2.3 + +# Install + +The platform ($GOROOT/bin) "go get" tool is the best method to install. + + go get github.com/mikkyang/id3-go + +This downloads and installs the package into your $GOPATH. If you only want to +recompile, use "go install". + + go install github.com/mikkyang/id3-go + +# Usage + +An import allows access to the package. + + import ( + id3 "github.com/mikkyang/id3-go" + ) + +Version specific details can be accessed through the subpackages. + + import ( + "github.com/mikkyang/id3-go/v1" + "github.com/mikkyang/id3-go/v2" + ) + +# Quick Start + +To access the tag of a file, first open the file using the package's `Open` +function. + + mp3File, err := id3.Open("All-In.mp3") + +It's also a good idea to ensure that the file is closed using `defer`. + + defer mp3File.Close() + +## Accessing Information + +Some commonly used data have methods in the tag for easier access. These +methods are for `Title`, `Artist`, `Album`, `Year`, `Genre`, and `Comments`. + + mp3File.SetArtist("Okasian") + fmt.Println(mp3File.Artist()) + +# ID3v2 Frames + +v2 Frames can be accessed directly by using the `Frame` or `Frames` method +of the file, which return the first frame or a slice of frames as `Framer` +interfaces. These interfaces allow read access to general details of the file. + + lyricsFrame := mp3File.Frame("USLT") + lyrics := lyricsFrame.String() + +If more specific information is needed, or frame-specific write access is +needed, then the interface must be cast into the appropriate underlying type. +The example provided does not check for errors, but it is recommended to do +so. + + lyricsFrame := mp3File.Frame("USLT").(*v2.UnsynchTextFrame) + +## Adding Frames + +For common fields, a frame will automatically be created with the `Set` method. +For other frames or more fine-grained control, frames can be created with the +corresponding constructor, usually prefixed by `New`. These constructors require +the first argument to be a FrameType struct, which are global variables named by +version. + + ft := V23FrameTypeMap["TIT2"] + text := "Hello" + textFrame := NewTextFrame(ft, text) + mp3File.AddFrames(textFrame) diff --git a/vendor/github.com/mikkyang/id3-go/encodedbytes/reader.go b/vendor/github.com/mikkyang/id3-go/encodedbytes/reader.go new file mode 100644 index 0000000..855095e --- /dev/null +++ b/vendor/github.com/mikkyang/id3-go/encodedbytes/reader.go @@ -0,0 +1,87 @@ +// Copyright 2013 Michael Yang. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package encodedbytes + +import ( + "errors" + "io" +) + +// Reader is a helper for Reading frame bytes +type Reader struct { + data []byte + index int // current Reading index +} + +func (r *Reader) Read(b []byte) (n int, err error) { + if len(b) == 0 { + return 0, nil + } + if r.index >= len(r.data) { + return 0, io.EOF + } + n = copy(b, r.data[r.index:]) + r.index += n + return +} + +func (r *Reader) ReadByte() (b byte, err error) { + if r.index >= len(r.data) { + return 0, io.EOF + } + b = r.data[r.index] + r.index++ + return +} + +func (r *Reader) ReadNumBytes(n int) ([]byte, error) { + if n <= 0 { + return []byte{}, nil + } + if r.index+n > len(r.data) { + return []byte{}, io.EOF + } + + b := make([]byte, n) + _, err := r.Read(b) + + return b, err +} + +// Read a number of bytes and cast to a string +func (r *Reader) ReadNumBytesString(n int) (string, error) { + b, err := r.ReadNumBytes(n) + return string(b), err +} + +// Read until the end of the data +func (r *Reader) ReadRest() ([]byte, error) { + return r.ReadNumBytes(len(r.data) - r.index) +} + +// Read until the end of the data and cast to a string +func (r *Reader) ReadRestString(encoding byte) (string, error) { + b, err := r.ReadRest() + if err != nil { + return "", err + } + + return Decoders[encoding].ConvertString(string(b)) +} + +// Read a null terminated string of specified encoding +func (r *Reader) ReadNullTermString(encoding byte) (string, error) { + atIndex, afterIndex := nullIndex(r.data[r.index:], encoding) + b, err := r.ReadNumBytes(afterIndex) + if err != nil { + return "", err + } + + if -1 == atIndex { + return "", errors.New("could not read null terminated string") + } + return Decoders[encoding].ConvertString(string(b[:atIndex])) +} + +func NewReader(b []byte) *Reader { return &Reader{b, 0} } diff --git a/vendor/github.com/mikkyang/id3-go/encodedbytes/util.go b/vendor/github.com/mikkyang/id3-go/encodedbytes/util.go new file mode 100644 index 0000000..766596c --- /dev/null +++ b/vendor/github.com/mikkyang/id3-go/encodedbytes/util.go @@ -0,0 +1,152 @@ +// Copyright 2013 Michael Yang. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package encodedbytes + +import ( + "bytes" + "errors" + iconv "github.com/djimenez/iconv-go" +) + +const ( + BytesPerInt = 4 + SynchByteLength = 7 + NormByteLength = 8 + NativeEncoding = 3 +) + +type Encoding struct { + Name string + NullLength int +} + +var ( + EncodingMap = [...]Encoding{ + {Name: "ISO-8859-1", NullLength: 1}, + {Name: "UTF-16", NullLength: 2}, + {Name: "UTF-16BE", NullLength: 2}, + {Name: "UTF-8", NullLength: 1}, + } + Decoders = make([]*iconv.Converter, len(EncodingMap)) + Encoders = make([]*iconv.Converter, len(EncodingMap)) +) + +func init() { + n := EncodingForIndex(NativeEncoding) + for i, e := range EncodingMap { + Decoders[i], _ = iconv.NewConverter(e.Name, n) + Encoders[i], _ = iconv.NewConverter(n, e.Name) + } +} + +// Form an integer from concatenated bits +func ByteInt(buf []byte, base uint) (i uint32, err error) { + if len(buf) > BytesPerInt { + err = errors.New("byte integer: invalid []byte length") + return + } + + for _, b := range buf { + if base < NormByteLength && b >= (1<>= base + } + + return bytes +} + +func SynchBytes(n uint32) []byte { + return IntBytes(n, SynchByteLength) +} + +func NormBytes(n uint32) []byte { + return IntBytes(n, NormByteLength) +} + +func EncodingForIndex(b byte) string { + encodingIndex := int(b) + if encodingIndex < 0 || encodingIndex > len(EncodingMap) { + encodingIndex = 0 + } + + return EncodingMap[encodingIndex].Name +} + +func EncodingNullLengthForIndex(b byte) int { + encodingIndex := int(b) + if encodingIndex < 0 || encodingIndex > len(EncodingMap) { + encodingIndex = 0 + } + + return EncodingMap[encodingIndex].NullLength +} + +func IndexForEncoding(e string) byte { + for i, v := range EncodingMap { + if v.Name == e { + return byte(i) + } + } + + return 0 +} + +func nullIndex(data []byte, encoding byte) (atIndex, afterIndex int) { + byteCount := EncodingNullLengthForIndex(encoding) + limit := len(data) + null := bytes.Repeat([]byte{0x0}, byteCount) + + for i, _ := range data[:limit/byteCount] { + atIndex = byteCount * i + afterIndex = atIndex + byteCount + + if bytes.Equal(data[atIndex:afterIndex], null) { + return + } + } + + atIndex = -1 + afterIndex = -1 + return +} + +func EncodedDiff(newEncoding byte, newString string, oldEncoding byte, oldString string) (int, error) { + newEncodedString, err := Encoders[newEncoding].ConvertString(newString) + if err != nil { + return 0, err + } + + oldEncodedString, err := Encoders[oldEncoding].ConvertString(oldString) + if err != nil { + return 0, err + } + + return len(newEncodedString) - len(oldEncodedString), nil +} diff --git a/vendor/github.com/mikkyang/id3-go/encodedbytes/writer.go b/vendor/github.com/mikkyang/id3-go/encodedbytes/writer.go new file mode 100644 index 0000000..2e0658c --- /dev/null +++ b/vendor/github.com/mikkyang/id3-go/encodedbytes/writer.go @@ -0,0 +1,63 @@ +// Copyright 2013 Michael Yang. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package encodedbytes + +import ( + "bytes" + "io" +) + +// Writer is a helper for writing frame bytes +type Writer struct { + data []byte + index int // current writing index +} + +func (w *Writer) Write(b []byte) (n int, err error) { + if len(b) == 0 { + return 0, nil + } + if w.index >= len(w.data) { + return 0, io.EOF + } + n = copy(w.data[w.index:], b) + w.index += n + return +} + +func (w *Writer) WriteByte(b byte) (err error) { + if w.index >= len(w.data) { + return io.EOF + } + w.data[w.index] = b + w.index++ + return +} + +func (w *Writer) WriteString(s string, encoding byte) (err error) { + encodedString, err := Encoders[encoding].ConvertString(s) + if err != nil { + return err + } + + _, err = w.Write([]byte(encodedString)) + if err != nil { + return err + } + + return +} + +func (w *Writer) WriteNullTermString(s string, encoding byte) (err error) { + if err = w.WriteString(s, encoding); err != nil { + return + } + + nullLength := EncodingNullLengthForIndex(encoding) + _, err = w.Write(bytes.Repeat([]byte{0x0}, nullLength)) + + return +} + +func NewWriter(b []byte) *Writer { return &Writer{b, 0} } diff --git a/vendor/github.com/mikkyang/id3-go/id3.go b/vendor/github.com/mikkyang/id3-go/id3.go new file mode 100644 index 0000000..8f6e364 --- /dev/null +++ b/vendor/github.com/mikkyang/id3-go/id3.go @@ -0,0 +1,116 @@ +// Copyright 2013 Michael Yang. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package id3 + +import ( + "errors" + "github.com/mikkyang/id3-go/v1" + "github.com/mikkyang/id3-go/v2" + "os" +) + +const ( + LatestVersion = 3 +) + +// Tagger represents the metadata of a tag +type Tagger interface { + Title() string + Artist() string + Album() string + Year() string + Genre() string + Comments() []string + SetTitle(string) + SetArtist(string) + SetAlbum(string) + SetYear(string) + SetGenre(string) + AllFrames() []v2.Framer + Frames(string) []v2.Framer + Frame(string) v2.Framer + DeleteFrames(string) []v2.Framer + AddFrames(...v2.Framer) + Bytes() []byte + Dirty() bool + Padding() uint + Size() int + Version() string +} + +// File represents the tagged file +type File struct { + Tagger + originalSize int + file *os.File +} + +// Parses an open file +func Parse(file *os.File) (*File, error) { + res := &File{file: file} + + if v2Tag := v2.ParseTag(file); v2Tag != nil { + res.Tagger = v2Tag + res.originalSize = v2Tag.Size() + } else if v1Tag := v1.ParseTag(file); v1Tag != nil { + res.Tagger = v1Tag + } else { + // Add a new tag if none exists + res.Tagger = v2.NewTag(LatestVersion) + } + + return res, nil +} + +// Opens a new tagged file +func Open(name string) (*File, error) { + fi, err := os.OpenFile(name, os.O_RDWR, 0666) + if err != nil { + return nil, err + } + + file, err := Parse(fi) + if err != nil { + return nil, err + } + + return file, nil +} + +// Saves any edits to the tagged file +func (f *File) Close() error { + defer f.file.Close() + + if !f.Dirty() { + return nil + } + + switch f.Tagger.(type) { + case (*v1.Tag): + if _, err := f.file.Seek(-v1.TagSize, os.SEEK_END); err != nil { + return err + } + case (*v2.Tag): + if f.Size() > f.originalSize { + start := int64(f.originalSize + v2.HeaderSize) + offset := int64(f.Tagger.Size() - f.originalSize) + + if err := shiftBytesBack(f.file, start, offset); err != nil { + return err + } + } + + if _, err := f.file.Seek(0, os.SEEK_SET); err != nil { + return err + } + default: + return errors.New("Close: unknown tag version") + } + + if _, err := f.file.Write(f.Tagger.Bytes()); err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/mikkyang/id3-go/util.go b/vendor/github.com/mikkyang/id3-go/util.go new file mode 100644 index 0000000..ae9ca2a --- /dev/null +++ b/vendor/github.com/mikkyang/id3-go/util.go @@ -0,0 +1,59 @@ +// Copyright 2013 Michael Yang. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package id3 + +import ( + "io" + "os" +) + +func shiftBytesBack(file *os.File, start, offset int64) error { + stat, err := file.Stat() + if err != nil { + return err + } + end := stat.Size() + + wrBuf := make([]byte, offset) + rdBuf := make([]byte, offset) + + wrOffset := offset + rdOffset := start + + rn, err := file.ReadAt(wrBuf, rdOffset) + if err != nil && err != io.EOF { + panic(err) + } + rdOffset += int64(rn) + + for { + if rdOffset >= end { + break + } + + n, err := file.ReadAt(rdBuf, rdOffset) + if err != nil && err != io.EOF { + return err + } + + if rdOffset+int64(n) > end { + n = int(end - rdOffset) + } + + if _, err := file.WriteAt(wrBuf[:rn], wrOffset); err != nil { + return err + } + + rdOffset += int64(n) + wrOffset += int64(rn) + copy(wrBuf, rdBuf) + rn = n + } + + if _, err := file.WriteAt(wrBuf[:rn], wrOffset); err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/mikkyang/id3-go/v1/id3v1.go b/vendor/github.com/mikkyang/id3-go/v1/id3v1.go new file mode 100644 index 0000000..589dd3b --- /dev/null +++ b/vendor/github.com/mikkyang/id3-go/v1/id3v1.go @@ -0,0 +1,148 @@ +// Copyright 2013 Michael Yang. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package v1 + +import ( + v2 "github.com/mikkyang/id3-go/v2" + "io" + "os" +) + +const ( + TagSize = 128 +) + +var ( + Genres = []string{ + "Blues", "Classic Rock", "Country", "Dance", + "Disco", "Funk", "Grunge", "Hip-Hop", + "Jazz", "Metal", "New Age", "Oldies", + "Other", "Pop", "R&B", "Rap", + "Reggae", "Rock", "Techno", "Industrial", + "Alternative", "Ska", "Death Metal", "Pranks", + "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", + "Vocal", "Jazz+Funk", "Fusion", "Trance", + "Classical", "Instrumental", "Acid", "House", + "Game", "Sound Clip", "Gospel", "Noise", + "AlternRock", "Bass", "Soul", "Punk", + "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", + "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", + "Electronic", "Pop-Folk", "Eurodance", "Dream", + "Southern Rock", "Comedy", "Cult", "Gangsta", + "Top 40", "Christian Rap", "Pop/Funk", "Jungle", + "Native American", "Cabaret", "New Wave", "Psychadelic", + "Rave", "Showtunes", "Trailer", "Lo-Fi", + "Tribal", "Acid Punk", "Acid Jazz", "Polka", + "Retro", "Musical", "Rock & Roll", "Hard Rock", + } +) + +// Tag represents an ID3v1 tag +type Tag struct { + title, artist, album, year, comment string + genre byte + dirty bool +} + +func ParseTag(readSeeker io.ReadSeeker) *Tag { + readSeeker.Seek(-TagSize, os.SEEK_END) + + data := make([]byte, TagSize) + n, err := io.ReadFull(readSeeker, data) + if n < TagSize || err != nil || string(data[:3]) != "TAG" { + return nil + } + + return &Tag{ + title: string(data[3:33]), + artist: string(data[33:63]), + album: string(data[63:93]), + year: string(data[93:97]), + comment: string(data[97:127]), + genre: data[127], + dirty: false, + } +} + +func (t Tag) Dirty() bool { + return t.dirty +} + +func (t Tag) Title() string { return t.title } +func (t Tag) Artist() string { return t.artist } +func (t Tag) Album() string { return t.album } +func (t Tag) Year() string { return t.year } + +func (t Tag) Genre() string { + if int(t.genre) < len(Genres) { + return Genres[t.genre] + } + + return "" +} + +func (t Tag) Comments() []string { + return []string{t.comment} +} + +func (t *Tag) SetTitle(text string) { + t.title = text + t.dirty = true +} + +func (t *Tag) SetArtist(text string) { + t.artist = text + t.dirty = true +} + +func (t *Tag) SetAlbum(text string) { + t.album = text + t.dirty = true +} + +func (t *Tag) SetYear(text string) { + t.year = text + t.dirty = true +} + +func (t *Tag) SetGenre(text string) { + t.genre = 255 + for i, genre := range Genres { + if text == genre { + t.genre = byte(i) + break + } + } + t.dirty = true +} + +func (t Tag) Bytes() []byte { + data := make([]byte, TagSize) + + copy(data[:3], []byte("TAG")) + copy(data[3:33], []byte(t.title)) + copy(data[33:63], []byte(t.artist)) + copy(data[63:93], []byte(t.album)) + copy(data[93:97], []byte(t.year)) + copy(data[97:127], []byte(t.comment)) + data[127] = t.genre + + return data +} + +func (t Tag) Size() int { + return TagSize +} + +func (t Tag) Version() string { + return "1.0" +} + +// Dummy methods to satisfy Tagger interface +func (t Tag) Padding() uint { return 0 } +func (t Tag) AllFrames() []v2.Framer { return []v2.Framer{} } +func (t Tag) Frame(id string) v2.Framer { return nil } +func (t Tag) Frames(id string) []v2.Framer { return []v2.Framer{} } +func (t Tag) DeleteFrames(id string) []v2.Framer { return []v2.Framer{} } +func (t Tag) AddFrames(f ...v2.Framer) {} diff --git a/vendor/github.com/mikkyang/id3-go/v2/frame.go b/vendor/github.com/mikkyang/id3-go/v2/frame.go new file mode 100644 index 0000000..cb6b9b4 --- /dev/null +++ b/vendor/github.com/mikkyang/id3-go/v2/frame.go @@ -0,0 +1,585 @@ +// Copyright 2013 Michael Yang. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package v2 + +import ( + "errors" + "fmt" + "github.com/mikkyang/id3-go/encodedbytes" +) + +const ( + FrameHeaderSize = 10 +) + +// FrameType holds frame id metadata and constructor method +// A set number of these are created in the version specific files +type FrameType struct { + id string + description string + constructor func(FrameHead, []byte) Framer +} + +// Framer provides a generic interface for frames +// This is the default type returned when creating frames +type Framer interface { + Id() string + Size() uint + StatusFlags() byte + FormatFlags() byte + String() string + Bytes() []byte + setOwner(*Tag) +} + +// FrameHead represents the header of each frame +// Additional metadata is kept through the embedded frame type +// These do not usually need to be manually created +type FrameHead struct { + FrameType + statusFlags byte + formatFlags byte + size uint32 + owner *Tag +} + +func (ft FrameType) Id() string { + return ft.id +} + +func (h FrameHead) Size() uint { + return uint(h.size) +} + +func (h *FrameHead) changeSize(diff int) { + if diff >= 0 { + h.size += uint32(diff) + } else { + h.size -= uint32(-diff) + } + + if h.owner != nil { + h.owner.changeSize(diff) + } +} + +func (h FrameHead) StatusFlags() byte { + return h.statusFlags +} + +func (h FrameHead) FormatFlags() byte { + return h.formatFlags +} + +func (h *FrameHead) setOwner(t *Tag) { + h.owner = t +} + +// DataFrame is the default frame for binary data +type DataFrame struct { + FrameHead + data []byte +} + +func NewDataFrame(ft FrameType, data []byte) *DataFrame { + head := FrameHead{ + FrameType: ft, + size: uint32(len(data)), + } + + return &DataFrame{head, data} +} + +func ParseDataFrame(head FrameHead, data []byte) Framer { + return &DataFrame{head, data} +} + +func (f DataFrame) Data() []byte { + return f.data +} + +func (f *DataFrame) SetData(b []byte) { + diff := len(b) - len(f.data) + f.changeSize(diff) + f.data = b +} + +func (f DataFrame) String() string { + return "" +} + +func (f DataFrame) Bytes() []byte { + return f.data +} + +// IdFrame represents identification tags +type IdFrame struct { + FrameHead + ownerIdentifier string + identifier []byte +} + +func NewIdFrame(ft FrameType, ownerId string, id []byte) *IdFrame { + head := FrameHead{ + FrameType: ft, + size: uint32(1 + len(ownerId) + len(id)), + } + + return &IdFrame{ + FrameHead: head, + ownerIdentifier: ownerId, + identifier: id, + } +} + +func ParseIdFrame(head FrameHead, data []byte) Framer { + var err error + f := &IdFrame{FrameHead: head} + rd := encodedbytes.NewReader(data) + + if f.ownerIdentifier, err = rd.ReadNullTermString(encodedbytes.NativeEncoding); err != nil { + return nil + } + + if f.identifier, err = rd.ReadRest(); len(f.identifier) > 64 || err != nil { + return nil + } + + return f +} + +func (f IdFrame) OwnerIdentifier() string { + return f.ownerIdentifier +} + +func (f *IdFrame) SetOwnerIdentifier(ownerId string) { + f.changeSize(len(ownerId) - len(f.ownerIdentifier)) + f.ownerIdentifier = ownerId +} + +func (f IdFrame) Identifier() []byte { + return f.identifier +} + +func (f *IdFrame) SetIdentifier(id []byte) error { + if len(id) > 64 { + return errors.New("identifier: identifier too long") + } + + f.changeSize(len(id) - len(f.identifier)) + f.identifier = id + + return nil +} + +func (f IdFrame) String() string { + return fmt.Sprintf("%s: %v", f.ownerIdentifier, f.identifier) +} + +func (f IdFrame) Bytes() []byte { + var err error + bytes := make([]byte, f.Size()) + wr := encodedbytes.NewWriter(bytes) + + if err = wr.WriteString(f.ownerIdentifier, encodedbytes.NativeEncoding); err != nil { + return bytes + } + + if _, err = wr.Write(f.identifier); err != nil { + return bytes + } + + return bytes +} + +// TextFramer represents frames that contain encoded text +type TextFramer interface { + Framer + Encoding() string + SetEncoding(string) error + Text() string + SetText(string) error +} + +// TextFrame represents frames that contain encoded text +type TextFrame struct { + FrameHead + encoding byte + text string +} + +func NewTextFrame(ft FrameType, text string) *TextFrame { + head := FrameHead{ + FrameType: ft, + size: uint32(1 + len(text)), + } + + return &TextFrame{ + FrameHead: head, + text: text, + } +} + +func ParseTextFrame(head FrameHead, data []byte) Framer { + var err error + f := &TextFrame{FrameHead: head} + rd := encodedbytes.NewReader(data) + + if f.encoding, err = rd.ReadByte(); err != nil { + return nil + } + + if f.text, err = rd.ReadRestString(f.encoding); err != nil { + return nil + } + + return f +} + +func (f TextFrame) Encoding() string { + return encodedbytes.EncodingForIndex(f.encoding) +} + +func (f *TextFrame) SetEncoding(encoding string) error { + i := byte(encodedbytes.IndexForEncoding(encoding)) + if i < 0 { + return errors.New("encoding: invalid encoding") + } + + diff, err := encodedbytes.EncodedDiff(i, f.text, f.encoding, f.text) + if err != nil { + return err + } + + f.changeSize(diff) + f.encoding = i + return nil +} + +func (f TextFrame) Text() string { + return f.text +} + +func (f *TextFrame) SetText(text string) error { + diff, err := encodedbytes.EncodedDiff(f.encoding, text, f.encoding, f.text) + if err != nil { + return err + } + + f.changeSize(diff) + f.text = text + return nil +} + +func (f TextFrame) String() string { + return f.text +} + +func (f TextFrame) Bytes() []byte { + var err error + bytes := make([]byte, f.Size()) + wr := encodedbytes.NewWriter(bytes) + + if err = wr.WriteByte(f.encoding); err != nil { + return bytes + } + + if err = wr.WriteString(f.text, f.encoding); err != nil { + return bytes + } + + return bytes +} + +type DescTextFrame struct { + TextFrame + description string +} + +func NewDescTextFrame(ft FrameType, desc, text string) *DescTextFrame { + f := NewTextFrame(ft, text) + nullLength := encodedbytes.EncodingNullLengthForIndex(f.encoding) + f.size += uint32(len(desc) + nullLength) + + return &DescTextFrame{ + TextFrame: *f, + description: desc, + } +} + +// DescTextFrame represents frames that contain encoded text and descriptions +func ParseDescTextFrame(head FrameHead, data []byte) Framer { + var err error + f := new(DescTextFrame) + f.FrameHead = head + rd := encodedbytes.NewReader(data) + + if f.encoding, err = rd.ReadByte(); err != nil { + return nil + } + + if f.description, err = rd.ReadNullTermString(f.encoding); err != nil { + return nil + } + + if f.text, err = rd.ReadRestString(f.encoding); err != nil { + return nil + } + + return f +} + +func (f DescTextFrame) Description() string { + return f.description +} + +func (f *DescTextFrame) SetDescription(description string) error { + diff, err := encodedbytes.EncodedDiff(f.encoding, description, f.encoding, f.description) + if err != nil { + return err + } + + f.changeSize(diff) + f.description = description + return nil +} + +func (f *DescTextFrame) SetEncoding(encoding string) error { + i := byte(encodedbytes.IndexForEncoding(encoding)) + if i < 0 { + return errors.New("encoding: invalid encoding") + } + + descDiff, err := encodedbytes.EncodedDiff(i, f.text, f.encoding, f.text) + if err != nil { + return err + } + + newNullLength := encodedbytes.EncodingNullLengthForIndex(i) + oldNullLength := encodedbytes.EncodingNullLengthForIndex(f.encoding) + nullDiff := newNullLength - oldNullLength + + textDiff, err := encodedbytes.EncodedDiff(i, f.description, f.encoding, f.description) + if err != nil { + return err + } + + f.changeSize(descDiff + nullDiff + textDiff) + f.encoding = i + return nil +} + +func (f DescTextFrame) String() string { + return fmt.Sprintf("%s: %s", f.description, f.text) +} + +func (f DescTextFrame) Bytes() []byte { + var err error + bytes := make([]byte, f.Size()) + wr := encodedbytes.NewWriter(bytes) + + if err = wr.WriteByte(f.encoding); err != nil { + return bytes + } + + if err = wr.WriteString(f.description, f.encoding); err != nil { + return bytes + } + + if err = wr.WriteString(f.text, f.encoding); err != nil { + return bytes + } + + return bytes +} + +// UnsynchTextFrame represents frames that contain unsynchronized text +type UnsynchTextFrame struct { + DescTextFrame + language string +} + +func NewUnsynchTextFrame(ft FrameType, desc, text string) *UnsynchTextFrame { + f := NewDescTextFrame(ft, desc, text) + f.size += uint32(3) + + return &UnsynchTextFrame{ + DescTextFrame: *f, + language: "eng", + } +} + +func ParseUnsynchTextFrame(head FrameHead, data []byte) Framer { + var err error + f := new(UnsynchTextFrame) + f.FrameHead = head + rd := encodedbytes.NewReader(data) + + if f.encoding, err = rd.ReadByte(); err != nil { + return nil + } + + if f.language, err = rd.ReadNumBytesString(3); err != nil { + return nil + } + + if f.description, err = rd.ReadNullTermString(f.encoding); err != nil { + return nil + } + + if f.text, err = rd.ReadRestString(f.encoding); err != nil { + return nil + } + + return f +} + +func (f UnsynchTextFrame) Language() string { + return f.language +} + +func (f *UnsynchTextFrame) SetLanguage(language string) error { + if len(language) != 3 { + return errors.New("language: invalid language string") + } + + f.language = language + f.changeSize(0) + return nil +} + +func (f UnsynchTextFrame) String() string { + return fmt.Sprintf("%s\t%s:\n%s", f.language, f.description, f.text) +} + +func (f UnsynchTextFrame) Bytes() []byte { + var err error + bytes := make([]byte, f.Size()) + wr := encodedbytes.NewWriter(bytes) + + if err = wr.WriteByte(f.encoding); err != nil { + return bytes + } + + if err = wr.WriteString(f.language, encodedbytes.NativeEncoding); err != nil { + return bytes + } + + if err = wr.WriteNullTermString(f.description, f.encoding); err != nil { + return bytes + } + + if err = wr.WriteString(f.text, f.encoding); err != nil { + return bytes + } + + return bytes +} + +// ImageFrame represent frames that have media attached +type ImageFrame struct { + DataFrame + encoding byte + mimeType string + pictureType byte + description string +} + +func ParseImageFrame(head FrameHead, data []byte) Framer { + var err error + f := new(ImageFrame) + f.FrameHead = head + rd := encodedbytes.NewReader(data) + + if f.encoding, err = rd.ReadByte(); err != nil { + return nil + } + + if f.mimeType, err = rd.ReadNullTermString(encodedbytes.NativeEncoding); err != nil { + return nil + } + + if f.pictureType, err = rd.ReadByte(); err != nil { + return nil + } + + if f.description, err = rd.ReadNullTermString(f.encoding); err != nil { + return nil + } + + if f.data, err = rd.ReadRest(); err != nil { + return nil + } + + return f +} + +func (f ImageFrame) Encoding() string { + return encodedbytes.EncodingForIndex(f.encoding) +} + +func (f *ImageFrame) SetEncoding(encoding string) error { + i := byte(encodedbytes.IndexForEncoding(encoding)) + if i < 0 { + return errors.New("encoding: invalid encoding") + } + + diff, err := encodedbytes.EncodedDiff(i, f.description, f.encoding, f.description) + if err != nil { + return err + } + + f.changeSize(diff) + f.encoding = i + return nil +} + +func (f ImageFrame) MIMEType() string { + return f.mimeType +} + +func (f *ImageFrame) SetMIMEType(mimeType string) { + diff := len(mimeType) - len(f.mimeType) + if mimeType[len(mimeType)-1] != 0 { + nullTermBytes := append([]byte(mimeType), 0x00) + f.mimeType = string(nullTermBytes) + diff += 1 + } else { + f.mimeType = mimeType + } + + f.changeSize(diff) +} + +func (f ImageFrame) String() string { + return fmt.Sprintf("%s\t%s: ", f.mimeType, f.description) +} + +func (f ImageFrame) Bytes() []byte { + var err error + bytes := make([]byte, f.Size()) + wr := encodedbytes.NewWriter(bytes) + + if err = wr.WriteByte(f.encoding); err != nil { + return bytes + } + + if err = wr.WriteString(f.mimeType, encodedbytes.NativeEncoding); err != nil { + return bytes + } + + if err = wr.WriteByte(f.pictureType); err != nil { + return bytes + } + + if err = wr.WriteString(f.description, f.encoding); err != nil { + return bytes + } + + if n, err := wr.Write(f.data); n < len(f.data) || err != nil { + return bytes + } + + return bytes +} diff --git a/vendor/github.com/mikkyang/id3-go/v2/id3v2.go b/vendor/github.com/mikkyang/id3-go/v2/id3v2.go new file mode 100644 index 0000000..a75ec33 --- /dev/null +++ b/vendor/github.com/mikkyang/id3-go/v2/id3v2.go @@ -0,0 +1,350 @@ +// Copyright 2013 Michael Yang. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package v2 + +import ( + "fmt" + "github.com/mikkyang/id3-go/encodedbytes" + "io" + "os" +) + +const ( + HeaderSize = 10 +) + +// Tag represents an ID3v2 tag +type Tag struct { + *Header + frames map[string][]Framer + padding uint + commonMap map[string]FrameType + frameHeaderSize int + frameConstructor func(io.Reader) Framer + frameBytesConstructor func(Framer) []byte + dirty bool +} + +// Creates a new tag +func NewTag(version byte) *Tag { + header := &Header{version: version} + + t := &Tag{ + Header: header, + frames: make(map[string][]Framer), + dirty: false, + } + + switch t.version { + case 2: + t.commonMap = V22CommonFrame + t.frameConstructor = ParseV22Frame + t.frameHeaderSize = V22FrameHeaderSize + t.frameBytesConstructor = V22Bytes + case 3: + t.commonMap = V23CommonFrame + t.frameConstructor = ParseV23Frame + t.frameHeaderSize = FrameHeaderSize + t.frameBytesConstructor = V23Bytes + default: + t.commonMap = V23CommonFrame + t.frameConstructor = ParseV23Frame + t.frameHeaderSize = FrameHeaderSize + t.frameBytesConstructor = V23Bytes + } + + return t +} + +// Parses a new tag +func ParseTag(readSeeker io.ReadSeeker) *Tag { + header := ParseHeader(readSeeker) + + if header == nil { + return nil + } + + t := NewTag(header.version) + t.Header = header + + var frame Framer + size := int(t.size) + for size > 0 { + frame = t.frameConstructor(readSeeker) + + if frame == nil { + break + } + + id := frame.Id() + t.frames[id] = append(t.frames[id], frame) + frame.setOwner(t) + + size -= t.frameHeaderSize + int(frame.Size()) + } + + t.padding = uint(size) + if _, err := readSeeker.Seek(int64(HeaderSize+t.Size()), os.SEEK_SET); err != nil { + return nil + } + + return t +} + +// Real size of the tag +func (t Tag) RealSize() int { + size := uint(t.size) - t.padding + return int(size) +} + +func (t *Tag) changeSize(diff int) { + if d := int(t.padding) - diff; d < 0 { + t.padding = 0 + t.size += uint32(-d) + } else { + t.padding = uint(d) + } + + t.dirty = true +} + +// Modified status of the tag +func (t Tag) Dirty() bool { + return t.dirty +} + +func (t Tag) Bytes() []byte { + data := make([]byte, t.Size()) + + index := 0 + for _, v := range t.frames { + for _, f := range v { + size := t.frameHeaderSize + int(f.Size()) + copy(data[index:index+size], t.frameBytesConstructor(f)) + + index += size + } + } + + return append(t.Header.Bytes(), data...) +} + +// The amount of padding in the tag +func (t Tag) Padding() uint { + return t.padding +} + +// All frames +func (t Tag) AllFrames() []Framer { + // Most of the time each ID will only have one frame + m := len(t.frames) + frames := make([]Framer, m) + + i := 0 + for _, frameSlice := range t.frames { + if i >= m { + frames = append(frames, frameSlice...) + } + + n := copy(frames[i:], frameSlice) + i += n + if n < len(frameSlice) { + frames = append(frames, frameSlice[n:]...) + } + } + + return frames +} + +// All frames with specified ID +func (t Tag) Frames(id string) []Framer { + if frames, ok := t.frames[id]; ok && frames != nil { + return frames + } + + return []Framer{} +} + +// First frame with specified ID +func (t Tag) Frame(id string) Framer { + if frames := t.Frames(id); len(frames) != 0 { + return frames[0] + } + + return nil +} + +// Delete and return all frames with specified ID +func (t *Tag) DeleteFrames(id string) []Framer { + frames := t.Frames(id) + if frames == nil { + return nil + } + + diff := 0 + for _, frame := range frames { + frame.setOwner(nil) + diff += t.frameHeaderSize + int(frame.Size()) + } + t.changeSize(-diff) + + delete(t.frames, id) + + return frames +} + +// Add frames +func (t *Tag) AddFrames(frames ...Framer) { + for _, frame := range frames { + t.changeSize(t.frameHeaderSize + int(frame.Size())) + + id := frame.Id() + t.frames[id] = append(t.frames[id], frame) + frame.setOwner(t) + } +} + +func (t Tag) Title() string { + return t.textFrameText(t.commonMap["Title"]) +} + +func (t Tag) Artist() string { + return t.textFrameText(t.commonMap["Artist"]) +} + +func (t Tag) Album() string { + return t.textFrameText(t.commonMap["Album"]) +} + +func (t Tag) Year() string { + return t.textFrameText(t.commonMap["Year"]) +} + +func (t Tag) Genre() string { + return t.textFrameText(t.commonMap["Genre"]) +} + +func (t Tag) Comments() []string { + frames := t.Frames(t.commonMap["Comments"].Id()) + if frames == nil { + return nil + } + + comments := make([]string, len(frames)) + for i, frame := range frames { + comments[i] = frame.String() + } + + return comments +} + +func (t *Tag) SetTitle(text string) { + t.setTextFrameText(t.commonMap["Title"], text) +} + +func (t *Tag) SetArtist(text string) { + t.setTextFrameText(t.commonMap["Artist"], text) +} + +func (t *Tag) SetAlbum(text string) { + t.setTextFrameText(t.commonMap["Album"], text) +} + +func (t *Tag) SetYear(text string) { + t.setTextFrameText(t.commonMap["Year"], text) +} + +func (t *Tag) SetGenre(text string) { + t.setTextFrameText(t.commonMap["Genre"], text) +} + +func (t *Tag) textFrame(ft FrameType) TextFramer { + if frame := t.Frame(ft.Id()); frame != nil { + if textFramer, ok := frame.(TextFramer); ok { + return textFramer + } + } + + return nil +} + +func (t Tag) textFrameText(ft FrameType) string { + if frame := t.textFrame(ft); frame != nil { + return frame.Text() + } + + return "" +} + +func (t *Tag) setTextFrameText(ft FrameType, text string) { + if frame := t.textFrame(ft); frame != nil { + frame.SetEncoding("UTF-8") + frame.SetText(text) + } else { + f := NewTextFrame(ft, text) + f.SetEncoding("UTF-8") + t.AddFrames(f) + } +} + +func ParseHeader(reader io.Reader) *Header { + data := make([]byte, HeaderSize) + n, err := io.ReadFull(reader, data) + if n < HeaderSize || err != nil || string(data[:3]) != "ID3" { + return nil + } + + size, err := encodedbytes.SynchInt(data[6:]) + if err != nil { + return nil + } + + header := &Header{ + version: data[3], + revision: data[4], + flags: data[5], + size: size, + } + + switch header.version { + case 2: + header.unsynchronization = isBitSet(header.flags, 7) + header.compression = isBitSet(header.flags, 6) + case 3: + header.unsynchronization = isBitSet(header.flags, 7) + header.extendedHeader = isBitSet(header.flags, 6) + header.experimental = isBitSet(header.flags, 5) + } + + return header +} + +// Header represents the data of the header of the entire tag +type Header struct { + version, revision byte + flags byte + unsynchronization bool + compression bool + experimental bool + extendedHeader bool + size uint32 +} + +func (h Header) Version() string { + return fmt.Sprintf("2.%d.%d", h.version, h.revision) +} + +func (h Header) Size() int { + return int(h.size) +} + +func (h Header) Bytes() []byte { + data := make([]byte, 0, HeaderSize) + + data = append(data, "ID3"...) + data = append(data, h.version, h.revision, h.flags) + data = append(data, encodedbytes.SynchBytes(h.size)...) + + return data +} diff --git a/vendor/github.com/mikkyang/id3-go/v2/id3v22.go b/vendor/github.com/mikkyang/id3-go/v2/id3v22.go new file mode 100644 index 0000000..b1b224b --- /dev/null +++ b/vendor/github.com/mikkyang/id3-go/v2/id3v22.go @@ -0,0 +1,131 @@ +// Copyright 2013 Michael Yang. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package v2 + +import ( + "github.com/mikkyang/id3-go/encodedbytes" + "io" +) + +const ( + V22FrameHeaderSize = 6 +) + +var ( + // Common frame IDs + V22CommonFrame = map[string]FrameType{ + "Title": V22FrameTypeMap["TT2"], + "Artist": V22FrameTypeMap["TP1"], + "Album": V22FrameTypeMap["TAL"], + "Year": V22FrameTypeMap["TYE"], + "Genre": V22FrameTypeMap["TCO"], + "Comments": V22FrameTypeMap["COM"], + } + + // V22FrameTypeMap specifies the frame IDs and constructors allowed in ID3v2.2 + V22FrameTypeMap = map[string]FrameType{ + "BUF": FrameType{id: "BUF", description: "Recommended buffer size", constructor: ParseDataFrame}, + "CNT": FrameType{id: "CNT", description: "Play counter", constructor: ParseDataFrame}, + "COM": FrameType{id: "COM", description: "Comments", constructor: ParseUnsynchTextFrame}, + "CRA": FrameType{id: "CRA", description: "Audio encryption", constructor: ParseDataFrame}, + "CRM": FrameType{id: "CRM", description: "Encrypted meta frame", constructor: ParseDataFrame}, + "ETC": FrameType{id: "ETC", description: "Event timing codes", constructor: ParseDataFrame}, + "EQU": FrameType{id: "EQU", description: "Equalization", constructor: ParseDataFrame}, + "GEO": FrameType{id: "GEO", description: "General encapsulated object", constructor: ParseDataFrame}, + "IPL": FrameType{id: "IPL", description: "Involved people list", constructor: ParseDataFrame}, + "LNK": FrameType{id: "LNK", description: "Linked information", constructor: ParseDataFrame}, + "MCI": FrameType{id: "MCI", description: "Music CD Identifier", constructor: ParseDataFrame}, + "MLL": FrameType{id: "MLL", description: "MPEG location lookup table", constructor: ParseDataFrame}, + "PIC": FrameType{id: "PIC", description: "Attached picture", constructor: ParseDataFrame}, + "POP": FrameType{id: "POP", description: "Popularimeter", constructor: ParseDataFrame}, + "REV": FrameType{id: "REV", description: "Reverb", constructor: ParseDataFrame}, + "RVA": FrameType{id: "RVA", description: "Relative volume adjustment", constructor: ParseDataFrame}, + "SLT": FrameType{id: "SLT", description: "Synchronized lyric/text", constructor: ParseDataFrame}, + "STC": FrameType{id: "STC", description: "Synced tempo codes", constructor: ParseDataFrame}, + "TAL": FrameType{id: "TAL", description: "Album/Movie/Show title", constructor: ParseTextFrame}, + "TBP": FrameType{id: "TBP", description: "BPM (Beats Per Minute)", constructor: ParseTextFrame}, + "TCM": FrameType{id: "TCM", description: "Composer", constructor: ParseTextFrame}, + "TCO": FrameType{id: "TCO", description: "Content type", constructor: ParseTextFrame}, + "TCR": FrameType{id: "TCR", description: "Copyright message", constructor: ParseTextFrame}, + "TDA": FrameType{id: "TDA", description: "Date", constructor: ParseTextFrame}, + "TDY": FrameType{id: "TDY", description: "Playlist delay", constructor: ParseTextFrame}, + "TEN": FrameType{id: "TEN", description: "Encoded by", constructor: ParseTextFrame}, + "TFT": FrameType{id: "TFT", description: "File type", constructor: ParseTextFrame}, + "TIM": FrameType{id: "TIM", description: "Time", constructor: ParseTextFrame}, + "TKE": FrameType{id: "TKE", description: "Initial key", constructor: ParseTextFrame}, + "TLA": FrameType{id: "TLA", description: "Language(s)", constructor: ParseTextFrame}, + "TLE": FrameType{id: "TLE", description: "Length", constructor: ParseTextFrame}, + "TMT": FrameType{id: "TMT", description: "Media type", constructor: ParseTextFrame}, + "TOA": FrameType{id: "TOA", description: "Original artist(s)/performer(s)", constructor: ParseTextFrame}, + "TOF": FrameType{id: "TOF", description: "Original filename", constructor: ParseTextFrame}, + "TOL": FrameType{id: "TOL", description: "Original Lyricist(s)/text writer(s)", constructor: ParseTextFrame}, + "TOR": FrameType{id: "TOR", description: "Original release year", constructor: ParseTextFrame}, + "TOT": FrameType{id: "TOT", description: "Original album/Movie/Show title", constructor: ParseTextFrame}, + "TP1": FrameType{id: "TP1", description: "Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group", constructor: ParseTextFrame}, + "TP2": FrameType{id: "TP2", description: "Band/Orchestra/Accompaniment", constructor: ParseTextFrame}, + "TP3": FrameType{id: "TP3", description: "Conductor/Performer refinement", constructor: ParseTextFrame}, + "TP4": FrameType{id: "TP4", description: "Interpreted, remixed, or otherwise modified by", constructor: ParseTextFrame}, + "TPA": FrameType{id: "TPA", description: "Part of a set", constructor: ParseTextFrame}, + "TPB": FrameType{id: "TPB", description: "Publisher", constructor: ParseTextFrame}, + "TRC": FrameType{id: "TRC", description: "ISRC (International Standard Recording Code)", constructor: ParseTextFrame}, + "TRD": FrameType{id: "TRD", description: "Recording dates", constructor: ParseTextFrame}, + "TRK": FrameType{id: "TRK", description: "Track number/Position in set", constructor: ParseTextFrame}, + "TSI": FrameType{id: "TSI", description: "Size", constructor: ParseTextFrame}, + "TSS": FrameType{id: "TSS", description: "Software/hardware and settings used for encoding", constructor: ParseTextFrame}, + "TT1": FrameType{id: "TT1", description: "Content group description", constructor: ParseTextFrame}, + "TT2": FrameType{id: "TT2", description: "Title/Songname/Content description", constructor: ParseTextFrame}, + "TT3": FrameType{id: "TT3", description: "Subtitle/Description refinement", constructor: ParseTextFrame}, + "TXT": FrameType{id: "TXT", description: "Lyricist/text writer", constructor: ParseTextFrame}, + "TXX": FrameType{id: "TXX", description: "User defined text information frame", constructor: ParseDescTextFrame}, + "TYE": FrameType{id: "TYE", description: "Year", constructor: ParseTextFrame}, + "UFI": FrameType{id: "UFI", description: "Unique file identifier", constructor: ParseDataFrame}, + "ULT": FrameType{id: "ULT", description: "Unsychronized lyric/text transcription", constructor: ParseDataFrame}, + "WAF": FrameType{id: "WAF", description: "Official audio file webpage", constructor: ParseDataFrame}, + "WAR": FrameType{id: "WAR", description: "Official artist/performer webpage", constructor: ParseDataFrame}, + "WAS": FrameType{id: "WAS", description: "Official audio source webpage", constructor: ParseDataFrame}, + "WCM": FrameType{id: "WCM", description: "Commercial information", constructor: ParseDataFrame}, + "WCP": FrameType{id: "WCP", description: "Copyright/Legal information", constructor: ParseDataFrame}, + "WPB": FrameType{id: "WPB", description: "Publishers official webpage", constructor: ParseDataFrame}, + "WXX": FrameType{id: "WXX", description: "User defined URL link frame", constructor: ParseDataFrame}, + } +) + +func ParseV22Frame(reader io.Reader) Framer { + data := make([]byte, V22FrameHeaderSize) + if n, err := io.ReadFull(reader, data); n < V22FrameHeaderSize || err != nil { + return nil + } + + id := string(data[:3]) + t, ok := V22FrameTypeMap[id] + if !ok { + return nil + } + + size, err := encodedbytes.NormInt(data[3:6]) + if err != nil { + return nil + } + + h := FrameHead{ + FrameType: t, + size: size, + } + + frameData := make([]byte, size) + if n, err := io.ReadFull(reader, frameData); n < int(size) || err != nil { + return nil + } + + return t.constructor(h, frameData) +} + +func V22Bytes(f Framer) []byte { + headBytes := make([]byte, 0, V22FrameHeaderSize) + + headBytes = append(headBytes, f.Id()...) + headBytes = append(headBytes, encodedbytes.NormBytes(uint32(f.Size()))[1:]...) + + return append(headBytes, f.Bytes()...) +} diff --git a/vendor/github.com/mikkyang/id3-go/v2/id3v23.go b/vendor/github.com/mikkyang/id3-go/v2/id3v23.go new file mode 100644 index 0000000..ed9daf7 --- /dev/null +++ b/vendor/github.com/mikkyang/id3-go/v2/id3v23.go @@ -0,0 +1,161 @@ +// Copyright 2013 Michael Yang. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package v2 + +import ( + "github.com/mikkyang/id3-go/encodedbytes" + "io" +) + +var ( + // Common frame IDs + V23CommonFrame = map[string]FrameType{ + "Title": V23FrameTypeMap["TIT2"], + "Artist": V23FrameTypeMap["TPE1"], + "Album": V23FrameTypeMap["TALB"], + "Year": V23FrameTypeMap["TYER"], + "Genre": V23FrameTypeMap["TCON"], + "Comments": V23FrameTypeMap["COMM"], + } + + // V23DeprecatedTypeMap contains deprecated frame IDs from ID3v2.2 + V23DeprecatedTypeMap = map[string]string{ + "BUF": "RBUF", "COM": "COMM", "CRA": "AENC", "EQU": "EQUA", + "ETC": "ETCO", "GEO": "GEOB", "MCI": "MCDI", "MLL": "MLLT", + "PIC": "APIC", "POP": "POPM", "REV": "RVRB", "RVA": "RVAD", + "SLT": "SYLT", "STC": "SYTC", "TAL": "TALB", "TBP": "TBPM", + "TCM": "TCOM", "TCO": "TCON", "TCR": "TCOP", "TDA": "TDAT", + "TDY": "TDLY", "TEN": "TENC", "TFT": "TFLT", "TIM": "TIME", + "TKE": "TKEY", "TLA": "TLAN", "TLE": "TLEN", "TMT": "TMED", + "TOA": "TOPE", "TOF": "TOFN", "TOL": "TOLY", "TOR": "TORY", + "TOT": "TOAL", "TP1": "TPE1", "TP2": "TPE2", "TP3": "TPE3", + "TP4": "TPE4", "TPA": "TPOS", "TPB": "TPUB", "TRC": "TSRC", + "TRD": "TRDA", "TRK": "TRCK", "TSI": "TSIZ", "TSS": "TSSE", + "TT1": "TIT1", "TT2": "TIT2", "TT3": "TIT3", "TXT": "TEXT", + "TXX": "TXXX", "TYE": "TYER", "UFI": "UFID", "ULT": "USLT", + "WAF": "WOAF", "WAR": "WOAR", "WAS": "WOAS", "WCM": "WCOM", + "WCP": "WCOP", "WPB": "WPB", "WXX": "WXXX", + } + + // V23FrameTypeMap specifies the frame IDs and constructors allowed in ID3v2.3 + V23FrameTypeMap = map[string]FrameType{ + "AENC": FrameType{id: "AENC", description: "Audio encryption", constructor: ParseDataFrame}, + "APIC": FrameType{id: "APIC", description: "Attached picture", constructor: ParseImageFrame}, + "COMM": FrameType{id: "COMM", description: "Comments", constructor: ParseUnsynchTextFrame}, + "COMR": FrameType{id: "COMR", description: "Commercial frame", constructor: ParseDataFrame}, + "ENCR": FrameType{id: "ENCR", description: "Encryption method registration", constructor: ParseDataFrame}, + "EQUA": FrameType{id: "EQUA", description: "Equalization", constructor: ParseDataFrame}, + "ETCO": FrameType{id: "ETCO", description: "Event timing codes", constructor: ParseDataFrame}, + "GEOB": FrameType{id: "GEOB", description: "General encapsulated object", constructor: ParseDataFrame}, + "GRID": FrameType{id: "GRID", description: "Group identification registration", constructor: ParseDataFrame}, + "IPLS": FrameType{id: "IPLS", description: "Involved people list", constructor: ParseDataFrame}, + "LINK": FrameType{id: "LINK", description: "Linked information", constructor: ParseDataFrame}, + "MCDI": FrameType{id: "MCDI", description: "Music CD identifier", constructor: ParseDataFrame}, + "MLLT": FrameType{id: "MLLT", description: "MPEG location lookup table", constructor: ParseDataFrame}, + "OWNE": FrameType{id: "OWNE", description: "Ownership frame", constructor: ParseDataFrame}, + "PRIV": FrameType{id: "PRIV", description: "Private frame", constructor: ParseDataFrame}, + "PCNT": FrameType{id: "PCNT", description: "Play counter", constructor: ParseDataFrame}, + "POPM": FrameType{id: "POPM", description: "Popularimeter", constructor: ParseDataFrame}, + "POSS": FrameType{id: "POSS", description: "Position synchronisation frame", constructor: ParseDataFrame}, + "RBUF": FrameType{id: "RBUF", description: "Recommended buffer size", constructor: ParseDataFrame}, + "RVAD": FrameType{id: "RVAD", description: "Relative volume adjustment", constructor: ParseDataFrame}, + "RVRB": FrameType{id: "RVRB", description: "Reverb", constructor: ParseDataFrame}, + "SYLT": FrameType{id: "SYLT", description: "Synchronized lyric/text", constructor: ParseDataFrame}, + "SYTC": FrameType{id: "SYTC", description: "Synchronized tempo codes", constructor: ParseDataFrame}, + "TALB": FrameType{id: "TALB", description: "Album/Movie/Show title", constructor: ParseTextFrame}, + "TBPM": FrameType{id: "TBPM", description: "BPM (beats per minute)", constructor: ParseTextFrame}, + "TCOM": FrameType{id: "TCOM", description: "Composer", constructor: ParseTextFrame}, + "TCON": FrameType{id: "TCON", description: "Content type", constructor: ParseTextFrame}, + "TCOP": FrameType{id: "TCOP", description: "Copyright message", constructor: ParseTextFrame}, + "TDAT": FrameType{id: "TDAT", description: "Date", constructor: ParseTextFrame}, + "TDLY": FrameType{id: "TDLY", description: "Playlist delay", constructor: ParseTextFrame}, + "TENC": FrameType{id: "TENC", description: "Encoded by", constructor: ParseTextFrame}, + "TEXT": FrameType{id: "TEXT", description: "Lyricist/Text writer", constructor: ParseTextFrame}, + "TFLT": FrameType{id: "TFLT", description: "File type", constructor: ParseTextFrame}, + "TIME": FrameType{id: "TIME", description: "Time", constructor: ParseTextFrame}, + "TIT1": FrameType{id: "TIT1", description: "Content group description", constructor: ParseTextFrame}, + "TIT2": FrameType{id: "TIT2", description: "Title/songname/content description", constructor: ParseTextFrame}, + "TIT3": FrameType{id: "TIT3", description: "Subtitle/Description refinement", constructor: ParseTextFrame}, + "TKEY": FrameType{id: "TKEY", description: "Initial key", constructor: ParseTextFrame}, + "TLAN": FrameType{id: "TLAN", description: "Language(s)", constructor: ParseTextFrame}, + "TLEN": FrameType{id: "TLEN", description: "Length", constructor: ParseTextFrame}, + "TMED": FrameType{id: "TMED", description: "Media type", constructor: ParseTextFrame}, + "TOAL": FrameType{id: "TOAL", description: "Original album/movie/show title", constructor: ParseTextFrame}, + "TOFN": FrameType{id: "TOFN", description: "Original filename", constructor: ParseTextFrame}, + "TOLY": FrameType{id: "TOLY", description: "Original lyricist(s)/text writer(s)", constructor: ParseTextFrame}, + "TOPE": FrameType{id: "TOPE", description: "Original artist(s)/performer(s)", constructor: ParseTextFrame}, + "TORY": FrameType{id: "TORY", description: "Original release year", constructor: ParseTextFrame}, + "TOWN": FrameType{id: "TOWN", description: "File owner/licensee", constructor: ParseTextFrame}, + "TPE1": FrameType{id: "TPE1", description: "Lead performer(s)/Soloist(s)", constructor: ParseTextFrame}, + "TPE2": FrameType{id: "TPE2", description: "Band/orchestra/accompaniment", constructor: ParseTextFrame}, + "TPE3": FrameType{id: "TPE3", description: "Conductor/performer refinement", constructor: ParseTextFrame}, + "TPE4": FrameType{id: "TPE4", description: "Interpreted, remixed, or otherwise modified by", constructor: ParseTextFrame}, + "TPOS": FrameType{id: "TPOS", description: "Part of a set", constructor: ParseTextFrame}, + "TPUB": FrameType{id: "TPUB", description: "Publisher", constructor: ParseTextFrame}, + "TRCK": FrameType{id: "TRCK", description: "Track number/Position in set", constructor: ParseTextFrame}, + "TRDA": FrameType{id: "TRDA", description: "Recording dates", constructor: ParseTextFrame}, + "TRSN": FrameType{id: "TRSN", description: "Internet radio station name", constructor: ParseTextFrame}, + "TRSO": FrameType{id: "TRSO", description: "Internet radio station owner", constructor: ParseTextFrame}, + "TSIZ": FrameType{id: "TSIZ", description: "Size", constructor: ParseTextFrame}, + "TSRC": FrameType{id: "TSRC", description: "ISRC (international standard recording code)", constructor: ParseTextFrame}, + "TSSE": FrameType{id: "TSSE", description: "Software/Hardware and settings used for encoding", constructor: ParseTextFrame}, + "TYER": FrameType{id: "TYER", description: "Year", constructor: ParseTextFrame}, + "TXXX": FrameType{id: "TXXX", description: "User defined text information frame", constructor: ParseDescTextFrame}, + "UFID": FrameType{id: "UFID", description: "Unique file identifier", constructor: ParseIdFrame}, + "USER": FrameType{id: "USER", description: "Terms of use", constructor: ParseDataFrame}, + "TCMP": FrameType{id: "TCMP", description: "Part of a compilation (iTunes extension)", constructor: ParseTextFrame}, + "USLT": FrameType{id: "USLT", description: "Unsychronized lyric/text transcription", constructor: ParseUnsynchTextFrame}, + "WCOM": FrameType{id: "WCOM", description: "Commercial information", constructor: ParseDataFrame}, + "WCOP": FrameType{id: "WCOP", description: "Copyright/Legal information", constructor: ParseDataFrame}, + "WOAF": FrameType{id: "WOAF", description: "Official audio file webpage", constructor: ParseDataFrame}, + "WOAR": FrameType{id: "WOAR", description: "Official artist/performer webpage", constructor: ParseDataFrame}, + "WOAS": FrameType{id: "WOAS", description: "Official audio source webpage", constructor: ParseDataFrame}, + "WORS": FrameType{id: "WORS", description: "Official internet radio station homepage", constructor: ParseDataFrame}, + "WPAY": FrameType{id: "WPAY", description: "Payment", constructor: ParseDataFrame}, + "WPUB": FrameType{id: "WPUB", description: "Publishers official webpage", constructor: ParseDataFrame}, + "WXXX": FrameType{id: "WXXX", description: "User defined URL link frame", constructor: ParseDataFrame}, + } +) + +func ParseV23Frame(reader io.Reader) Framer { + data := make([]byte, FrameHeaderSize) + if n, err := io.ReadFull(reader, data); n < FrameHeaderSize || err != nil { + return nil + } + + id := string(data[:4]) + t, ok := V23FrameTypeMap[id] + if !ok { + return nil + } + + size, err := encodedbytes.NormInt(data[4:8]) + if err != nil { + return nil + } + + h := FrameHead{ + FrameType: t, + statusFlags: data[8], + formatFlags: data[9], + size: size, + } + + frameData := make([]byte, size) + if n, err := io.ReadFull(reader, frameData); n < int(size) || err != nil { + return nil + } + + return t.constructor(h, frameData) +} + +func V23Bytes(f Framer) []byte { + headBytes := make([]byte, 0, FrameHeaderSize) + + headBytes = append(headBytes, f.Id()...) + headBytes = append(headBytes, encodedbytes.NormBytes(uint32(f.Size()))...) + headBytes = append(headBytes, f.StatusFlags(), f.FormatFlags()) + + return append(headBytes, f.Bytes()...) +} diff --git a/vendor/github.com/mikkyang/id3-go/v2/util.go b/vendor/github.com/mikkyang/id3-go/v2/util.go new file mode 100644 index 0000000..2384314 --- /dev/null +++ b/vendor/github.com/mikkyang/id3-go/v2/util.go @@ -0,0 +1,8 @@ +// Copyright 2013 Michael Yang. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package v2 + +func isBitSet(flag, index byte) bool { + return flag&(1<