diff --git a/go.mod b/go.mod index 76ad786315..b498ce187f 100644 --- a/go.mod +++ b/go.mod @@ -412,3 +412,5 @@ replace go-micro.dev/v4 => github.com/butonic/go-micro/v4 v4.11.1-0.202411151126 exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible replace github.com/go-micro/plugins/v4/store/nats-js-kv => github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a + +replace github.com/dhowden/tag => github.com/dschmidt/tag v0.0.0-20260730135516-8eb6fc4a6973 diff --git a/go.sum b/go.sum index b19a184304..0fef85f7b3 100644 --- a/go.sum +++ b/go.sum @@ -297,8 +297,6 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= -github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg= -github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= @@ -310,6 +308,8 @@ github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pM github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dschmidt/tag v0.0.0-20260730135516-8eb6fc4a6973 h1:XnAwTTbblPJI6OVkRwqVA2MGjre29B/+wsi16lQfJ+E= +github.com/dschmidt/tag v0.0.0-20260730135516-8eb6fc4a6973/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e h1:rcHHSQqzCgvlwP0I/fQ8rQMn/MpHE5gWSLdtpxtP6KQ= diff --git a/services/thumbnails/pkg/preprocessor/preprocessor.go b/services/thumbnails/pkg/preprocessor/preprocessor.go index 4c46f3fbec..7b93c3f3f0 100644 --- a/services/thumbnails/pkg/preprocessor/preprocessor.go +++ b/services/thumbnails/pkg/preprocessor/preprocessor.go @@ -89,7 +89,7 @@ func (i AudioDecoder) Convert(r io.Reader) (any, error) { return nil, err } - picture := m.Picture() + picture := selectCoverArt(m.Pictures()) if picture == nil { return nil, thumbnailerErrors.ErrNoImageFromAudioFile } @@ -102,6 +102,27 @@ func (i AudioDecoder) Convert(r io.Reader) (any, error) { return converter.Convert(bytes.NewReader(picture.Data)) } +// frontCoverType is the ID3/APIC picture type byte for a front cover, shared by +// the ID3v2, FLAC and OGG tag formats (see github.com/dhowden/tag pictureTypes). +const frontCoverType byte = 0x03 + +// selectCoverArt deterministically picks the cover art from an audio file's +// embedded pictures: the explicitly tagged front cover if present, otherwise +// the first available picture. Many taggers store the cover as type "Other" +// (0x00) instead of "Cover (front)" (0x03), so the fallback is the common case. +// It returns nil when there are no pictures. +func selectCoverArt(pictures []tag.Picture) *tag.Picture { + if len(pictures) == 0 { + return nil + } + for i := range pictures { + if pictures[i].RawType == frontCoverType { + return &pictures[i] + } + } + return &pictures[0] +} + // TxtToImageConverter is a converter for the text file type TxtToImageConverter struct { fontLoader *FontLoader diff --git a/services/thumbnails/pkg/preprocessor/preprocessor_test.go b/services/thumbnails/pkg/preprocessor/preprocessor_test.go index a76a0512c0..5ef3a9c548 100644 --- a/services/thumbnails/pkg/preprocessor/preprocessor_test.go +++ b/services/thumbnails/pkg/preprocessor/preprocessor_test.go @@ -6,6 +6,7 @@ import ( "os" "testing" + "github.com/dhowden/tag" "golang.org/x/image/font" "golang.org/x/image/font/opentype" @@ -127,6 +128,28 @@ var _ = Describe("ImageDecoder", func() { }) }) + Describe("selectCoverArt", func() { + front := tag.Picture{RawType: frontCoverType, MIMEType: "image/jpeg", Data: []byte("front")} + back := tag.Picture{RawType: 0x04, MIMEType: "image/jpeg", Data: []byte("back")} + other := tag.Picture{RawType: 0x00, MIMEType: "image/png", Data: []byte("other")} + + It("returns nil when there are no pictures", func() { + Expect(selectCoverArt(nil)).To(BeNil()) + Expect(selectCoverArt([]tag.Picture{})).To(BeNil()) + }) + It("prefers the front cover regardless of order", func() { + got := selectCoverArt([]tag.Picture{back, other, front}) + Expect(got).ToNot(BeNil()) + Expect(got.RawType).To(Equal(frontCoverType)) + Expect(got.Data).To(Equal([]byte("front"))) + }) + It("falls back to the first picture when none is a front cover", func() { + got := selectCoverArt([]tag.Picture{other, back}) + Expect(got).ToNot(BeNil()) + Expect(got.Data).To(Equal([]byte("other"))) + }) + }) + Describe("should decode text", func() { var decoder TxtToImageConverter BeforeEach(func() { diff --git a/vendor/github.com/dhowden/tag/dsf.go b/vendor/github.com/dhowden/tag/dsf.go index 8ca34f21da..4782356eb0 100644 --- a/vendor/github.com/dhowden/tag/dsf.go +++ b/vendor/github.com/dhowden/tag/dsf.go @@ -96,6 +96,10 @@ func (m metadataDSF) Picture() *Picture { return m.id3.Picture() } +func (m metadataDSF) Pictures() []Picture { + return m.id3.Pictures() +} + func (m metadataDSF) Lyrics() string { return m.id3.Lyrics() } diff --git a/vendor/github.com/dhowden/tag/id3v1.go b/vendor/github.com/dhowden/tag/id3v1.go index 0953f0beed..56ef454b00 100644 --- a/vendor/github.com/dhowden/tag/id3v1.go +++ b/vendor/github.com/dhowden/tag/id3v1.go @@ -140,5 +140,6 @@ 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) Pictures() []Picture { return nil } func (m metadataID3v1) Lyrics() string { return "" } func (m metadataID3v1) Comment() string { return m["comment"].(string) } diff --git a/vendor/github.com/dhowden/tag/id3v2frames.go b/vendor/github.com/dhowden/tag/id3v2frames.go index 8397eba8fc..5338363f82 100644 --- a/vendor/github.com/dhowden/tag/id3v2frames.go +++ b/vendor/github.com/dhowden/tag/id3v2frames.go @@ -546,6 +546,7 @@ type Picture struct { Ext string // Extension of the picture file. MIMEType string // MIMEType of the picture. Type string // Type of the picture (see pictureTypes). + RawType byte // Raw picture type byte (see pictureTypes); 0x03 is the front cover. Description string // Description. Data []byte // Raw picture data. } @@ -596,6 +597,7 @@ func readPICFrame(b []byte) (*Picture, error) { Ext: ext, MIMEType: mimeType, Type: pictureTypes[picType], + RawType: picType, Description: desc, Data: descDataSplit[1], }, nil @@ -649,6 +651,7 @@ func readAPICFrame(b []byte) (*Picture, error) { Ext: ext, MIMEType: mimeType, Type: pictureTypes[picType], + RawType: picType, Description: desc, Data: descDataSplit[1], }, nil diff --git a/vendor/github.com/dhowden/tag/id3v2metadata.go b/vendor/github.com/dhowden/tag/id3v2metadata.go index 4279f64c18..26a47c78c7 100644 --- a/vendor/github.com/dhowden/tag/id3v2metadata.go +++ b/vendor/github.com/dhowden/tag/id3v2metadata.go @@ -150,3 +150,28 @@ func (m metadataID3v2) Picture() *Picture { } return v.(*Picture) } + +// Pictures returns all attached pictures. Duplicate picture frames are stored +// under suffixed keys ("APIC", "APIC_0", "APIC_1", ...) by readFrames, so we +// walk that sequence to return them in file order. +func (m metadataID3v2) Pictures() []Picture { + base := frames.Name("picture", m.Format()) + if base == "" { + return nil + } + var pics []Picture + for i := -1; ; i++ { + key := base + if i >= 0 { + key = base + "_" + strconv.Itoa(i) + } + v, ok := m.frames[key] + if !ok { + break + } + if p, ok := v.(*Picture); ok { + pics = append(pics, *p) + } + } + return pics +} diff --git a/vendor/github.com/dhowden/tag/mp4.go b/vendor/github.com/dhowden/tag/mp4.go index 19c1a8107a..4662bf493e 100644 --- a/vendor/github.com/dhowden/tag/mp4.go +++ b/vendor/github.com/dhowden/tag/mp4.go @@ -377,3 +377,13 @@ func (m metadataMP4) Picture() *Picture { p, _ := v.(*Picture) return p } + +// Pictures returns the attached cover art. The MP4 "covr" atom carries no +// picture type, so at most a single untyped picture is returned. +func (m metadataMP4) Pictures() []Picture { + p := m.Picture() + if p == nil { + return nil + } + return []Picture{*p} +} diff --git a/vendor/github.com/dhowden/tag/tag.go b/vendor/github.com/dhowden/tag/tag.go index 306f1d7df1..1454d72fa5 100644 --- a/vendor/github.com/dhowden/tag/tag.go +++ b/vendor/github.com/dhowden/tag/tag.go @@ -135,6 +135,9 @@ type Metadata interface { // Picture returns a picture, or nil if not available. Picture() *Picture + // Pictures returns all attached pictures, or nil if none are available. + Pictures() []Picture + // Lyrics returns the lyrics, or an empty string if unavailable. Lyrics() string diff --git a/vendor/github.com/dhowden/tag/vorbis.go b/vendor/github.com/dhowden/tag/vorbis.go index f1c6a198b6..f08a8bcebc 100644 --- a/vendor/github.com/dhowden/tag/vorbis.go +++ b/vendor/github.com/dhowden/tag/vorbis.go @@ -22,8 +22,9 @@ func newMetadataVorbis() *metadataVorbis { } type metadataVorbis struct { - c map[string]string // the vorbis comments - p *Picture + c map[string]string // the vorbis comments + p *Picture + ps []*Picture } func (m *metadataVorbis) readVorbisComment(r io.Reader) error { @@ -135,13 +136,16 @@ func (m *metadataVorbis) readPictureBlock(r io.Reader) error { return err } - m.p = &Picture{ + pic := &Picture{ Ext: ext, MIMEType: mime, Type: pictureType, + RawType: byte(b), Description: desc, Data: data, } + m.p = pic + m.ps = append(m.ps, pic) return nil } @@ -261,6 +265,17 @@ func (m *metadataVorbis) Comment() string { return m.c["description"] } +func (m *metadataVorbis) Pictures() []Picture { + if len(m.ps) == 0 { + return nil + } + pics := make([]Picture, len(m.ps)) + for i, p := range m.ps { + pics[i] = *p + } + return pics +} + func (m *metadataVorbis) Picture() *Picture { return m.p } diff --git a/vendor/modules.txt b/vendor/modules.txt index 54d40e0986..1601e0894d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -373,7 +373,7 @@ github.com/dgraph-io/ristretto/z/simd # github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f ## explicit github.com/dgryski/go-rendezvous -# github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 +# github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 => github.com/dschmidt/tag v0.0.0-20260730135516-8eb6fc4a6973 ## explicit; go 1.20 github.com/dhowden/tag # github.com/distribution/reference v0.6.0 @@ -2738,3 +2738,4 @@ stash.kopano.io/kgol/rndm # github.com/unrolled/secure => github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 # go-micro.dev/v4 => github.com/butonic/go-micro/v4 v4.11.1-0.20241115112658-b5d4de5ed9b3 # github.com/go-micro/plugins/v4/store/nats-js-kv => github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a +# github.com/dhowden/tag => github.com/dschmidt/tag v0.0.0-20260730135516-8eb6fc4a6973