parent
9363cc1f0e
commit
727f9b82b9
@ -0,0 +1,22 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2013 Michael Yang <mikkyangg@gmail.com>
|
||||
|
||||
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.
|
@ -0,0 +1,85 @@
|
||||
# id3
|
||||
|
||||
[](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)
|
@ -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].String(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].String(string(b[:atIndex]))
|
||||
}
|
||||
|
||||
func NewReader(b []byte) *Reader { return &Reader{b, 0} }
|
@ -0,0 +1,177 @@
|
||||
// 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"
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
)
|
||||
|
||||
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))
|
||||
|
||||
Decoders = make([]*encoding.Decoder, len(EncodingMap))
|
||||
Encoders = make([]*encoding.Encoder, len(EncodingMap))
|
||||
)
|
||||
|
||||
//func init() {
|
||||
// n := EncodingForIndex(NativeEncoding)
|
||||
// for i, e := range EncodingMap {
|
||||
// d := charmap.ISO8859_1.NewDecoder()
|
||||
// Decoders[i], _ = iconv.NewConverter(e.Name, n)
|
||||
// Encoders[i], _ = iconv.NewConverter(n, e.Name)
|
||||
// }
|
||||
//}
|
||||
|
||||
func init() {
|
||||
Decoders[0] = charmap.ISO8859_1.NewDecoder()
|
||||
Decoders[1] = unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder()
|
||||
Decoders[2] = unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder()
|
||||
Decoders[3] = unicode.UTF8.NewDecoder()
|
||||
Encoders[0] = charmap.ISO8859_1.NewEncoder()
|
||||
Encoders[1] = unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewEncoder()
|
||||
Encoders[2] = unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewEncoder()
|
||||
Encoders[3] = unicode.UTF8.NewEncoder()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
err = errors.New("byte integer: exceed max bit")
|
||||
return
|
||||
}
|
||||
|
||||
i = (i << base) | uint32(b)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func SynchInt(buf []byte) (i uint32, err error) {
|
||||
i, err = ByteInt(buf, SynchByteLength)
|
||||
return
|
||||
}
|
||||
|
||||
func NormInt(buf []byte) (i uint32, err error) {
|
||||
i, err = ByteInt(buf, NormByteLength)
|
||||
return
|
||||
}
|
||||
|
||||
// Form a byte slice from an integer
|
||||
func IntBytes(n uint32, base uint) []byte {
|
||||
mask := uint32(1<<base - 1)
|
||||
bytes := make([]byte, BytesPerInt)
|
||||
|
||||
for i, _ := range bytes {
|
||||
bytes[len(bytes)-i-1] = byte(n & mask)
|
||||
n >>= 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
|
||||
//}
|
||||
|
||||
newEncodedString, err := Encoders[newEncoding].String(newString)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
oldEncodedString, err := Encoders[oldEncoding].String(oldString)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(newEncodedString) - len(oldEncodedString), nil
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
// 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"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSynch(t *testing.T) {
|
||||
synch := []byte{0x44, 0x7a, 0x70, 0x04}
|
||||
const synchResult = 144619524
|
||||
|
||||
if result, err := SynchInt(synch); result != synchResult {
|
||||
t.Errorf("encodedbytes.SynchInt(%v) = %d with error %v, want %d", synch, result, err, synchResult)
|
||||
}
|
||||
if result := SynchBytes(synchResult); !bytes.Equal(result, synch) {
|
||||
t.Errorf("encodedbytes.SynchBytes(%d) = %v, want %v", synchResult, result, synch)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNorm(t *testing.T) {
|
||||
norm := []byte{0x0b, 0x95, 0xae, 0xb4}
|
||||
const normResult = 194358964
|
||||
|
||||
if result, err := NormInt(norm); result != normResult {
|
||||
t.Errorf("encodedbytes.NormInt(%v) = %d with error %v, want %d", norm, result, err, normResult)
|
||||
}
|
||||
if result := NormBytes(normResult); !bytes.Equal(result, norm) {
|
||||
t.Errorf("encodedbytes.NormBytes(%d) = %v, want %v", normResult, result, norm)
|
||||
}
|
||||
}
|
@ -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].String(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} }
|
@ -0,0 +1,124 @@
|
||||
// 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"
|
||||
"os"
|
||||
|
||||
v1 "github.com/akhilrex/podgrab/internal/id3/v1"
|
||||
v2 "github.com/akhilrex/podgrab/internal/id3/v2"
|
||||
)
|
||||
|
||||
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)
|
||||
SetComment(string)
|
||||
SetGenre(string)
|
||||
SetDate(string) // Added
|
||||
SetReleaseYear(string) // Added
|
||||
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, forcev2 bool) (*File, error) {
|
||||
res := &File{file: file}
|
||||
|
||||
if forcev2 {
|
||||
res.Tagger = v2.NewTag(2)
|
||||
} else {
|
||||
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, forceV2 bool) (*File, error) {
|
||||
fi, err := os.OpenFile(name, os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := Parse(fi, forceV2)
|
||||
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
|
||||
}
|
@ -0,0 +1,278 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
v2 "github.com/akhilrex/podgrab/internal/id3/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
testFile = "test.mp3"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
file, err := os.OpenFile("test.mp3", os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
t.Errorf("Parse: unable to open file")
|
||||
}
|
||||
|
||||
tagger, err := Parse(file)
|
||||
if err != nil {
|
||||
t.Errorf("Parse: could not parse")
|
||||
}
|
||||
|
||||
tag, ok := tagger.Tagger.(*v2.Tag)
|
||||
if !ok {
|
||||
t.Errorf("Parse: incorrect tagger type")
|
||||
}
|
||||
|
||||
if s := tag.Artist(); s != "Paloalto\x00" {
|
||||
t.Errorf("Parse: incorrect artist, %v", s)
|
||||
}
|
||||
|
||||
if s := tag.Title(); s != "Nice Life (Feat. Basick)" {
|
||||
t.Errorf("Parse: incorrect title, %v", s)
|
||||
}
|
||||
|
||||
if s := tag.Album(); s != "Chief Life" {
|
||||
t.Errorf("Parse: incorrect album, %v", s)
|
||||
}
|
||||
|
||||
parsedFrame := tagger.Frame("COMM")
|
||||
resultFrame, ok := parsedFrame.(*v2.UnsynchTextFrame)
|
||||
if !ok {
|
||||
t.Error("Couldn't cast frame")
|
||||
}
|
||||
|
||||
expected := "✓"
|
||||
actual := resultFrame.Description()
|
||||
|
||||
if expected != actual {
|
||||
t.Errorf("Expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
actual = resultFrame.Text()
|
||||
if expected != actual {
|
||||
t.Errorf("Expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestOpen(t *testing.T) {
|
||||
file, err := Open(testFile)
|
||||
if err != nil {
|
||||
t.Errorf("Open: unable to open file")
|
||||
}
|
||||
|
||||
tag, ok := file.Tagger.(*v2.Tag)
|
||||
if !ok {
|
||||
t.Errorf("Open: incorrect tagger type")
|
||||
}
|
||||
|
||||
if s := tag.Artist(); s != "Paloalto\x00" {
|
||||
t.Errorf("Open: incorrect artist, %v", s)
|
||||
}
|
||||
|
||||
if s := tag.Title(); s != "Nice Life (Feat. Basick)" {
|
||||
t.Errorf("Open: incorrect title, %v", s)
|
||||
}
|
||||
|
||||
if s := tag.Album(); s != "Chief Life" {
|
||||
t.Errorf("Open: incorrect album, %v", s)
|
||||
}
|
||||
|
||||
parsedFrame := file.Frame("COMM")
|
||||
resultFrame, ok := parsedFrame.(*v2.UnsynchTextFrame)
|
||||
if !ok {
|
||||
t.Error("Couldn't cast frame")
|
||||
}
|
||||
|
||||
expected := "✓"
|
||||
actual := resultFrame.Description()
|
||||
|
||||
if expected != actual {
|
||||
t.Errorf("Expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
actual = resultFrame.Text()
|
||||
if expected != actual {
|
||||
t.Errorf("Expected %q, got %q", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
before, err := ioutil.ReadFile(testFile)
|
||||
if err != nil {
|
||||
t.Errorf("test file error")
|
||||
}
|
||||
|
||||
file, err := Open(testFile)
|
||||
if err != nil {
|
||||
t.Errorf("Close: unable to open file")
|
||||
}
|
||||
beforeCutoff := file.originalSize
|
||||
|
||||
file.SetArtist("Paloalto")
|
||||
file.SetTitle("Test test test test test test")
|
||||
|
||||
afterCutoff := file.Size()
|
||||
|
||||
if err := file.Close(); err != nil {
|
||||
t.Errorf("Close: unable to close file")
|
||||
}
|
||||
|
||||
after, err := ioutil.ReadFile(testFile)
|
||||
if err != nil {
|
||||
t.Errorf("Close: unable to reopen file")
|
||||
}
|
||||
|
||||
if !bytes.Equal(before[beforeCutoff:], after[afterCutoff:]) {
|
||||
t.Errorf("Close: nontag data lost on close")
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(testFile, before, 0666); err != nil {
|
||||
t.Errorf("Close: unable to write original contents to test file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadonly(t *testing.T) {
|
||||
before, err := ioutil.ReadFile(testFile)
|
||||
if err != nil {
|
||||
t.Errorf("test file error")
|
||||
}
|
||||
|
||||
file, err := Open(testFile)
|
||||
if err != nil {
|
||||
t.Errorf("Readonly: unable to open file")
|
||||
}
|
||||
|
||||
file.Title()
|
||||
file.Artist()
|
||||
file.Album()
|
||||
file.Year()
|
||||
file.Genre()
|
||||
file.Comments()
|
||||
|
||||
if err := file.Close(); err != nil {
|
||||
t.Errorf("Readonly: unable to close file")
|
||||
}
|
||||
|
||||
after, err := ioutil.ReadFile(testFile)
|
||||
if err != nil {
|
||||
t.Errorf("Readonly: unable to reopen file")
|
||||
}
|
||||
|
||||
if !bytes.Equal(before, after) {
|
||||
t.Errorf("Readonly: tag data modified without set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddTag(t *testing.T) {
|
||||
tempFile, err := ioutil.TempFile("", "notag")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
file, err := Open(tempFile.Name())
|
||||
if err != nil {
|
||||
t.Errorf("AddTag: unable to open empty file")
|
||||
}
|
||||
|
||||
tag := file.Tagger
|
||||
|
||||
if tag == nil {
|
||||
t.Errorf("AddTag: no tag added to file")
|
||||
}
|
||||
|
||||
file.SetArtist("Michael")
|
||||
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
t.Errorf("AddTag: error closing new file")
|
||||
}
|
||||
|
||||
reopenBytes, err := ioutil.ReadFile(tempFile.Name())
|
||||
if err != nil {
|
||||
t.Errorf("AddTag: error reopening file")
|
||||
}
|
||||
|
||||
expectedBytes := tag.Bytes()
|
||||
if !bytes.Equal(expectedBytes, reopenBytes) {
|
||||
t.Errorf("AddTag: tag not written correctly: %v", reopenBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsynchTextFrame_RoundTrip(t *testing.T) {
|
||||
var (
|
||||
err error
|
||||
tempfile *os.File
|
||||
f *File
|
||||
tagger *v2.Tag
|
||||
ft v2.FrameType
|
||||
utextFrame *v2.UnsynchTextFrame
|
||||
parsedFrame v2.Framer
|
||||
resultFrame *v2.UnsynchTextFrame
|
||||
ok bool
|
||||
expected, actual string
|
||||
)
|
||||
|
||||
tempfile, err = ioutil.TempFile("", "id3v2")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tagger = v2.NewTag(3)
|
||||
ft = v2.V23FrameTypeMap["COMM"]
|
||||
utextFrame = v2.NewUnsynchTextFrame(ft, "Comment", "Foo")
|
||||
tagger.AddFrames(utextFrame)
|
||||
|
||||
_, err = tempfile.Write(tagger.Bytes())
|
||||
tempfile.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, err = Open(tempfile.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
parsedFrame = f.Frame("COMM")
|
||||
if resultFrame, ok = parsedFrame.(*v2.UnsynchTextFrame); !ok {
|
||||
t.Error("Couldn't cast frame")
|
||||
} else {
|
||||
expected = utextFrame.Description()
|
||||
actual = resultFrame.Description()
|
||||
if expected != actual {
|
||||
t.Errorf("Expected %q, got %q", expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUTF16CommPanic(t *testing.T) {
|
||||
osFile, err := os.Open(testFile)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tempfile, err := ioutil.TempFile("", "utf16_comm")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
io.Copy(tempfile, osFile)
|
||||
osFile.Close()
|
||||
tempfile.Close()
|
||||
for i := 0; i < 2; i++ {
|
||||
file, err := Open(tempfile.Name())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
// 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 (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
v2 "github.com/akhilrex/podgrab/internal/id3/v2"
|
||||
)
|
||||
|
||||
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) SetComment(text string) {
|
||||
length := 28
|
||||
if len(text) < length {
|
||||
length = len((text))
|
||||
}
|
||||
t.comment = text[0:length]
|
||||
t.dirty = true
|
||||
}
|
||||
|
||||
func (t *Tag) SetDate(text string) {
|
||||
t.year = text
|
||||
t.dirty = true
|
||||
print("SetDate() not implemented for ID3v1 tags")
|
||||
}
|
||||
|
||||
func (t *Tag) SetReleaseYear(text string) {
|
||||
t.year = text
|
||||
t.dirty = true
|
||||
print("SetReleaseYear() not implemented for ID3v1 tags")
|
||||
}
|
||||
|
||||
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) {}
|
@ -0,0 +1,586 @@
|
||||
// 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/akhilrex/podgrab/internal/id3/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 "<binary data>"
|
||||
}
|
||||
|
||||
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: <binary data>", 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
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnsynchTextFrameSetEncoding(t *testing.T) {
|
||||
f := NewUnsynchTextFrame(V23CommonFrame["Comments"], "Foo", "Bar")
|
||||
size := f.Size()
|
||||
expectedDiff := 11
|
||||
|
||||
err := f.SetEncoding("UTF-16")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
newSize := f.Size()
|
||||
if int(newSize-size) != expectedDiff {
|
||||
t.Errorf("expected size to increase to %d, but it was %d", size+1, newSize)
|
||||
}
|
||||
|
||||
size = newSize
|
||||
err = f.SetEncoding("ISO-8859-1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
newSize = f.Size()
|
||||
if int(newSize-size) != -expectedDiff {
|
||||
t.Errorf("expected size to decrease to %d, but it was %d", size-1, newSize)
|
||||
}
|
||||
}
|
@ -0,0 +1,372 @@
|
||||
// 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"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/akhilrex/podgrab/internal/id3/encodedbytes"
|
||||
)
|
||||
|
||||
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) SetComment(text string) {
|
||||
t.setUnsynchTextFrameText(t.commonMap["Comments"], 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) SetDate(text string) {
|
||||
t.setTextFrameText(t.commonMap["Date"], text)
|
||||
}
|
||||
|
||||
func (t *Tag) SetReleaseYear(text string) {
|
||||
t.setTextFrameText(t.commonMap["ReleaseYear"], 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 (t *Tag) setUnsynchTextFrameText(ft FrameType, text string) {
|
||||
if frame := t.textFrame(ft); frame != nil {
|
||||
frame.SetEncoding("UTF-8")
|
||||
frame.SetText(text)
|
||||
} else {
|
||||
f := NewUnsynchTextFrame(ft, "comments", 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
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
// 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 (
|
||||
"io"
|
||||
|
||||
"github.com/akhilrex/podgrab/internal/id3/encodedbytes"
|
||||
)
|
||||
|
||||
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"],
|
||||
"Date": V22FrameTypeMap["TDA"],
|
||||
"ReleaseYear": V22FrameTypeMap["TOR"],
|
||||
}
|
||||
|
||||
// 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()...)
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestV22Frame(t *testing.T) {
|
||||
textData := []byte{84, 84, 50, 0, 0, 13, 0, 77, 105, 99, 104, 97, 101, 108, 32, 89, 97, 110, 103}
|
||||
frame := ParseV22Frame(bytes.NewReader(textData))
|
||||
textFrame, ok := frame.(*TextFrame)
|
||||
if !ok {
|
||||
t.Errorf("ParseV23Frame on text data returns wrong type")
|
||||
}
|
||||
|
||||
const text = "Michael Yang"
|
||||
if ft := textFrame.Text(); ft != text {
|
||||
t.Errorf("ParseV23Frame incorrect text, expected %s not %s", text, ft)
|
||||
}
|
||||
|
||||
const encoding = "ISO-8859-1"
|
||||
if e := textFrame.Encoding(); e != encoding {
|
||||
t.Errorf("ParseV23Frame incorrect encoding, expected %s not %s", encoding, e)
|
||||
}
|
||||
|
||||
if b := V22Bytes(frame); !bytes.Equal(textData, b) {
|
||||
t.Errorf("V23Bytes produces different byte slice, expected %v not %v", textData, b)
|
||||
}
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
// 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 (
|
||||
"io"
|
||||
|
||||
"github.com/akhilrex/podgrab/internal/id3/encodedbytes"
|
||||
)
|
||||
|
||||
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"],
|
||||
"Date": V23FrameTypeMap["TDAT"],
|
||||
"ReleaseYear": V23FrameTypeMap["TORY"],
|
||||
}
|
||||
|
||||
// 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()...)
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestV23Frame(t *testing.T) {
|
||||
textData := []byte{84, 80, 69, 49, 0, 0, 0, 13, 0, 0, 0, 77, 105, 99, 104, 97, 101, 108, 32, 89, 97, 110, 103}
|
||||
frame := ParseV23Frame(bytes.NewReader(textData))
|
||||
textFrame, ok := frame.(*TextFrame)
|
||||
if !ok {
|
||||
t.Errorf("ParseV23Frame on text data returns wrong type")
|
||||
}
|
||||
|
||||
const text = "Michael Yang"
|
||||
if ft := textFrame.Text(); ft != text {
|
||||
t.Errorf("ParseV23Frame incorrect text, expected %s not %s", text, ft)
|
||||
}
|
||||
|
||||
const encoding = "ISO-8859-1"
|
||||
if e := textFrame.Encoding(); e != encoding {
|
||||
t.Errorf("ParseV23Frame incorrect encoding, expected %s not %s", encoding, e)
|
||||
}
|
||||
|
||||
if b := V23Bytes(frame); !bytes.Equal(textData, b) {
|
||||
t.Errorf("V23Bytes produces different byte slice, expected %v not %v", textData, b)
|
||||
}
|
||||
}
|
@ -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<<index) != 0
|
||||
}
|
Loading…
Reference in new issue