diff --git a/flac.go b/flac.go index defcf9a..bd1c831 100644 --- a/flac.go +++ b/flac.go @@ -288,6 +288,10 @@ func (m *metadataFLAC) Disc() (int, int) { return x, n } +func (m *metadataFLAC) Lyrics() string { + return m.c["lyrics"] +} + func (m *metadataFLAC) Picture() *Picture { return m.p } diff --git a/id3v1.go b/id3v1.go index 2243e84..09bed90 100644 --- a/id3v1.go +++ b/id3v1.go @@ -134,3 +134,4 @@ func (m metadataID3v1) AlbumArtist() string { return "" } func (m metadataID3v1) Composer() string { return "" } func (metadataID3v1) Disc() (int, int) { return 0, 0 } func (m metadataID3v1) Picture() *Picture { return nil } +func (m metadataID3v1) Lyrics() string { return "" } diff --git a/id3v2.go b/id3v2.go index f6dff5a..cf1a66c 100644 --- a/id3v2.go +++ b/id3v2.go @@ -218,6 +218,13 @@ func readID3v2Frames(r io.Reader, h *ID3v2Header) (map[string]interface{}, error } result[rawName] = txt + case name == "COMM" || name == "USLT": + t, err := readTextWithDescrFrame(b) + if err != nil { + return nil, err + } + result[rawName] = t + case name == "APIC": p, err := readAPICFrame(b) if err != nil { diff --git a/id3v2frames.go b/id3v2frames.go index 389017c..9a5a67d 100644 --- a/id3v2frames.go +++ b/id3v2frames.go @@ -37,9 +37,15 @@ func decodeText(enc byte, b []byte) (string, error) { return decodeISO8859(b), nil case 1: // UTF-16 with byte order marker + if len(b) == 1 { + return "", nil + } return decodeUTF16WithBOM(b) case 2: // UTF-16 without byte order (assuming BigEndian) + if len(b) == 1 { + return "", nil + } return decodeUTF16(b, binary.BigEndian), nil case 3: // UTF-8 @@ -50,6 +56,17 @@ func decodeText(enc byte, b []byte) (string, error) { } } +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) + } +} + func decodeISO8859(b []byte) string { r := make([]rune, len(b)) for i, x := range b { @@ -81,6 +98,54 @@ func decodeUTF16(b []byte, bo binary.ByteOrder) string { return string(utf16.Decode(s)) } +// Comm is a type used in COMM and USLT tag. It's a text with a description and +// a specified 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 { + return fmt.Sprintf("Text{Lang: '%v', Description: '%v', %v lines}", + t.Language, t.Description, strings.Count(t.Text, "\n")) +} + +// IDv2.{3,4} +// -- Header +//
+//
+// -- readTextWithDescrFrame +// Text encoding $xx +// Language $xx xx xx +// Content descriptor $00 (00) +// Lyrics/text +func readTextWithDescrFrame(b []byte) (*Comm, error) { + enc := b[0] + delim, err := encodingDelim(enc) + if err != nil { + return nil, err + } + + descTextSplit := bytes.SplitN(b[4:], delim, 2) + desc, err := decodeText(enc, descTextSplit[0]) + if err != nil { + return nil, fmt.Errorf("error decoding tag description text: %v", err) + } + + text, err := decodeText(enc, descTextSplit[1]) + if err != nil { + return nil, fmt.Errorf("error decoding tag text: %v", err) + } + + return &Comm{ + Language: string(b[1:4]), + Description: desc, + Text: text, + }, nil +} + var pictureTypes = map[byte]string{ 0x00: "Other", 0x01: "32x32 pixels 'file icon' (PNG only)", @@ -135,7 +200,12 @@ func readPICFrame(b []byte) (*Picture, error) { ext := string(b[1:4]) picType := b[4] - descDataSplit := bytes.SplitN(b[5:], []byte{0}, 2) + delim, err := encodingDelim(enc) + if err != nil { + return nil, err + } + + descDataSplit := bytes.SplitN(b[5:], delim, 2) desc, err := decodeText(enc, descDataSplit[0]) if err != nil { return nil, fmt.Errorf("error decoding PIC description text: %v", err) @@ -175,7 +245,12 @@ func readAPICFrame(b []byte) (*Picture, error) { b = mimeDataSplit[1] picType := b[0] - descDataSplit := bytes.SplitN(b[1:], []byte{0}, 2) + delim, err := encodingDelim(enc) + if err != nil { + return nil, err + } + + descDataSplit := bytes.SplitN(b[1:], delim, 2) desc, err := decodeText(enc, descDataSplit[0]) if err != nil { return nil, fmt.Errorf("error decoding APIC description text: %v", err) diff --git a/id3v2metadata.go b/id3v2metadata.go index 7b8d29e..cf0f122 100644 --- a/id3v2metadata.go +++ b/id3v2metadata.go @@ -37,6 +37,7 @@ var frames = frameNames(map[string][2]string{ "disc": [2]string{"TPA", "TPOS"}, "genre": [2]string{"TCO", "TCON"}, "picture": [2]string{"PIC", "APIC"}, + "lyrics": [2]string{"", "USLT"}, }) // metadataID3v2 is the implementation of Metadata used for ID3v2 tags. @@ -112,6 +113,14 @@ func (m metadataID3v2) Disc() (int, int) { return parseXofN(m.getString(frames.Name("disc", m.Format()))) } +func (m metadataID3v2) Lyrics() string { + t, ok := m.frames[frames.Name("lyrics", m.Format())] + if !ok { + return "" + } + return t.(*Comm).Text +} + func (m metadataID3v2) Picture() *Picture { v, ok := m.frames[frames.Name("picture", m.Format())] if !ok { diff --git a/mp4.go b/mp4.go index c83275c..52279c7 100644 --- a/mp4.go +++ b/mp4.go @@ -222,6 +222,14 @@ func (m metadataMP4) Disc() (int, int) { return x, 0 } +func (m metadataMP4) Lyrics() string { + t, ok := m["\xa9lyr"] + if !ok { + return "" + } + return t.(string) +} + func (m metadataMP4) Picture() *Picture { v, ok := m["covr"] if !ok { diff --git a/tag.go b/tag.go index fe37411..57a15a1 100644 --- a/tag.go +++ b/tag.go @@ -87,9 +87,12 @@ type Metadata interface { // Disc returns the disc number and total discs, or zero values if unavailable. Disc() (int, int) - // Picture returns a picture, or nil if not avilable. + // Picture returns a picture, or nil if not available. Picture() *Picture + // Lyrics returns the lyrics, or an empty string if unavailable. + Lyrics() string + // Raw returns the raw mapping of retrieved tag names and associated values. // NB: tag/atom names are not standardised between formats. Raw() map[string]interface{} diff --git a/tag/tag.go b/tag/tag.go index 8cad726..c389a75 100644 --- a/tag/tag.go +++ b/tag/tag.go @@ -75,4 +75,5 @@ func printMetadata(m tag.Metadata) { fmt.Printf(" Disc: %v of %v\n", disc, discCount) fmt.Printf(" Picture: %v\n", m.Picture()) + fmt.Printf(" Lyrics: %v\n", m.Lyrics()) }