From 7fac753a706047462c15420ead61b8122dcb71d0 Mon Sep 17 00:00:00 2001 From: Xavier Henner Date: Mon, 18 May 2015 09:32:54 +0200 Subject: [PATCH 1/6] Add support for lyrics. Not supported in id3v1 and id3v22 --- flac.go | 4 ++++ id3v1.go | 1 + id3v2.go | 7 +++++++ id3v2frames.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ id3v2metadata.go | 9 +++++++++ mp4.go | 8 ++++++++ tag.go | 2 ++ 7 files changed, 75 insertions(+) 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 cd63f33..a2af714 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..9876dfd 100644 --- a/id3v2frames.go +++ b/id3v2frames.go @@ -81,6 +81,21 @@ 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")) +} + var pictureTypes = map[byte]string{ 0x00: "Other", 0x01: "32x32 pixels 'file icon' (PNG only)", @@ -158,6 +173,35 @@ func readPICFrame(b []byte) (*Picture, error) { }, nil } +// 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] + descTextSplit := bytes.SplitN(b[4:], []byte{0}, 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 description text: %v", err) + } + + return &Comm{ + Language: string(b[1:4]), + Description: desc, + Text: text, + }, nil +} + // IDv2.{3,4} // -- Header //
diff --git a/id3v2metadata.go b/id3v2metadata.go index 7b8d29e..79cf955 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 "toto" + } + 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..89caab4 100644 --- a/tag.go +++ b/tag.go @@ -90,6 +90,8 @@ type Metadata interface { // Picture returns a picture, or nil if not avilable. Picture() *Picture + 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{} From 0ae35897fad3b1df4aac9d3a73974d791c0934e8 Mon Sep 17 00:00:00 2001 From: Xavier Henner Date: Mon, 18 May 2015 09:56:47 +0200 Subject: [PATCH 2/6] Typo, forgotten debug string and a missing comment --- id3v2metadata.go | 2 +- tag.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/id3v2metadata.go b/id3v2metadata.go index 79cf955..cf0f122 100644 --- a/id3v2metadata.go +++ b/id3v2metadata.go @@ -116,7 +116,7 @@ func (m metadataID3v2) Disc() (int, int) { func (m metadataID3v2) Lyrics() string { t, ok := m.frames[frames.Name("lyrics", m.Format())] if !ok { - return "toto" + return "" } return t.(*Comm).Text } diff --git a/tag.go b/tag.go index 89caab4..57a15a1 100644 --- a/tag.go +++ b/tag.go @@ -87,9 +87,10 @@ 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. From c895efbcf8b7fc1056ea89c491f6d52b39d44eb3 Mon Sep 17 00:00:00 2001 From: David Howden Date: Tue, 19 May 2015 08:03:36 +1000 Subject: [PATCH 3/6] Revert "Add support for lyrics." --- flac.go | 4 ---- id3v1.go | 1 - id3v2.go | 7 ------- id3v2frames.go | 44 -------------------------------------------- id3v2metadata.go | 9 --------- mp4.go | 8 -------- tag.go | 5 +---- 7 files changed, 1 insertion(+), 77 deletions(-) diff --git a/flac.go b/flac.go index bd1c831..defcf9a 100644 --- a/flac.go +++ b/flac.go @@ -288,10 +288,6 @@ 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 09bed90..2243e84 100644 --- a/id3v1.go +++ b/id3v1.go @@ -134,4 +134,3 @@ 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 a2af714..cd63f33 100644 --- a/id3v2.go +++ b/id3v2.go @@ -218,13 +218,6 @@ 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 9876dfd..389017c 100644 --- a/id3v2frames.go +++ b/id3v2frames.go @@ -81,21 +81,6 @@ 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")) -} - var pictureTypes = map[byte]string{ 0x00: "Other", 0x01: "32x32 pixels 'file icon' (PNG only)", @@ -173,35 +158,6 @@ func readPICFrame(b []byte) (*Picture, error) { }, nil } -// 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] - descTextSplit := bytes.SplitN(b[4:], []byte{0}, 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 description text: %v", err) - } - - return &Comm{ - Language: string(b[1:4]), - Description: desc, - Text: text, - }, nil -} - // IDv2.{3,4} // -- Header //
diff --git a/id3v2metadata.go b/id3v2metadata.go index cf0f122..7b8d29e 100644 --- a/id3v2metadata.go +++ b/id3v2metadata.go @@ -37,7 +37,6 @@ 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. @@ -113,14 +112,6 @@ 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 52279c7..c83275c 100644 --- a/mp4.go +++ b/mp4.go @@ -222,14 +222,6 @@ 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 57a15a1..fe37411 100644 --- a/tag.go +++ b/tag.go @@ -87,12 +87,9 @@ 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 available. + // Picture returns a picture, or nil if not avilable. 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{} From 0f62dc116ab9031cff5911bb63697b0171403cf8 Mon Sep 17 00:00:00 2001 From: Xavier Henner Date: Mon, 18 May 2015 09:32:54 +0200 Subject: [PATCH 4/6] Add support for lyrics. Not supported in id3v1 and id3v22 --- flac.go | 4 ++++ id3v1.go | 1 + id3v2.go | 7 +++++++ id3v2frames.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ id3v2metadata.go | 9 +++++++++ mp4.go | 8 ++++++++ tag.go | 5 ++++- 7 files changed, 77 insertions(+), 1 deletion(-) 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 cd63f33..a2af714 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..9876dfd 100644 --- a/id3v2frames.go +++ b/id3v2frames.go @@ -81,6 +81,21 @@ 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")) +} + var pictureTypes = map[byte]string{ 0x00: "Other", 0x01: "32x32 pixels 'file icon' (PNG only)", @@ -158,6 +173,35 @@ func readPICFrame(b []byte) (*Picture, error) { }, nil } +// 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] + descTextSplit := bytes.SplitN(b[4:], []byte{0}, 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 description text: %v", err) + } + + return &Comm{ + Language: string(b[1:4]), + Description: desc, + Text: text, + }, nil +} + // IDv2.{3,4} // -- Header //
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{} From 68d82a6107764ece61adab4a8c54c7bfe89311a1 Mon Sep 17 00:00:00 2001 From: David Howden Date: Tue, 19 May 2015 22:09:32 +1000 Subject: [PATCH 5/6] Fix errors caused by incorrect delimiters --- id3v2frames.go | 95 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 63 insertions(+), 32 deletions(-) diff --git a/id3v2frames.go b/id3v2frames.go index 9876dfd..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 { @@ -83,7 +100,6 @@ func decodeUTF16(b []byte, bo binary.ByteOrder) string { // 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 @@ -96,6 +112,40 @@ func (t Comm) String() string { 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)", @@ -150,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) @@ -173,35 +228,6 @@ func readPICFrame(b []byte) (*Picture, error) { }, nil } -// 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] - descTextSplit := bytes.SplitN(b[4:], []byte{0}, 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 description text: %v", err) - } - - return &Comm{ - Language: string(b[1:4]), - Description: desc, - Text: text, - }, nil -} - // IDv2.{3,4} // -- Header //
@@ -219,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) From e5ad564af4d9ad2d86c3a6b12226bd257dd1a4b0 Mon Sep 17 00:00:00 2001 From: David Howden Date: Tue, 19 May 2015 22:10:04 +1000 Subject: [PATCH 6/6] Added Lyrics output to tag tool --- tag/tag.go | 1 + 1 file changed, 1 insertion(+) 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()) }