Load artwork from embedded
This commit is contained in:
+68
-1
@@ -1,12 +1,17 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
_ "image/gif"
|
_ "image/gif"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/dhowden/tag"
|
||||||
"github.com/navidrome/navidrome/consts"
|
"github.com/navidrome/navidrome/consts"
|
||||||
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
"github.com/navidrome/navidrome/resources"
|
"github.com/navidrome/navidrome/resources"
|
||||||
_ "golang.org/x/image/webp"
|
_ "golang.org/x/image/webp"
|
||||||
@@ -25,5 +30,67 @@ type artwork struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *artwork) Get(ctx context.Context, id string, size int) (io.ReadCloser, error) {
|
func (a *artwork) Get(ctx context.Context, id string, size int) (io.ReadCloser, error) {
|
||||||
return resources.FS().Open(consts.PlaceholderAlbumArt)
|
r, _, err := a.get(ctx, id, size)
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *artwork) get(ctx context.Context, id string, size int) (io.ReadCloser, string, error) {
|
||||||
|
artId, err := model.ParseArtworkID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", errors.New("invalid ID")
|
||||||
|
}
|
||||||
|
id = artId.ID
|
||||||
|
al, err := a.ds.Album(ctx).Get(id)
|
||||||
|
if errors.Is(err, model.ErrNotFound) {
|
||||||
|
r, path := fromPlaceholder()()
|
||||||
|
return r, path, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
r, path := extractImage(ctx, artId,
|
||||||
|
fromTag(al.EmbedArtPath),
|
||||||
|
fromPlaceholder(),
|
||||||
|
)
|
||||||
|
return r, path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractImage(ctx context.Context, artId model.ArtworkID, extractFuncs ...func() (io.ReadCloser, string)) (io.ReadCloser, string) {
|
||||||
|
for _, f := range extractFuncs {
|
||||||
|
r, path := f()
|
||||||
|
if r != nil {
|
||||||
|
log.Trace(ctx, "Found artwork", "artId", artId, "path", path)
|
||||||
|
return r, path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Error(ctx, "extractImage should never reach this point!", "artId", artId, "path")
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromTag(path string) func() (io.ReadCloser, string) {
|
||||||
|
return func() (io.ReadCloser, string) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
m, err := tag.ReadFrom(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
picture := m.Picture()
|
||||||
|
if picture == nil {
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
return io.NopCloser(bytes.NewReader(picture.Data)), path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromPlaceholder() func() (io.ReadCloser, string) {
|
||||||
|
return func() (io.ReadCloser, string) {
|
||||||
|
r, _ := resources.FS().Open(consts.PlaceholderAlbumArt)
|
||||||
|
return r, consts.PlaceholderAlbumArt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/consts"
|
||||||
|
"github.com/navidrome/navidrome/log"
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/tests"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = FDescribe("Artwork", func() {
|
||||||
|
var aw *artwork
|
||||||
|
var ds model.DataStore
|
||||||
|
ctx := log.NewContext(context.TODO())
|
||||||
|
var alOnlyEmbed, alEmbedNotFound model.Album
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
ds = &tests.MockDataStore{MockedTranscoding: &tests.MockTranscodingRepo{}}
|
||||||
|
alOnlyEmbed = model.Album{ID: "222", Name: "Only embed", EmbedArtPath: "tests/fixtures/test.mp3"}
|
||||||
|
alEmbedNotFound = model.Album{ID: "333", Name: "Embed not found", EmbedArtPath: "tests/fixtures/NON_EXISTENT.mp3"}
|
||||||
|
// {ID: "666", Name: "All options", EmbedArtPath: "tests/fixtures/test.mp3",
|
||||||
|
// ImageFiles: "tests/fixtures/cover.jpg:tests/fixtures/front.png"},
|
||||||
|
//})
|
||||||
|
aw = NewArtwork(ds).(*artwork)
|
||||||
|
})
|
||||||
|
|
||||||
|
When("cover art is not found", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
ds.Album(ctx).(*tests.MockAlbumRepo).SetData(model.Albums{
|
||||||
|
alOnlyEmbed,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
It("returns placeholder if album is not in the DB", func() {
|
||||||
|
_, path, err := aw.get(context.Background(), "al-999-0", 0)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(path).To(Equal(consts.PlaceholderAlbumArt))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
When("album has only embed images", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
ds.Album(ctx).(*tests.MockAlbumRepo).SetData(model.Albums{
|
||||||
|
alOnlyEmbed,
|
||||||
|
alEmbedNotFound,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
It("returns embed cover", func() {
|
||||||
|
_, path, err := aw.get(context.Background(), alOnlyEmbed.CoverArtID().String(), 0)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(path).To(Equal("tests/fixtures/test.mp3"))
|
||||||
|
})
|
||||||
|
It("returns placeholder if embed path is not available", func() {
|
||||||
|
_, path, err := aw.get(context.Background(), alEmbedNotFound.CoverArtID().String(), 0)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(path).To(Equal(consts.PlaceholderAlbumArt))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/log"
|
|
||||||
"github.com/navidrome/navidrome/model"
|
|
||||||
"github.com/navidrome/navidrome/tests"
|
|
||||||
. "github.com/onsi/ginkgo/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ = Describe("Artwork", func() {
|
|
||||||
var ds model.DataStore
|
|
||||||
ctx := log.NewContext(context.TODO())
|
|
||||||
|
|
||||||
BeforeEach(func() {
|
|
||||||
ds = &tests.MockDataStore{MockedTranscoding: &tests.MockTranscodingRepo{}}
|
|
||||||
ds.Album(ctx).(*tests.MockAlbumRepo).SetData(model.Albums{
|
|
||||||
{ID: "222", EmbedArtPath: "tests/fixtures/test.mp3"},
|
|
||||||
{ID: "333"},
|
|
||||||
{ID: "444", EmbedArtPath: "tests/fixtures/cover.jpg"},
|
|
||||||
})
|
|
||||||
ds.MediaFile(ctx).(*tests.MockMediaFileRepo).SetData(model.MediaFiles{
|
|
||||||
{ID: "123", AlbumID: "222", Path: "tests/fixtures/test.mp3", HasCoverArt: true},
|
|
||||||
{ID: "456", AlbumID: "222", Path: "tests/fixtures/test.ogg", HasCoverArt: false},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
+5
-1
@@ -22,7 +22,11 @@ type ArtworkID struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (id ArtworkID) String() string {
|
func (id ArtworkID) String() string {
|
||||||
return fmt.Sprintf("%s-%s-%x", id.Kind.prefix, id.ID, id.LastAccess.Unix())
|
s := fmt.Sprintf("%s-%s", id.Kind.prefix, id.ID)
|
||||||
|
if id.LastAccess.Unix() < 0 {
|
||||||
|
return s + "-0"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s-%x", s, id.LastAccess.Unix())
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseArtworkID(id string) (ArtworkID, error) {
|
func ParseArtworkID(id string) (ArtworkID, error) {
|
||||||
|
|||||||
+2
-2
@@ -69,11 +69,11 @@ func (mf MediaFile) ContentType() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (mf MediaFile) CoverArtID() ArtworkID {
|
func (mf MediaFile) CoverArtID() ArtworkID {
|
||||||
// If it is a mediaFile, and it has cover art, return it (if feature is disabled, skip)
|
// If it has a cover art, return it (if feature is disabled, skip)
|
||||||
if mf.HasCoverArt && !conf.Server.DevFastAccessCoverArt {
|
if mf.HasCoverArt && !conf.Server.DevFastAccessCoverArt {
|
||||||
return artworkIDFromMediaFile(mf)
|
return artworkIDFromMediaFile(mf)
|
||||||
}
|
}
|
||||||
// if the mediaFile does not have a coverArt, fallback to the album cover
|
// if it does not have a coverArt, fallback to the album cover
|
||||||
return artworkIDFromAlbum(Album{ID: mf.AlbumID, UpdatedAt: mf.UpdatedAt})
|
return artworkIDFromAlbum(Album{ID: mf.AlbumID, UpdatedAt: mf.UpdatedAt})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
@@ -37,7 +37,7 @@ const mapToAudioLists = (item) => {
|
|||||||
musicSrc: subsonic.streamUrl(trackId),
|
musicSrc: subsonic.streamUrl(trackId),
|
||||||
cover: subsonic.getCoverArtUrl(
|
cover: subsonic.getCoverArtUrl(
|
||||||
{
|
{
|
||||||
coverArtId: config.devFastAccessCoverArt ? item.albumId : trackId,
|
id: config.devFastAccessCoverArt ? item.albumId : trackId,
|
||||||
updatedAt: item.updatedAt,
|
updatedAt: item.updatedAt,
|
||||||
},
|
},
|
||||||
300
|
300
|
||||||
|
|||||||
@@ -51,10 +51,14 @@ const getCoverArtUrl = (record, size) => {
|
|||||||
...(size && { size }),
|
...(size && { size }),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (record.coverArtId) {
|
const lastUpdate = Math.floor(Date.parse(record.updatedAt) / 1000).toString(
|
||||||
return baseUrl(url('getCoverArt', record.coverArtId, options))
|
16
|
||||||
|
)
|
||||||
|
const id = record.id + '-' + lastUpdate
|
||||||
|
if (record.album) {
|
||||||
|
return baseUrl(url('getCoverArt', 'mf-' + id, options))
|
||||||
} else {
|
} else {
|
||||||
return baseUrl(url('getCoverArt', 'not_found', size && { size }))
|
return baseUrl(url('getCoverArt', 'al-' + id, options))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user