diff --git a/README.md b/README.md index ba2e3e6..de81883 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ type Metadata interface { Picture() *Picture // Artwork Lyrics() string + Comment() string Raw() map[string]interface{} // NB: raw tag names are not consistent across formats. } diff --git a/cmd/tag/main.go b/cmd/tag/main.go index 61ba0db..c34e319 100644 --- a/cmd/tag/main.go +++ b/cmd/tag/main.go @@ -95,4 +95,5 @@ func printMetadata(m tag.Metadata) { fmt.Printf(" Picture: %v\n", m.Picture()) fmt.Printf(" Lyrics: %v\n", m.Lyrics()) + fmt.Printf(" Comment: %v\n", m.Comment()) } diff --git a/dsf.go b/dsf.go new file mode 100644 index 0000000..d826a74 --- /dev/null +++ b/dsf.go @@ -0,0 +1,110 @@ +// 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 ( + "errors" + "io" +) + +// ReadDSFTags reads DSF metadata from the io.ReadSeeker, returning the resulting +// metadata in a Metadata implementation, or non-nil error if there was a problem. +// samples: http://www.2l.no/hires/index.html +func ReadDSFTags(r io.ReadSeeker) (Metadata, error) { + dsd, err := readString(r, 4) + if err != nil { + return nil, err + } + if dsd != "DSD " { + return nil, errors.New("expected 'DSD '") + } + + _, err = r.Seek(int64(16), io.SeekCurrent) + if err != nil { + return nil, err + } + + n4, err := readBytes(r, 8) + if err != nil { + return nil, err + } + id3Pointer := getIntLittleEndian(n4) + + _, err = r.Seek(int64(id3Pointer), io.SeekStart) + if err != nil { + return nil, err + } + + id3, err := ReadID3v2Tags(r) + if err != nil { + return nil, err + } + + return metadataDSF{id3}, nil +} + +type metadataDSF struct { + id3 Metadata +} + +func (m metadataDSF) Format() Format { + return m.id3.Format() +} + +func (m metadataDSF) FileType() FileType { + return DSF +} + +func (m metadataDSF) Title() string { + return m.id3.Title() +} + +func (m metadataDSF) Album() string { + return m.id3.Album() +} + +func (m metadataDSF) Artist() string { + return m.id3.Artist() +} + +func (m metadataDSF) AlbumArtist() string { + return m.id3.AlbumArtist() +} + +func (m metadataDSF) Composer() string { + return m.id3.Composer() +} + +func (m metadataDSF) Year() int { + return m.id3.Year() +} + +func (m metadataDSF) Genre() string { + return m.id3.Genre() +} + +func (m metadataDSF) Track() (int, int) { + return m.id3.Track() +} + +func (m metadataDSF) Disc() (int, int) { + return m.id3.Disc() +} + +func (m metadataDSF) Picture() *Picture { + return m.id3.Picture() +} + +func (m metadataDSF) Lyrics() string { + return m.id3.Lyrics() +} + +func (m metadataDSF) Comment() string { + return m.id3.Comment() +} + +func (m metadataDSF) Raw() map[string]interface{} { + return m.id3.Raw() +} diff --git a/id3v1.go b/id3v1.go index d333500..0953f0b 100644 --- a/id3v1.go +++ b/id3v1.go @@ -141,3 +141,4 @@ 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 "" } +func (m metadataID3v1) Comment() string { return m["comment"].(string) } diff --git a/id3v2metadata.go b/id3v2metadata.go index b5dfd05..6185963 100644 --- a/id3v2metadata.go +++ b/id3v2metadata.go @@ -43,6 +43,7 @@ var frames = frameNames(map[string][2]string{ "genre": [2]string{"TCO", "TCON"}, "picture": [2]string{"PIC", "APIC"}, "lyrics": [2]string{"", "USLT"}, + "comment": [2]string{"COM", "COMM"}, }) // metadataID3v2 is the implementation of Metadata used for ID3v2 tags. @@ -119,6 +120,18 @@ func (m metadataID3v2) Lyrics() string { return t.(*Comm).Text } +func (m metadataID3v2) Comment() string { + t, ok := m.frames[frames.Name("comment", m.Format())] + if !ok { + return "" + } + // id3v23 has Text, id3v24 has Description + if t.(*Comm).Description == "" { + return trimString(t.(*Comm).Text) + } + return trimString(t.(*Comm).Description) +} + 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 a6e3472..3970d91 100644 --- a/mp4.go +++ b/mp4.go @@ -344,6 +344,14 @@ func (m metadataMP4) Lyrics() string { return t.(string) } +func (m metadataMP4) Comment() string { + t, ok := m.data["\xa9cmt"] + if !ok { + return "" + } + return t.(string) +} + func (m metadataMP4) Picture() *Picture { v, ok := m.data["covr"] if !ok { diff --git a/tag.go b/tag.go index edffac1..306f1d7 100644 --- a/tag.go +++ b/tag.go @@ -50,6 +50,9 @@ func ReadFrom(r io.ReadSeeker) (Metadata, error) { case string(b[0:3]) == "ID3": return ReadID3v2Tags(r) + + case string(b[0:4]) == "DSD ": + return ReadDSFTags(r) } m, err := ReadID3v1Tags(r) @@ -91,6 +94,7 @@ const ( ALAC FileType = "ALAC" // Apple Lossless file FIXME: actually detect this FLAC FileType = "FLAC" // FLAC file OGG FileType = "OGG" // OGG file + DSF FileType = "DSF" // DSF file DSD Sony format see https://dsd-guide.com/sites/default/files/white-papers/DSFFileFormatSpec_E.pdf ) // Metadata is an interface which is used to describe metadata retrieved by this package. @@ -134,6 +138,9 @@ type Metadata interface { // Lyrics returns the lyrics, or an empty string if unavailable. Lyrics() string + // Comment returns the comment, or an empty string if unavailable. + Comment() 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_test.go b/tag_test.go index 0b79003..1e06012 100644 --- a/tag_test.go +++ b/tag_test.go @@ -39,15 +39,17 @@ var fullMetadata = testMetadata{ Track: 3, TrackTotal: 6, Year: 2000, + Comment: "Test Comment", } var mp3id3v11Metadata = testMetadata{ - Album: "Test Album", - Artist: "Test Artist", - Genre: "Jazz", - Lyrics: "", - Title: "Test Title", - Track: 3, - Year: 2000, + Album: "Test Album", + Artist: "Test Artist", + Genre: "Jazz", + Lyrics: "", + Title: "Test Title", + Track: 3, + Year: 2000, + Comment: "Test Comment", } type testData struct { @@ -116,6 +118,7 @@ func compareMetadata(t *testing.T, m Metadata, tt testData) { testValue(t, tt.Lyrics, m.Lyrics()) testValue(t, tt.Title, m.Title()) testValue(t, tt.Year, m.Year()) + testValue(t, tt.Comment, m.Comment()) disc, discTotal := m.Disc() testValue(t, tt.Disc, disc) diff --git a/testdata/with_tags/sample.dsf b/testdata/with_tags/sample.dsf new file mode 100644 index 0000000..8e8a603 Binary files /dev/null and b/testdata/with_tags/sample.dsf differ diff --git a/testdata/with_tags/sample.id3v11.mp3 b/testdata/with_tags/sample.id3v11.mp3 index cd85493..2c6aa4d 100644 Binary files a/testdata/with_tags/sample.id3v11.mp3 and b/testdata/with_tags/sample.id3v11.mp3 differ diff --git a/util.go b/util.go index ec28ac6..ff9c4f1 100644 --- a/util.go +++ b/util.go @@ -32,6 +32,15 @@ func getInt(b []byte) int { return n } +func getIntLittleEndian(b []byte) int { + var n int + for i := len(b) - 1; i >= 0; i-- { + n = n << 8 + n |= int(b[i]) + } + return n +} + func readBytes(r io.Reader, n int) ([]byte, error) { b := make([]byte, n) _, err := io.ReadFull(r, b) diff --git a/util_test.go b/util_test.go index aae0bcd..a122fce 100644 --- a/util_test.go +++ b/util_test.go @@ -79,3 +79,38 @@ func TestGetInt(t *testing.T) { } } } + +func TestGetIntLittleEndian(t *testing.T) { + tests := []struct { + input []byte + output int + }{ + { + []byte{}, + 0, + }, + { + []byte{0x01}, + 1, + }, + { + []byte{0xF1, 0xF2}, + 0xF2F1, + }, + { + []byte{0xF1, 0xF2, 0xF3}, + 0xF3F2F1, + }, + { + []byte{0xF1, 0xF2, 0xF3, 0xF4}, + 0xF4F3F2F1, + }, + } + + for ii, tt := range tests { + got := getIntLittleEndian(tt.input) + if got != tt.output { + t.Errorf("[%d] getInt(%v) = %v, expected %v", ii, tt.input, got, tt.output) + } + } +} diff --git a/vorbis.go b/vorbis.go index 07f8903..9f5ecb8 100644 --- a/vorbis.go +++ b/vorbis.go @@ -243,6 +243,13 @@ func (m *metadataVorbis) Lyrics() string { return m.c["lyrics"] } +func (m *metadataVorbis) Comment() string { + if m.c["comment"] != "" { + return m.c["comment"] + } + return m.c["description"] +} + func (m *metadataVorbis) Picture() *Picture { return m.p }