Initial commit
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
// Copyright 2015, David Howden
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ID3v2Header is a type which represents an ID3v2 tag header.
|
||||
type ID3v2Header struct {
|
||||
Version Format
|
||||
Unsynchronisation bool
|
||||
ExtendedHeader bool
|
||||
Experimental bool
|
||||
Size int
|
||||
}
|
||||
|
||||
// readID3v2Header reads the ID3v2 header from the given io.Reader.
|
||||
func readID3v2Header(r io.Reader) (*ID3v2Header, error) {
|
||||
b, err := readBytes(r, 10)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("expected to read 10 bytes (ID3v2Header): %v", err)
|
||||
}
|
||||
|
||||
if string(b[0:3]) != "ID3" {
|
||||
return nil, fmt.Errorf("expected to read \"ID3\"")
|
||||
}
|
||||
|
||||
b = b[3:]
|
||||
var vers Format
|
||||
switch uint(b[0]) {
|
||||
case 2:
|
||||
vers = ID3v2_2
|
||||
case 3:
|
||||
vers = ID3v2_3
|
||||
case 4:
|
||||
vers = ID3v2_4
|
||||
case 0, 1:
|
||||
fallthrough
|
||||
default:
|
||||
return nil, fmt.Errorf("ID3 version: %v, expected: 2, 3 or 4", uint(b[0]))
|
||||
}
|
||||
|
||||
// NB: We ignore b[1] (the revision) as we don't currently rely on it.
|
||||
return &ID3v2Header{
|
||||
Version: vers,
|
||||
Unsynchronisation: getBit(b[2], 7),
|
||||
ExtendedHeader: getBit(b[2], 6),
|
||||
Experimental: getBit(b[2], 5),
|
||||
Size: get7BitChunkedInt(b[3:7]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ID3v2FrameFlags is a type which represents the flags which can be set on an ID3v2 frame.
|
||||
type ID3v2FrameFlags struct {
|
||||
// Message
|
||||
TagAlterPreservation bool
|
||||
FileAlterPreservation bool
|
||||
ReadOnly bool
|
||||
|
||||
// Format
|
||||
GroupIdentity bool
|
||||
Compression bool
|
||||
Encryption bool
|
||||
Unsynchronisation bool
|
||||
DataLengthIndicator bool
|
||||
}
|
||||
|
||||
func readID3v2FrameFlags(r io.Reader) (*ID3v2FrameFlags, error) {
|
||||
b, err := readBytes(r, 2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg := b[0]
|
||||
fmt := b[1]
|
||||
|
||||
return &ID3v2FrameFlags{
|
||||
TagAlterPreservation: getBit(msg, 6),
|
||||
FileAlterPreservation: getBit(msg, 5),
|
||||
ReadOnly: getBit(msg, 4),
|
||||
GroupIdentity: getBit(fmt, 7),
|
||||
Compression: getBit(fmt, 3),
|
||||
Encryption: getBit(fmt, 2),
|
||||
Unsynchronisation: getBit(fmt, 1),
|
||||
DataLengthIndicator: getBit(fmt, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readID3v2_2FrameHeader(r io.Reader) (name string, size int, headerSize int, err error) {
|
||||
name, err = readString(r, 3)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
size, err = readInt(r, 3)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
headerSize = 6
|
||||
return
|
||||
}
|
||||
|
||||
func readID3v2_3FrameHeader(r io.Reader) (name string, size int, headerSize int, err error) {
|
||||
name, err = readString(r, 4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
size, err = readInt(r, 4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
headerSize = 8
|
||||
return
|
||||
}
|
||||
|
||||
func readID3v2_4FrameHeader(r io.Reader) (name string, size int, headerSize int, err error) {
|
||||
name, err = readString(r, 4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
size, err = read7BitChunkedInt(r, 4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
headerSize = 8
|
||||
return
|
||||
}
|
||||
|
||||
// readID3v2Frames reads ID3v2 frames from the given reader using the ID3v2Header.
|
||||
func readID3v2Frames(r io.Reader, h *ID3v2Header) (map[string]interface{}, error) {
|
||||
offset := 10 // the size of the header
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for offset < h.Size {
|
||||
var err error
|
||||
var name string
|
||||
var size, headerSize int
|
||||
var flags *ID3v2FrameFlags
|
||||
|
||||
switch h.Version {
|
||||
case ID3v2_2:
|
||||
name, size, headerSize, err = readID3v2_2FrameHeader(r)
|
||||
|
||||
case ID3v2_3:
|
||||
name, size, headerSize, err = readID3v2_3FrameHeader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
flags, err = readID3v2FrameFlags(r)
|
||||
headerSize += 2
|
||||
|
||||
case ID3v2_4:
|
||||
name, size, headerSize, err = readID3v2_4FrameHeader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
flags, err = readID3v2FrameFlags(r)
|
||||
headerSize += 2
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset += headerSize + size
|
||||
|
||||
// Check this stuff out...
|
||||
if flags != nil && flags.DataLengthIndicator {
|
||||
_, err = read7BitChunkedInt(r, 4) // read 4
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
size -= 4
|
||||
}
|
||||
|
||||
if flags != nil && flags.Unsynchronisation {
|
||||
// FIXME: Implement this.
|
||||
continue
|
||||
}
|
||||
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
break
|
||||
}
|
||||
|
||||
b, err := readBytes(r, size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case name[0] == 'T':
|
||||
txt, err := readTFrame(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[name] = txt
|
||||
|
||||
case name == "APIC":
|
||||
p, err := readAPICFrame(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[name] = p
|
||||
|
||||
case name == "PIC":
|
||||
p, err := readPICFrame(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[name] = p
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ReadID3v2Tags parses ID3v2.{2,3,4} tags from the given io.Reader into a Metadata, returning
|
||||
// non-nil error on failure.
|
||||
func ReadID3v2Tags(r io.Reader) (Metadata, error) {
|
||||
h, err := readID3v2Header(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, err := readID3v2Frames(r, h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return metadataID3v2{header: h, frames: f}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user