mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-08-01 02:11:15 -04:00
fix(thumbnails): pick audio cover art deterministically
Switch to the dschmidt/tag fork and select the front cover (picture type 0x03) when tagged, else the first available picture, instead of the lib's single non-deterministic Picture().
This commit is contained in:
2
go.mod
2
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
|
||||
|
||||
4
go.sum
4
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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
4
vendor/github.com/dhowden/tag/dsf.go
generated
vendored
4
vendor/github.com/dhowden/tag/dsf.go
generated
vendored
@@ -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()
|
||||
}
|
||||
|
||||
1
vendor/github.com/dhowden/tag/id3v1.go
generated
vendored
1
vendor/github.com/dhowden/tag/id3v1.go
generated
vendored
@@ -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) }
|
||||
|
||||
3
vendor/github.com/dhowden/tag/id3v2frames.go
generated
vendored
3
vendor/github.com/dhowden/tag/id3v2frames.go
generated
vendored
@@ -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
|
||||
|
||||
25
vendor/github.com/dhowden/tag/id3v2metadata.go
generated
vendored
25
vendor/github.com/dhowden/tag/id3v2metadata.go
generated
vendored
@@ -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
|
||||
}
|
||||
|
||||
10
vendor/github.com/dhowden/tag/mp4.go
generated
vendored
10
vendor/github.com/dhowden/tag/mp4.go
generated
vendored
@@ -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}
|
||||
}
|
||||
|
||||
3
vendor/github.com/dhowden/tag/tag.go
generated
vendored
3
vendor/github.com/dhowden/tag/tag.go
generated
vendored
@@ -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
|
||||
|
||||
|
||||
21
vendor/github.com/dhowden/tag/vorbis.go
generated
vendored
21
vendor/github.com/dhowden/tag/vorbis.go
generated
vendored
@@ -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
|
||||
}
|
||||
|
||||
3
vendor/modules.txt
vendored
3
vendor/modules.txt
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user