// 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" "errors" "fmt" "strings" "unicode/utf16" ) // 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 // 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)", "TCMP": "iTunes Compilation Flag", "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", "TSO2": "iTunes uses this for Album Artist sort order", "TSOC": "iTunes uses this for Composer sort order", "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)", "TCMP": "iTunes Compilation Flag", "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", "TSO2": "iTunes uses this for Album Artist sort order", "TSOA": "Album sort order", "TSOC": "iTunes uses this for Composer sort order", "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 } func readWFrame(b []byte) (string, error) { // Frame text is always encoded in ISO-8859-1 b = append([]byte{0}, b...) return readTFrame(b) } func readTFrame(b []byte) (string, error) { if len(b) == 0 { return "", nil } txt, err := decodeText(b[0], b[1:]) if err != nil { return "", err } return strings.Join(strings.Split(txt, string(singleZero)), ""), nil } const ( encodingISO8859 byte = 0 encodingUTF16WithBOM byte = 1 encodingUTF16 byte = 2 encodingUTF8 byte = 3 ) func decodeText(enc byte, b []byte) (string, error) { if len(b) == 0 { return "", nil } switch enc { case encodingISO8859: // ISO-8859-1 return decodeISO8859(b), nil case encodingUTF16WithBOM: // UTF-16 with byte order marker if len(b) == 1 { return "", nil } return decodeUTF16WithBOM(b) case encodingUTF16: // UTF-16 without byte order (assuming BigEndian) if len(b) == 1 { return "", nil } return decodeUTF16(b, binary.BigEndian) case encodingUTF8: // UTF-8 return string(b), nil default: // Fallback to ISO-8859-1 return decodeISO8859(b), nil } } var ( singleZero = []byte{0} doubleZero = []byte{0, 0} ) func dataSplit(b []byte, enc byte) [][]byte { delim := singleZero if enc == encodingUTF16 || enc == encodingUTF16WithBOM { delim = doubleZero } result := bytes.SplitN(b, delim, 2) if len(result) != 2 { return result } if len(result[1]) == 0 { return result } if result[1][0] == 0 { // there was a double (or triple) 0 and we cut too early result[0] = append(result[0], result[1][0]) result[1] = result[1][1:] } return result } 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) { if len(b) < 2 { return "", errors.New("invalid encoding: expected at least 2 bytes for UTF-16 byte order mark") } var bo binary.ByteOrder switch { case b[0] == 0xFE && b[1] == 0xFF: bo = binary.BigEndian b = b[2:] case b[0] == 0xFF && b[1] == 0xFE: bo = binary.LittleEndian b = b[2:] default: bo = DefaultUTF16WithBOMByteOrder } return decodeUTF16(b, bo) } 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") } 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)), nil } // 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 type Comm struct { Language string Description string Text string } // String returns a string representation of the underlying Comm instance. func (t Comm) String() string { 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) } // IDv2.{3,4} // -- Header //
//
// -- readTextWithDescrFrame(data, true, true) // Text encoding $xx // Language $xx xx xx // Content descriptor $00 (00) // Lyrics/text // -- Header //
//
// -- readTextWithDescrFrame(data, false, ) // Text encoding $xx // Description $00 (00) // Value func readTextWithDescrFrame(b []byte, hasLang bool, encoded bool) (*Comm, error) { enc := b[0] b = b[1:] c := &Comm{} if hasLang { if len(b) < 3 { return nil, fmt.Errorf("hasLang set but not enough data for language information") } c.Language = string(b[:3]) b = b[3:] } descTextSplit := dataSplit(b, enc) if len(descTextSplit) < 1 { return nil, fmt.Errorf("error decoding tag description text: invalid encoding") } desc, err := decodeText(enc, descTextSplit[0]) if err != nil { return nil, fmt.Errorf("error decoding tag description text: %v", err) } c.Description = desc if len(descTextSplit) == 1 { return c, nil } if !encoded { enc = byte(0) } text, err := decodeText(enc, descTextSplit[1]) if err != nil { return nil, fmt.Errorf("error decoding tag text: %v", err) } c.Text = text return c, nil } // UFID is composed of a provider (frequently a URL and a binary identifier) // The identifier can be a text (Musicbrainz use texts, but not necessary) type UFID struct { Provider string Identifier []byte } func (u UFID) String() string { return fmt.Sprintf("%v (%v)", u.Provider, string(u.Identifier)) } func readUFID(b []byte) (*UFID, error) { result := bytes.SplitN(b, singleZero, 2) if len(result) != 2 { return nil, errors.New("expected to split UFID data into 2 pieces") } return &UFID{ Provider: string(result[0]), Identifier: result[1], }, nil } 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 $00 (00) // Picture data func readPICFrame(b []byte) (*Picture, error) { if len(b) < 5 { return nil, errors.New("invalid PIC frame") } enc := b[0] ext := string(b[1:4]) picType := b[4] descDataSplit := dataSplit(b[5:], enc) if len(descDataSplit) != 2 { return nil, errors.New("error decoding PIC description text: invalid encoding") } 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 //
// -- readAPICFrame // Text encoding $xx // MIME type $00 // Picture type $xx // Description $00 (00) // Picture data func readAPICFrame(b []byte) (*Picture, error) { if len(b) == 0 { return nil, errors.New("error decoding APIC: invalid encoding") } enc := b[0] mimeDataSplit := bytes.SplitN(b[1:], singleZero, 2) if len(mimeDataSplit) != 2 { return nil, errors.New("error decoding APIC: invalid encoding") } mimeType := string(mimeDataSplit[0]) b = mimeDataSplit[1] if len(b) < 1 { return nil, fmt.Errorf("error decoding APIC mimetype") } picType := b[0] descDataSplit := dataSplit(b[1:], enc) if len(descDataSplit) != 2 { return nil, errors.New("error decoding APIC description text: invalid encoding") } 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 }