mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
fix(artwork): resolve private playlists with an admin context
This commit is contained in:
parent
7713a6d6b2
commit
55608b2d20
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/navidrome/navidrome/consts"
|
"github.com/navidrome/navidrome/consts"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/model/request"
|
||||||
"github.com/navidrome/navidrome/utils/slice"
|
"github.com/navidrome/navidrome/utils/slice"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -34,9 +35,20 @@ func Fingerprint() string {
|
|||||||
return hex.EncodeToString(sum[:])
|
return hex.EncodeToString(sum[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// withAdminUser wraps ctx with the first admin so repos that apply a per-user visibility
|
||||||
|
// filter (playlists) expose private rows during this headless work; no admin yet -> unchanged.
|
||||||
|
func withAdminUser(ctx context.Context, ds model.DataStore) context.Context {
|
||||||
|
admin, err := ds.User(ctx).FindFirstAdmin()
|
||||||
|
if err != nil || admin == nil || admin.ID == "" {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
return request.WithUser(ctx, *admin)
|
||||||
|
}
|
||||||
|
|
||||||
// Backfill enqueues artwork resolution for every entity when the config fingerprint changed
|
// Backfill enqueues artwork resolution for every entity when the config fingerprint changed
|
||||||
// (or was never stored), artists first so those pages resolve before the larger backlog.
|
// (or was never stored), artists first so those pages resolve before the larger backlog.
|
||||||
func Backfill(ctx context.Context, ds model.DataStore) (bool, error) {
|
func Backfill(ctx context.Context, ds model.DataStore) (bool, error) {
|
||||||
|
ctx = withAdminUser(ctx, ds)
|
||||||
current := Fingerprint()
|
current := Fingerprint()
|
||||||
props := ds.Property(ctx)
|
props := ds.Property(ctx)
|
||||||
stored, err := props.DefaultGet(FingerprintPropertyKey, "")
|
stored, err := props.DefaultGet(FingerprintPropertyKey, "")
|
||||||
|
|||||||
@ -7,11 +7,35 @@ import (
|
|||||||
"github.com/navidrome/navidrome/conf"
|
"github.com/navidrome/navidrome/conf"
|
||||||
"github.com/navidrome/navidrome/conf/configtest"
|
"github.com/navidrome/navidrome/conf/configtest"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/model/request"
|
||||||
"github.com/navidrome/navidrome/tests"
|
"github.com/navidrome/navidrome/tests"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// visibilityPlaylistDS models playlist_repository's userFilter: a private playlist is only
|
||||||
|
// visible when the ctx carries an admin, so headless work must wrap ctx with one first.
|
||||||
|
type visibilityPlaylistDS struct {
|
||||||
|
*tests.MockDataStore
|
||||||
|
private model.Playlist
|
||||||
|
tracks model.PlaylistTrackRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *visibilityPlaylistDS) Playlist(ctx context.Context) model.PlaylistRepository {
|
||||||
|
repo := tests.CreateMockPlaylistRepo()
|
||||||
|
repo.TracksRepo = v.tracks
|
||||||
|
if u, ok := request.UserFrom(ctx); ok && u.IsAdmin {
|
||||||
|
repo.SetData(model.Playlists{v.private})
|
||||||
|
}
|
||||||
|
return repo
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminUserRepo() *tests.MockedUserRepo {
|
||||||
|
repo := tests.CreateMockUserRepo()
|
||||||
|
Expect(repo.Put(&model.User{ID: "admin", UserName: "admin", IsAdmin: true})).To(Succeed())
|
||||||
|
return repo
|
||||||
|
}
|
||||||
|
|
||||||
// orderTrackingQueueRepo records the item kind of each Enqueue call, so tests can
|
// orderTrackingQueueRepo records the item kind of each Enqueue call, so tests can
|
||||||
// assert phase ordering (artists-first) that same-priority timestamps can't guarantee.
|
// assert phase ordering (artists-first) that same-priority timestamps can't guarantee.
|
||||||
type orderTrackingQueueRepo struct {
|
type orderTrackingQueueRepo struct {
|
||||||
@ -118,6 +142,20 @@ var _ = Describe("Housekeeping", func() {
|
|||||||
Expect(stored).To(Equal(Fingerprint()))
|
Expect(stored).To(Equal(Fingerprint()))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("enqueues a private playlist by resolving it under an admin context", func() {
|
||||||
|
ds.MockedUser = adminUserRepo()
|
||||||
|
vds := &visibilityPlaylistDS{
|
||||||
|
MockDataStore: ds,
|
||||||
|
private: model.Playlist{ID: "plPrivate", OwnerID: "admin"},
|
||||||
|
tracks: &tests.MockPlaylistTrackRepo{},
|
||||||
|
}
|
||||||
|
|
||||||
|
did, err := Backfill(ctx, vds)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(did).To(BeTrue())
|
||||||
|
Expect(findQueued(queueRepo.MockArtworkQueueRepo, "pl", "plPrivate")).ToNot(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
It("enqueues artists before albums/playlists/radios, all at Backfill priority", func() {
|
It("enqueues artists before albums/playlists/radios, all at Backfill priority", func() {
|
||||||
seedEntities()
|
seedEntities()
|
||||||
Expect(propRepo.Put(FingerprintPropertyKey, "stale-fingerprint")).To(Succeed())
|
Expect(propRepo.Put(FingerprintPropertyKey, "stale-fingerprint")).To(Succeed())
|
||||||
|
|||||||
@ -114,6 +114,9 @@ func (w *Worker) RunPrune(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *Worker) drain(ctx context.Context, concurrency int) (int, error) {
|
func (w *Worker) drain(ctx context.Context, concurrency int) (int, error) {
|
||||||
|
// Resolved per drain, not once in Run: the worker starts at boot, possibly before any
|
||||||
|
// admin exists, so a late-created admin is picked up on the next poll (private playlists).
|
||||||
|
ctx = withAdminUser(ctx, w.deps.ds)
|
||||||
batch, err := w.deps.ds.ArtworkQueue(ctx).DequeueBatch(2 * concurrency)
|
batch, err := w.deps.ds.ArtworkQueue(ctx).DequeueBatch(2 * concurrency)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
|
|||||||
@ -213,6 +213,27 @@ var _ = Describe("Worker", func() {
|
|||||||
Expect(it.RetryAt).To(BeTemporally("==", dequeued.Add(time.Minute)))
|
Expect(it.RetryAt).To(BeTemporally("==", dequeued.Add(time.Minute)))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("resolves a private playlist under an admin context instead of failing forever", func() {
|
||||||
|
ds.MockedUser = adminUserRepo()
|
||||||
|
vds := &visibilityPlaylistDS{
|
||||||
|
MockDataStore: ds,
|
||||||
|
private: model.Playlist{ID: "plPriv", OwnerID: "admin"},
|
||||||
|
tracks: &tests.MockPlaylistTrackRepo{},
|
||||||
|
}
|
||||||
|
w = NewWorker(vds, store, prov, ffm)
|
||||||
|
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "pl", ItemID: "plPriv"})).To(Succeed())
|
||||||
|
|
||||||
|
n, err := w.drain(ctx, 1)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(n).To(Equal(1))
|
||||||
|
|
||||||
|
// Resolved as absent (no art) and removed — not stuck failing on ErrNotFound forever.
|
||||||
|
Expect(findQueued(queueRepo, "pl", "plPriv")).To(BeNil())
|
||||||
|
ia, err := artRepo.GetItemArtwork("pl", "plPriv", model.ImageTypePrimary)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(ia.Hash).To(BeEmpty())
|
||||||
|
})
|
||||||
|
|
||||||
It("returns zero when the queue is empty", func() {
|
It("returns zero when the queue is empty", func() {
|
||||||
n, err := w.drain(ctx, 2)
|
n, err := w.drain(ctx, 2)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user