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"
|
|
|
|
)
|
|
|
|
|
2016-01-03 13:00:45 +01:00
|
|
|
// DefaultUTF16WithBOMByteOrder is the byte order used when the "UTF16 with BOM" encoding
|
|
|
|
// is specified without a corresponding BOM in the data.
|
|
|
|
var DefaultUTF16WithBOMByteOrder binary.ByteOrder = binary.LittleEndian
|
|
|
|
|
2015-06-28 01:53:07 +02:00
|
|
|
// ID3v2.2.0 frames (see http://id3.org/id3v2-00, sec 4).
|
|
|
|
var id3v22Frames = map[string]string{
|
|
|
|
"BUF": "Recommended buffer size",
|
|
|
|
|
|
|
|
"CNT": "Play counter",
|
|
|
|
"COM": "Comments",
|
|
|
|
"CRA": "Audio encryption",
|
|
|
|
"CRM": "Encrypted meta frame",
|
|
|
|
|
|
|
|
"ETC": "Event timing codes",
|
|
|
|
"EQU": "Equalization",
|
|
|
|
|
|
|
|
"GEO": "General encapsulated object",
|
|
|
|
|
|
|
|
"IPL": "Involved people list",
|
|
|
|
|
|
|
|
"LNK": "Linked information",
|
|
|
|
|
|
|
|
"MCI": "Music CD Identifier",
|
|
|
|
"MLL": "MPEG location lookup table",
|
|
|
|
|
|
|
|
"PIC": "Attached picture",
|
|
|
|
"POP": "Popularimeter",
|
|
|
|
|
|
|
|
"REV": "Reverb",
|
|
|
|
"RVA": "Relative volume adjustment",
|
|
|
|
|
|
|
|
"SLT": "Synchronized lyric/text",
|
|
|
|
"STC": "Synced tempo codes",
|
|
|
|
|
|
|
|
"TAL": "Album/Movie/Show title",
|
|
|
|
"TBP": "BPM (Beats Per Minute)",
|
|
|
|
"TCM": "Composer",
|
|
|
|
"TCO": "Content type",
|
|
|
|
"TCR": "Copyright message",
|
|
|
|
"TDA": "Date",
|
|
|
|
"TDY": "Playlist delay",
|
|
|
|
"TEN": "Encoded by",
|
|
|
|
"TFT": "File type",
|
|
|
|
"TIM": "Time",
|
|
|
|
"TKE": "Initial key",
|
|
|
|
"TLA": "Language(s)",
|
|
|
|
"TLE": "Length",
|
|
|
|
"TMT": "Media type",
|
|
|
|
"TOA": "Original artist(s)/performer(s)",
|
|
|
|
"TOF": "Original filename",
|
|
|
|
"TOL": "Original Lyricist(s)/text writer(s)",
|
|
|
|
"TOR": "Original release year",
|
|
|
|
"TOT": "Original album/Movie/Show title",
|
|
|
|
"TP1": "Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group",
|
|
|
|
"TP2": "Band/Orchestra/Accompaniment",
|
|
|
|
"TP3": "Conductor/Performer refinement",
|
|
|
|
"TP4": "Interpreted, remixed, or otherwise modified by",
|
|
|
|
"TPA": "Part of a set",
|
|
|
|
"TPB": "Publisher",
|
|
|
|
"TRC": "ISRC (International Standard Recording Code)",
|
|
|
|
"TRD": "Recording dates",
|
|
|
|
"TRK": "Track number/Position in set",
|
|
|
|
"TSI": "Size",
|
|
|
|
"TSS": "Software/hardware and settings used for encoding",
|
|
|
|
"TT1": "Content group description",
|
|
|
|
"TT2": "Title/Songname/Content description",
|
|
|
|
"TT3": "Subtitle/Description refinement",
|
|
|
|
"TXT": "Lyricist/text writer",
|
|
|
|
"TXX": "User defined text information frame",
|
|
|
|
"TYE": "Year",
|
|
|
|
|
|
|
|
"UFI": "Unique file identifier",
|
|
|
|
"ULT": "Unsychronized lyric/text transcription",
|
|
|
|
|
|
|
|
"WAF": "Official audio file webpage",
|
|
|
|
"WAR": "Official artist/performer webpage",
|
|
|
|
"WAS": "Official audio source webpage",
|
|
|
|
"WCM": "Commercial information",
|
|
|
|
"WCP": "Copyright/Legal information",
|
|
|
|
"WPB": "Publishers official webpage",
|
|
|
|
"WXX": "User defined URL link frame",
|
|
|
|
}
|
|
|
|
|
|
|
|
// ID3v2.3.0 frames (see http://id3.org/id3v2.3.0#Declared_ID3v2_frames).
|
|
|
|
var id3v23Frames = map[string]string{
|
|
|
|
"AENC": "Audio encryption]",
|
|
|
|
"APIC": "Attached picture",
|
|
|
|
"COMM": "Comments",
|
|
|
|
"COMR": "Commercial frame",
|
|
|
|
"ENCR": "Encryption method registration",
|
|
|
|
"EQUA": "Equalization",
|
|
|
|
"ETCO": "Event timing codes",
|
|
|
|
"GEOB": "General encapsulated object",
|
|
|
|
"GRID": "Group identification registration",
|
|
|
|
"IPLS": "Involved people list",
|
|
|
|
"LINK": "Linked information",
|
|
|
|
"MCDI": "Music CD identifier",
|
|
|
|
"MLLT": "MPEG location lookup table",
|
|
|
|
"OWNE": "Ownership frame",
|
|
|
|
"PRIV": "Private frame",
|
|
|
|
"PCNT": "Play counter",
|
|
|
|
"POPM": "Popularimeter",
|
|
|
|
"POSS": "Position synchronisation frame",
|
|
|
|
"RBUF": "Recommended buffer size",
|
|
|
|
"RVAD": "Relative volume adjustment",
|
|
|
|
"RVRB": "Reverb",
|
|
|
|
"SYLT": "Synchronized lyric/text",
|
|
|
|
"SYTC": "Synchronized tempo codes",
|
|
|
|
"TALB": "Album/Movie/Show title",
|
|
|
|
"TBPM": "BPM (beats per minute)",
|
2015-07-05 23:03:02 +02:00
|
|
|
"TCMP": "iTunes Compilation Flag",
|
2015-06-28 01:53:07 +02:00
|
|
|
"TCOM": "Composer",
|
|
|
|
"TCON": "Content type",
|
|
|
|
"TCOP": "Copyright message",
|
|
|
|
"TDAT": "Date",
|
|
|
|
"TDLY": "Playlist delay",
|
|
|
|
"TENC": "Encoded by",
|
|
|
|
"TEXT": "Lyricist/Text writer",
|
|
|
|
"TFLT": "File type",
|
|
|
|
"TIME": "Time",
|
|
|
|
"TIT1": "Content group description",
|
|
|
|
"TIT2": "Title/songname/content description",
|
|
|
|
"TIT3": "Subtitle/Description refinement",
|
|
|
|
"TKEY": "Initial key",
|
|
|
|
"TLAN": "Language(s)",
|
|
|
|
"TLEN": "Length",
|
|
|
|
"TMED": "Media type",
|
|
|
|
"TOAL": "Original album/movie/show title",
|
|
|
|
"TOFN": "Original filename",
|
|
|
|
"TOLY": "Original lyricist(s)/text writer(s)",
|
|
|
|
"TOPE": "Original artist(s)/performer(s)",
|
|
|
|
"TORY": "Original release year",
|
|
|
|
"TOWN": "File owner/licensee",
|
|
|
|
"TPE1": "Lead performer(s)/Soloist(s)",
|
|
|
|
"TPE2": "Band/orchestra/accompaniment",
|
|
|
|
"TPE3": "Conductor/performer refinement",
|
|
|
|
"TPE4": "Interpreted, remixed, or otherwise modified by",
|
|
|
|
"TPOS": "Part of a set",
|
|
|
|
"TPUB": "Publisher",
|
|
|
|
"TRCK": "Track number/Position in set",
|
|
|
|
"TRDA": "Recording dates",
|
|
|
|
"TRSN": "Internet radio station name",
|
|
|
|
"TRSO": "Internet radio station owner",
|
|
|
|
"TSIZ": "Size",
|
2015-07-05 23:03:02 +02:00
|
|
|
"TSO2": "iTunes uses this for Album Artist sort order",
|
|
|
|
"TSOC": "iTunes uses this for Composer sort order",
|
2015-06-28 01:53:07 +02:00
|
|
|
"TSRC": "ISRC (international standard recording code)",
|
|
|
|
"TSSE": "Software/Hardware and settings used for encoding",
|
|
|
|
"TYER": "Year",
|
|
|
|
"TXXX": "User defined text information frame",
|
|
|
|
"UFID": "Unique file identifier",
|
|
|
|
"USER": "Terms of use",
|
|
|
|
"USLT": "Unsychronized lyric/text transcription",
|
|
|
|
"WCOM": "Commercial information",
|
|
|
|
"WCOP": "Copyright/Legal information",
|
|
|
|
"WOAF": "Official audio file webpage",
|
|
|
|
"WOAR": "Official artist/performer webpage",
|
|
|
|
"WOAS": "Official audio source webpage",
|
|
|
|
"WORS": "Official internet radio station homepage",
|
|
|
|
"WPAY": "Payment",
|
|
|
|
"WPUB": "Publishers official webpage",
|
|
|
|
"WXXX": "User defined URL link frame",
|
|
|
|
}
|
|
|
|
|
|
|
|
// ID3v2.4.0 frames (see http://id3.org/id3v2.4.0-frames, sec 4).
|
|
|
|
var id3v24Frames = map[string]string{
|
|
|
|
"AENC": "Audio encryption",
|
|
|
|
"APIC": "Attached picture",
|
|
|
|
"ASPI": "Audio seek point index",
|
|
|
|
|
|
|
|
"COMM": "Comments",
|
|
|
|
"COMR": "Commercial frame",
|
|
|
|
|
|
|
|
"ENCR": "Encryption method registration",
|
|
|
|
"EQU2": "Equalisation (2)",
|
|
|
|
"ETCO": "Event timing codes",
|
|
|
|
|
|
|
|
"GEOB": "General encapsulated object",
|
|
|
|
"GRID": "Group identification registration",
|
|
|
|
|
|
|
|
"LINK": "Linked information",
|
|
|
|
|
|
|
|
"MCDI": "Music CD identifier",
|
|
|
|
"MLLT": "MPEG location lookup table",
|
|
|
|
|
|
|
|
"OWNE": "Ownership frame",
|
|
|
|
|
|
|
|
"PRIV": "Private frame",
|
|
|
|
"PCNT": "Play counter",
|
|
|
|
"POPM": "Popularimeter",
|
|
|
|
"POSS": "Position synchronisation frame",
|
|
|
|
|
|
|
|
"RBUF": "Recommended buffer size",
|
|
|
|
"RVA2": "Relative volume adjustment (2)",
|
|
|
|
"RVRB": "Reverb",
|
|
|
|
|
|
|
|
"SEEK": "Seek frame",
|
|
|
|
"SIGN": "Signature frame",
|
|
|
|
"SYLT": "Synchronised lyric/text",
|
|
|
|
"SYTC": "Synchronised tempo codes",
|
|
|
|
|
|
|
|
"TALB": "Album/Movie/Show title",
|
|
|
|
"TBPM": "BPM (beats per minute)",
|
2015-07-05 23:03:02 +02:00
|
|
|
"TCMP": "iTunes Compilation Flag",
|
2015-06-28 01:53:07 +02:00
|
|
|
"TCOM": "Composer",
|
|
|
|
"TCON": "Content type",
|
|
|
|
"TCOP": "Copyright message",
|
|
|
|
"TDEN": "Encoding time",
|
|
|
|
"TDLY": "Playlist delay",
|
|
|
|
"TDOR": "Original release time",
|
|
|
|
"TDRC": "Recording time",
|
|
|
|
"TDRL": "Release time",
|
|
|
|
"TDTG": "Tagging time",
|
|
|
|
"TENC": "Encoded by",
|
|
|
|
"TEXT": "Lyricist/Text writer",
|
|
|
|
"TFLT": "File type",
|
|
|
|
"TIPL": "Involved people list",
|
|
|
|
"TIT1": "Content group description",
|
|
|
|
"TIT2": "Title/songname/content description",
|
|
|
|
"TIT3": "Subtitle/Description refinement",
|
|
|
|
"TKEY": "Initial key",
|
|
|
|
"TLAN": "Language(s)",
|
|
|
|
"TLEN": "Length",
|
|
|
|
"TMCL": "Musician credits list",
|
|
|
|
"TMED": "Media type",
|
|
|
|
"TMOO": "Mood",
|
|
|
|
"TOAL": "Original album/movie/show title",
|
|
|
|
"TOFN": "Original filename",
|
|
|
|
"TOLY": "Original lyricist(s)/text writer(s)",
|
|
|
|
"TOPE": "Original artist(s)/performer(s)",
|
|
|
|
"TOWN": "File owner/licensee",
|
|
|
|
"TPE1": "Lead performer(s)/Soloist(s)",
|
|
|
|
"TPE2": "Band/orchestra/accompaniment",
|
|
|
|
"TPE3": "Conductor/performer refinement",
|
|
|
|
"TPE4": "Interpreted, remixed, or otherwise modified by",
|
|
|
|
"TPOS": "Part of a set",
|
|
|
|
"TPRO": "Produced notice",
|
|
|
|
"TPUB": "Publisher",
|
|
|
|
"TRCK": "Track number/Position in set",
|
|
|
|
"TRSN": "Internet radio station name",
|
|
|
|
"TRSO": "Internet radio station owner",
|
2015-07-05 23:03:02 +02:00
|
|
|
"TSO2": "iTunes uses this for Album Artist sort order",
|
2015-06-28 01:53:07 +02:00
|
|
|
"TSOA": "Album sort order",
|
2015-07-05 23:03:02 +02:00
|
|
|
"TSOC": "iTunes uses this for Composer sort order",
|
2015-06-28 01:53:07 +02:00
|
|
|
"TSOP": "Performer sort order",
|
|
|
|
"TSOT": "Title sort order",
|
|
|
|
"TSRC": "ISRC (international standard recording code)",
|
|
|
|
"TSSE": "Software/Hardware and settings used for encoding",
|
|
|
|
"TSST": "Set subtitle",
|
|
|
|
"TXXX": "User defined text information frame",
|
|
|
|
|
|
|
|
"UFID": "Unique file identifier",
|
|
|
|
"USER": "Terms of use",
|
|
|
|
"USLT": "Unsynchronised lyric/text transcription",
|
|
|
|
|
|
|
|
"WCOM": "Commercial information",
|
|
|
|
"WCOP": "Copyright/Legal information",
|
|
|
|
"WOAF": "Official audio file webpage",
|
|
|
|
"WOAR": "Official artist/performer webpage",
|
|
|
|
"WOAS": "Official audio source webpage",
|
|
|
|
"WORS": "Official Internet radio station homepage",
|
|
|
|
"WPAY": "Payment",
|
|
|
|
"WPUB": "Publishers official webpage",
|
|
|
|
"WXXX": "User defined URL link frame",
|
|
|
|
}
|
|
|
|
|
|
|
|
// ID3 frames that are defined in the specs.
|
|
|
|
var id3Frames = map[Format]map[string]string{
|
|
|
|
ID3v2_2: id3v22Frames,
|
|
|
|
ID3v2_3: id3v23Frames,
|
|
|
|
ID3v2_4: id3v24Frames,
|
|
|
|
}
|
|
|
|
|
|
|
|
func validID3Frame(version Format, name string) bool {
|
|
|
|
names, ok := id3Frames[version]
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
_, ok = names[name]
|
|
|
|
return ok
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
}
|
2018-02-19 08:01:22 +01:00
|
|
|
return decodeUTF16WithBOM(b)
|
2015-03-19 13:21:53 +01:00
|
|
|
|
|
|
|
case 2: // UTF-16 without byte order (assuming BigEndian)
|
2015-05-19 14:09:32 +02:00
|
|
|
if len(b) == 1 {
|
|
|
|
return "", nil
|
|
|
|
}
|
2018-02-19 08:01:22 +01:00
|
|
|
return decodeUTF16(b, binary.BigEndian)
|
2015-03-19 13:21:53 +01:00
|
|
|
|
|
|
|
case 3: // UTF-8
|
|
|
|
return string(b), nil
|
|
|
|
|
2016-05-09 14:37:03 +02:00
|
|
|
default: // Fallback to ISO-8859-1
|
|
|
|
return decodeISO8859(b), nil
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
2016-05-09 14:40:03 +02:00
|
|
|
default: // see decodeText above
|
|
|
|
return []byte{0}, nil
|
2015-05-19 14:09:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
}
|
2015-05-24 05:13:06 +02:00
|
|
|
|
2015-05-22 00:37:12 +02:00
|
|
|
result := bytes.SplitN(b, delim, 2)
|
2015-05-24 05:13:06 +02:00
|
|
|
if len(result) != 2 {
|
2015-05-24 05:13:32 +02:00
|
|
|
return result, nil
|
|
|
|
}
|
2015-05-22 00:37:12 +02:00
|
|
|
|
2015-05-24 05:13:32 +02:00
|
|
|
if len(result[1]) == 0 {
|
2015-05-22 00:37:12 +02:00
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if result[1][0] == 0 {
|
|
|
|
// there was a double (or triple) 0 and we cut too early
|
2018-04-01 23:26:56 +02:00
|
|
|
result[0] = append(result[0], result[1][0])
|
2015-05-22 00:37:12 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2018-02-19 08:01:22 +01:00
|
|
|
func decodeUTF16WithBOM(b []byte) (string, error) {
|
|
|
|
if len(b) < 2 {
|
|
|
|
return "", errors.New("invalid encoding: expected at least 2 bytes for UTF-16 byte order mark")
|
|
|
|
}
|
|
|
|
|
2015-03-19 13:21:53 +01:00
|
|
|
var bo binary.ByteOrder
|
|
|
|
switch {
|
|
|
|
case b[0] == 0xFE && b[1] == 0xFF:
|
|
|
|
bo = binary.BigEndian
|
2016-01-03 13:04:12 +01:00
|
|
|
b = b[2:]
|
2015-03-19 13:21:53 +01:00
|
|
|
|
|
|
|
case b[0] == 0xFF && b[1] == 0xFE:
|
|
|
|
bo = binary.LittleEndian
|
2016-01-03 13:04:12 +01:00
|
|
|
b = b[2:]
|
2015-03-19 13:21:53 +01:00
|
|
|
|
|
|
|
default:
|
2016-01-03 13:04:12 +01:00
|
|
|
bo = DefaultUTF16WithBOMByteOrder
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
2016-01-03 13:04:12 +01:00
|
|
|
return decodeUTF16(b, bo)
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
2018-02-19 08:01:22 +01:00
|
|
|
func decodeUTF16(b []byte, bo binary.ByteOrder) (string, error) {
|
|
|
|
if len(b)%2 != 0 {
|
|
|
|
return "", errors.New("invalid encoding: expected even number of bytes for UTF-16 encoded text")
|
|
|
|
}
|
2015-03-19 13:21:53 +01:00
|
|
|
s := make([]uint16, 0, len(b)/2)
|
|
|
|
for i := 0; i < len(b); i += 2 {
|
|
|
|
s = append(s, bo.Uint16(b[i:i+2]))
|
|
|
|
}
|
2018-02-19 08:01:22 +01:00
|
|
|
return string(utf16.Decode(s)), nil
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
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-06-27 11:24:33 +02:00
|
|
|
if len(descTextSplit) == 1 {
|
|
|
|
return c, nil
|
|
|
|
}
|
|
|
|
|
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]
|
2017-10-22 12:42:29 +02:00
|
|
|
if len(b) < 1 {
|
|
|
|
return nil, fmt.Errorf("error decoding APIC mimetype")
|
|
|
|
}
|
2015-03-19 13:21:53 +01:00
|
|
|
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
|
|
|
|
}
|