refactor(artwork): move ImageUploadService to artwork.Uploader

Relocate the image-upload service from core to core/artwork as
artwork.Uploader, co-locating it with the resolver/worker/serving that own
the artwork state it invalidates. MaxImageUploadSize moves too — its only
callers are the two image-upload handlers — which lets core/image_upload.go
be deleted entirely.

Extract the shared "clear resolved state + re-queue at Bump" invalidation
into artwork.Refresh and fold nativeapi's refreshArtwork handler onto it,
removing the duplicated DeleteForItem+Enqueue block that had drifted into
three places.

The wire provider moves from core's set to artwork's; the
playlists.ImageUploadService binding moves to the top-level injector so
core/artwork stays unaware of playlists. Behavior is unchanged.
This commit is contained in:
Deluan 2026-07-24 16:22:27 -04:00
parent 8a0ff264f1
commit 53babc7a2a
30 changed files with 140 additions and 124 deletions

View File

@ -12,7 +12,7 @@ import (
"strings"
"github.com/Masterminds/squirrel"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/playlists"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
@ -260,7 +260,7 @@ func runImport(ctx context.Context, files []string) {
ctx = request.WithUser(ctx, *user)
}
pls := playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
pls := playlists.NewPlaylists(ds, artwork.NewUploader(ds))
for _, file := range files {
absPath, err := filepath.Abs(file)

View File

@ -9,7 +9,7 @@ import (
"os"
"strings"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/playlists"
"github.com/navidrome/navidrome/db"
"github.com/navidrome/navidrome/log"
@ -82,7 +82,7 @@ func runScanner(ctx context.Context) {
sqlDB := db.Db()
defer db.Db().Close()
ds := persistence.New(sqlDB)
pls := playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
pls := playlists.NewPlaylists(ds, artwork.NewUploader(ds))
// Parse targets from command line or file
var scanTargets []model.ScanTarget

View File

@ -65,8 +65,8 @@ func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
share := core.NewShare(dataStore)
imageUploadService := core.NewImageUploadService(dataStore)
playlistsPlaylists := playlists.NewPlaylists(dataStore, imageUploadService)
uploader := artwork.NewUploader(dataStore)
playlistsPlaylists := playlists.NewPlaylists(dataStore, uploader)
insights := metrics.GetInstance(dataStore)
broker := events.GetBroker()
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
@ -76,7 +76,7 @@ func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router {
library := core.NewLibrary(dataStore, modelScanner, watcher, broker, manager)
user := core.NewUser(dataStore, manager)
maintenance := core.NewMaintenance(dataStore)
router := nativeapi.New(dataStore, share, playlistsPlaylists, insights, library, user, maintenance, manager, imageUploadService)
router := nativeapi.New(dataStore, share, playlistsPlaylists, insights, library, user, maintenance, manager, uploader)
return router
}
@ -98,8 +98,8 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
agentsAgents := agents.GetAgents(dataStore, manager)
matcherMatcher := matcher.New(dataStore)
provider := external.NewProvider(dataStore, agentsAgents, matcherMatcher)
imageUploadService := core.NewImageUploadService(dataStore)
playlistsPlaylists := playlists.NewPlaylists(dataStore, imageUploadService)
uploader := artwork.NewUploader(dataStore)
playlistsPlaylists := playlists.NewPlaylists(dataStore, uploader)
modelScanner := scanner.New(ctx, dataStore, broker, playlistsPlaylists, metricsMetrics)
playTracker := scrobbler.GetPlayTracker(dataStore, broker, manager)
playbackServer := playback.GetInstance(dataStore)
@ -125,8 +125,8 @@ func CreateJellyfinAPIRouter(ctx context.Context) *jellyfin.Router {
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
manager := plugins.GetManager(dataStore, broker, metricsMetrics)
playTracker := scrobbler.GetPlayTracker(dataStore, broker, manager)
imageUploadService := core.NewImageUploadService(dataStore)
playlistsPlaylists := playlists.NewPlaylists(dataStore, imageUploadService)
uploader := artwork.NewUploader(dataStore)
playlistsPlaylists := playlists.NewPlaylists(dataStore, uploader)
agentsAgents := agents.GetAgents(dataStore, manager)
matcherMatcher := matcher.New(dataStore)
provider := external.NewProvider(dataStore, agentsAgents, matcherMatcher)
@ -183,8 +183,8 @@ func CreateScanner(ctx context.Context) model.Scanner {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
broker := events.GetBroker()
imageUploadService := core.NewImageUploadService(dataStore)
playlistsPlaylists := playlists.NewPlaylists(dataStore, imageUploadService)
uploader := artwork.NewUploader(dataStore)
playlistsPlaylists := playlists.NewPlaylists(dataStore, uploader)
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
modelScanner := scanner.New(ctx, dataStore, broker, playlistsPlaylists, metricsMetrics)
return modelScanner
@ -194,8 +194,8 @@ func CreateScanWatcher(ctx context.Context) scanner.Watcher {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
broker := events.GetBroker()
imageUploadService := core.NewImageUploadService(dataStore)
playlistsPlaylists := playlists.NewPlaylists(dataStore, imageUploadService)
uploader := artwork.NewUploader(dataStore)
playlistsPlaylists := playlists.NewPlaylists(dataStore, uploader)
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
modelScanner := scanner.New(ctx, dataStore, broker, playlistsPlaylists, metricsMetrics)
watcher := scanner.GetWatcher(dataStore, modelScanner)
@ -234,7 +234,7 @@ func getPluginManager() *plugins.Manager {
// wire_injectors.go:
var allProviders = wire.NewSet(core.Set, artwork.Set, server.New, subsonic.New, jellyfin.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, scanner.New, scanner.GetWatcher, metrics.GetPrometheusInstance, db.Db, plugins.GetManager, sonic.New, wire.Bind(new(agents.PluginLoader), new(*plugins.Manager)), wire.Bind(new(scrobbler.PluginLoader), new(*plugins.Manager)), wire.Bind(new(lyrics.PluginLoader), new(*plugins.Manager)), wire.Bind(new(sonic.PluginLoader), new(*plugins.Manager)), wire.Bind(new(sonic.Engine), new(*sonic.Sonic)), wire.Bind(new(nativeapi.PluginManager), new(*plugins.Manager)), wire.Bind(new(core.PluginUnloader), new(*plugins.Manager)), wire.Bind(new(plugins.PluginMetricsRecorder), new(metrics.Metrics)), wire.Bind(new(core.Watcher), new(scanner.Watcher)))
var allProviders = wire.NewSet(core.Set, artwork.Set, server.New, subsonic.New, jellyfin.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, scanner.New, scanner.GetWatcher, metrics.GetPrometheusInstance, db.Db, plugins.GetManager, sonic.New, wire.Bind(new(agents.PluginLoader), new(*plugins.Manager)), wire.Bind(new(scrobbler.PluginLoader), new(*plugins.Manager)), wire.Bind(new(lyrics.PluginLoader), new(*plugins.Manager)), wire.Bind(new(sonic.PluginLoader), new(*plugins.Manager)), wire.Bind(new(sonic.Engine), new(*sonic.Sonic)), wire.Bind(new(nativeapi.PluginManager), new(*plugins.Manager)), wire.Bind(new(core.PluginUnloader), new(*plugins.Manager)), wire.Bind(new(plugins.PluginMetricsRecorder), new(metrics.Metrics)), wire.Bind(new(core.Watcher), new(scanner.Watcher)), wire.Bind(new(playlists.ImageUploadService), new(artwork.Uploader)))
func GetPluginManager(ctx context.Context) *plugins.Manager {
manager := getPluginManager()

View File

@ -14,6 +14,7 @@ import (
"github.com/navidrome/navidrome/core/lyrics"
"github.com/navidrome/navidrome/core/metrics"
"github.com/navidrome/navidrome/core/playback"
"github.com/navidrome/navidrome/core/playlists"
"github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/core/sonic"
"github.com/navidrome/navidrome/db"
@ -56,6 +57,7 @@ var allProviders = wire.NewSet(
wire.Bind(new(core.PluginUnloader), new(*plugins.Manager)),
wire.Bind(new(plugins.PluginMetricsRecorder), new(metrics.Metrics)),
wire.Bind(new(core.Watcher), new(scanner.Watcher)),
wire.Bind(new(playlists.ImageUploadService), new(artwork.Uploader)),
)
func CreateDataStore() model.DataStore {

21
core/artwork/refresh.go Normal file
View File

@ -0,0 +1,21 @@
package artwork
import (
"context"
"fmt"
"github.com/navidrome/navidrome/model"
)
// Refresh clears an item's resolved artwork state and re-queues it at Bump priority, so a
// deliberate refresh (image upload or manual re-resolve) drops the current pick and re-resolves it.
func Refresh(ctx context.Context, ds model.DataStore, kind, id string) error {
if err := ds.Artwork(ctx).DeleteForItem(kind, id); err != nil {
return fmt.Errorf("clearing artwork state: %w", err)
}
item := model.ArtworkQueueItem{ItemKind: kind, ItemID: id, ImageType: model.ImageTypePrimary, Priority: model.ArtworkPriorityBump}
if err := ds.ArtworkQueue(ctx).Enqueue(item); err != nil {
return fmt.Errorf("enqueuing artwork refresh: %w", err)
}
return nil
}

View File

@ -1,4 +1,4 @@
package core
package artwork
import (
"context"
@ -15,14 +15,6 @@ import (
"github.com/navidrome/navidrome/utils"
)
type ImageUploadService interface {
SetImage(ctx context.Context, entityType string, entityID string, name string, oldPath string, reader io.Reader, ext string) (filename string, err error)
RemoveImage(ctx context.Context, path string) error
// EnqueueArtwork clears an item's resolved state and re-queues it at Bump priority. Callers
// must invoke it AFTER persisting the new filename, so the worker never resolves the old one.
EnqueueArtwork(ctx context.Context, entityType, entityID string)
}
// MaxImageUploadSize returns the configured MaxImageUploadSize in bytes, or the built-in default
// when it's unset/invalid. Shared by every API that accepts image uploads.
func MaxImageUploadSize() int64 {
@ -33,6 +25,16 @@ func MaxImageUploadSize() int64 {
return int64(size)
}
// Uploader stores a user-uploaded entity image and invalidates that entity's artwork state so the
// upload becomes the served cover.
type Uploader interface {
SetImage(ctx context.Context, entityType string, entityID string, name string, oldPath string, reader io.Reader, ext string) (filename string, err error)
RemoveImage(ctx context.Context, path string) error
// EnqueueArtwork clears an item's resolved state and re-queues it at Bump priority. Callers
// must invoke it AFTER persisting the new filename, so the worker never resolves the old one.
EnqueueArtwork(ctx context.Context, entityType, entityID string)
}
// uploadEntityKind maps an upload's entity type to its artwork kind prefix, so a
// successful upload can clear and re-queue that item's artwork state.
var uploadEntityKind = map[string]string{
@ -41,15 +43,15 @@ var uploadEntityKind = map[string]string{
consts.EntityRadio: model.KindRadioArtwork.Prefix(),
}
type imageUploadService struct {
type uploader struct {
ds model.DataStore
}
func NewImageUploadService(ds model.DataStore) ImageUploadService {
return &imageUploadService{ds: ds}
func NewUploader(ds model.DataStore) Uploader {
return &uploader{ds: ds}
}
func (s *imageUploadService) SetImage(ctx context.Context, entityType string, entityID string, name string, oldPath string, reader io.Reader, ext string) (string, error) {
func (s *uploader) SetImage(ctx context.Context, entityType string, entityID string, name string, oldPath string, reader io.Reader, ext string) (string, error) {
filename := imageFilename(entityID, name, ext)
absPath := model.UploadedImagePath(entityType, filename)
@ -79,22 +81,17 @@ func (s *imageUploadService) SetImage(ctx context.Context, entityType string, en
// EnqueueArtwork clears the item's resolved state and re-queues it at Bump priority: the
// upload is now the top-priority source, so the worker re-resolves and the UI swaps.
func (s *imageUploadService) EnqueueArtwork(ctx context.Context, entityType, id string) {
func (s *uploader) EnqueueArtwork(ctx context.Context, entityType, id string) {
kind, ok := uploadEntityKind[entityType]
if !ok {
return
}
if err := s.ds.Artwork(ctx).DeleteForItem(kind, id); err != nil {
log.Warn(ctx, "Could not clear artwork state after upload", "kind", kind, "id", id, err)
}
item := model.ArtworkQueueItem{ItemKind: kind, ItemID: id, ImageType: model.ImageTypePrimary,
Priority: model.ArtworkPriorityBump}
if err := s.ds.ArtworkQueue(ctx).Enqueue(item); err != nil {
log.Warn(ctx, "Could not enqueue artwork after upload", "kind", kind, "id", id, err)
if err := Refresh(ctx, s.ds, kind, id); err != nil {
log.Warn(ctx, "Could not refresh artwork after upload", "kind", kind, "id", id, err)
}
}
func (s *imageUploadService) RemoveImage(ctx context.Context, path string) error {
func (s *uploader) RemoveImage(ctx context.Context, path string) error {
if path == "" {
return nil
}

View File

@ -1,4 +1,4 @@
package core_test
package artwork
import (
"context"
@ -9,15 +9,14 @@ import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("ImageUploadService", func() {
var svc core.ImageUploadService
var _ = Describe("Uploader", func() {
var svc Uploader
var tmpDir string
var artRepo *tests.MockArtworkRepo
var queueRepo *tests.MockArtworkQueueRepo
@ -29,7 +28,7 @@ var _ = Describe("ImageUploadService", func() {
artRepo = tests.CreateMockArtworkRepo()
queueRepo = tests.CreateMockArtworkQueueRepo()
ds := &tests.MockDataStore{MockedArtwork: artRepo, MockedArtworkQueue: queueRepo}
svc = core.NewImageUploadService(ds)
svc = NewUploader(ds)
})
Describe("SetImage", func() {
@ -155,21 +154,21 @@ var _ = Describe("MaxImageUploadSize", func() {
It("returns the configured size when valid", func() {
conf.Server.MaxImageUploadSize = "20MB"
Expect(core.MaxImageUploadSize()).To(Equal(int64(20_000_000)))
Expect(MaxImageUploadSize()).To(Equal(int64(20_000_000)))
})
It("returns the default size when config is empty", func() {
conf.Server.MaxImageUploadSize = ""
Expect(core.MaxImageUploadSize()).To(Equal(int64(10_000_000)))
Expect(MaxImageUploadSize()).To(Equal(int64(10_000_000)))
})
It("returns the default size when config is invalid", func() {
conf.Server.MaxImageUploadSize = "not-a-size"
Expect(core.MaxImageUploadSize()).To(Equal(int64(10_000_000)))
Expect(MaxImageUploadSize()).To(Equal(int64(10_000_000)))
})
It("parses raw byte values", func() {
conf.Server.MaxImageUploadSize = "52428800"
Expect(core.MaxImageUploadSize()).To(Equal(int64(52_428_800)))
Expect(MaxImageUploadSize()).To(Equal(int64(52_428_800)))
})
})

View File

@ -9,4 +9,5 @@ var Set = wire.NewSet(
GetImageCache,
NewWorker,
ProvideImageStore,
NewUploader,
)

View File

@ -11,7 +11,7 @@ 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/playlists"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/criteria"
@ -43,7 +43,7 @@ var _ = Describe("Playlists - Import", func() {
var folder *model.Folder
BeforeEach(func() {
DeferCleanup(configtest.SetupConfig())
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
ds.MockedMediaFile = &mockedMediaFileRepo{}
libPath, _ := os.Getwd()
// Set up library with the actual library path that matches the folder
@ -118,7 +118,7 @@ var _ = Describe("Playlists - Import", func() {
mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}})
ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3", "test.ogg"}}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
plsFolder := &model.Folder{ID: "1", LibraryID: 1, LibraryPath: tmpDir, Path: "", Name: ""}
pls, err := ps.ImportFromFolder(ctx, plsFolder, "test.m3u")
@ -136,7 +136,7 @@ var _ = Describe("Playlists - Import", func() {
mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}})
ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
plsFolder := &model.Folder{ID: "1", LibraryID: 1, LibraryPath: tmpDir, Path: "", Name: ""}
pls, err := ps.ImportFromFolder(ctx, plsFolder, "test.m3u")
@ -155,7 +155,7 @@ var _ = Describe("Playlists - Import", func() {
mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}})
ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
plsFolder := &model.Folder{ID: "1", LibraryID: 1, LibraryPath: tmpDir, Path: "", Name: ""}
pls, err := ps.ImportFromFolder(ctx, plsFolder, "test.m3u")
@ -174,7 +174,7 @@ var _ = Describe("Playlists - Import", func() {
mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}})
ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
plsFolder := &model.Folder{ID: "1", LibraryID: 1, LibraryPath: tmpDir, Path: "", Name: ""}
pls, err := ps.ImportFromFolder(ctx, plsFolder, "test.m3u")
@ -192,7 +192,7 @@ var _ = Describe("Playlists - Import", func() {
mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}})
ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
plsFolder := &model.Folder{ID: "1", LibraryID: 1, LibraryPath: tmpDir, Path: "", Name: ""}
pls, err := ps.ImportFromFolder(ctx, plsFolder, "test.m3u")
@ -209,7 +209,7 @@ var _ = Describe("Playlists - Import", func() {
mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}})
ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
plsFolder := &model.Folder{ID: "1", LibraryID: 1, LibraryPath: tmpDir, Path: "", Name: ""}
pls, err := ps.ImportFromFolder(ctx, plsFolder, "test.m3u")
@ -226,7 +226,7 @@ var _ = Describe("Playlists - Import", func() {
mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}})
ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
plsFolder := &model.Folder{ID: "1", LibraryID: 1, LibraryPath: tmpDir, Path: "", Name: ""}
pls, err := ps.ImportFromFolder(ctx, plsFolder, "test.m3u")
@ -244,7 +244,7 @@ var _ = Describe("Playlists - Import", func() {
mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}})
ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
plsFolder := &model.Folder{ID: "1", LibraryID: 1, LibraryPath: tmpDir, Path: "", Name: ""}
pls, err := ps.ImportFromFolder(ctx, plsFolder, "test.m3u")
@ -258,7 +258,7 @@ var _ = Describe("Playlists - Import", func() {
tmpDir := GinkgoT().TempDir()
mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}})
ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
m3u := "#EXTALBUMARTURL:https://example.com/new-cover.jpg\ntest.mp3\n"
plsFile := filepath.Join(tmpDir, "test.m3u")
@ -285,7 +285,7 @@ var _ = Describe("Playlists - Import", func() {
tmpDir := GinkgoT().TempDir()
mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}})
ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
plsFile := filepath.Join(tmpDir, "test.m3u")
Expect(os.WriteFile(plsFile, []byte("test.mp3\n"), 0600)).To(Succeed())
@ -311,7 +311,7 @@ var _ = Describe("Playlists - Import", func() {
tmpDir := GinkgoT().TempDir()
mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}})
ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
m3u := "test.mp3\n"
plsFile := filepath.Join(tmpDir, "test.m3u")
@ -388,7 +388,7 @@ var _ = Describe("Playlists - Import", func() {
tmpDir := GinkgoT().TempDir()
mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}})
ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{}}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
// Create the playlist file on disk with the filesystem's normalization form
plsFile := tmpDir + "/" + filesystemName + ".m3u"
@ -448,7 +448,7 @@ var _ = Describe("Playlists - Import", func() {
"def.mp3", // This is playlists/def.mp3 relative to plsDir
},
}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
})
It("handles relative paths that reference files in other libraries", func() {
@ -604,7 +604,7 @@ var _ = Describe("Playlists - Import", func() {
},
}
// Recreate playlists service to pick up new mock
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
// Create playlist in music library that references both tracks
plsContent := "#PLAYLIST:Same Path Test\nalbum/track.mp3\n../classical/album/track.mp3"
@ -662,7 +662,7 @@ var _ = Describe("Playlists - Import", func() {
},
}
ds.MockedFolder = mockFolderRepo
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
plsContent := "#PLAYLIST:My Playlist\ntest.mp3\ntest.ogg\n"
plsFile := filepath.Join(tmpDir, "my-playlist.m3u")
@ -681,7 +681,7 @@ var _ = Describe("Playlists - Import", func() {
libDir := filepath.Join(tmpDir, "music")
Expect(os.Mkdir(libDir, 0755)).To(Succeed())
mockLibRepo.SetData([]model.Library{{ID: 1, Path: libDir}})
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
plsContent := "#PLAYLIST:External Playlist\n" + libDir + "/test.mp3\n"
plsFile := filepath.Join(tmpDir, "external.m3u")
@ -704,7 +704,7 @@ var _ = Describe("Playlists - Import", func() {
},
}
ds.MockedFolder = mockFolderRepo
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
plsFile := filepath.Join(tmpDir, "test.m3u")
Expect(os.WriteFile(plsFile, []byte("test.mp3\n"), 0600)).To(Succeed())
@ -724,7 +724,7 @@ var _ = Describe("Playlists - Import", func() {
},
}
ds.MockedFolder = mockFolderRepo
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
plsFile := filepath.Join(tmpDir, "test.m3u")
Expect(os.WriteFile(plsFile, []byte("test.mp3\n"), 0600)).To(Succeed())
@ -744,7 +744,7 @@ var _ = Describe("Playlists - Import", func() {
},
}
ds.MockedFolder = mockFolderRepo
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
plsFile := filepath.Join(tmpDir, "test.m3u")
Expect(os.WriteFile(plsFile, []byte("test.mp3\n"), 0600)).To(Succeed())
@ -767,7 +767,7 @@ var _ = Describe("Playlists - Import", func() {
BeforeEach(func() {
repo = &mockedMediaFileFromListRepo{}
ds.MockedMediaFile = repo
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
mockLibRepo.SetData([]model.Library{{ID: 1, Path: "/music"}, {ID: 2, Path: "/new"}})
ctx = request.WithUser(ctx, model.User{ID: "123"})
})

View File

@ -52,8 +52,8 @@ type Playlists interface {
TracksRepository(ctx context.Context, playlistId string, refreshSmartPlaylist bool) rest.Repository
}
// ImageUploadService is a local interface satisfied by core.ImageUploadService.
// Defined here to avoid an import cycle between core and core/playlists.
// ImageUploadService is a local interface satisfied by artwork.Uploader.
// Defined here to avoid an import cycle between core/artwork and core/playlists.
type ImageUploadService interface {
SetImage(ctx context.Context, entityType string, entityID string, name string, oldPath string, reader io.Reader, ext string) (filename string, err error)
RemoveImage(ctx context.Context, path string) error

View File

@ -8,7 +8,7 @@ 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/playlists"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/criteria"
@ -42,7 +42,7 @@ var _ = Describe("Playlists", func() {
"pls-1": {ID: "pls-1", Name: "My Playlist", OwnerID: "user-1"},
}
mockPlsRepo.TracksRepo = mockTracks
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
})
It("allows owner to delete their playlist", func() {
@ -82,7 +82,7 @@ var _ = Describe("Playlists", func() {
"pls-1": {ID: "pls-1", Name: "My Playlist", OwnerID: "user-1"},
}
mockPlsRepo.TracksRepo = mockTracks
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
})
It("returns the playlist's track repository", func() {
@ -103,7 +103,7 @@ var _ = Describe("Playlists", func() {
"pls-smart": {ID: "pls-smart", Name: "Smart", OwnerID: "user-1",
Rules: &criteria.Criteria{Expression: criteria.Contains{"title": "test"}}},
}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
})
It("creates a new playlist with owner set from context", func() {
@ -161,7 +161,7 @@ var _ = Describe("Playlists", func() {
Rules: &criteria.Criteria{Expression: criteria.Contains{"title": "test"}}},
}
mockPlsRepo.TracksRepo = mockTracks
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
})
It("allows owner to update their playlist", func() {
@ -219,7 +219,7 @@ var _ = Describe("Playlists", func() {
"pls-other": {ID: "pls-other", Name: "Other's", OwnerID: "other-user"},
}
mockPlsRepo.TracksRepo = mockTracks
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
})
It("allows owner to add tracks", func() {
@ -267,7 +267,7 @@ var _ = Describe("Playlists", func() {
Rules: &criteria.Criteria{Expression: criteria.Contains{"title": "test"}}},
}
mockPlsRepo.TracksRepo = mockTracks
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
})
It("allows owner to remove tracks", func() {
@ -301,7 +301,7 @@ var _ = Describe("Playlists", func() {
Rules: &criteria.Criteria{Expression: criteria.Contains{"title": "test"}}},
}
mockPlsRepo.TracksRepo = mockTracks
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
})
It("allows owner to reorder", func() {
@ -330,7 +330,7 @@ var _ = Describe("Playlists", func() {
"pls-1": {ID: "pls-1", Name: "My Playlist", OwnerID: "user-1"},
"pls-other": {ID: "pls-other", Name: "Other's", OwnerID: "other-user"},
}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
})
It("saves image file and updates UploadedImage", func() {
@ -400,7 +400,7 @@ var _ = Describe("Playlists", func() {
"pls-empty": {ID: "pls-empty", Name: "No Cover", OwnerID: "user-1"},
"pls-other": {ID: "pls-other", Name: "Other's", OwnerID: "other-user"},
}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
})
It("removes file and clears UploadedImage", func() {

View File

@ -5,7 +5,7 @@ import (
"time"
"github.com/deluan/rest"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/playlists"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/criteria"
@ -37,7 +37,7 @@ var _ = Describe("REST Adapter", func() {
mockPlsRepo.Data = map[string]*model.Playlist{
"pls-1": {ID: "pls-1", Name: "My Playlist", OwnerID: "user-1"},
}
ps = playlists.NewPlaylists(ds, core.NewImageUploadService(ds))
ps = playlists.NewPlaylists(ds, artwork.NewUploader(ds))
})
Describe("Save", func() {

View File

@ -24,8 +24,6 @@ var Set = wire.NewSet(
NewLibrary,
NewUser,
NewMaintenance,
NewImageUploadService,
wire.Bind(new(playlists.ImageUploadService), new(ImageUploadService)),
stream.NewTranscodeDecider,
agents.GetAgents,
external.NewProvider,

View File

@ -13,7 +13,7 @@ import (
"github.com/Masterminds/squirrel"
"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/metrics"
"github.com/navidrome/navidrome/core/playlists"
"github.com/navidrome/navidrome/core/storage/storagetest"
@ -275,7 +275,7 @@ var _ = BeforeSuite(func() {
buildTestFS()
s := scanner.New(ctx, initDS, events.NoopBroker(),
playlists.NewPlaylists(initDS, core.NewImageUploadService(initDS)), metrics.NewNoopInstance())
playlists.NewPlaylists(initDS, artwork.NewUploader(initDS)), metrics.NewNoopInstance())
_, err = s.ScanAll(ctx, true)
Expect(err).ToNot(HaveOccurred())

View File

@ -5,7 +5,7 @@ import (
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/metrics"
"github.com/navidrome/navidrome/core/playlists"
"github.com/navidrome/navidrome/db"
@ -31,7 +31,7 @@ var _ = Describe("Controller", func() {
DeferCleanup(configtest.SetupConfig())
ds = &tests.MockDataStore{RealDS: persistence.New(db.Db())}
ds.MockedProperty = &tests.MockedPropertyRepo{}
ctrl = scanner.New(ctx, ds, events.NoopBroker(), playlists.NewPlaylists(ds, core.NewImageUploadService(ds)), metrics.NewNoopInstance())
ctrl = scanner.New(ctx, ds, events.NoopBroker(), playlists.NewPlaylists(ds, artwork.NewUploader(ds)), metrics.NewNoopInstance())
})
It("includes last scan error", func() {

View File

@ -12,7 +12,7 @@ import (
"github.com/dustin/go-humanize"
"github.com/google/uuid"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/metrics"
"github.com/navidrome/navidrome/core/playlists"
"github.com/navidrome/navidrome/core/storage/storagetest"
@ -40,7 +40,7 @@ func BenchmarkScan(b *testing.B) {
ds := persistence.New(db.Db())
conf.Server.DevExternalScanner = false
s := scanner.New(context.Background(), ds, events.NoopBroker(),
playlists.NewPlaylists(ds, core.NewImageUploadService(ds)), metrics.NewNoopInstance())
playlists.NewPlaylists(ds, artwork.NewUploader(ds)), metrics.NewNoopInstance())
fs := storagetest.FakeFS{}
storagetest.Register("fake", &fs)

View File

@ -11,7 +11,7 @@ import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/metrics"
"github.com/navidrome/navidrome/core/playlists"
"github.com/navidrome/navidrome/core/storage/storagetest"
@ -78,7 +78,7 @@ var _ = Describe("Scanner - Multi-Library", Ordered, func() {
Expect(ds.User(ctx).Put(&adminUser)).To(Succeed())
s = scanner.New(ctx, ds, events.NoopBroker(),
playlists.NewPlaylists(ds, core.NewImageUploadService(ds)), metrics.NewNoopInstance())
playlists.NewPlaylists(ds, artwork.NewUploader(ds)), metrics.NewNoopInstance())
// Create two test libraries (let DB auto-assign IDs)
lib1 = model.Library{Name: "Rock Collection", Path: "rock:///music"}

View File

@ -10,7 +10,7 @@ import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/metrics"
"github.com/navidrome/navidrome/core/playlists"
"github.com/navidrome/navidrome/core/storage/storagetest"
@ -66,7 +66,7 @@ var _ = Describe("ScanFolders", Ordered, func() {
Expect(ds.User(ctx).Put(&adminUser)).To(Succeed())
s = scanner.New(ctx, ds, events.NoopBroker(),
playlists.NewPlaylists(ds, core.NewImageUploadService(ds)), metrics.NewNoopInstance())
playlists.NewPlaylists(ds, artwork.NewUploader(ds)), metrics.NewNoopInstance())
lib = model.Library{ID: 1, Name: "Fake Library", Path: "fake:///music"}
Expect(ds.Library(ctx).Put(&lib)).To(Succeed())

View File

@ -13,7 +13,7 @@ import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/metrics"
"github.com/navidrome/navidrome/core/playlists"
"github.com/navidrome/navidrome/core/storage/storagetest"
@ -86,7 +86,7 @@ var _ = Describe("Scanner", Ordered, func() {
Expect(ds.User(ctx).Put(&adminUser)).To(Succeed())
s = scanner.New(ctx, ds, events.NoopBroker(),
playlists.NewPlaylists(ds, core.NewImageUploadService(ds)), metrics.NewNoopInstance())
playlists.NewPlaylists(ds, artwork.NewUploader(ds)), metrics.NewNoopInstance())
lib = model.Library{ID: 1, Name: "Fake Library", Path: "fake:///music"}
Expect(ds.Library(ctx).Put(&lib)).To(Succeed())

View File

@ -326,7 +326,7 @@ func setupTestDB() {
decider,
core.NewPlayers(ds),
scrobbler.NewPlayTracker(ds, events.NoopBroker(), nil),
playlists.NewPlaylists(ds, core.NewImageUploadService(ds)),
playlists.NewPlaylists(ds, artwork.NewUploader(ds)),
providerFake,
sonicSvc,
lyrics.NewLyrics(ds, nil),

View File

@ -16,7 +16,7 @@ import (
"github.com/dustin/go-humanize"
"github.com/go-chi/chi/v5"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
@ -98,7 +98,7 @@ func (api *Router) postItemImage(w http.ResponseWriter, r *http.Request) {
}
// The limit caps the decoded image (native endpoint semantics); Jellyfin clients base64-encode
// the wire body (4/3 bigger), so the read cap allows for inflation.
limit := core.MaxImageUploadSize()
limit := artwork.MaxImageUploadSize()
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, limit*4/3+4))
if err != nil {
log.Warn(ctx, "Jellyfin API: cover upload rejected: body exceeds MaxImageUploadSize",

View File

@ -4,6 +4,7 @@ import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
)
@ -32,15 +33,8 @@ func (api *Router) refreshArtwork() http.HandlerFunc {
http.Error(w, "invalid artwork kind", http.StatusBadRequest)
return
}
if err := api.ds.Artwork(ctx).DeleteForItem(kind, id); err != nil {
log.Error(ctx, "Error clearing artwork state", "kind", kind, "id", id, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
item := model.ArtworkQueueItem{ItemKind: kind, ItemID: id, ImageType: model.ImageTypePrimary,
Priority: model.ArtworkPriorityBump}
if err := api.ds.ArtworkQueue(ctx).Enqueue(item); err != nil {
log.Error(ctx, "Error enqueuing artwork refresh", "kind", kind, "id", id, err)
if err := artwork.Refresh(ctx, api.ds, kind, id); err != nil {
log.Error(ctx, "Error refreshing artwork", "kind", kind, "id", id, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}

View File

@ -14,7 +14,7 @@ import (
"strings"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
@ -31,7 +31,7 @@ func checkImageUploadPermission(w http.ResponseWriter, r *http.Request) bool {
}
func handleImageUpload(saveFn func(ctx context.Context, reader io.Reader, ext string) error) http.HandlerFunc {
maxImageSize := core.MaxImageUploadSize()
maxImageSize := artwork.MaxImageUploadSize()
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if !checkImageUploadPermission(w, r) {

View File

@ -13,6 +13,7 @@ import (
"github.com/go-chi/chi/v5/middleware"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/metrics"
playlistsvc "github.com/navidrome/navidrome/core/playlists"
"github.com/navidrome/navidrome/log"
@ -44,10 +45,10 @@ type Router struct {
users core.User
maintenance core.Maintenance
pluginManager PluginManager
imgUpload core.ImageUploadService
imgUpload artwork.Uploader
}
func New(ds model.DataStore, share core.Share, playlists playlistsvc.Playlists, insights metrics.Insights, libraryService core.Library, userService core.User, maintenance core.Maintenance, pluginManager PluginManager, imgUpload core.ImageUploadService) *Router {
func New(ds model.DataStore, share core.Share, playlists playlistsvc.Playlists, insights metrics.Insights, libraryService core.Library, userService core.User, maintenance core.Maintenance, pluginManager PluginManager, imgUpload artwork.Uploader) *Router {
r := &Router{ds: ds, share: share, playlists: playlists, insights: insights, libs: libraryService, users: userService, maintenance: maintenance, pluginManager: pluginManager, imgUpload: imgUpload}
r.Handler = r.routes()
return r

View File

@ -409,7 +409,7 @@ func setupTestDB() {
streamerSpy = &harness.SpyStreamer{}
decider := stream.NewTranscodeDecider(ds, harness.NoopFFmpeg{})
s := scanner.New(ctx, ds, events.NoopBroker(),
playlists.NewPlaylists(ds, core.NewImageUploadService(ds)), metrics.NewNoopInstance())
playlists.NewPlaylists(ds, artwork.NewUploader(ds)), metrics.NewNoopInstance())
router = subsonic.New(
ds,
noopArtwork{},
@ -419,7 +419,7 @@ func setupTestDB() {
noopProvider{},
s,
events.NoopBroker(),
playlists.NewPlaylists(ds, core.NewImageUploadService(ds)),
playlists.NewPlaylists(ds, artwork.NewUploader(ds)),
scrobbler.NewPlayTracker(ds, events.NoopBroker(), nil),
core.NewShare(ds),
playback.PlaybackServer(nil),

View File

@ -114,7 +114,7 @@ var _ = Describe("Artwork Serving", Ordered, func() {
Expect(ds.User(ctx).SetUserLibraries(adminUser.ID, []int{artLib.ID})).To(Succeed())
s := scanner.New(ctx, ds, events.NoopBroker(),
playlists.NewPlaylists(ds, core.NewImageUploadService(ds)), metrics.NewNoopInstance())
playlists.NewPlaylists(ds, artwork.NewUploader(ds)), metrics.NewNoopInstance())
_, err := s.ScanAll(ctx, true)
Expect(err).ToNot(HaveOccurred())
@ -228,10 +228,10 @@ var _ = Describe("Artwork Serving", Ordered, func() {
func buildArtworkRouter(art artwork.Service) *subsonic.Router {
decider := stream.NewTranscodeDecider(ds, harness.NoopFFmpeg{})
s := scanner.New(ctx, ds, events.NoopBroker(),
playlists.NewPlaylists(ds, core.NewImageUploadService(ds)), metrics.NewNoopInstance())
playlists.NewPlaylists(ds, artwork.NewUploader(ds)), metrics.NewNoopInstance())
return subsonic.New(
ds, art, streamerSpy, noopArchiver{}, core.NewPlayers(ds), noopProvider{}, s,
events.NoopBroker(), playlists.NewPlaylists(ds, core.NewImageUploadService(ds)),
events.NoopBroker(), playlists.NewPlaylists(ds, artwork.NewUploader(ds)),
scrobbler.NewPlayTracker(ds, events.NoopBroker(), nil), core.NewShare(ds),
playback.PlaybackServer(nil), metrics.NewNoopInstance(), lyrics.NewLyrics(ds, nil), decider, nil,
)

View File

@ -6,7 +6,7 @@ import (
"github.com/Masterminds/squirrel"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/metrics"
"github.com/navidrome/navidrome/core/playlists"
"github.com/navidrome/navidrome/core/storage/storagetest"
@ -53,7 +53,7 @@ var _ = Describe("Multi-Library Support", Ordered, func() {
// Run incremental scan to import lib2 content (lib1 files unchanged → skipped)
s := scanner.New(ctx, ds, events.NoopBroker(),
playlists.NewPlaylists(ds, core.NewImageUploadService(ds)), metrics.NewNoopInstance())
playlists.NewPlaylists(ds, artwork.NewUploader(ds)), metrics.NewNoopInstance())
_, err = s.ScanAll(ctx, false)
Expect(err).ToNot(HaveOccurred())

View File

@ -9,6 +9,7 @@ import (
"github.com/Masterminds/squirrel"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/agents"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/lyrics"
"github.com/navidrome/navidrome/core/matcher"
"github.com/navidrome/navidrome/core/metrics"
@ -43,7 +44,7 @@ func buildSonicRouter(provider sonic.Provider) *subsonic.Router {
noopProvider{},
nil, // scanner
events.NoopBroker(),
playlists.NewPlaylists(ds, core.NewImageUploadService(ds)),
playlists.NewPlaylists(ds, artwork.NewUploader(ds)),
scrobbler.NewPlayTracker(ds, events.NoopBroker(), nil),
core.NewShare(ds),
playback.PlaybackServer(nil),

View File

@ -13,7 +13,7 @@ import (
"testing/fstest"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/core/ffmpeg"
"github.com/navidrome/navidrome/core/metrics"
@ -74,7 +74,7 @@ func SetupDB(ctx context.Context, users ...*model.User) *DB {
}
s := scanner.New(ctx, ds, events.NoopBroker(),
playlists.NewPlaylists(ds, core.NewImageUploadService(ds)), metrics.NewNoopInstance())
playlists.NewPlaylists(ds, artwork.NewUploader(ds)), metrics.NewNoopInstance())
_, err := s.ScanAll(ctx, true)
Expect(err).ToNot(HaveOccurred())

View File

@ -38,7 +38,9 @@ describe('CoverImage', () => {
it('shows neither a broken <img> nor a canvas while loading a record with no blurhash', () => {
useImageUrl.mockReturnValue({ imgUrl: null, loading: true })
const { container } = render(<CoverImage record={{ id: 'al-2', name: 'X' }} />)
const { container } = render(
<CoverImage record={{ id: 'al-2', name: 'X' }} />,
)
expect(container.querySelector('img')).toBeNull()
expect(container.querySelector('canvas')).toBeNull()
})