2015-03-19 13:21:53 +01:00
|
|
|
// 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"
|
2015-04-26 17:55:59 +02:00
|
|
|
"strconv"
|
2015-03-19 13:21:53 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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 {
|
2015-06-28 04:34:42 +02:00
|
|
|
// Message (ID3 2.3.0 and 2.4.0)
|
2015-03-19 13:21:53 +01:00
|
|
|
TagAlterPreservation bool
|
|
|
|
FileAlterPreservation bool
|
|
|
|
ReadOnly bool
|
|
|
|
|
2015-06-28 04:34:42 +02:00
|
|
|
// Format (ID3 2.3.0 and 2.4.0)
|
|
|
|
Compression bool
|
|
|
|
Encryption bool
|
|
|
|
GroupIdentity bool
|
|
|
|
// ID3 2.4.0 only (see http://id3.org/id3v2.4.0-structure sec 4.1)
|
2015-03-19 13:21:53 +01:00
|
|
|
Unsynchronisation bool
|
|
|
|
DataLengthIndicator bool
|
|
|
|
}
|
|
|
|
|
2015-06-28 04:34:42 +02:00
|
|
|
func readID3v23FrameFlags(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, 7),
|
|
|
|
FileAlterPreservation: getBit(msg, 6),
|
|
|
|
ReadOnly: getBit(msg, 5),
|
|
|
|
Compression: getBit(fmt, 7),
|
|
|
|
Encryption: getBit(fmt, 6),
|
|
|
|
GroupIdentity: getBit(fmt, 5),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func readID3v24FrameFlags(r io.Reader) (*ID3v2FrameFlags, error) {
|
2015-03-19 13:21:53 +01:00
|
|
|
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),
|
2015-06-28 04:34:42 +02:00
|
|
|
GroupIdentity: getBit(fmt, 6),
|
2015-03-19 13:21:53 +01:00
|
|
|
Compression: getBit(fmt, 3),
|
|
|
|
Encryption: getBit(fmt, 2),
|
|
|
|
Unsynchronisation: getBit(fmt, 1),
|
|
|
|
DataLengthIndicator: getBit(fmt, 0),
|
|
|
|
}, nil
|
2015-06-28 04:34:42 +02:00
|
|
|
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2015-06-28 04:34:42 +02:00
|
|
|
flags, err = readID3v23FrameFlags(r)
|
2015-03-19 13:21:53 +01:00
|
|
|
headerSize += 2
|
|
|
|
|
|
|
|
case ID3v2_4:
|
|
|
|
name, size, headerSize, err = readID3v2_4FrameHeader(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-06-28 04:34:42 +02:00
|
|
|
flags, err = readID3v24FrameFlags(r)
|
2015-03-19 13:21:53 +01:00
|
|
|
headerSize += 2
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-04-26 17:55:59 +02:00
|
|
|
|
2015-06-28 01:53:07 +02:00
|
|
|
// Avoid corrupted padding (see http://id3.org/Compliance%20Issues).
|
|
|
|
if !validID3Frame(h.Version, name) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// FIXME: Do we still need this?
|
2015-04-26 16:56:26 +02:00
|
|
|
// if size=0, we certainly are in a padding zone. ignore the rest of
|
2015-04-30 22:07:08 +02:00
|
|
|
// the tags
|
|
|
|
if size == 0 {
|
2015-04-26 16:56:26 +02:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2015-03-19 13:21:53 +01:00
|
|
|
offset += headerSize + size
|
|
|
|
|
2015-06-28 04:34:42 +02:00
|
|
|
if flags != nil {
|
|
|
|
if flags.Compression {
|
|
|
|
_, err = read7BitChunkedInt(r, 4) // read 4
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
size -= 4
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
2015-06-28 04:34:42 +02:00
|
|
|
if flags.Encryption {
|
|
|
|
_, err = readBytes(r, 1) // read 1 byte of encryption method
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
size -= 1
|
|
|
|
}
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
2015-04-27 15:11:04 +02:00
|
|
|
b, err := readBytes(r, size)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// There can be multiple tag with the same name. Append a number to the
|
|
|
|
// name if there is more than one.
|
|
|
|
rawName := name
|
|
|
|
if _, ok := result[rawName]; ok {
|
|
|
|
for i := 0; ok; i++ {
|
|
|
|
rawName = name + "_" + strconv.Itoa(i)
|
|
|
|
_, ok = result[rawName]
|
|
|
|
}
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
switch {
|
2015-05-22 00:59:20 +02:00
|
|
|
case name == "TXXX" || name == "TXX":
|
|
|
|
t, err := readTextWithDescrFrame(b, false, true) // no lang, but enc
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
result[rawName] = t
|
|
|
|
|
2015-03-19 13:21:53 +01:00
|
|
|
case name[0] == 'T':
|
2015-05-24 04:20:16 +02:00
|
|
|
txt, err := readTFrame(b)
|
2015-05-22 00:59:20 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
result[rawName] = txt
|
|
|
|
|
2015-05-22 01:20:55 +02:00
|
|
|
case name == "UFID" || name == "UFI":
|
2015-05-24 04:11:52 +02:00
|
|
|
t, err := readUFID(b)
|
2015-05-22 01:20:55 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
result[rawName] = t
|
|
|
|
|
2015-05-22 00:59:20 +02:00
|
|
|
case name == "WXXX" || name == "WXX":
|
|
|
|
t, err := readTextWithDescrFrame(b, false, false) // no lang, no enc
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
result[rawName] = t
|
|
|
|
|
|
|
|
case name[0] == 'W':
|
2015-05-24 04:20:16 +02:00
|
|
|
txt, err := readWFrame(b)
|
2015-03-19 13:21:53 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-04-27 15:11:04 +02:00
|
|
|
result[rawName] = txt
|
2015-03-19 13:21:53 +01:00
|
|
|
|
2015-05-18 09:32:54 +02:00
|
|
|
case name == "COMM" || name == "USLT":
|
2015-05-22 00:59:20 +02:00
|
|
|
t, err := readTextWithDescrFrame(b, true, true) // both lang and enc
|
2015-05-18 09:32:54 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
result[rawName] = t
|
|
|
|
|
2015-04-27 15:11:04 +02:00
|
|
|
case name == "APIC":
|
2015-03-19 13:21:53 +01:00
|
|
|
p, err := readAPICFrame(b)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-04-27 15:11:04 +02:00
|
|
|
result[rawName] = p
|
2015-03-19 13:21:53 +01:00
|
|
|
|
2015-04-27 15:11:04 +02:00
|
|
|
case name == "PIC":
|
2015-03-19 13:21:53 +01:00
|
|
|
p, err := readPICFrame(b)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-04-27 15:11:04 +02:00
|
|
|
result[rawName] = p
|
2015-06-28 01:53:57 +02:00
|
|
|
|
2015-05-24 21:19:15 +02:00
|
|
|
default:
|
|
|
|
result[rawName] = b
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
2015-05-02 01:58:05 +02:00
|
|
|
type unsynchroniser struct {
|
|
|
|
io.Reader
|
|
|
|
ff bool
|
2015-04-26 16:56:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// filter io.Reader which skip the Unsynchronisation bytes
|
2015-05-03 01:58:08 +02:00
|
|
|
func (r *unsynchroniser) Read(p []byte) (int, error) {
|
2015-05-02 01:58:05 +02:00
|
|
|
b := make([]byte, 1)
|
2015-05-03 01:58:08 +02:00
|
|
|
i := 0
|
2015-05-02 01:58:05 +02:00
|
|
|
for i < len(p) {
|
2015-05-03 01:58:08 +02:00
|
|
|
if n, err := r.Reader.Read(b); err != nil || n == 0 {
|
2015-04-26 16:56:26 +02:00
|
|
|
return i, err
|
|
|
|
}
|
2015-05-02 01:58:05 +02:00
|
|
|
if r.ff && b[0] == 0x00 {
|
|
|
|
r.ff = false
|
2015-04-30 22:07:08 +02:00
|
|
|
continue
|
|
|
|
}
|
2015-05-02 01:58:05 +02:00
|
|
|
p[i] = b[0]
|
2015-04-30 22:07:08 +02:00
|
|
|
i++
|
2015-05-02 01:58:05 +02:00
|
|
|
r.ff = (b[0] == 0xFF)
|
2015-04-26 16:56:26 +02:00
|
|
|
}
|
2015-05-03 01:58:08 +02:00
|
|
|
return i, nil
|
2015-04-26 16:56:26 +02:00
|
|
|
}
|
|
|
|
|
2015-04-14 16:06:32 +02:00
|
|
|
// ReadID3v2Tags parses ID3v2.{2,3,4} tags from the io.ReadSeeker into a Metadata, returning
|
2015-03-19 13:21:53 +01:00
|
|
|
// non-nil error on failure.
|
2015-04-14 16:06:32 +02:00
|
|
|
func ReadID3v2Tags(r io.ReadSeeker) (Metadata, error) {
|
2015-03-19 13:21:53 +01:00
|
|
|
h, err := readID3v2Header(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-04-26 16:56:26 +02:00
|
|
|
|
|
|
|
var ur io.Reader
|
2015-05-02 01:58:05 +02:00
|
|
|
ur = r
|
2015-04-26 16:56:26 +02:00
|
|
|
if h.Unsynchronisation {
|
2015-05-02 01:58:05 +02:00
|
|
|
ur = &unsynchroniser{Reader: r}
|
2015-04-26 16:56:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
f, err := readID3v2Frames(ur, h)
|
2015-03-19 13:21:53 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return metadataID3v2{header: h, frames: f}, nil
|
|
|
|
}
|