mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 02:17:25 +02:00
feat(archiver): add folder cover image to downloaded zips (#6224)
* feat(archiver): add folder cover image to downloaded zips Album, artist, playlist and share downloads now include the item's cover as folder.<ext>, the image most players and car stereos show for the files next to it. This helps users who copy transcoded downloads to offline devices, since transcoding drops the embedded artwork (#5841). Album folders get the album cover, the artist zip root gets the artist image, and playlist and share zips get the playlist or shared item's cover at the root. The image is the same one getCoverArt serves, resized to 500px (not square), and named by its detected type. Items without artwork get no image, and a cover that fails to load is logged and skipped so it never breaks the archive. The archiver reads covers through a new core.CoverArtReader interface, implemented by artwork.CoverArtReader, to avoid an import cycle between core and core/artwork. * refactor(archiver): read covers through artwork.Artwork directly The archiver no longer needs a local CoverArtReader interface and adapter. The only reason core/artwork imported core was a core.AbsolutePath call in loadArtistFolder, which now reads the library path from the repository and cleans it the same way. With the import cycle gone, the archiver takes artwork.Artwork and treats ErrUnavailable and ErrNotFound as no cover. Covers are now written after each album's tracks (and after all tracks for the archive root), so a slow artwork lookup does not delay the first bytes of the download. * fix(archiver): read share covers as admin so private playlists keep theirs Public share downloads run with an anonymous context, and the playlist repository hides private playlists from anonymous users, so a zip of a shared private playlist silently had no folder image. Like the public image handler, the share itself is the authorization: the cover lookup now runs with an admin user. Only the cover read is elevated; streaming keeps the anonymous context, so the transcode limiter still keys public downloads the same way. * style(archiver): trim comments Shorten the comments added by the folder cover image change and drop the ones the names already explain. * fix(archiver): add one cover per album folder in artist zips Albums with the same name share a zip folder (pre-existing naming), so an artist zip with two such albums wrote two folder.<ext> entries at the same path. Keep the first cover and skip the rest for that folder.
This commit is contained in:
parent
659d067aba
commit
83fff44c82
5 changed files with 270 additions and 29 deletions
|
|
@ -95,7 +95,7 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
|
|||
transcodingCache := stream.GetTranscodingCache()
|
||||
mediaStreamer := stream.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
||||
share := core.NewShare(dataStore)
|
||||
archiver := core.NewArchiver(mediaStreamer, dataStore, share)
|
||||
archiver := core.NewArchiver(mediaStreamer, dataStore, share, artworkArtwork)
|
||||
players := core.NewPlayers(dataStore)
|
||||
broker := events.GetBroker()
|
||||
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
|
||||
|
|
@ -152,7 +152,7 @@ func CreatePublicRouter() *public.Router {
|
|||
transcodingCache := stream.GetTranscodingCache()
|
||||
mediaStreamer := stream.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
||||
share := core.NewShare(dataStore)
|
||||
archiver := core.NewArchiver(mediaStreamer, dataStore, share)
|
||||
archiver := core.NewArchiver(mediaStreamer, dataStore, share, artworkArtwork)
|
||||
router := public.New(dataStore, artworkArtwork, mediaStreamer, share, archiver)
|
||||
return router
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,20 +7,27 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/core/stream"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/persistence"
|
||||
"github.com/navidrome/navidrome/utils/slice"
|
||||
"github.com/navidrome/navidrome/utils/str"
|
||||
)
|
||||
|
||||
const archiveCoverArtSize = 500
|
||||
|
||||
type Archiver interface {
|
||||
ZipAlbum(ctx context.Context, id string, format string, bitrate int, w io.Writer) error
|
||||
ZipArtist(ctx context.Context, id string, format string, bitrate int, w io.Writer) error
|
||||
|
|
@ -28,18 +35,19 @@ type Archiver interface {
|
|||
ZipPlaylist(ctx context.Context, id string, format string, bitrate int, w io.Writer) error
|
||||
}
|
||||
|
||||
func NewArchiver(ms stream.MediaStreamer, ds model.DataStore, shares Share) Archiver {
|
||||
return &archiver{ds: ds, ms: ms, shares: shares}
|
||||
func NewArchiver(ms stream.MediaStreamer, ds model.DataStore, shares Share, artwork artwork.Artwork) Archiver {
|
||||
return &archiver{ds: ds, ms: ms, shares: shares, artwork: artwork}
|
||||
}
|
||||
|
||||
type archiver struct {
|
||||
ds model.DataStore
|
||||
ms stream.MediaStreamer
|
||||
shares Share
|
||||
ds model.DataStore
|
||||
ms stream.MediaStreamer
|
||||
shares Share
|
||||
artwork artwork.Artwork
|
||||
}
|
||||
|
||||
func (a *archiver) ZipAlbum(ctx context.Context, id string, format string, bitrate int, out io.Writer) error {
|
||||
return a.zipAlbums(ctx, id, format, bitrate, out, squirrel.Eq{"album_id": id})
|
||||
return a.zipAlbums(ctx, id, format, bitrate, out, squirrel.Eq{"album_id": id}, model.ArtworkID{})
|
||||
}
|
||||
|
||||
func (a *archiver) ZipArtist(ctx context.Context, id string, format string, bitrate int, out io.Writer) error {
|
||||
|
|
@ -49,10 +57,11 @@ func (a *archiver) ZipArtist(ctx context.Context, id string, format string, bitr
|
|||
persistence.ParticipantIDFilter("media_file", id, model.RoleAlbumArtist),
|
||||
squirrel.Eq{"missing": false},
|
||||
}
|
||||
return a.zipAlbums(ctx, id, format, bitrate, out, filter)
|
||||
return a.zipAlbums(ctx, id, format, bitrate, out, filter, model.Artist{ID: id}.CoverArtID())
|
||||
}
|
||||
|
||||
func (a *archiver) zipAlbums(ctx context.Context, id string, format string, bitrate int, out io.Writer, filters squirrel.Sqlizer) error {
|
||||
// rootArt, when set, is added to the archive root.
|
||||
func (a *archiver) zipAlbums(ctx context.Context, id string, format string, bitrate int, out io.Writer, filters squirrel.Sqlizer, rootArt model.ArtworkID) error {
|
||||
mfs, err := a.ds.MediaFile(ctx).GetAll(model.QueryOptions{Filters: filters, Sort: "album"})
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error loading mediafiles from artist", "id", id, err)
|
||||
|
|
@ -80,7 +89,10 @@ func (a *archiver) zipAlbums(ctx context.Context, id string, format string, bitr
|
|||
return addErr
|
||||
}
|
||||
}
|
||||
// After the tracks, so a slow artwork lookup doesn't delay the first bytes.
|
||||
a.addCoverArtToZip(ctx, z, album[0].AlbumCoverArtID(), folder)
|
||||
}
|
||||
a.addCoverArtToZip(ctx, z, rootArt, "")
|
||||
err = z.Close()
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error closing zip file", "id", id, err)
|
||||
|
|
@ -170,7 +182,10 @@ func (a *archiver) ZipShare(ctx context.Context, s *model.Share, out io.Writer)
|
|||
return model.ErrNotAuthorized
|
||||
}
|
||||
log.Debug(ctx, "Zipping share", "name", s.ID, "format", s.Format, "bitrate", s.MaxBitRate, "numTracks", len(s.Tracks))
|
||||
return a.zipMediaFiles(ctx, s.ID, s.ID, s.Format, s.MaxBitRate, out, s.Tracks, false)
|
||||
// The share is the authorization (as in the public image handler): an anonymous lookup would
|
||||
// hide a private playlist. Only the cover read is elevated.
|
||||
coverCtx := request.WithUser(ctx, model.User{IsAdmin: true})
|
||||
return a.zipMediaFiles(ctx, s.ID, s.ID, s.Format, s.MaxBitRate, out, s.Tracks, coverCtx, s.CoverArtID(), false)
|
||||
}
|
||||
|
||||
func (a *archiver) ZipPlaylist(ctx context.Context, id string, format string, bitrate int, out io.Writer) error {
|
||||
|
|
@ -181,10 +196,10 @@ func (a *archiver) ZipPlaylist(ctx context.Context, id string, format string, bi
|
|||
}
|
||||
mfs := pls.MediaFiles()
|
||||
log.Debug(ctx, "Zipping playlist", "name", pls.Name, "format", format, "bitrate", bitrate, "numTracks", len(mfs))
|
||||
return a.zipMediaFiles(ctx, id, pls.Name, format, bitrate, out, mfs, true)
|
||||
return a.zipMediaFiles(ctx, id, pls.Name, format, bitrate, out, mfs, ctx, pls.CoverArtID(), true)
|
||||
}
|
||||
|
||||
func (a *archiver) zipMediaFiles(ctx context.Context, id, name string, format string, bitrate int, out io.Writer, mfs model.MediaFiles, addM3U bool) error {
|
||||
func (a *archiver) zipMediaFiles(ctx context.Context, id, name string, format string, bitrate int, out io.Writer, mfs model.MediaFiles, coverCtx context.Context, coverArt model.ArtworkID, addM3U bool) error {
|
||||
z := createZipWriter(out, format, bitrate)
|
||||
|
||||
zippedMfs := make(model.MediaFiles, len(mfs))
|
||||
|
|
@ -199,6 +214,7 @@ func (a *archiver) zipMediaFiles(ctx context.Context, id, name string, format st
|
|||
mf.Path = file
|
||||
zippedMfs[idx] = mf
|
||||
}
|
||||
a.addCoverArtToZip(coverCtx, z, coverArt, "")
|
||||
|
||||
// Add M3U file if requested
|
||||
if addM3U && len(zippedMfs) > 0 {
|
||||
|
|
@ -276,3 +292,61 @@ func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.Med
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addCoverArtToZip adds the cover as dir/folder.<ext>. Errors are logged, never returned.
|
||||
func (a *archiver) addCoverArtToZip(ctx context.Context, z *zip.Writer, artID model.ArtworkID, dir string) {
|
||||
if artID.ID == "" {
|
||||
return
|
||||
}
|
||||
// Buffered so a failed read leaves no empty entry.
|
||||
data, err := a.readCoverArt(ctx, artID)
|
||||
if errors.Is(err, artwork.ErrUnavailable) || errors.Is(err, model.ErrNotFound) {
|
||||
log.Debug(ctx, "No cover art to add to zip", "artID", artID)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Error reading cover art for zipping", "artID", artID, err)
|
||||
return
|
||||
}
|
||||
ext := coverArtExtension(data)
|
||||
if ext == "" {
|
||||
log.Warn(ctx, "Unknown cover art image type, not adding it to zip", "artID", artID)
|
||||
return
|
||||
}
|
||||
w, err := z.CreateHeader(&zip.FileHeader{
|
||||
Name: path.Join(dir, "folder."+ext),
|
||||
Modified: time.Now(),
|
||||
Method: zip.Store,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Error creating cover art zip entry", "artID", artID, err)
|
||||
return
|
||||
}
|
||||
if _, err = w.Write(data); err != nil {
|
||||
log.Warn(ctx, "Error zipping cover art", "artID", artID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *archiver) readCoverArt(ctx context.Context, artID model.ArtworkID) ([]byte, error) {
|
||||
img, err := a.artwork.Get(ctx, artID, archiveCoverArtSize, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer img.Close()
|
||||
return io.ReadAll(img)
|
||||
}
|
||||
|
||||
// Resizing may re-encode the image, so the type comes from its bytes.
|
||||
func coverArtExtension(data []byte) string {
|
||||
switch http.DetectContentType(data) {
|
||||
case "image/jpeg":
|
||||
return "jpg"
|
||||
case "image/png":
|
||||
return "png"
|
||||
case "image/webp":
|
||||
return "webp"
|
||||
case "image/gif":
|
||||
return "gif"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
|
|
@ -11,8 +12,10 @@ import (
|
|||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/core/stream"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/persistence"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
|
@ -25,13 +28,15 @@ var _ = Describe("Archiver", func() {
|
|||
ms *mockMediaStreamer
|
||||
ds *mockDataStore
|
||||
sh *mockShare
|
||||
ca *mockCoverArt
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ms = &mockMediaStreamer{}
|
||||
sh = &mockShare{}
|
||||
ds = &mockDataStore{}
|
||||
arch = core.NewArchiver(ms, ds, sh)
|
||||
ca = &mockCoverArt{images: map[string][]byte{}}
|
||||
arch = core.NewArchiver(ms, ds, sh, ca)
|
||||
})
|
||||
|
||||
Context("ZipAlbum", func() {
|
||||
|
|
@ -332,8 +337,179 @@ var _ = Describe("Archiver", func() {
|
|||
Expect(string(m3uContent)).To(Equal(expectedM3U))
|
||||
})
|
||||
})
|
||||
Context("cover art", func() {
|
||||
var (
|
||||
jpegData = []byte("\xff\xd8\xff\xe0 fake jpeg")
|
||||
pngData = []byte("\x89PNG\x0d\x0a\x1a\x0a fake png")
|
||||
)
|
||||
|
||||
mockAlbumTracks := func(filter squirrel.Sqlizer, mfs model.MediaFiles) {
|
||||
mfRepo := &mockMediaFileRepository{}
|
||||
mfRepo.On("GetAll", []model.QueryOptions{{Filters: filter, Sort: "album"}}).Return(mfs, nil)
|
||||
ds.On("MediaFile", mock.Anything).Return(mfRepo)
|
||||
ms.On("NewStream", mock.Anything, mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("test")), nil)
|
||||
}
|
||||
|
||||
It("adds the album cover to the album folder", func() {
|
||||
ca.images["al-1"] = jpegData
|
||||
mockAlbumTracks(squirrel.Eq{"album_id": "1"}, model.MediaFiles{
|
||||
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album/Promo", DiscNumber: 1},
|
||||
})
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
Expect(arch.ZipAlbum(context.Background(), "1", "mp3", 128, out)).To(Succeed())
|
||||
|
||||
files := readZip(out)
|
||||
Expect(files).To(HaveLen(2))
|
||||
Expect(files).To(HaveKeyWithValue("Album_Promo/folder.jpg", jpegData))
|
||||
Expect(ca.requests).To(ConsistOf(coverRequest{id: "al-1", size: 500, square: false}))
|
||||
})
|
||||
|
||||
It("adds the artist image to the root and each album cover to its folder", func() {
|
||||
ca.images["ar-1"] = pngData
|
||||
ca.images["al-1"] = jpegData
|
||||
ca.images["al-2"] = jpegData
|
||||
mockAlbumTracks(squirrel.And{
|
||||
persistence.ParticipantIDFilter("media_file", "1", model.RoleAlbumArtist),
|
||||
squirrel.Eq{"missing": false},
|
||||
}, model.MediaFiles{
|
||||
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album 1", DiscNumber: 1},
|
||||
{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumID: "2", Album: "Album 2", DiscNumber: 1},
|
||||
})
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
Expect(arch.ZipArtist(context.Background(), "1", "mp3", 128, out)).To(Succeed())
|
||||
|
||||
files := readZip(out)
|
||||
Expect(files).To(HaveLen(5))
|
||||
Expect(files).To(HaveKeyWithValue("folder.png", pngData))
|
||||
Expect(files).To(HaveKeyWithValue("Album 1/folder.jpg", jpegData))
|
||||
Expect(files).To(HaveKeyWithValue("Album 2/folder.jpg", jpegData))
|
||||
})
|
||||
|
||||
It("puts each same-named album's cover in that album's own folder", func() {
|
||||
ca.images["al-1"] = jpegData
|
||||
ca.images["al-2"] = pngData
|
||||
mockAlbumTracks(squirrel.And{
|
||||
persistence.ParticipantIDFilter("media_file", "1", model.RoleAlbumArtist),
|
||||
squirrel.Eq{"missing": false},
|
||||
}, model.MediaFiles{
|
||||
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Greatest Hits", Year: 2001, DiscNumber: 1},
|
||||
{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumID: "2", Album: "Greatest Hits", Year: 2005, DiscNumber: 1},
|
||||
})
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
Expect(arch.ZipArtist(context.Background(), "1", "mp3", 128, out)).To(Succeed())
|
||||
|
||||
files := readZip(out)
|
||||
Expect(files).To(HaveKeyWithValue("Greatest Hits [2001]/folder.jpg", jpegData))
|
||||
Expect(files).To(HaveKeyWithValue("Greatest Hits [2005]/folder.png", pngData))
|
||||
})
|
||||
|
||||
It("adds the playlist cover to the root", func() {
|
||||
ca.images["pl-1"] = jpegData
|
||||
plRepo := &mockPlaylistRepository{}
|
||||
plRepo.On("GetWithTracks", "1", true, false).Return(&model.Playlist{
|
||||
ID: "1",
|
||||
Name: "Test Playlist",
|
||||
Tracks: []model.PlaylistTrack{
|
||||
{MediaFile: model.MediaFile{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Artist: "Artist 1", Title: "track1"}},
|
||||
},
|
||||
}, nil)
|
||||
ds.On("Playlist", mock.Anything).Return(plRepo)
|
||||
ms.On("NewStream", mock.Anything, mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("test")), nil)
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
Expect(arch.ZipPlaylist(context.Background(), "1", "mp3", 128, out)).To(Succeed())
|
||||
|
||||
files := readZip(out)
|
||||
Expect(files).To(HaveLen(3))
|
||||
Expect(files).To(HaveKeyWithValue("folder.jpg", jpegData))
|
||||
Expect(files).To(HaveKey("Test Playlist.m3u"))
|
||||
})
|
||||
|
||||
It("adds the shared item's cover to the root, even for a private playlist", func() {
|
||||
ca.images["pl-10"] = jpegData
|
||||
ms.On("NewStream", mock.Anything, mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("test")), nil)
|
||||
share := &model.Share{
|
||||
ID: "1",
|
||||
Downloadable: true,
|
||||
Format: "mp3",
|
||||
MaxBitRate: 128,
|
||||
ResourceType: "playlist",
|
||||
ResourceIDs: "10",
|
||||
Tracks: model.MediaFiles{
|
||||
{ID: "1", Path: "test_data/01 - track1.mp3", Suffix: "mp3", Artist: "Artist 1", Title: "track1"},
|
||||
},
|
||||
}
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
Expect(arch.ZipShare(context.Background(), share, out)).To(Succeed())
|
||||
|
||||
files := readZip(out)
|
||||
Expect(files).To(HaveLen(2))
|
||||
Expect(files).To(HaveKeyWithValue("folder.jpg", jpegData))
|
||||
Expect(ca.requests).To(ConsistOf(coverRequest{id: "pl-10", size: 500, square: false, admin: true}))
|
||||
})
|
||||
|
||||
It("still builds the archive when the cover cannot be read", func() {
|
||||
ca.err = errors.New("boom")
|
||||
mockAlbumTracks(squirrel.Eq{"album_id": "1"}, model.MediaFiles{
|
||||
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album", DiscNumber: 1},
|
||||
})
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
Expect(arch.ZipAlbum(context.Background(), "1", "mp3", 128, out)).To(Succeed())
|
||||
|
||||
files := readZip(out)
|
||||
Expect(files).To(HaveLen(1))
|
||||
Expect(files).To(HaveKey("Album/01 - track1.mp3"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func readZip(out *bytes.Buffer) map[string][]byte {
|
||||
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
files := make(map[string][]byte, len(zr.File))
|
||||
for _, f := range zr.File {
|
||||
r, err := f.Open()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
data, err := io.ReadAll(r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_ = r.Close()
|
||||
files[f.Name] = data
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
type coverRequest struct {
|
||||
id string
|
||||
size int
|
||||
square bool
|
||||
admin bool
|
||||
}
|
||||
|
||||
type mockCoverArt struct {
|
||||
artwork.Artwork
|
||||
images map[string][]byte
|
||||
err error
|
||||
requests []coverRequest
|
||||
}
|
||||
|
||||
func (m *mockCoverArt) Get(ctx context.Context, artID model.ArtworkID, size int, square bool) (*artwork.Image, error) {
|
||||
user, _ := request.UserFrom(ctx)
|
||||
m.requests = append(m.requests, coverRequest{id: artID.String(), size: size, square: square, admin: user.IsAdmin})
|
||||
if m.err != nil {
|
||||
return nil, m.err
|
||||
}
|
||||
data, ok := m.images[artID.String()]
|
||||
if !ok {
|
||||
return nil, artwork.ErrUnavailable
|
||||
}
|
||||
return &artwork.Image{ReadCloser: io.NopCloser(bytes.NewReader(data))}, nil
|
||||
}
|
||||
|
||||
type mockDataStore struct {
|
||||
mock.Mock
|
||||
model.DataStore
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
|
|
@ -169,8 +168,9 @@ func loadArtistFolder(ctx context.Context, ds model.DataStore, albums model.Albu
|
|||
folderPath = filepath.Dir(folderPath)
|
||||
}
|
||||
|
||||
// TODO: Hacky, but the easiest way to get the folder ID ATM
|
||||
libPath := core.AbsolutePath(ctx, ds, libID, "")
|
||||
// Cleaned like the album paths; Join keeps an empty path empty, Clean would return ".".
|
||||
libPath, _ := ds.Library(ctx).GetPath(libID)
|
||||
libPath = filepath.Join(libPath)
|
||||
folderID := model.FolderID(model.Library{ID: libID, Path: libPath}, folderPath)
|
||||
|
||||
log.Trace(ctx, "Artwork: Calculating artist folder details", "folderPath", folderPath, "folderID", folderID,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
|
|
@ -47,11 +46,11 @@ var _ = Describe("loadArtistFolder", func() {
|
|||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
DeferCleanup(stubCoreAbsolutePath())
|
||||
|
||||
updatedAt = time.Now().Truncate(time.Second).Add(5 * time.Minute)
|
||||
repo = &fakeFolderRepo{result: []model.Folder{{ImagesUpdatedAt: updatedAt}}}
|
||||
ds = &tests.MockDataStore{MockedFolder: repo}
|
||||
libRepo := &tests.MockLibraryRepo{}
|
||||
libRepo.SetData(model.Libraries{{ID: 1, Path: filepath.FromSlash("/music")}})
|
||||
ds = &tests.MockDataStore{MockedFolder: repo, MockedLibrary: libRepo}
|
||||
albums = model.Albums{{LibraryID: 1, ID: "album1", Name: "Album 1"}}
|
||||
})
|
||||
|
||||
|
|
@ -107,11 +106,3 @@ var _ = Describe("loadArtistFolder", func() {
|
|||
Expect(upd).To(BeZero())
|
||||
})
|
||||
})
|
||||
|
||||
func stubCoreAbsolutePath() func() {
|
||||
original := core.AbsolutePath
|
||||
core.AbsolutePath = func(context.Context, model.DataStore, int, string) string {
|
||||
return filepath.FromSlash("/music")
|
||||
}
|
||||
return func() { core.AbsolutePath = original }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue