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 (
|
|
|
|
"bytes"
|
|
|
|
"encoding/binary"
|
2015-05-24 04:12:54 +02:00
|
|
|
"errors"
|
2015-03-19 13:21:53 +01:00
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
"unicode/utf16"
|
|
|
|
)
|
|
|
|
|
2015-05-24 04:20:16 +02:00
|
|
|
func readWFrame(b []byte) (string, error) {
|
|
|
|
// Frame text is always encoded in ISO-8859-1
|
|
|
|
b = append([]byte{0}, b...)
|
|
|
|
return readTFrame(b)
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
2015-05-24 04:20:16 +02:00
|
|
|
func readTFrame(b []byte) (string, error) {
|
2015-03-19 13:21:53 +01:00
|
|
|
if len(b) == 0 {
|
|
|
|
return "", nil
|
|
|
|
}
|
2015-05-24 04:20:16 +02:00
|
|
|
|
|
|
|
txt, err := decodeText(b[0], b[1:])
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return strings.Join(strings.Split(txt, string([]byte{0})), ""), nil
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func decodeText(enc byte, b []byte) (string, error) {
|
|
|
|
if len(b) == 0 {
|
|
|
|
return "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
switch enc {
|
|
|
|
case 0: // ISO-8859-1
|
|
|
|
return decodeISO8859(b), nil
|
|
|
|
|
|
|
|
case 1: // UTF-16 with byte order marker
|
2015-05-19 14:09:32 +02:00
|
|
|
if len(b) == 1 {
|
|
|
|
return "", nil
|
|
|
|
}
|
2015-03-19 13:21:53 +01:00
|
|
|
return decodeUTF16WithBOM(b)
|
|
|
|
|
|
|
|
case 2: // UTF-16 without byte order (assuming BigEndian)
|
2015-05-19 14:09:32 +02:00
|
|
|
if len(b) == 1 {
|
|
|
|
return "", nil
|
|
|
|
}
|
2015-03-19 13:21:53 +01:00
|
|
|
return decodeUTF16(b, binary.BigEndian), nil
|
|
|
|
|
|
|
|
case 3: // UTF-8
|
|
|
|
return string(b), nil
|
|
|
|
|
|
|
|
default:
|
|
|
|
return "", fmt.Errorf("invalid encoding byte %x", enc)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-19 14:09:32 +02:00
|
|
|
func encodingDelim(enc byte) ([]byte, error) {
|
|
|
|
switch enc {
|
|
|
|
case 0, 3: // see decodeText above
|
|
|
|
return []byte{0}, nil
|
|
|
|
case 1, 2: // see decodeText above
|
|
|
|
return []byte{0, 0}, nil
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("invalid encoding byte %x", enc)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-22 00:37:12 +02:00
|
|
|
func dataSplit(b []byte, enc byte) ([][]byte, error) {
|
|
|
|
delim, err := encodingDelim(enc)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
result := bytes.SplitN(b, delim, 2)
|
|
|
|
|
|
|
|
if len(result) <= 1 {
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if result[1][0] == 0 {
|
|
|
|
// there was a double (or triple) 0 and we cut too early
|
|
|
|
result[0] = append(result[0], make([]byte, 0)...)
|
|
|
|
result[1] = result[1][1:]
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
2015-03-19 13:21:53 +01:00
|
|
|
func decodeISO8859(b []byte) string {
|
|
|
|
r := make([]rune, len(b))
|
|
|
|
for i, x := range b {
|
|
|
|
r[i] = rune(x)
|
|
|
|
}
|
|
|
|
return string(r)
|
|
|
|
}
|
|
|
|
|
|
|
|
func decodeUTF16WithBOM(b []byte) (string, error) {
|
|
|
|
var bo binary.ByteOrder
|
|
|
|
switch {
|
|
|
|
case b[0] == 0xFE && b[1] == 0xFF:
|
|
|
|
bo = binary.BigEndian
|
|
|
|
|
|
|
|
case b[0] == 0xFF && b[1] == 0xFE:
|
|
|
|
bo = binary.LittleEndian
|
|
|
|
|
|
|
|
default:
|
|
|
|
return "", fmt.Errorf("invalid byte order marker %x %x", b[0], b[1])
|
|
|
|
}
|
|
|
|
return decodeUTF16(b[2:], bo), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func decodeUTF16(b []byte, bo binary.ByteOrder) string {
|
|
|
|
s := make([]uint16, 0, len(b)/2)
|
|
|
|
for i := 0; i < len(b); i += 2 {
|
|
|
|
s = append(s, bo.Uint16(b[i:i+2]))
|
|
|
|
}
|
|
|
|
return string(utf16.Decode(s))
|
|
|
|
}
|
|
|
|
|
2015-05-22 00:59:20 +02:00
|
|
|
// Comm is a type used in COMM, UFID, TXXX, WXXX and USLT tag.
|
|
|
|
// It's a text with a description and a specified language
|
|
|
|
// For WXXX, TXXX and UFID, we don't set a Language
|
|
|
|
|
2015-05-18 09:32:54 +02:00
|
|
|
type Comm struct {
|
|
|
|
Language string
|
|
|
|
Description string
|
|
|
|
Text string
|
|
|
|
}
|
|
|
|
|
|
|
|
// String returns a string representation of the underlying Comm instance.
|
|
|
|
func (t Comm) String() string {
|
2015-05-22 00:59:20 +02:00
|
|
|
if t.Language != "" {
|
|
|
|
return fmt.Sprintf("Text{Lang: '%v', Description: '%v', %v lines}",
|
|
|
|
t.Language, t.Description, strings.Count(t.Text, "\n"))
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("Text{Description: '%v', %v}", t.Description, t.Text)
|
2015-05-18 09:32:54 +02:00
|
|
|
}
|
|
|
|
|
2015-05-19 14:09:32 +02:00
|
|
|
// IDv2.{3,4}
|
|
|
|
// -- Header
|
|
|
|
// <Header for 'Unsynchronised lyrics/text transcription', ID: "USLT">
|
|
|
|
// <Header for 'Comment', ID: "COMM">
|
2015-05-22 00:59:20 +02:00
|
|
|
// -- readTextWithDescrFrame(data, true, true)
|
2015-05-19 14:09:32 +02:00
|
|
|
// Text encoding $xx
|
|
|
|
// Language $xx xx xx
|
|
|
|
// Content descriptor <text string according to encoding> $00 (00)
|
|
|
|
// Lyrics/text <full text string according to encoding>
|
2015-05-22 00:59:20 +02:00
|
|
|
// -- Header
|
|
|
|
// <Header for 'User defined text information frame', ID: "TXXX">
|
|
|
|
// <Header for 'User defined URL link frame', ID: "WXXX">
|
|
|
|
// -- readTextWithDescrFrame(data, false, <isDataEncoded>)
|
|
|
|
// Text encoding $xx
|
|
|
|
// Description <text string according to encoding> $00 (00)
|
|
|
|
// Value <text string according to encoding>
|
|
|
|
func readTextWithDescrFrame(b []byte, hasLang bool, encoded bool) (*Comm, error) {
|
2015-05-19 14:09:32 +02:00
|
|
|
enc := b[0]
|
2015-05-24 04:20:42 +02:00
|
|
|
b = b[1:]
|
2015-05-22 00:37:12 +02:00
|
|
|
|
2015-05-24 04:20:42 +02:00
|
|
|
c := &Comm{}
|
2015-05-22 00:59:20 +02:00
|
|
|
if hasLang {
|
2015-05-24 04:20:42 +02:00
|
|
|
c.Language = string(b[:3])
|
|
|
|
b = b[3:]
|
2015-05-22 00:59:20 +02:00
|
|
|
}
|
2015-05-24 04:20:42 +02:00
|
|
|
|
|
|
|
descTextSplit, err := dataSplit(b, enc)
|
2015-05-19 14:09:32 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-05-22 00:59:20 +02:00
|
|
|
|
2015-05-19 14:09:32 +02:00
|
|
|
desc, err := decodeText(enc, descTextSplit[0])
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error decoding tag description text: %v", err)
|
|
|
|
}
|
2015-05-24 04:20:42 +02:00
|
|
|
c.Description = desc
|
2015-05-19 14:09:32 +02:00
|
|
|
|
2015-05-22 00:59:20 +02:00
|
|
|
if !encoded {
|
|
|
|
enc = byte(0)
|
|
|
|
}
|
2015-05-19 14:09:32 +02:00
|
|
|
text, err := decodeText(enc, descTextSplit[1])
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error decoding tag text: %v", err)
|
|
|
|
}
|
2015-05-24 04:20:42 +02:00
|
|
|
c.Text = text
|
2015-05-19 14:09:32 +02:00
|
|
|
|
2015-05-24 04:20:42 +02:00
|
|
|
return c, nil
|
2015-05-19 14:09:32 +02:00
|
|
|
}
|
|
|
|
|
2015-05-24 04:11:52 +02:00
|
|
|
// UFID is composed of a provider (frequently a URL and a binary identifier)
|
2015-05-22 01:20:55 +02:00
|
|
|
// The identifier can be a text (Musicbrainz use texts, but not necessary)
|
2015-05-24 04:11:52 +02:00
|
|
|
type UFID struct {
|
2015-05-22 01:20:55 +02:00
|
|
|
Provider string
|
|
|
|
Identifier []byte
|
|
|
|
}
|
|
|
|
|
2015-05-24 04:11:52 +02:00
|
|
|
func (u UFID) String() string {
|
2015-05-22 01:20:55 +02:00
|
|
|
return fmt.Sprintf("%v (%v)", u.Provider, string(u.Identifier))
|
|
|
|
}
|
|
|
|
|
2015-05-24 04:11:52 +02:00
|
|
|
func readUFID(b []byte) (*UFID, error) {
|
2015-05-22 01:20:55 +02:00
|
|
|
result := bytes.SplitN(b, []byte{0}, 2)
|
2015-05-24 04:12:54 +02:00
|
|
|
if len(result) != 2 {
|
|
|
|
return nil, errors.New("expected to split UFID data into 2 pieces")
|
|
|
|
}
|
2015-05-22 01:20:55 +02:00
|
|
|
|
2015-05-24 04:11:52 +02:00
|
|
|
return &UFID{
|
2015-05-22 01:20:55 +02:00
|
|
|
Provider: string(result[0]),
|
|
|
|
Identifier: result[1],
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2015-03-19 13:21:53 +01:00
|
|
|
var pictureTypes = map[byte]string{
|
|
|
|
0x00: "Other",
|
|
|
|
0x01: "32x32 pixels 'file icon' (PNG only)",
|
|
|
|
0x02: "Other file icon",
|
|
|
|
0x03: "Cover (front)",
|
|
|
|
0x04: "Cover (back)",
|
|
|
|
0x05: "Leaflet page",
|
|
|
|
0x06: "Media (e.g. lable side of CD)",
|
|
|
|
0x07: "Lead artist/lead performer/soloist",
|
|
|
|
0x08: "Artist/performer",
|
|
|
|
0x09: "Conductor",
|
|
|
|
0x0A: "Band/Orchestra",
|
|
|
|
0x0B: "Composer",
|
|
|
|
0x0C: "Lyricist/text writer",
|
|
|
|
0x0D: "Recording Location",
|
|
|
|
0x0E: "During recording",
|
|
|
|
0x0F: "During performance",
|
|
|
|
0x10: "Movie/video screen capture",
|
|
|
|
0x11: "A bright coloured fish",
|
|
|
|
0x12: "Illustration",
|
|
|
|
0x13: "Band/artist logotype",
|
|
|
|
0x14: "Publisher/Studio logotype",
|
|
|
|
}
|
|
|
|
|
|
|
|
// Picture is a type which represents an attached picture extracted from metadata.
|
|
|
|
type Picture struct {
|
|
|
|
Ext string // Extension of the picture file.
|
|
|
|
MIMEType string // MIMEType of the picture.
|
|
|
|
Type string // Type of the picture (see pictureTypes).
|
|
|
|
Description string // Description.
|
|
|
|
Data []byte // Raw picture data.
|
|
|
|
}
|
|
|
|
|
|
|
|
// String returns a string representation of the underlying Picture instance.
|
|
|
|
func (p Picture) String() string {
|
|
|
|
return fmt.Sprintf("Picture{Ext: %v, MIMEType: %v, Type: %v, Description: %v, Data.Size: %v}",
|
|
|
|
p.Ext, p.MIMEType, p.Type, p.Description, len(p.Data))
|
|
|
|
}
|
|
|
|
|
|
|
|
// IDv2.2
|
|
|
|
// -- Header
|
|
|
|
// Attached picture "PIC"
|
|
|
|
// Frame size $xx xx xx
|
|
|
|
// -- readPICFrame
|
|
|
|
// Text encoding $xx
|
|
|
|
// Image format $xx xx xx
|
|
|
|
// Picture type $xx
|
|
|
|
// Description <textstring> $00 (00)
|
|
|
|
// Picture data <binary data>
|
|
|
|
func readPICFrame(b []byte) (*Picture, error) {
|
|
|
|
enc := b[0]
|
|
|
|
ext := string(b[1:4])
|
|
|
|
picType := b[4]
|
|
|
|
|
2015-05-22 00:37:12 +02:00
|
|
|
descDataSplit, err := dataSplit(b[5:], enc)
|
2015-05-19 14:09:32 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-03-19 13:21:53 +01:00
|
|
|
desc, err := decodeText(enc, descDataSplit[0])
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error decoding PIC description text: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var mimeType string
|
|
|
|
switch ext {
|
|
|
|
case "jpeg", "jpg":
|
|
|
|
mimeType = "image/jpeg"
|
|
|
|
case "png":
|
|
|
|
mimeType = "image/png"
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Picture{
|
|
|
|
Ext: ext,
|
|
|
|
MIMEType: mimeType,
|
|
|
|
Type: pictureTypes[picType],
|
|
|
|
Description: desc,
|
|
|
|
Data: descDataSplit[1],
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// IDv2.{3,4}
|
|
|
|
// -- Header
|
|
|
|
// <Header for 'Attached picture', ID: "APIC">
|
|
|
|
// -- readAPICFrame
|
|
|
|
// Text encoding $xx
|
|
|
|
// MIME type <text string> $00
|
|
|
|
// Picture type $xx
|
|
|
|
// Description <text string according to encoding> $00 (00)
|
|
|
|
// Picture data <binary data>
|
|
|
|
func readAPICFrame(b []byte) (*Picture, error) {
|
|
|
|
enc := b[0]
|
|
|
|
mimeDataSplit := bytes.SplitN(b[1:], []byte{0}, 2)
|
|
|
|
mimeType := string(mimeDataSplit[0])
|
|
|
|
|
|
|
|
b = mimeDataSplit[1]
|
|
|
|
picType := b[0]
|
|
|
|
|
2015-05-22 00:37:12 +02:00
|
|
|
descDataSplit, err := dataSplit(b[1:], enc)
|
2015-05-19 14:09:32 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-03-19 13:21:53 +01:00
|
|
|
desc, err := decodeText(enc, descDataSplit[0])
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error decoding APIC description text: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var ext string
|
|
|
|
switch mimeType {
|
|
|
|
case "image/jpeg":
|
|
|
|
ext = "jpg"
|
|
|
|
case "image/png":
|
|
|
|
ext = "png"
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Picture{
|
|
|
|
Ext: ext,
|
|
|
|
MIMEType: mimeType,
|
|
|
|
Type: pictureTypes[picType],
|
|
|
|
Description: desc,
|
|
|
|
Data: descDataSplit[1],
|
|
|
|
}, nil
|
|
|
|
}
|