mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 02:17:25 +02:00
refactor(persistence): stateless repositories with per-call context (#6149)
* refactor(persistence): adopt generic deluan/rest repository API Pin deluan/rest to the refactor branch. REST-facing repository methods take a context and return typed values. Drop DataStore.Resource and ResourceRepository; the native API names typed repositories directly through a per-request adapter that later commits remove. * refactor(persistence): base repository helpers take a context * refactor(persistence): LibraryRepository takes a context per call * refactor(persistence): PropertyRepository takes a context per call * refactor(persistence): UserPropsRepository takes a context per call * refactor(persistence): TranscodingRepository takes a context per call * refactor(persistence): ShareRepository takes a context per call * refactor(persistence): PlayerRepository takes a context per call * refactor(persistence): RadioRepository takes a context per call * refactor(persistence): PlayQueueRepository takes a context per call * refactor(persistence): Tag and Genre repositories take a context per call * refactor(persistence): PluginRepository takes a context per call * refactor(persistence): Scrobble repositories take a context per call * refactor(persistence): FolderRepository takes a context per call * refactor(persistence): Artwork repositories take a context per call * refactor(persistence): UserRepository takes a context per call * refactor(persistence): ArtistRepository takes a context per call ReadAll no longer rewrites the shared sort mappings for the role filter; it works on a per-call copy. * test(persistence): assert artist role sort sanitization in ReadAll * refactor(persistence): AlbumRepository takes a context per call * test(persistence): pass the test context to album repository helpers * refactor(persistence): MediaFileRepository takes a context per call * refactor(persistence): Playlist repositories take a context per call * refactor(persistence): build all repositories once per store * refactor(core): REST repository wrappers are built once * refactor(persistence): repositories are stateless Remove the context field from the base repository and the per-request REST adapter. Enable the containedctx linter so no repository can hold a request context again. * chore(lint): skip containedctx in test files * refactor: share simplifications from the stateless repositories sweep Add deleteOwnedAll on sqlRepository and use it in player/share Delete to remove the duplicated bulk-delete loop; have Share.Repository() return model.ShareRepository so subsonic sharing.go drops its repeated type assertions. * chore(core): assert REST wrappers implement Persistable * chore: reformat imports * perf(persistence): build repositories on first use Each transaction store used to construct all 21 repositories up front, paying for filter and sort mapping setup the block never touched. Fields are now sync.OnceValue thunks, so a store only builds what it uses. * fix(persistence): clean plugin references per deleted user A bulk user delete that fails on a later id had already removed the earlier rows but skipped their plugin cleanup. Cleanup now runs right after each successful delete. * fix(core): unload disabled plugins even when a user delete fails A bulk delete can fail on a later id after earlier users were removed and their plugins auto-disabled. The wrapper returned before unloading, leaving those plugins running until the next successful delete or a restart. * chore(deps): pin deluan/rest to v1.0.1 Replaces the pseudo-version of the refactor branch with the tagged release. REST error messages now name the bare type (Artist, not model.Artist). * test: use the spec context instead of context.Background() Replace the context.Background()/context.TODO() calls this branch added to tests with the spec's ctx, GinkgoT().Context(), or t/b.Context(), so repository calls are bound to the running spec's lifetime. * test: declare the spec context once per Describe Set ctx from GinkgoT().Context() first in each top-level BeforeEach and reuse it, building user contexts on top of it instead of repeating inline calls.
This commit is contained in:
parent
83fff44c82
commit
b293b96256
339 changed files with 6219 additions and 6172 deletions
|
|
@ -9,6 +9,7 @@ linters:
|
|||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- containedctx
|
||||
- copyloopvar
|
||||
- dogsled
|
||||
- durationcheck
|
||||
|
|
@ -57,6 +58,9 @@ linters:
|
|||
- gosec
|
||||
path: _test\.go
|
||||
text: "G703"
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- containedctx
|
||||
- path-except: 'db/migrations/'
|
||||
linters:
|
||||
- forbidigo
|
||||
|
|
|
|||
|
|
@ -357,7 +357,7 @@ var _ = Describe("lastfmAgent", func() {
|
|||
var httpClient *tests.FakeHttpClient
|
||||
var track *model.MediaFile
|
||||
BeforeEach(func() {
|
||||
_ = ds.UserProps(ctx).Put("user-1", sessionKeyProperty, "SK-1")
|
||||
_ = ds.UserProps().Put(ctx, "user-1", sessionKeyProperty, "SK-1")
|
||||
httpClient = &tests.FakeHttpClient{}
|
||||
client := newClient("API_KEY", "SECRET", httpClient)
|
||||
agent = lastFMConstructor(ds)
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ var _ = Describe("auth_router", func() {
|
|||
})
|
||||
|
||||
storedSessionKey := func(userID string) string {
|
||||
key, _ := userProps.Get(userID, sessionKeyProperty)
|
||||
key, _ := userProps.Get(GinkgoT().Context(), userID, sessionKeyProperty)
|
||||
return key
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ var _ = Describe("listenBrainzAgent", func() {
|
|||
BeforeEach(func() {
|
||||
ds = &tests.MockDataStore{}
|
||||
ctx = context.Background()
|
||||
_ = ds.UserProps(ctx).Put("user-1", sessionKeyProperty, "SK-1")
|
||||
_ = ds.UserProps().Put(ctx, "user-1", sessionKeyProperty, "SK-1")
|
||||
httpClient = &tests.FakeHttpClient{}
|
||||
agent = listenBrainzConstructor(ds)
|
||||
agent.client = newClient("http://localhost:8080", httpClient)
|
||||
|
|
|
|||
|
|
@ -174,28 +174,28 @@ func queueTotal(stats []model.ArtworkQueueStat) int64 {
|
|||
}
|
||||
|
||||
func collectStatus(ctx context.Context, ds model.DataStore) (statusReport, error) {
|
||||
q := ds.ArtworkQueue(ctx)
|
||||
q := ds.ArtworkQueue()
|
||||
var rep statusReport
|
||||
var err error
|
||||
if rep.queue, err = q.CountQueued(nil, nil); err != nil {
|
||||
if rep.queue, err = q.CountQueued(ctx, nil, nil); err != nil {
|
||||
return rep, fmt.Errorf("breaking the artwork queue down by kind: %w", err)
|
||||
}
|
||||
|
||||
for _, k := range artwork.ReprocessKinds {
|
||||
sources, err := q.SourcesInUse(k)
|
||||
sources, err := q.SourcesInUse(ctx, k)
|
||||
if err != nil {
|
||||
return rep, fmt.Errorf("listing the sources in use by %s artwork: %w", k, err)
|
||||
}
|
||||
slices.Sort(sources)
|
||||
for _, s := range sources {
|
||||
n, err := q.CountBySource(k, []string{s})
|
||||
n, err := q.CountBySource(ctx, k, []string{s})
|
||||
if err != nil {
|
||||
return rep, fmt.Errorf("counting %s artwork resolved from %s: %w", k, displaySource(s), err)
|
||||
}
|
||||
rep.sources = append(rep.sources, sourceCount{kind: k, source: s, count: n})
|
||||
// An absent state is exactly a row with no source, so it needs no second query.
|
||||
if s == "" {
|
||||
failed, err := q.CountBySource(k, []string{model.ArtworkSourceFailed})
|
||||
failed, err := q.CountBySource(ctx, k, []string{model.ArtworkSourceFailed})
|
||||
if err != nil {
|
||||
return rep, fmt.Errorf("counting failed %s artwork: %w", k, err)
|
||||
}
|
||||
|
|
@ -205,7 +205,7 @@ func collectStatus(ctx context.Context, ds model.DataStore) (statusReport, error
|
|||
}
|
||||
|
||||
rep.current, rep.inputs = artwork.ConfigFingerprint(), artwork.FingerprintInputs()
|
||||
if rep.stored, err = ds.Property(ctx).DefaultGet(consts.ArtConfFingerprintPropertyKey, ""); err != nil {
|
||||
if rep.stored, err = ds.Property().DefaultGet(ctx, consts.ArtConfFingerprintPropertyKey, ""); err != nil {
|
||||
return rep, fmt.Errorf("reading the stored artwork fingerprint: %w", err)
|
||||
}
|
||||
return rep, nil
|
||||
|
|
@ -442,13 +442,13 @@ func promptConfirm(in io.Reader, verb string) confirmFunc {
|
|||
|
||||
// validateSources rejects a typo'd source: matching nothing silently reads as "nothing to do" when
|
||||
// it means the filter was wrong. Checked table-wide, so a filter is never a typo for one --kind only.
|
||||
func validateSources(q model.ArtworkQueueRepository, sources []string) error {
|
||||
func validateSources(ctx context.Context, q model.ArtworkQueueRepository, sources []string) error {
|
||||
if len(sources) == 0 {
|
||||
return nil
|
||||
}
|
||||
var inUse []string
|
||||
for _, k := range artwork.ReprocessKinds {
|
||||
found, err := q.SourcesInUse(k)
|
||||
found, err := q.SourcesInUse(ctx, k)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing the sources in use by %s artwork: %w", k, err)
|
||||
}
|
||||
|
|
@ -475,8 +475,8 @@ func validateSources(q model.ArtworkQueueRepository, sources []string) error {
|
|||
// actually inserted; the two differ because an already-queued row is left untouched.
|
||||
func reprocessArtwork(ctx context.Context, ds model.DataStore, kinds []model.Kind, sources []string,
|
||||
imageAgents artwork.ImageAgentCount, dryRun bool, confirm confirmFunc, out io.Writer) error {
|
||||
q := ds.ArtworkQueue(ctx)
|
||||
if err := validateSources(q, sources); err != nil {
|
||||
q := ds.ArtworkQueue()
|
||||
if err := validateSources(ctx, q, sources); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -495,7 +495,7 @@ func reprocessArtwork(ctx context.Context, ds model.DataStore, kinds []model.Kin
|
|||
matched := make([]int64, len(kinds))
|
||||
var total, external int64
|
||||
for i, k := range kinds {
|
||||
n, err := q.CountBySource(k, sources)
|
||||
n, err := q.CountBySource(ctx, k, sources)
|
||||
if err != nil {
|
||||
return fmt.Errorf("counting %s artwork: %w", k, err)
|
||||
}
|
||||
|
|
@ -523,7 +523,7 @@ func reprocessArtwork(ctx context.Context, ds model.DataStore, kinds []model.Kin
|
|||
if matched[i] == 0 {
|
||||
continue
|
||||
}
|
||||
n, err := q.EnqueueBySource(k, sources, model.ArtworkPriorityRecheck)
|
||||
n, err := q.EnqueueBySource(ctx, k, sources, model.ArtworkPriorityRecheck)
|
||||
if err != nil {
|
||||
return fmt.Errorf("queueing %s artwork: %w", k, err)
|
||||
}
|
||||
|
|
@ -590,8 +590,8 @@ func parseAll[T comparable](values []string, parse func(string) (T, error)) ([]T
|
|||
|
||||
func cancelArtwork(ctx context.Context, ds model.DataStore, kinds []model.Kind, priorities []int,
|
||||
dryRun bool, confirm confirmFunc, out io.Writer) error {
|
||||
q := ds.ArtworkQueue(ctx)
|
||||
matched, err := q.CountQueued(kinds, priorities)
|
||||
q := ds.ArtworkQueue()
|
||||
matched, err := q.CountQueued(ctx, kinds, priorities)
|
||||
if err != nil {
|
||||
return fmt.Errorf("counting queued artwork: %w", err)
|
||||
}
|
||||
|
|
@ -612,7 +612,7 @@ func cancelArtwork(ctx context.Context, ds model.DataStore, kinds []model.Kind,
|
|||
return nil
|
||||
}
|
||||
|
||||
cancelled, err := q.PurgeQueued(kinds, priorities)
|
||||
cancelled, err := q.PurgeQueued(ctx, kinds, priorities)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cancelling queued artwork: %w", err)
|
||||
}
|
||||
|
|
@ -984,11 +984,11 @@ func runExplain(ctx context.Context, args []string) {
|
|||
}
|
||||
rep := explainReport{kind: kind, id: id, name: name}
|
||||
if artwork.KeepsState(kind) {
|
||||
rep.stored, err = ds.Artwork(ctx).GetItemArtwork(kind, id, model.ImageTypePrimary)
|
||||
rep.stored, err = ds.Artwork().GetItemArtwork(ctx, kind, id, model.ImageTypePrimary)
|
||||
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||
log.Fatal(ctx, "Failed to read artwork state", "kind", kind, "id", id, err)
|
||||
}
|
||||
rep.queued, err = ds.ArtworkQueue(ctx).Get(kind, id, model.ImageTypePrimary)
|
||||
rep.queued, err = ds.ArtworkQueue().Get(ctx, kind, id, model.ImageTypePrimary)
|
||||
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||
log.Fatal(ctx, "Failed to read the artwork queue", "kind", kind, "id", id, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -491,17 +491,17 @@ var _ = Describe("explain/reprocess source round trip", func() {
|
|||
|
||||
It("names the absent state as reprocess --source accepts it", func() {
|
||||
ds := &tests.MockDataStore{}
|
||||
art := ds.Artwork(ctx).(*tests.MockArtworkRepo)
|
||||
Expect(art.PutItemArtwork(&model.ItemArtwork{ItemKind: model.KindArtistArtwork.Prefix(),
|
||||
art := ds.Artwork().(*tests.MockArtworkRepo)
|
||||
Expect(art.PutItemArtwork(ctx, &model.ItemArtwork{ItemKind: model.KindArtistArtwork.Prefix(),
|
||||
ItemID: "ar-1", ImageType: model.ImageTypePrimary})).To(Succeed())
|
||||
|
||||
shown := storedSource(formatExplain(explainReport{kind: model.KindArtistArtwork, id: "ar-1",
|
||||
stored: &model.ItemArtwork{AttemptedAt: time.Now()}}))
|
||||
|
||||
q := ds.ArtworkQueue(ctx)
|
||||
Expect(validateSources(q, repositorySources([]string{shown}))).To(Succeed(),
|
||||
q := ds.ArtworkQueue()
|
||||
Expect(validateSources(ctx, q, repositorySources([]string{shown}))).To(Succeed(),
|
||||
"explain's spelling of a source must be pasteable into --source")
|
||||
Expect(validateSources(q, repositorySources([]string{"(" + shown + ")"}))).ToNot(Succeed(),
|
||||
Expect(validateSources(ctx, q, repositorySources([]string{"(" + shown + ")"}))).ToNot(Succeed(),
|
||||
"a parenthesised name would be rejected, so explain must not print one")
|
||||
})
|
||||
})
|
||||
|
|
@ -573,7 +573,7 @@ var _ = Describe("reprocessArtwork", func() {
|
|||
decline := func(io.Writer, int64, int64) bool { return false }
|
||||
|
||||
put := func(kind model.Kind, id, source string) {
|
||||
Expect(art.PutItemArtwork(&model.ItemArtwork{ItemKind: kind.Prefix(), ItemID: id,
|
||||
Expect(art.PutItemArtwork(ctx, &model.ItemArtwork{ItemKind: kind.Prefix(), ItemID: id,
|
||||
ImageType: model.ImageTypePrimary, Hash: "h" + id, Source: source})).To(Succeed())
|
||||
}
|
||||
|
||||
|
|
@ -584,8 +584,8 @@ var _ = Describe("reprocessArtwork", func() {
|
|||
conf.Server.EnableM3UExternalAlbumArt = false
|
||||
imageAgents = artwork.ImageAgentCount{Artist: 1, Album: 1}
|
||||
ds = &tests.MockDataStore{}
|
||||
art = ds.Artwork(ctx).(*tests.MockArtworkRepo)
|
||||
queue = ds.ArtworkQueue(ctx).(*tests.MockArtworkQueueRepo)
|
||||
art = ds.Artwork().(*tests.MockArtworkRepo)
|
||||
queue = ds.ArtworkQueue().(*tests.MockArtworkQueueRepo)
|
||||
out.Reset()
|
||||
put(model.KindArtistArtwork, "ar-1", "external:deezer")
|
||||
put(model.KindArtistArtwork, "ar-2", "")
|
||||
|
|
@ -601,19 +601,19 @@ var _ = Describe("reprocessArtwork", func() {
|
|||
Expect(out.String()).To(ContainSubstring("album"))
|
||||
Expect(out.String()).To(ContainSubstring("TOTAL"))
|
||||
Expect(out.String()).To(ContainSubstring("Dry run"))
|
||||
Expect(queue.Count()).To(BeZero())
|
||||
Expect(queue.Count(ctx)).To(BeZero())
|
||||
})
|
||||
|
||||
It("queues nothing when the operator declines", func() {
|
||||
Expect(reprocessArtwork(ctx, ds, kinds, nil, imageAgents, false, decline, &out)).To(Succeed())
|
||||
|
||||
Expect(out.String()).To(ContainSubstring("Aborted"))
|
||||
Expect(queue.Count()).To(BeZero())
|
||||
Expect(queue.Count(ctx)).To(BeZero())
|
||||
})
|
||||
|
||||
DescribeTable("records the applied config only for a run that leaves nothing on the old one",
|
||||
func(selected []model.Kind, sources []string, dryRun, applied bool) {
|
||||
Expect(ds.Property(ctx).Put(consts.ArtConfFingerprintPropertyKey, "stale-fingerprint")).To(Succeed())
|
||||
Expect(ds.Property().Put(ctx, consts.ArtConfFingerprintPropertyKey, "stale-fingerprint")).To(Succeed())
|
||||
|
||||
Expect(reprocessArtwork(ctx, ds, selected, sources, imageAgents, dryRun, accept, &out)).To(Succeed())
|
||||
|
||||
|
|
@ -621,7 +621,7 @@ var _ = Describe("reprocessArtwork", func() {
|
|||
if applied {
|
||||
want = artwork.ConfigFingerprint()
|
||||
}
|
||||
Expect(ds.Property(ctx).Get(consts.ArtConfFingerprintPropertyKey)).To(Equal(want))
|
||||
Expect(ds.Property().Get(ctx, consts.ArtConfFingerprintPropertyKey)).To(Equal(want))
|
||||
},
|
||||
Entry("every kind, unfiltered", artwork.ReprocessKinds, nil, false, true),
|
||||
Entry("every kind, but nothing matched", artwork.ReprocessKinds, []string{}, false, true),
|
||||
|
|
@ -633,14 +633,14 @@ var _ = Describe("reprocessArtwork", func() {
|
|||
It("queues the matching items at recheck priority, leaving their artwork state alone", func() {
|
||||
Expect(reprocessArtwork(ctx, ds, kinds, []string{"external:deezer"}, imageAgents, false, accept, &out)).To(Succeed())
|
||||
|
||||
Expect(queue.Count()).To(Equal(int64(2)))
|
||||
queued, err := queue.Get(model.KindAlbumArtwork, "al-1", model.ImageTypePrimary)
|
||||
Expect(queue.Count(ctx)).To(Equal(int64(2)))
|
||||
queued, err := queue.Get(ctx, model.KindAlbumArtwork, "al-1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(queued.Priority).To(Equal(model.ArtworkPriorityRecheck))
|
||||
_, err = queue.Get(model.KindAlbumArtwork, "al-2", model.ImageTypePrimary)
|
||||
_, err = queue.Get(ctx, model.KindAlbumArtwork, "al-2", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound), "a non-matching source must not be queued")
|
||||
|
||||
stored, err := art.GetItemArtwork(model.KindAlbumArtwork, "al-1", model.ImageTypePrimary)
|
||||
stored, err := art.GetItemArtwork(ctx, model.KindAlbumArtwork, "al-1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(stored.Hash).To(Equal("hal-1"), "bulk reprocessing must not blank the current artwork")
|
||||
})
|
||||
|
|
@ -648,20 +648,20 @@ var _ = Describe("reprocessArtwork", func() {
|
|||
It("targets the absent state", func() {
|
||||
Expect(reprocessArtwork(ctx, ds, kinds, []string{""}, imageAgents, false, accept, &out)).To(Succeed())
|
||||
|
||||
Expect(queue.Count()).To(Equal(int64(1)))
|
||||
_, err := queue.Get(model.KindArtistArtwork, "ar-2", model.ImageTypePrimary)
|
||||
Expect(queue.Count(ctx)).To(Equal(int64(1)))
|
||||
_, err := queue.Get(ctx, model.KindArtistArtwork, "ar-2", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("reports matched and queued separately when part of the set is already queued", func() {
|
||||
Expect(queue.Enqueue(model.ArtworkQueueItem{ItemKind: "ar", ItemID: "ar-1",
|
||||
Expect(queue.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "ar", ItemID: "ar-1",
|
||||
ImageType: model.ImageTypePrimary, Priority: model.ArtworkPriorityBump})).To(Succeed())
|
||||
|
||||
Expect(reprocessArtwork(ctx, ds, kinds, []string{"external:deezer"}, imageAgents, false, accept, &out)).To(Succeed())
|
||||
|
||||
Expect(out.String()).To(ContainSubstring("Queued 1 of 2 matched items"))
|
||||
Expect(out.String()).To(ContainSubstring("Already queued, left unchanged: 1"))
|
||||
queued, err := queue.Get(model.KindArtistArtwork, "ar-1", model.ImageTypePrimary)
|
||||
queued, err := queue.Get(ctx, model.KindArtistArtwork, "ar-1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(queued.Priority).To(Equal(model.ArtworkPriorityBump),
|
||||
"an already-queued row keeps its priority and backoff")
|
||||
|
|
@ -675,7 +675,7 @@ var _ = Describe("reprocessArtwork", func() {
|
|||
}, &out)).To(Succeed())
|
||||
|
||||
Expect(out.String()).To(ContainSubstring("Nothing"))
|
||||
Expect(queue.Count()).To(BeZero())
|
||||
Expect(queue.Count(ctx)).To(BeZero())
|
||||
})
|
||||
|
||||
It("reports an empty selection as a dry run when one was asked for", func() {
|
||||
|
|
@ -767,7 +767,7 @@ var _ = Describe("reprocessArtwork", func() {
|
|||
Expect(err.Error()).To(ContainSubstring("external:deezer"))
|
||||
Expect(err.Error()).To(ContainSubstring("folder"))
|
||||
Expect(err.Error()).To(ContainSubstring("absent"), "the empty source prints under its user-facing name")
|
||||
Expect(queue.Count()).To(BeZero())
|
||||
Expect(queue.Count(ctx)).To(BeZero())
|
||||
})
|
||||
|
||||
It("accepts the absent filter with nothing absent, still rejecting a typo", func() {
|
||||
|
|
@ -777,7 +777,7 @@ var _ = Describe("reprocessArtwork", func() {
|
|||
imageAgents, false, accept, &out)).To(Succeed(),
|
||||
"a reserved source must stay valid once the library has none of it")
|
||||
Expect(out.String()).To(ContainSubstring("Nothing matches"))
|
||||
Expect(queue.Count()).To(BeZero())
|
||||
Expect(queue.Count(ctx)).To(BeZero())
|
||||
|
||||
Expect(reprocessArtwork(ctx, ds, kinds, repositorySources([]string{"absnt"}),
|
||||
imageAgents, true, accept, &out)).ToNot(Succeed(), "a typo must still be rejected")
|
||||
|
|
@ -797,7 +797,7 @@ var _ = Describe("reprocessArtwork", func() {
|
|||
|
||||
Expect(out.String()).To(ContainSubstring("Nothing matches"),
|
||||
"a well-formed filter must not be reported as a typo because of the kinds selected")
|
||||
Expect(queue.Count()).To(BeZero())
|
||||
Expect(queue.Count(ctx)).To(BeZero())
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -816,19 +816,19 @@ var _ = Describe("collectStatus", func() {
|
|||
|
||||
BeforeEach(func() {
|
||||
ds = &tests.MockDataStore{}
|
||||
art = ds.Artwork(ctx).(*tests.MockArtworkRepo)
|
||||
queue = ds.ArtworkQueue(ctx).(*tests.MockArtworkQueueRepo)
|
||||
art = ds.Artwork().(*tests.MockArtworkRepo)
|
||||
queue = ds.ArtworkQueue().(*tests.MockArtworkQueueRepo)
|
||||
put := func(kind model.Kind, id, source, hash string, attempted time.Time) {
|
||||
Expect(art.PutItemArtwork(&model.ItemArtwork{ItemKind: kind.Prefix(), ItemID: id,
|
||||
Expect(art.PutItemArtwork(ctx, &model.ItemArtwork{ItemKind: kind.Prefix(), ItemID: id,
|
||||
ImageType: model.ImageTypePrimary, Source: source, Hash: hash, AttemptedAt: attempted})).To(Succeed())
|
||||
}
|
||||
put(model.KindArtistArtwork, "ar-1", "external:deezer", "h1", time.Now())
|
||||
put(model.KindArtistArtwork, "ar-2", "", "", time.Now().Add(-24*time.Hour))
|
||||
// ar-3 is absent because it gave up, so the two absent artists split across the columns.
|
||||
Expect(art.PutItemArtwork(&model.ItemArtwork{ItemKind: "ar", ItemID: "ar-3",
|
||||
Expect(art.PutItemArtwork(ctx, &model.ItemArtwork{ItemKind: "ar", ItemID: "ar-3",
|
||||
ImageType: model.ImageTypePrimary, LastFailure: "[]", AttemptedAt: time.Now()})).To(Succeed())
|
||||
put(model.KindAlbumArtwork, "al-1", "folder", "h2", time.Now())
|
||||
Expect(queue.Enqueue(model.ArtworkQueueItem{ItemKind: "ar", ItemID: "ar-9",
|
||||
Expect(queue.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "ar", ItemID: "ar-9",
|
||||
ImageType: model.ImageTypePrimary, Priority: model.ArtworkPriorityBackfill})).To(Succeed())
|
||||
})
|
||||
|
||||
|
|
@ -848,7 +848,7 @@ var _ = Describe("collectStatus", func() {
|
|||
})
|
||||
|
||||
It("compares the stored fingerprint against the current one", func() {
|
||||
Expect(ds.Property(ctx).Put(consts.ArtConfFingerprintPropertyKey, "old-fingerprint")).To(Succeed())
|
||||
Expect(ds.Property().Put(ctx, consts.ArtConfFingerprintPropertyKey, "old-fingerprint")).To(Succeed())
|
||||
|
||||
rep, err := collectStatus(ctx, ds)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -860,7 +860,7 @@ var _ = Describe("collectStatus", func() {
|
|||
It("queues nothing", func() {
|
||||
_, err := collectStatus(ctx, ds)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(queue.Count()).To(Equal(int64(1)), "status must not enqueue anything")
|
||||
Expect(queue.Count(ctx)).To(Equal(int64(1)), "status must not enqueue anything")
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -973,21 +973,21 @@ var _ = Describe("refreshItems", func() {
|
|||
albums := tests.CreateMockAlbumRepo()
|
||||
albums.SetData(model.Albums{{ID: "al-1"}, {ID: "al-3"}})
|
||||
ds = &tests.MockDataStore{MockedAlbum: albums}
|
||||
art = ds.Artwork(ctx).(*tests.MockArtworkRepo)
|
||||
queue = ds.ArtworkQueue(ctx).(*tests.MockArtworkQueueRepo)
|
||||
art = ds.Artwork().(*tests.MockArtworkRepo)
|
||||
queue = ds.ArtworkQueue().(*tests.MockArtworkQueueRepo)
|
||||
out.Reset()
|
||||
})
|
||||
|
||||
It("clears the stored state and queues each id at Bump priority", func() {
|
||||
Expect(art.PutItemArtwork(&model.ItemArtwork{ItemKind: model.KindAlbumArtwork.Prefix(),
|
||||
Expect(art.PutItemArtwork(ctx, &model.ItemArtwork{ItemKind: model.KindAlbumArtwork.Prefix(),
|
||||
ItemID: "al-1", ImageType: model.ImageTypePrimary, Hash: "abc123"})).To(Succeed())
|
||||
|
||||
Expect(refreshItems(ctx, ds, []model.ArtworkID{
|
||||
{Kind: model.KindAlbumArtwork, ID: "al-1"}, {Kind: model.KindAlbumArtwork, ID: "al-3"}}, &out)).To(BeZero())
|
||||
|
||||
_, err := art.GetItemArtwork(model.KindAlbumArtwork, "al-1", model.ImageTypePrimary)
|
||||
_, err := art.GetItemArtwork(ctx, model.KindAlbumArtwork, "al-1", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
queued, err := queue.Get(model.KindAlbumArtwork, "al-1", model.ImageTypePrimary)
|
||||
queued, err := queue.Get(ctx, model.KindAlbumArtwork, "al-1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(queued.Priority).To(Equal(model.ArtworkPriorityBump))
|
||||
Expect(out.String()).To(Equal("al/al-1: queued\nal/al-3: queued\n"))
|
||||
|
|
@ -996,7 +996,7 @@ var _ = Describe("refreshItems", func() {
|
|||
It("skips an id that does not exist instead of queuing it", func() {
|
||||
Expect(refreshItems(ctx, ds, []model.ArtworkID{{Kind: model.KindAlbumArtwork, ID: "al-2"}}, &out)).To(Equal(1))
|
||||
|
||||
_, err := queue.Get(model.KindAlbumArtwork, "al-2", model.ImageTypePrimary)
|
||||
_, err := queue.Get(ctx, model.KindAlbumArtwork, "al-2", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound), "a typo must not leave an orphan queue row")
|
||||
Expect(out.String()).To(BeEmpty())
|
||||
})
|
||||
|
|
@ -1145,9 +1145,9 @@ var _ = Describe("cancelArtwork", func() {
|
|||
|
||||
BeforeEach(func() {
|
||||
ds = &tests.MockDataStore{}
|
||||
queue = ds.ArtworkQueue(ctx).(*tests.MockArtworkQueueRepo)
|
||||
queue = ds.ArtworkQueue().(*tests.MockArtworkQueueRepo)
|
||||
out.Reset()
|
||||
Expect(queue.Enqueue(
|
||||
Expect(queue.Enqueue(ctx,
|
||||
model.ArtworkQueueItem{ItemKind: "ar", ItemID: "ar-1", ImageType: model.ImageTypePrimary,
|
||||
Priority: model.ArtworkPriorityBackfill},
|
||||
model.ArtworkQueueItem{ItemKind: "ar", ItemID: "ar-2", ImageType: model.ImageTypePrimary,
|
||||
|
|
@ -1164,28 +1164,28 @@ var _ = Describe("cancelArtwork", func() {
|
|||
Expect(out.String()).To(ContainSubstring("backfill"))
|
||||
Expect(out.String()).To(ContainSubstring("TOTAL"))
|
||||
Expect(out.String()).To(ContainSubstring("Dry run"))
|
||||
Expect(queue.Count()).To(BeNumerically("==", 3))
|
||||
Expect(queue.Count(ctx)).To(BeNumerically("==", 3))
|
||||
})
|
||||
|
||||
It("cancels nothing when the operator declines", func() {
|
||||
Expect(cancelArtwork(ctx, ds, nil, nil, false, decline, &out)).To(Succeed())
|
||||
|
||||
Expect(out.String()).To(ContainSubstring("Aborted"))
|
||||
Expect(queue.Count()).To(BeNumerically("==", 3))
|
||||
Expect(queue.Count(ctx)).To(BeNumerically("==", 3))
|
||||
})
|
||||
|
||||
It("deletes the selected rows and leaves the rest queued", func() {
|
||||
Expect(cancelArtwork(ctx, ds, nil, []int{model.ArtworkPriorityBackfill}, false, accept, &out)).To(Succeed())
|
||||
|
||||
Expect(queue.Count()).To(BeNumerically("==", 1))
|
||||
_, err := queue.Get(model.KindArtistArtwork, "ar-2", model.ImageTypePrimary)
|
||||
Expect(queue.Count(ctx)).To(BeNumerically("==", 1))
|
||||
_, err := queue.Get(ctx, model.KindArtistArtwork, "ar-2", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred(), "a non-matching priority must stay queued")
|
||||
Expect(out.String()).To(ContainSubstring("Cancelled 2 of 2 matched items."))
|
||||
})
|
||||
|
||||
It("cancels every kind and priority when neither filter is given", func() {
|
||||
Expect(cancelArtwork(ctx, ds, nil, nil, false, accept, &out)).To(Succeed())
|
||||
Expect(queue.Count()).To(BeZero())
|
||||
Expect(queue.Count(ctx)).To(BeZero())
|
||||
})
|
||||
|
||||
It("stops at a selection that matches nothing instead of prompting", func() {
|
||||
|
|
@ -1196,7 +1196,7 @@ var _ = Describe("cancelArtwork", func() {
|
|||
Expect(cancelArtwork(ctx, ds, []model.Kind{model.KindPlaylistArtwork}, nil, false, refuse, &out)).To(Succeed())
|
||||
|
||||
Expect(out.String()).To(ContainSubstring("Nothing matches this selection."))
|
||||
Expect(queue.Count()).To(BeNumerically("==", 3))
|
||||
Expect(queue.Count(ctx)).To(BeNumerically("==", 3))
|
||||
})
|
||||
|
||||
It("reports a queue read failure instead of reporting nothing to cancel", func() {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ func runMissingList(ctx context.Context) {
|
|||
}
|
||||
|
||||
ds, ctx := getAdminContext(ctx)
|
||||
mfs, err := ds.MediaFile(ctx).GetCursor(model.QueryOptions{
|
||||
mfs, err := ds.MediaFile().GetCursor(ctx, model.QueryOptions{
|
||||
Filters: squirrel.Eq{"missing": true},
|
||||
Sort: "path",
|
||||
})
|
||||
|
|
@ -128,7 +128,7 @@ func runMissingFix(ctx context.Context, missingRef, targetRef string) {
|
|||
|
||||
// resolveMediaFile looks up a media file by ID first, then by path (optionally libraryID:path).
|
||||
func resolveMediaFile(ctx context.Context, ds model.DataStore, ref string) *model.MediaFile {
|
||||
mf, err := ds.MediaFile(ctx).Get(ref)
|
||||
mf, err := ds.MediaFile().Get(ctx, ref)
|
||||
if err == nil {
|
||||
return mf
|
||||
}
|
||||
|
|
@ -136,7 +136,7 @@ func resolveMediaFile(ctx context.Context, ds model.DataStore, ref string) *mode
|
|||
log.Fatal(ctx, "Error looking up media file", "ref", ref, err)
|
||||
}
|
||||
|
||||
mfs, err := ds.MediaFile(ctx).FindByPaths([]string{ref})
|
||||
mfs, err := ds.MediaFile().FindByPaths(ctx, []string{ref})
|
||||
if err != nil {
|
||||
log.Fatal(ctx, "Error looking up media file by path", "ref", ref, err)
|
||||
}
|
||||
|
|
|
|||
10
cmd/pls.go
10
cmd/pls.go
|
|
@ -109,7 +109,7 @@ func fetchPlaylists(ctx context.Context, ds model.DataStore, sort string) model.
|
|||
}
|
||||
options.Filters = squirrel.Eq{"owner_id": user.ID}
|
||||
}
|
||||
pls, err := ds.Playlist(ctx).GetAll(options)
|
||||
pls, err := ds.Playlist().GetAll(ctx, options)
|
||||
if err != nil {
|
||||
log.Fatal(ctx, "Failed to retrieve playlists", err)
|
||||
}
|
||||
|
|
@ -117,17 +117,17 @@ func fetchPlaylists(ctx context.Context, ds model.DataStore, sort string) model.
|
|||
}
|
||||
|
||||
func findPlaylist(ctx context.Context, ds model.DataStore, nameOrID string) *model.Playlist {
|
||||
playlist, err := ds.Playlist(ctx).GetWithTracks(nameOrID, true, false)
|
||||
playlist, err := ds.Playlist().GetWithTracks(ctx, nameOrID, true, false)
|
||||
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||
log.Fatal("Error retrieving playlist", "name", nameOrID, err)
|
||||
}
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
playlists, err := ds.Playlist(ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"playlist.name": nameOrID}})
|
||||
playlists, err := ds.Playlist().GetAll(ctx, model.QueryOptions{Filters: squirrel.Eq{"playlist.name": nameOrID}})
|
||||
if err != nil {
|
||||
log.Fatal("Error retrieving playlist", "name", nameOrID, err)
|
||||
}
|
||||
if len(playlists) > 0 {
|
||||
playlist, err = ds.Playlist(ctx).GetWithTracks(playlists[0].ID, true, false)
|
||||
playlist, err = ds.Playlist().GetWithTracks(ctx, playlists[0].ID, true, false)
|
||||
if err != nil {
|
||||
log.Fatal("Error retrieving playlist", "name", nameOrID, err)
|
||||
}
|
||||
|
|
@ -194,7 +194,7 @@ func runExport(ctx context.Context) {
|
|||
|
||||
exported := 0
|
||||
for _, pls := range allPls {
|
||||
plsWithTracks, err := ds.Playlist(ctx).GetWithTracks(pls.ID, true, false)
|
||||
plsWithTracks, err := ds.Playlist().GetWithTracks(ctx, pls.ID, true, false)
|
||||
if err != nil {
|
||||
log.Error("Error loading playlist tracks", "playlist", pls.Name, err)
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ func runPluginInfo(ctx context.Context, arg string) {
|
|||
}
|
||||
requirePluginsEnabled(ctx)
|
||||
ds, ctx := getAdminContext(ctx)
|
||||
p, err := ds.Plugin(ctx).Get(arg)
|
||||
p, err := ds.Plugin().Get(ctx, arg)
|
||||
if err != nil {
|
||||
log.Fatal(ctx, "Plugin not found", "id", arg, err)
|
||||
}
|
||||
|
|
@ -264,7 +264,7 @@ func runPluginValidate(ctx context.Context, arg string) {
|
|||
}
|
||||
requirePluginsEnabled(ctx)
|
||||
ds, ctx := getAdminContext(ctx)
|
||||
p, err := ds.Plugin(ctx).Get(arg)
|
||||
p, err := ds.Plugin().Get(ctx, arg)
|
||||
if err != nil {
|
||||
log.Fatal(ctx, "Plugin not found", "id", arg, err)
|
||||
}
|
||||
|
|
@ -329,7 +329,7 @@ func formatPluginList(list model.Plugins, format string) (string, error) {
|
|||
func runPluginList(ctx context.Context) {
|
||||
requirePluginsEnabled(ctx)
|
||||
ds, ctx := getAdminContext(ctx)
|
||||
list, err := ds.Plugin(ctx).GetAll()
|
||||
list, err := ds.Plugin().GetAll(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(ctx, "Failed to list plugins", err)
|
||||
}
|
||||
|
|
@ -372,7 +372,7 @@ var pluginEditCmd = &cobra.Command{
|
|||
Run: func(cmd *cobra.Command, args []string) {
|
||||
requirePluginsEnabled(cmd.Context())
|
||||
ds, ctx := getAdminContext(cmd.Context())
|
||||
cur, err := ds.Plugin(ctx).Get(args[0])
|
||||
cur, err := ds.Plugin().Get(ctx, args[0])
|
||||
if err != nil {
|
||||
log.Fatal(ctx, "Plugin not found", "id", args[0], err)
|
||||
}
|
||||
|
|
|
|||
10
cmd/root.go
10
cmd/root.go
|
|
@ -184,11 +184,11 @@ func schedulePeriodicScan(ctx context.Context) func() error {
|
|||
}
|
||||
|
||||
func pidHashChanged(ds model.DataStore) (bool, error) {
|
||||
pidAlbum, err := ds.Property(context.Background()).DefaultGet(consts.PIDAlbumKey, "")
|
||||
pidAlbum, err := ds.Property().DefaultGet(context.Background(), consts.PIDAlbumKey, "")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
pidTrack, err := ds.Property(context.Background()).DefaultGet(consts.PIDTrackKey, "")
|
||||
pidTrack, err := ds.Property().DefaultGet(context.Background(), consts.PIDTrackKey, "")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
|
@ -199,11 +199,11 @@ func pidHashChanged(ds model.DataStore) (bool, error) {
|
|||
func runInitialScan(ctx context.Context) func() error {
|
||||
return func() error {
|
||||
ds := CreateDataStore()
|
||||
fullScanRequired, err := ds.Property(ctx).DefaultGet(consts.FullScanAfterMigrationFlagKey, "0")
|
||||
fullScanRequired, err := ds.Property().DefaultGet(ctx, consts.FullScanAfterMigrationFlagKey, "0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inProgress, err := ds.Library(ctx).ScanInProgress()
|
||||
inProgress, err := ds.Library().ScanInProgress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -219,7 +219,7 @@ func runInitialScan(ctx context.Context) func() error {
|
|||
switch {
|
||||
case fullScanRequired == "1":
|
||||
log.Warn(ctx, "Full scan required after migration")
|
||||
_ = ds.Property(ctx).Delete(consts.FullScanAfterMigrationFlagKey)
|
||||
_ = ds.Property().Delete(ctx, consts.FullScanAfterMigrationFlagKey)
|
||||
case pidHasChanged:
|
||||
log.Warn(ctx, "PID config changed, performing full scan")
|
||||
fullScanRequired = "1"
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ var svcCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
type svcControl struct {
|
||||
ctx context.Context
|
||||
ctx context.Context //nolint:containedctx // service lifecycle ctx, cancelled by Stop
|
||||
cancel context.CancelFunc
|
||||
done chan struct{}
|
||||
}
|
||||
|
|
|
|||
26
cmd/user.go
26
cmd/user.go
|
|
@ -183,7 +183,7 @@ func runCreateUser(ctx context.Context) {
|
|||
ds, ctx := getAdminContext(ctx)
|
||||
|
||||
err := ds.WithTx(func(tx model.DataStore) error {
|
||||
existingUser, err := tx.User(ctx).FindByUsername(userID)
|
||||
existingUser, err := tx.User().FindByUsername(ctx, userID)
|
||||
if existingUser != nil {
|
||||
return fmt.Errorf("existing user '%s'", userID)
|
||||
}
|
||||
|
|
@ -193,7 +193,7 @@ func runCreateUser(ctx context.Context) {
|
|||
}
|
||||
|
||||
if len(libraryIds) > 0 && !setAdmin {
|
||||
user.Libraries, err = tx.Library(ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"id": libraryIds}})
|
||||
user.Libraries, err = tx.Library().GetAll(ctx, model.QueryOptions{Filters: squirrel.Eq{"id": libraryIds}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -202,13 +202,13 @@ func runCreateUser(ctx context.Context) {
|
|||
return libraryError(user.Libraries)
|
||||
}
|
||||
} else {
|
||||
user.Libraries, err = tx.Library(ctx).GetAll()
|
||||
user.Libraries, err = tx.Library().GetAll(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.User(ctx).Put(&user)
|
||||
err = tx.User().Put(ctx, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -218,7 +218,7 @@ func runCreateUser(ctx context.Context) {
|
|||
updatedIds[idx] = lib.ID
|
||||
}
|
||||
|
||||
err = tx.User(ctx).SetUserLibraries(user.ID, updatedIds)
|
||||
err = tx.User().SetUserLibraries(ctx, user.ID, updatedIds)
|
||||
return err
|
||||
})
|
||||
|
||||
|
|
@ -236,7 +236,7 @@ func runDeleteUser(ctx context.Context) {
|
|||
var user *model.User
|
||||
|
||||
err = ds.WithTx(func(tx model.DataStore) error {
|
||||
count, err := tx.User(ctx).CountAll()
|
||||
count, err := tx.User().CountAll(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -250,7 +250,7 @@ func runDeleteUser(ctx context.Context) {
|
|||
return err
|
||||
}
|
||||
|
||||
return tx.User(ctx).Delete(user.ID)
|
||||
return tx.User().Delete(ctx, user.ID)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -276,7 +276,7 @@ func runUserEdit(ctx context.Context) {
|
|||
}
|
||||
|
||||
if len(libraryIds) > 0 && !setAdmin {
|
||||
libraries, err := tx.Library(ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"id": libraryIds}})
|
||||
libraries, err := tx.Library().GetAll(ctx, model.QueryOptions{Filters: squirrel.Eq{"id": libraryIds}})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -291,7 +291,7 @@ func runUserEdit(ctx context.Context) {
|
|||
}
|
||||
|
||||
if setAdmin && !user.IsAdmin {
|
||||
libraries, err := tx.Library(ctx).GetAll()
|
||||
libraries, err := tx.Library().GetAll(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -337,7 +337,7 @@ func runUserEdit(ctx context.Context) {
|
|||
return nil
|
||||
}
|
||||
|
||||
err := tx.User(ctx).Put(user)
|
||||
err := tx.User().Put(ctx, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -348,7 +348,7 @@ func runUserEdit(ctx context.Context) {
|
|||
updatedIds[idx] = lib.ID
|
||||
}
|
||||
|
||||
err := tx.User(ctx).SetUserLibraries(user.ID, updatedIds)
|
||||
err := tx.User().SetUserLibraries(ctx, user.ID, updatedIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -393,13 +393,11 @@ func runUserList(ctx context.Context) {
|
|||
|
||||
ds, ctx := getAdminContext(ctx)
|
||||
|
||||
users, err := ds.User(ctx).ReadAll()
|
||||
userList, err := ds.User().ReadAll(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(ctx, "Failed to retrieve users", err)
|
||||
}
|
||||
|
||||
userList := users.(model.Users)
|
||||
|
||||
if outputFormat == "csv" {
|
||||
w := csv.NewWriter(os.Stdout)
|
||||
_ = w.Write([]string{
|
||||
|
|
|
|||
|
|
@ -52,14 +52,14 @@ func getAdminContext(ctx context.Context) (model.DataStore, context.Context) {
|
|||
}
|
||||
|
||||
func getUser(ctx context.Context, id string, ds model.DataStore) (*model.User, error) {
|
||||
user, err := ds.User(ctx).FindByUsername(id)
|
||||
user, err := ds.User().FindByUsername(ctx, id)
|
||||
|
||||
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||
return nil, fmt.Errorf("finding user by name: %w", err)
|
||||
}
|
||||
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
user, err = ds.User(ctx).Get(id)
|
||||
user, err = ds.User().Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("finding user by id: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ func (p *localAgent) AgentName() string {
|
|||
}
|
||||
|
||||
func (p *localAgent) GetArtistTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]Song, error) {
|
||||
top, err := p.ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
top, err := p.ds.MediaFile().GetAll(ctx, model.QueryOptions{
|
||||
Sort: "playCount",
|
||||
Order: "desc",
|
||||
Max: count,
|
||||
|
|
@ -43,7 +43,7 @@ func (p *localAgent) GetArtistTopSongs(ctx context.Context, id, artistName, mbid
|
|||
}
|
||||
|
||||
func (p *localAgent) GetSimilarSongsByTrack(ctx context.Context, id, name, artist, mbid string, count int) ([]Song, error) {
|
||||
seed, err := p.ds.MediaFile(ctx).Get(id)
|
||||
seed, err := p.ds.MediaFile().Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ func (p *localAgent) GetSimilarSongsByTrack(ctx context.Context, id, name, artis
|
|||
return nil, nil
|
||||
}
|
||||
// Ask for extra so we can drop the seed itself and still fill the count.
|
||||
candidates, err := p.ds.MediaFile(ctx).GetRandom(model.QueryOptions{
|
||||
candidates, err := p.ds.MediaFile().GetRandom(ctx, model.QueryOptions{
|
||||
Filters: squirrel.And{
|
||||
persistence.SongGenres.ByID(genreIDs),
|
||||
squirrel.Eq{"missing": false},
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ type SessionKeys struct {
|
|||
}
|
||||
|
||||
func (sk *SessionKeys) Put(ctx context.Context, userId, sessionKey string) error {
|
||||
return sk.DataStore.UserProps(ctx).Put(userId, sk.KeyName, sessionKey)
|
||||
return sk.DataStore.UserProps().Put(ctx, userId, sk.KeyName, sessionKey)
|
||||
}
|
||||
|
||||
func (sk *SessionKeys) Get(ctx context.Context, userId string) (string, error) {
|
||||
return sk.DataStore.UserProps(ctx).Get(userId, sk.KeyName)
|
||||
return sk.DataStore.UserProps().Get(ctx, userId, sk.KeyName)
|
||||
}
|
||||
|
||||
func (sk *SessionKeys) Delete(ctx context.Context, userId string) error {
|
||||
return sk.DataStore.UserProps(ctx).Delete(userId, sk.KeyName)
|
||||
return sk.DataStore.UserProps().Delete(ctx, userId, sk.KeyName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ func (a *archiver) ZipArtist(ctx context.Context, id string, format string, bitr
|
|||
|
||||
// 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"})
|
||||
mfs, err := a.ds.MediaFile().GetAll(ctx, model.QueryOptions{Filters: filters, Sort: "album"})
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error loading mediafiles from artist", "id", id, err)
|
||||
return err
|
||||
|
|
@ -189,7 +189,7 @@ func (a *archiver) ZipShare(ctx context.Context, s *model.Share, out io.Writer)
|
|||
}
|
||||
|
||||
func (a *archiver) ZipPlaylist(ctx context.Context, id string, format string, bitrate int, out io.Writer) error {
|
||||
pls, err := a.ds.Playlist(ctx).GetWithTracks(id, true, false)
|
||||
pls, err := a.ds.Playlist().GetWithTracks(ctx, id, true, false)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error loading mediafiles from playlist", "id", id, err)
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ var _ = Describe("Archiver", func() {
|
|||
Sort: "album",
|
||||
}}).Return(mfs, nil)
|
||||
|
||||
ds.On("MediaFile", mock.Anything).Return(mfRepo)
|
||||
ds.On("MediaFile").Return(mfRepo)
|
||||
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(3)
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
|
|
@ -84,7 +84,7 @@ var _ = Describe("Archiver", func() {
|
|||
Sort: "album",
|
||||
}}).Return(mfs, nil)
|
||||
|
||||
ds.On("MediaFile", mock.Anything).Return(mfRepo)
|
||||
ds.On("MediaFile").Return(mfRepo)
|
||||
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
|
|
@ -246,7 +246,7 @@ var _ = Describe("Archiver", func() {
|
|||
Filters: squirrel.Eq{"album_id": "1"},
|
||||
Sort: "album",
|
||||
}}).Return(mfs, nil)
|
||||
ds.On("MediaFile", mock.Anything).Return(mfRepo)
|
||||
ds.On("MediaFile").Return(mfRepo)
|
||||
|
||||
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).
|
||||
Return(nil, stream.ErrTooManyTranscodes).Once()
|
||||
|
|
@ -310,7 +310,7 @@ var _ = Describe("Archiver", func() {
|
|||
|
||||
plRepo := &mockPlaylistRepository{}
|
||||
plRepo.On("GetWithTracks", "1", true, false).Return(pls, nil)
|
||||
ds.On("Playlist", mock.Anything).Return(plRepo)
|
||||
ds.On("Playlist").Return(plRepo)
|
||||
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
|
|
@ -515,17 +515,17 @@ type mockDataStore struct {
|
|||
model.DataStore
|
||||
}
|
||||
|
||||
func (m *mockDataStore) MediaFile(ctx context.Context) model.MediaFileRepository {
|
||||
args := m.Called(ctx)
|
||||
func (m *mockDataStore) MediaFile() model.MediaFileRepository {
|
||||
args := m.Called()
|
||||
return args.Get(0).(model.MediaFileRepository)
|
||||
}
|
||||
|
||||
func (m *mockDataStore) Playlist(ctx context.Context) model.PlaylistRepository {
|
||||
args := m.Called(ctx)
|
||||
func (m *mockDataStore) Playlist() model.PlaylistRepository {
|
||||
args := m.Called()
|
||||
return args.Get(0).(model.PlaylistRepository)
|
||||
}
|
||||
|
||||
func (m *mockDataStore) Library(context.Context) model.LibraryRepository {
|
||||
func (m *mockDataStore) Library() model.LibraryRepository {
|
||||
return &mockLibraryRepository{}
|
||||
}
|
||||
|
||||
|
|
@ -534,7 +534,7 @@ type mockLibraryRepository struct {
|
|||
model.LibraryRepository
|
||||
}
|
||||
|
||||
func (m *mockLibraryRepository) GetPath(id int) (string, error) {
|
||||
func (m *mockLibraryRepository) GetPath(_ context.Context, id int) (string, error) {
|
||||
return "/music", nil
|
||||
}
|
||||
|
||||
|
|
@ -543,7 +543,7 @@ type mockMediaFileRepository struct {
|
|||
model.MediaFileRepository
|
||||
}
|
||||
|
||||
func (m *mockMediaFileRepository) GetAll(options ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
func (m *mockMediaFileRepository) GetAll(ctx context.Context, options ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
args := m.Called(options)
|
||||
return args.Get(0).(model.MediaFiles), args.Error(1)
|
||||
}
|
||||
|
|
@ -553,7 +553,7 @@ type mockPlaylistRepository struct {
|
|||
model.PlaylistRepository
|
||||
}
|
||||
|
||||
func (m *mockPlaylistRepository) GetWithTracks(id string, refreshSmartPlaylists, includeMissing bool) (*model.Playlist, error) {
|
||||
func (m *mockPlaylistRepository) GetWithTracks(_ context.Context, id string, refreshSmartPlaylists, includeMissing bool) (*model.Playlist, error) {
|
||||
args := m.Called(id, refreshSmartPlaylists, includeMissing)
|
||||
return args.Get(0).(*model.Playlist), args.Error(1)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,21 +59,21 @@ func entityExists(ctx context.Context, ds model.DataStore, artID model.ArtworkID
|
|||
var err error
|
||||
switch artID.Kind {
|
||||
case model.KindArtistArtwork:
|
||||
found, err = ds.Artist(ctx).Exists(artID.ID)
|
||||
found, err = ds.Artist().Exists(ctx, artID.ID)
|
||||
case model.KindAlbumArtwork:
|
||||
found, err = ds.Album(ctx).Exists(artID.ID)
|
||||
found, err = ds.Album().Exists(ctx, artID.ID)
|
||||
case model.KindMediaFileArtwork:
|
||||
found, err = ds.MediaFile(ctx).Exists(artID.ID)
|
||||
found, err = ds.MediaFile().Exists(ctx, artID.ID)
|
||||
case model.KindPlaylistArtwork:
|
||||
found, err = ds.Playlist(ctx).Exists(artID.ID)
|
||||
found, err = ds.Playlist().Exists(ctx, artID.ID)
|
||||
case model.KindRadioArtwork:
|
||||
found, err = ds.Radio(ctx).Exists(artID.ID)
|
||||
found, err = ds.Radio().Exists(ctx, artID.ID)
|
||||
case model.KindDiscArtwork:
|
||||
albumID, _, perr := model.ParseDiscArtworkID(artID.ID)
|
||||
if perr != nil {
|
||||
return false
|
||||
}
|
||||
found, err = ds.Album(ctx).Exists(albumID)
|
||||
found, err = ds.Album().Exists(ctx, albumID)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
|
@ -119,7 +119,7 @@ func (s *service) Get(ctx context.Context, artID model.ArtworkID, size int, squa
|
|||
}
|
||||
|
||||
func (s *service) serveEntity(ctx context.Context, artID model.ArtworkID, size int, square bool) (*Image, error) {
|
||||
ia, err := s.ds.Artwork(ctx).GetItemArtwork(artID.Kind, artID.ID, model.ImageTypePrimary)
|
||||
ia, err := s.ds.Artwork().GetItemArtwork(ctx, artID.Kind, artID.ID, model.ImageTypePrimary)
|
||||
switch {
|
||||
case errors.Is(err, model.ErrNotFound):
|
||||
return s.provisional(ctx, artID, size, square)
|
||||
|
|
@ -173,7 +173,7 @@ func (s *service) serveHash(ctx context.Context, artID model.ArtworkID, ia *mode
|
|||
log.Warn(ctx, "Artwork: Stored source is not an image file, re-resolving", "artID", artID, "path", ia.SourcePath)
|
||||
return s.dangling(ctx, artID)
|
||||
}
|
||||
art, err := s.ds.Artwork(ctx).GetImage(ia.Hash)
|
||||
art, err := s.ds.Artwork().GetImage(ctx, ia.Hash)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return s.dangling(ctx, artID)
|
||||
|
|
@ -264,13 +264,13 @@ func (s *service) serveMediaFile(ctx context.Context, artID model.ArtworkID, siz
|
|||
// The setting is not in the config fingerprint, so honor it at serve time: a direct mf- URL
|
||||
// must fall back to disc/album instead of serving stale persisted embedded art.
|
||||
if !conf.Server.EnableMediaFileCoverArt {
|
||||
mf, err := s.ds.MediaFile(ctx).Get(artID.ID)
|
||||
mf, err := s.ds.MediaFile().Get(ctx, artID.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.Get(ctx, mf.DiscCoverArtID(), size, square)
|
||||
}
|
||||
ia, err := s.ds.Artwork(ctx).GetItemArtwork(model.KindMediaFileArtwork, artID.ID, model.ImageTypePrimary)
|
||||
ia, err := s.ds.Artwork().GetItemArtwork(ctx, model.KindMediaFileArtwork, artID.ID, model.ImageTypePrimary)
|
||||
switch {
|
||||
case err == nil && ia.Hash != "":
|
||||
return s.serveHash(ctx, artID, ia, size, square)
|
||||
|
|
@ -283,7 +283,7 @@ func (s *service) serveMediaFile(ctx context.Context, artID model.ArtworkID, siz
|
|||
}
|
||||
noRow := errors.Is(err, model.ErrNotFound)
|
||||
|
||||
mf, err := s.ds.MediaFile(ctx).Get(artID.ID)
|
||||
mf, err := s.ds.MediaFile().Get(ctx, artID.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -342,7 +342,7 @@ func (s *service) dangling(ctx context.Context, artID model.ArtworkID) (*Image,
|
|||
}
|
||||
|
||||
func (s *service) enqueue(ctx context.Context, artID model.ArtworkID, priority int) {
|
||||
err := s.ds.ArtworkQueue(ctx).EnqueuePreservingBackoff(model.ArtworkQueueItem{
|
||||
err := s.ds.ArtworkQueue().EnqueuePreservingBackoff(ctx, model.ArtworkQueueItem{
|
||||
ItemKind: artID.Kind.Prefix(),
|
||||
ItemID: artID.ID,
|
||||
ImageType: model.ImageTypePrimary,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package artwork
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
|
|
@ -108,15 +109,15 @@ type fakeFolderRepo struct {
|
|||
otherAudioErr error
|
||||
}
|
||||
|
||||
func (f *fakeFolderRepo) GetAll(...model.QueryOptions) ([]model.Folder, error) {
|
||||
func (f *fakeFolderRepo) GetAll(context.Context, ...model.QueryOptions) ([]model.Folder, error) {
|
||||
return f.result, f.err
|
||||
}
|
||||
|
||||
func (f *fakeFolderRepo) HasAudioOutsideFolders(model.Folder, []string) (bool, error) {
|
||||
func (f *fakeFolderRepo) HasAudioOutsideFolders(context.Context, model.Folder, []string) (bool, error) {
|
||||
return f.hasOtherAudio, f.otherAudioErr
|
||||
}
|
||||
|
||||
func (f *fakeFolderRepo) Get(string) (*model.Folder, error) {
|
||||
func (f *fakeFolderRepo) Get(context.Context, string) (*model.Folder, error) {
|
||||
f.getCallCount++
|
||||
if f.getErr != nil {
|
||||
return nil, f.getErr
|
||||
|
|
|
|||
|
|
@ -45,8 +45,8 @@ var _ = Describe("Artwork", func() {
|
|||
hash, err := hashImage(bytes.NewReader(imgBytes))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(store.Write(hash, "image/jpeg", bytes.NewReader(imgBytes))).To(Succeed())
|
||||
Expect(artRepo.PutImage(&model.Artwork{Hash: hash, Mime: "image/jpeg"})).To(Succeed())
|
||||
Expect(artRepo.PutItemArtwork(&model.ItemArtwork{ItemKind: kind, ItemID: id, Hash: hash, Source: "external"})).To(Succeed())
|
||||
Expect(artRepo.PutImage(ctx, &model.Artwork{Hash: hash, Mime: "image/jpeg"})).To(Succeed())
|
||||
Expect(artRepo.PutItemArtwork(ctx, &model.ItemArtwork{ItemKind: kind, ItemID: id, Hash: hash, Source: "external"})).To(Succeed())
|
||||
seedEntity(kind, id)
|
||||
return hash
|
||||
}
|
||||
|
|
@ -56,9 +56,9 @@ var _ = Describe("Artwork", func() {
|
|||
GinkgoHelper()
|
||||
switch kind {
|
||||
case "al":
|
||||
Expect(albumRepo.Put(&model.Album{ID: id, Name: "Album"})).To(Succeed())
|
||||
Expect(albumRepo.Put(ctx, &model.Album{ID: id, Name: "Album"})).To(Succeed())
|
||||
case "mf":
|
||||
Expect(mfRepo.Put(&model.MediaFile{ID: id})).To(Succeed())
|
||||
Expect(mfRepo.Put(ctx, &model.MediaFile{ID: id})).To(Succeed())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,9 +147,9 @@ var _ = Describe("Artwork", func() {
|
|||
imgPath := filepath.Join(dir, "cover.jpg")
|
||||
Expect(os.WriteFile(imgPath, coverBytes, 0600)).To(Succeed())
|
||||
mtime := fileMtime(imgPath)
|
||||
Expect(artRepo.PutImage(&model.Artwork{Hash: "aaaaaaaaaaaaaaaa", Mime: "image/jpeg"})).To(Succeed())
|
||||
Expect(artRepo.PutImage(ctx, &model.Artwork{Hash: "aaaaaaaaaaaaaaaa", Mime: "image/jpeg"})).To(Succeed())
|
||||
seedEntity("al", "al2")
|
||||
Expect(artRepo.PutItemArtwork(&model.ItemArtwork{
|
||||
Expect(artRepo.PutItemArtwork(ctx, &model.ItemArtwork{
|
||||
ItemKind: "al", ItemID: "al2", Hash: "aaaaaaaaaaaaaaaa",
|
||||
Source: "folder", SourcePath: imgPath, RefMtime: mtime,
|
||||
})).To(Succeed())
|
||||
|
|
@ -163,9 +163,9 @@ var _ = Describe("Artwork", func() {
|
|||
dir := GinkgoT().TempDir()
|
||||
secretPath := filepath.Join(dir, "config.ini")
|
||||
Expect(os.WriteFile(secretPath, []byte("password=secret"), 0600)).To(Succeed())
|
||||
Expect(artRepo.PutImage(&model.Artwork{Hash: "dddddddddddddddd", Mime: "image/jpeg"})).To(Succeed())
|
||||
Expect(artRepo.PutImage(ctx, &model.Artwork{Hash: "dddddddddddddddd", Mime: "image/jpeg"})).To(Succeed())
|
||||
seedEntity("al", "alni")
|
||||
Expect(artRepo.PutItemArtwork(&model.ItemArtwork{
|
||||
Expect(artRepo.PutItemArtwork(ctx, &model.ItemArtwork{
|
||||
ItemKind: "al", ItemID: "alni", Hash: "dddddddddddddddd",
|
||||
Source: "folder", SourcePath: secretPath, RefMtime: fileMtime(secretPath),
|
||||
})).To(Succeed())
|
||||
|
|
@ -180,9 +180,9 @@ var _ = Describe("Artwork", func() {
|
|||
dir := GinkgoT().TempDir()
|
||||
secretPath := filepath.Join(dir, "config.ini")
|
||||
Expect(os.WriteFile(secretPath, secret, 0600)).To(Succeed())
|
||||
Expect(artRepo.PutImage(&model.Artwork{Hash: "eeeeeeeeeeeeeeee", Mime: "image/jpeg"})).To(Succeed())
|
||||
Expect(artRepo.PutImage(ctx, &model.Artwork{Hash: "eeeeeeeeeeeeeeee", Mime: "image/jpeg"})).To(Succeed())
|
||||
seedEntity("al", "alnic")
|
||||
Expect(artRepo.PutItemArtwork(&model.ItemArtwork{
|
||||
Expect(artRepo.PutItemArtwork(ctx, &model.ItemArtwork{
|
||||
ItemKind: "al", ItemID: "alnic", Hash: "eeeeeeeeeeeeeeee",
|
||||
Source: "folder", SourcePath: secretPath, RefMtime: fileMtime(secretPath),
|
||||
})).To(Succeed())
|
||||
|
|
@ -210,9 +210,9 @@ var _ = Describe("Artwork", func() {
|
|||
dir := GinkgoT().TempDir()
|
||||
imgPath := filepath.Join(dir, "cover.jpg")
|
||||
Expect(os.WriteFile(imgPath, coverBytes, 0600)).To(Succeed())
|
||||
Expect(artRepo.PutImage(&model.Artwork{Hash: "bbbbbbbbbbbbbbbb", Mime: "image/jpeg"})).To(Succeed())
|
||||
Expect(artRepo.PutImage(ctx, &model.Artwork{Hash: "bbbbbbbbbbbbbbbb", Mime: "image/jpeg"})).To(Succeed())
|
||||
seedEntity("al", "al3")
|
||||
Expect(artRepo.PutItemArtwork(&model.ItemArtwork{
|
||||
Expect(artRepo.PutItemArtwork(ctx, &model.ItemArtwork{
|
||||
ItemKind: "al", ItemID: "al3", Hash: "bbbbbbbbbbbbbbbb",
|
||||
Source: "folder", SourcePath: imgPath, RefMtime: fileMtime(imgPath) + 999,
|
||||
})).To(Succeed())
|
||||
|
|
@ -220,7 +220,7 @@ var _ = Describe("Artwork", func() {
|
|||
_, err := svc.Get(ctx, model.MustParseArtworkID("al-al3"), 0, false)
|
||||
Expect(err).To(MatchError(ErrUnavailable))
|
||||
Expect(queueRepo.Data[primaryKey("al", "al3")].Priority).To(Equal(model.ArtworkPriorityScan))
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al3", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al3", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Hash).To(Equal("bbbbbbbbbbbbbbbb"))
|
||||
})
|
||||
|
|
@ -229,9 +229,9 @@ var _ = Describe("Artwork", func() {
|
|||
dir := GinkgoT().TempDir()
|
||||
imgPath := filepath.Join(dir, "cover.jpg")
|
||||
Expect(os.WriteFile(imgPath, coverBytes, 0600)).To(Succeed())
|
||||
Expect(artRepo.PutImage(&model.Artwork{Hash: "cccccccccccccccc", Mime: "image/jpeg"})).To(Succeed())
|
||||
Expect(artRepo.PutImage(ctx, &model.Artwork{Hash: "cccccccccccccccc", Mime: "image/jpeg"})).To(Succeed())
|
||||
seedEntity("al", "al3b")
|
||||
Expect(artRepo.PutItemArtwork(&model.ItemArtwork{
|
||||
Expect(artRepo.PutItemArtwork(ctx, &model.ItemArtwork{
|
||||
ItemKind: "al", ItemID: "al3b", Hash: "cccccccccccccccc",
|
||||
Source: "folder", SourcePath: imgPath, RefMtime: fileMtime(imgPath) + 999,
|
||||
})).To(Succeed())
|
||||
|
|
@ -252,7 +252,7 @@ var _ = Describe("Artwork", func() {
|
|||
})
|
||||
|
||||
It("never re-enqueues an absent state on view, however old", func() {
|
||||
Expect(artRepo.PutItemArtwork(&model.ItemArtwork{
|
||||
Expect(artRepo.PutItemArtwork(ctx, &model.ItemArtwork{
|
||||
ItemKind: "al", ItemID: "al4", AttemptedAt: time.Now().Add(-365 * 24 * time.Hour),
|
||||
})).To(Succeed())
|
||||
|
||||
|
|
@ -272,7 +272,7 @@ var _ = Describe("Artwork", func() {
|
|||
Expect(readAll(img)).To(Equal(coverBytes))
|
||||
|
||||
Expect(queueRepo.Data[primaryKey("al", "al5")].Priority).To(Equal(model.ArtworkPriorityBump))
|
||||
_, err = artRepo.GetItemArtwork(model.KindAlbumArtwork, "al5", model.ImageTypePrimary)
|
||||
_, err = artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al5", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
|
|
@ -283,7 +283,7 @@ var _ = Describe("Artwork", func() {
|
|||
_, err := svc.Get(ctx, model.MustParseArtworkID("al-al6"), 0, false)
|
||||
Expect(err).To(MatchError(ErrUnavailable))
|
||||
Expect(queueRepo.Data[primaryKey("al", "al6")].Priority).To(Equal(model.ArtworkPriorityBump))
|
||||
_, err = artRepo.GetItemArtwork(model.KindAlbumArtwork, "al6", model.ImageTypePrimary)
|
||||
_, err = artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al6", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
})
|
||||
|
|
@ -310,7 +310,7 @@ var _ = Describe("Artwork", func() {
|
|||
|
||||
It("delegates to the album when the track's state is absent", func() {
|
||||
seedFoundStore("al", "albm", coverBytes)
|
||||
Expect(artRepo.PutItemArtwork(&model.ItemArtwork{ItemKind: "mf", ItemID: "mf2"})).To(Succeed())
|
||||
Expect(artRepo.PutItemArtwork(ctx, &model.ItemArtwork{ItemKind: "mf", ItemID: "mf2"})).To(Succeed())
|
||||
mfRepo.SetData(model.MediaFiles{{ID: "mf2", AlbumID: "albm"}})
|
||||
|
||||
img, err := svc.Get(ctx, model.MustParseArtworkID("mf-mf2"), 0, false)
|
||||
|
|
@ -343,7 +343,7 @@ var _ = Describe("Artwork", func() {
|
|||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(readAll(img))).To(BeNumerically(">", 0))
|
||||
Expect(queueRepo.Data[primaryKey("mf", "mf4")].Priority).To(Equal(model.ArtworkPriorityBump))
|
||||
_, err = artRepo.GetItemArtwork(model.KindMediaFileArtwork, "mf4", model.ImageTypePrimary)
|
||||
_, err = artRepo.GetItemArtwork(ctx, model.KindMediaFileArtwork, "mf4", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
|
|
@ -494,7 +494,7 @@ var _ = Describe("Artwork", func() {
|
|||
})
|
||||
|
||||
It("falls back to the artist placeholder for an absent artist", func() {
|
||||
Expect(artRepo.PutItemArtwork(&model.ItemArtwork{ItemKind: "ar", ItemID: "arph"})).To(Succeed())
|
||||
Expect(artRepo.PutItemArtwork(ctx, &model.ItemArtwork{ItemKind: "ar", ItemID: "arph"})).To(Succeed())
|
||||
|
||||
img, err := svc.GetOrPlaceholder(ctx, "ar-arph", 300, false)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -535,7 +535,7 @@ var _ = Describe("EntityExists", func() {
|
|||
artistRepo := tests.CreateMockArtistRepo()
|
||||
artistRepo.SetData(model.Artists{{ID: "ar1"}})
|
||||
radioRepo := tests.CreateMockedRadioRepo()
|
||||
Expect(radioRepo.Put(&model.Radio{ID: "ra1", Name: "R"})).To(Succeed())
|
||||
Expect(radioRepo.Put(ctx, &model.Radio{ID: "ra1", Name: "R"})).To(Succeed())
|
||||
ds = &tests.MockDataStore{MockedAlbum: albumRepo, MockedArtist: artistRepo, MockedRadio: radioRepo}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ func newDiscArtworkReader(ctx context.Context, ds model.DataStore, artID model.A
|
|||
return nil, fmt.Errorf("invalid disc artwork id '%s': %w", artID.ID, err)
|
||||
}
|
||||
|
||||
al, err := ds.Album(ctx).Get(albumID)
|
||||
al, err := ds.Album().Get(ctx, albumID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ func newDiscArtworkReader(ctx context.Context, ds model.DataStore, artID model.A
|
|||
}
|
||||
|
||||
// Query mediafiles for this album + disc to find folder associations and first track
|
||||
mfs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
mfs, err := ds.MediaFile().GetAll(ctx, model.QueryOptions{
|
||||
Sort: "track_number",
|
||||
Order: "ASC",
|
||||
Filters: squirrel.Eq{"album_id": albumID, "disc_number": discNumber},
|
||||
|
|
@ -88,7 +88,7 @@ func newDiscArtworkReader(ctx context.Context, ds model.DataStore, artID model.A
|
|||
// Resolve folder IDs to library-relative paths
|
||||
discFoldersRel := make(map[string]bool)
|
||||
if len(folderIDs) > 0 {
|
||||
folders, err := ds.Folder(ctx).GetAll(model.QueryOptions{
|
||||
folders, err := ds.Folder().GetAll(ctx, model.QueryOptions{
|
||||
Filters: squirrel.Eq{"folder.id": folderIDs},
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -44,20 +44,20 @@ var _ = Describe("Acquisition → serve loop", func() {
|
|||
|
||||
itemFound := func(kind model.Kind, id string) func() bool {
|
||||
return func() bool {
|
||||
ia, err := artRepo.GetItemArtwork(kind, id, model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, kind, id, model.ImageTypePrimary)
|
||||
return err == nil && ia.Hash != ""
|
||||
}
|
||||
}
|
||||
itemAbsent := func(kind model.Kind, id string) func() bool {
|
||||
return func() bool {
|
||||
ia, err := artRepo.GetItemArtwork(kind, id, model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, kind, id, model.ImageTypePrimary)
|
||||
return err == nil && ia.Hash == ""
|
||||
}
|
||||
}
|
||||
// Enqueues the way the serving paths do, so the drain is driven by a plain queue row.
|
||||
bump := func(kind, id string) {
|
||||
GinkgoHelper()
|
||||
Expect(ds.ArtworkQueue(ctx).EnqueuePreservingBackoff(model.ArtworkQueueItem{
|
||||
Expect(ds.ArtworkQueue().EnqueuePreservingBackoff(ctx, model.ArtworkQueueItem{
|
||||
ItemKind: kind, ItemID: id, ImageType: model.ImageTypePrimary,
|
||||
Priority: model.ArtworkPriorityBump,
|
||||
})).To(Succeed())
|
||||
|
|
@ -141,7 +141,7 @@ var _ = Describe("Acquisition → serve loop", func() {
|
|||
bump("al", "al1")
|
||||
runWorkerUntil(ctx, worker, itemFound(model.KindAlbumArtwork, "al1"))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("folder"))
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ var _ = Describe("Acquisition → serve loop", func() {
|
|||
bump("ar", "ar1")
|
||||
runWorkerUntil(ctx, worker, itemFound(model.KindArtistArtwork, "ar1"))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindArtistArtwork, "ar1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindArtistArtwork, "ar1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("upload"))
|
||||
|
||||
|
|
@ -175,14 +175,14 @@ var _ = Describe("Acquisition → serve loop", func() {
|
|||
bump("pl", "pl1")
|
||||
runWorkerUntil(ctx, worker, itemFound(model.KindPlaylistArtwork, "pl1"))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindPlaylistArtwork, "pl1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindPlaylistArtwork, "pl1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("generated"))
|
||||
|
||||
img, err := svc.Get(ctx, model.MustParseArtworkID("pl-pl1"), 0, false)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(img.Hash).To(Equal(ia.Hash))
|
||||
art, err := artRepo.GetImage(ia.Hash)
|
||||
art, err := artRepo.GetImage(ctx, ia.Hash)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(art.Mime).To(Equal("image/png"))
|
||||
Expect(len(readAll(img))).To(BeNumerically(">", 0))
|
||||
|
|
@ -194,7 +194,7 @@ var _ = Describe("Acquisition → serve loop", func() {
|
|||
bump("ra", "ra1")
|
||||
runWorkerUntil(ctx, worker, itemFound(model.KindRadioArtwork, "ra1"))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindRadioArtwork, "ra1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindRadioArtwork, "ra1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("upload"))
|
||||
|
||||
|
|
@ -216,12 +216,12 @@ var _ = Describe("Acquisition → serve loop", func() {
|
|||
provisionalBytes := readAll(provisional)
|
||||
Expect(len(provisionalBytes)).To(BeNumerically(">", 0))
|
||||
|
||||
_, err = artRepo.GetItemArtwork(model.KindMediaFileArtwork, "mf1", model.ImageTypePrimary)
|
||||
_, err = artRepo.GetItemArtwork(ctx, model.KindMediaFileArtwork, "mf1", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound), "provisional serving must not write a state row")
|
||||
|
||||
// The provisional read enqueued a Bump; drain it.
|
||||
runWorkerUntil(ctx, worker, itemFound(model.KindMediaFileArtwork, "mf1"))
|
||||
ia, err := artRepo.GetItemArtwork(model.KindMediaFileArtwork, "mf1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindMediaFileArtwork, "mf1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("embedded"))
|
||||
Expect(ia.Hash).To(Equal(provisional.Hash))
|
||||
|
|
@ -237,9 +237,9 @@ var _ = Describe("Acquisition → serve loop", func() {
|
|||
bump("al", "al1")
|
||||
runWorkerUntil(ctx, worker, itemFound(model.KindAlbumArtwork, "al1"))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
art, err := artRepo.GetImage(ia.Hash)
|
||||
art, err := artRepo.GetImage(ctx, ia.Hash)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(art.Mime).To(Equal("image/jpeg"))
|
||||
Expect(art.Width).To(BeNumerically(">", 0))
|
||||
|
|
@ -259,9 +259,9 @@ var _ = Describe("Acquisition → serve loop", func() {
|
|||
bump("ra", "ra1")
|
||||
runWorkerUntil(ctx, worker, itemFound(model.KindRadioArtwork, "ra1"))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindRadioArtwork, "ra1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindRadioArtwork, "ra1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
art, err := artRepo.GetImage(ia.Hash)
|
||||
art, err := artRepo.GetImage(ctx, ia.Hash)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(art.Mime).To(Equal("image/gif"))
|
||||
Expect(art.Width).To(BeNumerically("==", 4))
|
||||
|
|
@ -279,9 +279,9 @@ var _ = Describe("Acquisition → serve loop", func() {
|
|||
return itemFound(model.KindAlbumArtwork, "al1")() && itemFound(model.KindAlbumArtwork, "al2")()
|
||||
})
|
||||
|
||||
ia1, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al1", model.ImageTypePrimary)
|
||||
ia1, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
ia2, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al2", model.ImageTypePrimary)
|
||||
ia2, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al2", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia1.Hash).To(Equal(ia2.Hash), "identical bytes must share one content hash")
|
||||
Expect(readAll(mustGet(svc.Get(ctx, model.MustParseArtworkID("al-al2"), 0, false)))).To(Equal(coverBytes))
|
||||
|
|
@ -293,7 +293,7 @@ var _ = Describe("Acquisition → serve loop", func() {
|
|||
bump("ra", "ra1")
|
||||
runWorkerUntil(ctx, worker, itemFound(model.KindRadioArtwork, "ra1"))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindRadioArtwork, "ra1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindRadioArtwork, "ra1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
staleHash := ia.Hash
|
||||
|
||||
|
|
@ -308,7 +308,7 @@ var _ = Describe("Acquisition → serve loop", func() {
|
|||
|
||||
// That failed read enqueued a re-resolution.
|
||||
runWorkerUntil(ctx, worker, func() bool {
|
||||
cur, gerr := artRepo.GetItemArtwork(model.KindRadioArtwork, "ra1", model.ImageTypePrimary)
|
||||
cur, gerr := artRepo.GetItemArtwork(ctx, model.KindRadioArtwork, "ra1", model.ImageTypePrimary)
|
||||
return gerr == nil && cur.Hash != "" && cur.Hash != staleHash
|
||||
})
|
||||
img, err := svc.Get(ctx, model.MustParseArtworkID("ra-ra1"), 0, false)
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ var _ = Describe("Artist artwork resolution", func() {
|
|||
uploaded := ar.ID + "_upload.jpg"
|
||||
writeUploadedImage(consts.EntityArtist, uploaded, pngBytes("artist-uploaded"))
|
||||
ar.UploadedImage = uploaded
|
||||
Expect(rds.Artist(rctx).Put(&ar)).To(Succeed())
|
||||
Expect(rds.Artist().Put(rctx, &ar)).To(Succeed())
|
||||
|
||||
ia := acquire(model.KindArtistArtwork, ar.ID)
|
||||
Expect(ia.Source).To(Equal("upload"))
|
||||
|
|
@ -279,7 +279,7 @@ var _ = Describe("Artist artwork resolution", func() {
|
|||
|
||||
func soleArtist() model.Artist {
|
||||
GinkgoHelper()
|
||||
artists, err := rds.Artist(rctx).GetAll(model.QueryOptions{
|
||||
artists, err := rds.Artist().GetAll(rctx, model.QueryOptions{
|
||||
Filters: squirrel.Eq{"artist.name": "Artist"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
|
|||
|
|
@ -67,13 +67,17 @@ type fakeFolderRepo struct {
|
|||
result []model.Folder
|
||||
}
|
||||
|
||||
func (f *fakeFolderRepo) GetAll(...model.QueryOptions) ([]model.Folder, error) { return f.result, nil }
|
||||
func (f *fakeFolderRepo) GetAll(context.Context, ...model.QueryOptions) ([]model.Folder, error) {
|
||||
return f.result, nil
|
||||
}
|
||||
|
||||
func (f *fakeFolderRepo) HasAudioOutsideFolders(model.Folder, []string) (bool, error) {
|
||||
func (f *fakeFolderRepo) HasAudioOutsideFolders(context.Context, model.Folder, []string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (f *fakeFolderRepo) Get(string) (*model.Folder, error) { return nil, model.ErrNotFound }
|
||||
func (f *fakeFolderRepo) Get(context.Context, string) (*model.Folder, error) {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
func writeUpload(entityType, name, srcFixture string) string {
|
||||
GinkgoHelper()
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ var _ = Describe("MediaFile artwork resolution", func() {
|
|||
|
||||
func mediafileOn(relPath string) model.MediaFile {
|
||||
GinkgoHelper()
|
||||
mfs, err := rds.MediaFile(rctx).GetAll(model.QueryOptions{
|
||||
mfs, err := rds.MediaFile().GetAll(rctx, model.QueryOptions{
|
||||
Filters: squirrel.Like{"media_file.path": relPath},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
|
|||
|
|
@ -142,13 +142,13 @@ var _ = Describe("Playlist artwork resolution", func() {
|
|||
})
|
||||
scan()
|
||||
|
||||
mfs, err := rds.MediaFile(rctx).GetAll(model.QueryOptions{})
|
||||
mfs, err := rds.MediaFile().GetAll(rctx, model.QueryOptions{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mfs).To(HaveLen(2))
|
||||
|
||||
pl := model.Playlist{ID: "pl-7", Name: "Mix", OwnerID: "admin-1"}
|
||||
pl.AddMediaFilesByID([]string{mfs[0].ID, mfs[1].ID})
|
||||
Expect(rds.Playlist(rctx).Put(&pl)).To(Succeed())
|
||||
Expect(rds.Playlist().Put(rctx, &pl)).To(Succeed())
|
||||
|
||||
ia := acquire(model.KindPlaylistArtwork, pl.ID)
|
||||
Expect(ia.Source).To(Equal("generated"))
|
||||
|
|
@ -180,14 +180,14 @@ var _ = Describe("Playlist artwork resolution", func() {
|
|||
setLayout(layout)
|
||||
scan()
|
||||
|
||||
mfs, err := rds.MediaFile(rctx).GetAll(model.QueryOptions{})
|
||||
mfs, err := rds.MediaFile().GetAll(rctx, model.QueryOptions{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mfs).To(HaveLen(4))
|
||||
ids := slice.Map(mfs, func(mf model.MediaFile) string { return mf.ID })
|
||||
|
||||
pl := model.Playlist{ID: "pl-8", Name: "Four", OwnerID: "admin-1"}
|
||||
pl.AddMediaFilesByID(ids)
|
||||
Expect(rds.Playlist(rctx).Put(&pl)).To(Succeed())
|
||||
Expect(rds.Playlist().Put(rctx, &pl)).To(Succeed())
|
||||
|
||||
ia := acquire(model.KindPlaylistArtwork, pl.ID)
|
||||
Expect(ia.Source).To(Equal("generated"))
|
||||
|
|
@ -208,6 +208,6 @@ func putPlaylist(pl model.Playlist) model.Playlist {
|
|||
if pl.OwnerID == "" {
|
||||
pl.OwnerID = "admin-1"
|
||||
}
|
||||
Expect(rds.Playlist(rctx).Put(&pl)).To(Succeed())
|
||||
Expect(rds.Playlist().Put(rctx, &pl)).To(Succeed())
|
||||
return pl
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ var _ = Describe("Radio artwork resolution", func() {
|
|||
It("returns the uploaded image bytes", func() {
|
||||
writeUploadedImage(consts.EntityRadio, "rd-1_logo.jpg", pngBytes("radio-logo"))
|
||||
rd := model.Radio{ID: "rd-1", Name: "Test Radio", StreamUrl: "https://example.com/stream", UploadedImage: "rd-1_logo.jpg"}
|
||||
Expect(rds.Radio(rctx).Put(&rd)).To(Succeed())
|
||||
Expect(rds.Radio().Put(rctx, &rd)).To(Succeed())
|
||||
|
||||
ia := acquire(model.KindRadioArtwork, rd.ID)
|
||||
Expect(ia.Source).To(Equal("upload"))
|
||||
|
|
@ -35,7 +35,7 @@ var _ = Describe("Radio artwork resolution", func() {
|
|||
// (no files on disk — the resolver has no sources to fall back to)
|
||||
It("settles absent", func() {
|
||||
rd := model.Radio{ID: "rd-2", Name: "Bare Radio", StreamUrl: "https://example.com/stream"}
|
||||
Expect(rds.Radio(rctx).Put(&rd)).To(Succeed())
|
||||
Expect(rds.Radio().Put(rctx, &rd)).To(Succeed())
|
||||
|
||||
ia := acquire(model.KindRadioArtwork, rd.ID)
|
||||
Expect(ia.Hash).To(BeEmpty())
|
||||
|
|
|
|||
|
|
@ -99,11 +99,11 @@ func setupResolutionHarness() {
|
|||
rds = &tests.MockDataStore{RealDS: persistence.New(db.Db())}
|
||||
|
||||
adminUser := model.User{ID: "admin-1", UserName: "admin", Name: "Admin", IsAdmin: true, NewPassword: "password"}
|
||||
Expect(rds.User(rctx).Put(&adminUser)).To(Succeed())
|
||||
Expect(rds.User().Put(rctx, &adminUser)).To(Succeed())
|
||||
|
||||
lib := model.Library{ID: 1, Name: "Music", Path: fakeLibPath}
|
||||
Expect(rds.Library(rctx).Put(&lib)).To(Succeed())
|
||||
Expect(rds.User(rctx).SetUserLibraries(adminUser.ID, []int{lib.ID})).To(Succeed())
|
||||
Expect(rds.Library().Put(rctx, &lib)).To(Succeed())
|
||||
Expect(rds.User().SetUserLibraries(rctx, adminUser.ID, []int{lib.ID})).To(Succeed())
|
||||
|
||||
loadEmbeddedFixture()
|
||||
|
||||
|
|
@ -140,13 +140,13 @@ func scan() {
|
|||
func acquire(kind model.Kind, id string) model.ItemArtwork {
|
||||
GinkgoHelper()
|
||||
// Enqueues the way the serving paths do, so the drain is driven by a plain queue row.
|
||||
Expect(rds.ArtworkQueue(rctx).EnqueuePreservingBackoff(model.ArtworkQueueItem{
|
||||
Expect(rds.ArtworkQueue().EnqueuePreservingBackoff(rctx, model.ArtworkQueueItem{
|
||||
ItemKind: kind.Prefix(), ItemID: id, ImageType: model.ImageTypePrimary,
|
||||
Priority: model.ArtworkPriorityBump,
|
||||
})).To(Succeed())
|
||||
var ia *model.ItemArtwork
|
||||
runResolutionWorkerUntil(func() bool {
|
||||
got, err := rds.Artwork(rctx).GetItemArtwork(kind, id, model.ImageTypePrimary)
|
||||
got, err := rds.Artwork().GetItemArtwork(rctx, kind, id, model.ImageTypePrimary)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
|
@ -211,7 +211,7 @@ func expectAlbumFolderCover(al model.Album, suffix string) {
|
|||
// A drain settles every ready item, so byte-level folder assertions must precede any acquire.
|
||||
func requireNoStateRow(kind model.Kind, id string) {
|
||||
GinkgoHelper()
|
||||
_, err := rds.Artwork(rctx).GetItemArtwork(kind, id, model.ImageTypePrimary)
|
||||
_, err := rds.Artwork().GetItemArtwork(rctx, kind, id, model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound),
|
||||
"assert %s %q before acquiring any other entity in this spec", kind, id)
|
||||
}
|
||||
|
|
@ -266,7 +266,7 @@ func gridQuadrants(data []byte) [4]color.RGBA {
|
|||
// Store-backed sources only (embedded/generated); file-backed ones assert on ia.SourcePath.
|
||||
func storedBytes(ia model.ItemArtwork) []byte {
|
||||
GinkgoHelper()
|
||||
art, err := rds.Artwork(rctx).GetImage(ia.Hash)
|
||||
art, err := rds.Artwork().GetImage(rctx, ia.Hash)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
r, err := rstore.Open(ia.Hash, art.Mime)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -345,7 +345,7 @@ func replaceWithRealMP3(relPath string) {
|
|||
|
||||
func firstAlbum() model.Album {
|
||||
GinkgoHelper()
|
||||
albums, err := rds.Album(rctx).GetAll(model.QueryOptions{})
|
||||
albums, err := rds.Album().GetAll(rctx, model.QueryOptions{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(albums).To(HaveLen(1), "expected exactly one album, got %d", len(albums))
|
||||
return albums[0]
|
||||
|
|
@ -353,7 +353,7 @@ func firstAlbum() model.Album {
|
|||
|
||||
func albumByName(name string) model.Album {
|
||||
GinkgoHelper()
|
||||
albums, err := rds.Album(rctx).GetAll(model.QueryOptions{})
|
||||
albums, err := rds.Album().GetAll(rctx, model.QueryOptions{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
for _, al := range albums {
|
||||
if al.Name == name {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ func loadAlbumFoldersPaths(ctx context.Context, ds model.DataStore, album model.
|
|||
}
|
||||
|
||||
func loadFolders(ctx context.Context, ds model.DataStore, folderIDs []string) ([]model.Folder, error) {
|
||||
return ds.Folder(ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"folder.id": folderIDs, "missing": false}})
|
||||
return ds.Folder().GetAll(ctx, model.QueryOptions{Filters: squirrel.Eq{"folder.id": folderIDs, "missing": false}})
|
||||
}
|
||||
|
||||
// folderImages collects the folders' image files, sorted so files without
|
||||
|
|
@ -79,7 +79,7 @@ func albumRootParent(ctx context.Context, ds model.DataStore, folders []model.Fo
|
|||
if len(folders) < 2 && anyFolderHasImages(folders) {
|
||||
return nil, nil
|
||||
}
|
||||
parent, err := ds.Folder(ctx).Get(commonParentID)
|
||||
parent, err := ds.Folder().Get(ctx, commonParentID)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
log.Warn(ctx, "Artwork: Parent folder not found for album cover art lookup", "parentID", commonParentID)
|
||||
return nil, nil
|
||||
|
|
@ -91,7 +91,7 @@ func albumRootParent(ctx context.Context, ds model.DataStore, folders []model.Fo
|
|||
// The library root can never be an album root
|
||||
return nil, nil
|
||||
}
|
||||
hasOtherAudio, err := ds.Folder(ctx).HasAudioOutsideFolders(*parent, folderIDs)
|
||||
hasOtherAudio, err := ds.Folder().HasAudioOutsideFolders(ctx, *parent, folderIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,14 +169,14 @@ func loadArtistFolder(ctx context.Context, ds model.DataStore, albums model.Albu
|
|||
}
|
||||
|
||||
// Cleaned like the album paths; Join keeps an empty path empty, Clean would return ".".
|
||||
libPath, _ := ds.Library(ctx).GetPath(libID)
|
||||
libPath, _ := ds.Library().GetPath(ctx, 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,
|
||||
"libPath", libPath, "libID", libID, "albumPaths", paths)
|
||||
|
||||
folders, err := ds.Folder(ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"folder.id": folderID, "missing": false}})
|
||||
folders, err := ds.Folder().GetAll(ctx, model.QueryOptions{Filters: squirrel.Eq{"folder.id": folderID, "missing": false}})
|
||||
if err != nil || len(folders) == 0 {
|
||||
log.Warn(ctx, "Artwork: Could not find folder for artist", "folderPath", folderPath, "id", folderID,
|
||||
"libPath", libPath, "libID", libID, err)
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ func ConfigFingerprint() string {
|
|||
// resolved under it. Nothing re-resolves on its own; applying a change is an explicit reprocess.
|
||||
func ReconcileConfigFingerprint(ctx context.Context, ds model.DataStore) error {
|
||||
current := ConfigFingerprint()
|
||||
stored, err := ds.Property(ctx).DefaultGet(consts.ArtConfFingerprintPropertyKey, "")
|
||||
stored, err := ds.Property().DefaultGet(ctx, consts.ArtConfFingerprintPropertyKey, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -89,14 +89,14 @@ func ReconcileConfigFingerprint(ctx context.Context, ds model.DataStore) error {
|
|||
|
||||
// MarkConfigApplied records the current fingerprint as the one the library is resolved under.
|
||||
func MarkConfigApplied(ctx context.Context, ds model.DataStore) error {
|
||||
return ds.Property(ctx).Put(consts.ArtConfFingerprintPropertyKey, ConfigFingerprint())
|
||||
return ds.Property().Put(ctx, consts.ArtConfFingerprintPropertyKey, ConfigFingerprint())
|
||||
}
|
||||
|
||||
// enqueueMissingAll is the safety net for entities a scan never enqueued (added between scans, or scanner off).
|
||||
func enqueueMissingAll(ctx context.Context, ds model.DataStore) error {
|
||||
queue := ds.ArtworkQueue(ctx)
|
||||
queue := ds.ArtworkQueue()
|
||||
for _, kind := range ReprocessKinds {
|
||||
if _, err := queue.EnqueueAllMissing(kind, model.ArtworkPriorityRecheck); err != nil {
|
||||
if _, err := queue.EnqueueAllMissing(ctx, kind, model.ArtworkPriorityRecheck); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -108,31 +108,31 @@ func enqueueMissingAll(ctx context.Context, ds model.DataStore) error {
|
|||
func ItemName(ctx context.Context, ds model.DataStore, kind model.Kind, id string) (string, error) {
|
||||
switch kind {
|
||||
case model.KindArtistArtwork:
|
||||
ar, err := ds.Artist(ctx).Get(id)
|
||||
ar, err := ds.Artist().Get(ctx, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ar.Name, nil
|
||||
case model.KindAlbumArtwork:
|
||||
al, err := ds.Album(ctx).Get(id)
|
||||
al, err := ds.Album().Get(ctx, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return al.Name, nil
|
||||
case model.KindPlaylistArtwork:
|
||||
pls, err := ds.Playlist(ctx).Get(id)
|
||||
pls, err := ds.Playlist().Get(ctx, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return pls.Name, nil
|
||||
case model.KindRadioArtwork:
|
||||
rd, err := ds.Radio(ctx).Get(id)
|
||||
rd, err := ds.Radio().Get(ctx, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return rd.Name, nil
|
||||
case model.KindMediaFileArtwork:
|
||||
mf, err := ds.MediaFile(ctx).Get(id)
|
||||
mf, err := ds.MediaFile().Get(ctx, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -148,7 +148,7 @@ func discArtworkName(ctx context.Context, ds model.DataStore, id string) (string
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
al, err := ds.Album(ctx).Get(albumID)
|
||||
al, err := ds.Album().Get(ctx, albumID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -162,11 +162,11 @@ func discArtworkName(ctx context.Context, ds model.DataStore, id string) (string
|
|||
|
||||
// Refresh drops an item's resolved artwork state and re-queues it at Bump priority.
|
||||
func Refresh(ctx context.Context, ds model.DataStore, kind model.Kind, id string) error {
|
||||
if err := ds.Artwork(ctx).DeleteForItems(kind, []string{id}); err != nil {
|
||||
if err := ds.Artwork().DeleteForItems(ctx, kind, []string{id}); err != nil {
|
||||
return fmt.Errorf("clearing artwork state: %w", err)
|
||||
}
|
||||
item := model.ArtworkQueueItem{ItemKind: kind.Prefix(), ItemID: id, ImageType: model.ImageTypePrimary, Priority: model.ArtworkPriorityBump}
|
||||
if err := ds.ArtworkQueue(ctx).Enqueue(item); err != nil {
|
||||
if err := ds.ArtworkQueue().Enqueue(ctx, item); err != nil {
|
||||
return fmt.Errorf("enqueuing artwork refresh: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -95,15 +95,15 @@ var _ = Describe("Housekeeping", func() {
|
|||
It("records the current fingerprint when none was ever stored", func() {
|
||||
Expect(ReconcileConfigFingerprint(ctx, ds)).To(Succeed())
|
||||
|
||||
Expect(propRepo.Get(consts.ArtConfFingerprintPropertyKey)).To(Equal(ConfigFingerprint()))
|
||||
Expect(propRepo.Get(ctx, consts.ArtConfFingerprintPropertyKey)).To(Equal(ConfigFingerprint()))
|
||||
})
|
||||
|
||||
It("leaves a stale fingerprint stored, so the warning survives a restart", func() {
|
||||
Expect(propRepo.Put(consts.ArtConfFingerprintPropertyKey, "stale-fingerprint")).To(Succeed())
|
||||
Expect(propRepo.Put(ctx, consts.ArtConfFingerprintPropertyKey, "stale-fingerprint")).To(Succeed())
|
||||
|
||||
Expect(ReconcileConfigFingerprint(ctx, ds)).To(Succeed())
|
||||
|
||||
Expect(propRepo.Get(consts.ArtConfFingerprintPropertyKey)).To(Equal("stale-fingerprint"))
|
||||
Expect(propRepo.Get(ctx, consts.ArtConfFingerprintPropertyKey)).To(Equal("stale-fingerprint"))
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -153,7 +153,7 @@ var _ = Describe("ItemName", func() {
|
|||
{ID: "al-2", Name: "Sandinista!", Discs: model.Discs{2: "Side Three"}},
|
||||
})
|
||||
ds = &tests.MockDataStore{MockedAlbum: albumRepo}
|
||||
Expect(ds.Artist(ctx).(*tests.MockArtistRepo).Put(&model.Artist{ID: "ar-1", Name: "Radiohead"})).To(Succeed())
|
||||
Expect(ds.Artist().(*tests.MockArtistRepo).Put(ctx, &model.Artist{ID: "ar-1", Name: "Radiohead"})).To(Succeed())
|
||||
})
|
||||
|
||||
It("returns the album name", func() {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ func (v libraryView) Abs(rel string) string {
|
|||
// loadLibraryView resolves the MusicFS and absolute root path in a single
|
||||
// library lookup.
|
||||
func loadLibraryView(ctx context.Context, ds model.DataStore, libID int) (libraryView, error) {
|
||||
lib, err := ds.Library(ctx).Get(libID)
|
||||
lib, err := ds.Library().Get(ctx, libID)
|
||||
if err != nil {
|
||||
return libraryView{}, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ var _ = Describe("loadLibraryView", Ordered, func() {
|
|||
})
|
||||
|
||||
It("returns a view for a library backed by registered storage", func() {
|
||||
Expect(ds.Library(ctx).Put(&model.Library{ID: 1, Path: "fake:///music"})).To(Succeed())
|
||||
Expect(ds.Library().Put(ctx, &model.Library{ID: 1, Path: "fake:///music"})).To(Succeed())
|
||||
|
||||
lib, err := loadLibraryView(ctx, ds, 1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -45,7 +45,7 @@ var _ = Describe("loadLibraryView", Ordered, func() {
|
|||
})
|
||||
|
||||
It("returns an error when the library path uses an unregistered scheme", func() {
|
||||
Expect(ds.Library(ctx).Put(&model.Library{ID: 2, Path: "unsupported:///music"})).To(Succeed())
|
||||
Expect(ds.Library().Put(ctx, &model.Library{ID: 2, Path: "unsupported:///music"})).To(Succeed())
|
||||
_, err := loadLibraryView(ctx, ds, 2)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ type processor struct {
|
|||
// acquire resolves one queue item end to end: find an image, hash/decode/
|
||||
// blurhash it, place its bytes, and persist the resulting state.
|
||||
func (p *processor) acquire(ctx context.Context, item model.ArtworkQueueItem) (out outcome, got *acquired, retryIn time.Duration) {
|
||||
repo := p.ds.Artwork(ctx)
|
||||
repo := p.ds.Artwork()
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
log.Debug(ctx, "Artwork: Acquisition finished", "kind", item.ItemKind, "id", item.ItemID,
|
||||
|
|
@ -137,7 +137,7 @@ func (p *processor) acquire(ctx context.Context, item model.ArtworkQueueItem) (o
|
|||
log.Trace(ctx, "Artwork: Hashed image", "kind", item.ItemKind, "id", item.ItemID,
|
||||
"hash", hash, "bytes", len(data), "elapsed", time.Since(hashStart))
|
||||
|
||||
art, err := repo.GetImage(hash)
|
||||
art, err := repo.GetImage(ctx, hash)
|
||||
switch {
|
||||
case err == nil && art.Width > 0:
|
||||
log.Debug(ctx, "Artwork: Reusing a known image, skipping decode", "kind", item.ItemKind,
|
||||
|
|
@ -195,7 +195,7 @@ func (p *processor) persist(ctx context.Context, repo model.ArtworkRepository, i
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("writing image store: %w", err)
|
||||
}
|
||||
if err := repo.PutImage(art); err != nil {
|
||||
if err := repo.PutImage(ctx, art); err != nil {
|
||||
return nil, fmt.Errorf("persisting artwork image: %w", err)
|
||||
}
|
||||
ia := &model.ItemArtwork{
|
||||
|
|
@ -210,7 +210,7 @@ func (p *processor) persist(ctx context.Context, repo model.ArtworkRepository, i
|
|||
Trace: traceFrom(ctx).encode(sourcePath),
|
||||
}
|
||||
// PutItemArtwork stamps UpdatedAt on ia, so the returned struct matches the persisted row.
|
||||
if err := repo.PutItemArtwork(ia); err != nil {
|
||||
if err := repo.PutItemArtwork(ctx, ia); err != nil {
|
||||
return nil, fmt.Errorf("persisting item artwork state: %w", err)
|
||||
}
|
||||
return ia, nil
|
||||
|
|
@ -218,7 +218,7 @@ func (p *processor) persist(ctx context.Context, repo model.ArtworkRepository, i
|
|||
|
||||
// writeAbsent records a known-absent state: every source answered definitively "no".
|
||||
func writeAbsent(ctx context.Context, repo model.ArtworkRepository, item model.ArtworkQueueItem) outcome {
|
||||
err := repo.PutItemArtwork(&model.ItemArtwork{
|
||||
err := repo.PutItemArtwork(ctx, &model.ItemArtwork{
|
||||
ItemKind: item.ItemKind,
|
||||
ItemID: item.ItemID,
|
||||
ImageType: item.ImageType,
|
||||
|
|
|
|||
|
|
@ -93,14 +93,14 @@ var _ = Describe("processor.acquire", func() {
|
|||
out, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al1"})
|
||||
Expect(out).To(Equal(outcomeFound))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Hash).ToNot(BeEmpty())
|
||||
Expect(ia.Source).To(Equal("folder"))
|
||||
Expect(filepath.ToSlash(ia.SourcePath)).To(HaveSuffix("tests/fixtures/artist/an-album/cover.jpg"))
|
||||
Expect(ia.RefMtime).To(BeNumerically(">", 0))
|
||||
|
||||
art, err := artRepo.GetImage(ia.Hash)
|
||||
art, err := artRepo.GetImage(ctx, ia.Hash)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// Every placeholder is derived from the one shared thumbnail, so all three land together.
|
||||
Expect(art.BlurHash).ToNot(BeEmpty())
|
||||
|
|
@ -156,12 +156,12 @@ var _ = Describe("processor.acquire", func() {
|
|||
out, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al2"})
|
||||
Expect(out).To(Equal(outcomeFound))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al2", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al2", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("embedded"))
|
||||
Expect(filepath.ToSlash(ia.SourcePath)).To(HaveSuffix("tests/fixtures/artist/an-album/test.mp3"))
|
||||
|
||||
art, err := artRepo.GetImage(ia.Hash)
|
||||
art, err := artRepo.GetImage(ctx, ia.Hash)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(art.BlurHash).ToNot(BeEmpty())
|
||||
|
||||
|
|
@ -179,7 +179,7 @@ var _ = Describe("processor.acquire", func() {
|
|||
out, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al3"})
|
||||
Expect(out).To(Equal(outcomeAbsent))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al3", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al3", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Hash).To(BeEmpty())
|
||||
Expect(ia.Source).To(BeEmpty())
|
||||
|
|
@ -200,7 +200,7 @@ var _ = Describe("processor.acquire", func() {
|
|||
out, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al-io"})
|
||||
Expect(out).To(Equal(outcomeFailed))
|
||||
|
||||
_, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al-io", model.ImageTypePrimary)
|
||||
_, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al-io", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound), "an I/O fault must not be recorded as absent")
|
||||
})
|
||||
|
||||
|
|
@ -225,7 +225,7 @@ var _ = Describe("processor.acquire", func() {
|
|||
out, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "ra", ItemID: "ra-io"})
|
||||
Expect(out).To(Equal(outcomeFailed))
|
||||
|
||||
_, err := artRepo.GetItemArtwork(model.KindRadioArtwork, "ra-io", model.ImageTypePrimary)
|
||||
_, err := artRepo.GetItemArtwork(ctx, model.KindRadioArtwork, "ra-io", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound), "an unreadable upload must not be recorded as absent")
|
||||
})
|
||||
|
||||
|
|
@ -269,7 +269,7 @@ var _ = Describe("processor.acquire", func() {
|
|||
Expect(out).To(Equal(outcomeFailed))
|
||||
Expect(retryIn).To(BeZero(), "a plain failure asks for no particular delay")
|
||||
|
||||
_, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al4", model.ImageTypePrimary)
|
||||
_, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al4", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
|
|
@ -299,7 +299,7 @@ var _ = Describe("processor.acquire", func() {
|
|||
out, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "alstale"})
|
||||
Expect(out).To(Equal(outcomeFoundStale))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "alstale", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "alstale", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Hash).ToNot(BeEmpty())
|
||||
Expect(ia.Source).To(Equal("folder"))
|
||||
|
|
@ -316,10 +316,10 @@ var _ = Describe("processor.acquire", func() {
|
|||
out, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "alU"})
|
||||
Expect(out).To(Equal(outcomeFound))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "alU", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "alU", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("folder"))
|
||||
art, err := artRepo.GetImage(ia.Hash)
|
||||
art, err := artRepo.GetImage(ctx, ia.Hash)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(art.Width).To(BeZero())
|
||||
Expect(art.BlurHash).To(BeEmpty())
|
||||
|
|
@ -336,7 +336,7 @@ var _ = Describe("processor.acquire", func() {
|
|||
out, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "alE"})
|
||||
Expect(out).To(Equal(outcomeFailed))
|
||||
|
||||
_, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "alE", model.ImageTypePrimary)
|
||||
_, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "alE", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
|
|
@ -354,7 +354,7 @@ var _ = Describe("processor.acquire", func() {
|
|||
out, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "alX"})
|
||||
Expect(out).To(Equal(outcomeFailed))
|
||||
|
||||
_, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "alX", model.ImageTypePrimary)
|
||||
_, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "alX", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
|
|
@ -373,12 +373,12 @@ var _ = Describe("processor.acquire", func() {
|
|||
out, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "alext"})
|
||||
Expect(out).To(Equal(outcomeFound))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "alext", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "alext", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("external:deezerFake"))
|
||||
Expect(ia.Hash).ToNot(BeEmpty())
|
||||
|
||||
art, err := artRepo.GetImage(ia.Hash)
|
||||
art, err := artRepo.GetImage(ctx, ia.Hash)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
rc, err := store.Open(ia.Hash, art.Mime)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -397,7 +397,7 @@ var _ = Describe("processor.acquire", func() {
|
|||
|
||||
out1, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al5"})
|
||||
Expect(out1).To(Equal(outcomeFound))
|
||||
ia1, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al5", model.ImageTypePrimary)
|
||||
ia1, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al5", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// A re-decode instead of a hash dedup would overwrite this sentinel.
|
||||
|
|
@ -407,11 +407,11 @@ var _ = Describe("processor.acquire", func() {
|
|||
|
||||
out2, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al6"})
|
||||
Expect(out2).To(Equal(outcomeFound))
|
||||
ia2, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al6", model.ImageTypePrimary)
|
||||
ia2, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al6", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia2.Hash).To(Equal(ia1.Hash))
|
||||
|
||||
reused, err := artRepo.GetImage(ia1.Hash)
|
||||
reused, err := artRepo.GetImage(ctx, ia1.Hash)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(reused.BlurHash).To(Equal("SENTINEL"))
|
||||
})
|
||||
|
|
@ -437,7 +437,7 @@ var _ = Describe("processor.acquire", func() {
|
|||
folderRepo.result = []model.Folder{{Path: "album-a", ImageFiles: []string{"cover.jpg"}}}
|
||||
outN, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "alA"})
|
||||
Expect(outN).To(Equal(outcomeFound))
|
||||
iaA, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "alA", model.ImageTypePrimary)
|
||||
iaA, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "alA", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(iaA.Source).To(Equal("folder"))
|
||||
Expect(filepath.ToSlash(iaA.SourcePath)).To(HaveSuffix("album-a/cover.jpg"))
|
||||
|
|
@ -451,19 +451,19 @@ var _ = Describe("processor.acquire", func() {
|
|||
folderRepo.result = []model.Folder{{Path: "album-b", ImageFiles: []string{"cover.jpg"}}}
|
||||
outN, _, _ = proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "alB"})
|
||||
Expect(outN).To(Equal(outcomeFound))
|
||||
iaB, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "alB", model.ImageTypePrimary)
|
||||
iaB, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "alB", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(iaB.Hash).To(Equal(iaA.Hash))
|
||||
Expect(filepath.ToSlash(iaB.SourcePath)).To(HaveSuffix("album-b/cover.jpg"))
|
||||
Expect(iaB.RefMtime).To(Equal(time.Unix(2000, 0).UnixNano()))
|
||||
|
||||
iaAafter, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "alA", model.ImageTypePrimary)
|
||||
iaAafter, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "alA", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(filepath.ToSlash(iaAafter.SourcePath)).To(HaveSuffix("album-a/cover.jpg"))
|
||||
Expect(iaAafter.RefMtime).To(Equal(time.Unix(1000, 0).UnixNano()))
|
||||
|
||||
Expect(artRepo.Data).To(HaveLen(1))
|
||||
reused, err := artRepo.GetImage(iaA.Hash)
|
||||
reused, err := artRepo.GetImage(ctx, iaA.Hash)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(reused.BlurHash).To(Equal("SENTINEL"))
|
||||
})
|
||||
|
|
@ -483,7 +483,7 @@ var _ = Describe("processor.acquire", func() {
|
|||
out, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "ra", ItemID: "ra1"})
|
||||
Expect(out).To(Equal(outcomeFailed))
|
||||
|
||||
_, err := artRepo.GetItemArtwork(model.KindRadioArtwork, "ra1", model.ImageTypePrimary)
|
||||
_, err := artRepo.GetItemArtwork(ctx, model.KindRadioArtwork, "ra1", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
|
|
@ -504,7 +504,7 @@ var _ = Describe("processor.acquire", func() {
|
|||
out, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "ra", ItemID: "big"})
|
||||
Expect(out).To(Equal(outcomeFailed))
|
||||
|
||||
_, err = artRepo.GetItemArtwork(model.KindRadioArtwork, "big", model.ImageTypePrimary)
|
||||
_, err = artRepo.GetItemArtwork(ctx, model.KindRadioArtwork, "big", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
|
|
@ -565,12 +565,12 @@ var _ = Describe("processor.acquire", func() {
|
|||
|
||||
hash, err := hashImage(bytes.NewReader(imgBytes))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(artRepo.PutImage(&model.Artwork{Hash: hash, Mime: "application/octet-stream"})).To(Succeed())
|
||||
Expect(artRepo.PutImage(ctx, &model.Artwork{Hash: hash, Mime: "application/octet-stream"})).To(Succeed())
|
||||
|
||||
out, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "alM"})
|
||||
Expect(out).To(Equal(outcomeFound))
|
||||
|
||||
upgraded, err := artRepo.GetImage(hash)
|
||||
upgraded, err := artRepo.GetImage(ctx, hash)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(upgraded.Width).To(BeNumerically(">", 0))
|
||||
Expect(upgraded.BlurHash).ToNot(BeEmpty())
|
||||
|
|
@ -590,7 +590,7 @@ var _ = Describe("processor.acquire", func() {
|
|||
out, _, _ := proc.acquire(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al7"})
|
||||
Expect(out).To(Equal(outcomeFailed))
|
||||
|
||||
_, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al7", model.ImageTypePrimary)
|
||||
_, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al7", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ const pruneMinAge = time.Hour
|
|||
func prune(ctx context.Context, ds model.DataStore, store *ImageStore) error {
|
||||
start := time.Now()
|
||||
defer func() { log.Debug(ctx, "Artwork: Prune finished", "elapsed", time.Since(start)) }()
|
||||
repo := ds.Artwork(ctx)
|
||||
repo := ds.Artwork()
|
||||
|
||||
purged, err := repo.PurgeDanglingItems()
|
||||
purged, err := repo.PurgeDanglingItems(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ func prune(ctx context.Context, ds model.DataStore, store *ImageStore) error {
|
|||
}
|
||||
|
||||
// Queue rows for deleted entities would otherwise retry forever (Get -> not found -> failed).
|
||||
queuePurged, err := ds.ArtworkQueue(ctx).PurgeDangling()
|
||||
queuePurged, err := ds.ArtworkQueue().PurgeDangling(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ func prune(ctx context.Context, ds model.DataStore, store *ImageStore) error {
|
|||
|
||||
// Files younger than the grace window may belong to acquisitions whose rows aren't committed yet.
|
||||
cutoff := time.Now().Add(-pruneMinAge)
|
||||
orphans, err := repo.PurgeOrphans(cutoff)
|
||||
orphans, err := repo.PurgeOrphans(ctx, cutoff)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -44,7 +44,7 @@ func prune(ctx context.Context, ds model.DataStore, store *ImageStore) error {
|
|||
}
|
||||
|
||||
// Read after the delete, so the sweep below reclaims the files of the rows just removed.
|
||||
mimes, err := repo.GetMimeByHash()
|
||||
mimes, err := repo.GetMimeByHash(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,18 +18,20 @@ type flakyGetArtworkRepo struct {
|
|||
*tests.MockArtworkRepo
|
||||
}
|
||||
|
||||
func (f *flakyGetArtworkRepo) GetMimeByHash() (map[string]string, error) {
|
||||
func (f *flakyGetArtworkRepo) GetMimeByHash(context.Context) (map[string]string, error) {
|
||||
return nil, errors.New("db locked")
|
||||
}
|
||||
|
||||
var _ = Describe("Prune", func() {
|
||||
var ctx context.Context
|
||||
var ds *tests.MockDataStore
|
||||
var store *ImageStore
|
||||
var awRepo *tests.MockArtworkRepo
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = GinkgoT().Context()
|
||||
ds = &tests.MockDataStore{}
|
||||
awRepo = ds.Artwork(context.Background()).(*tests.MockArtworkRepo)
|
||||
awRepo = ds.Artwork().(*tests.MockArtworkRepo)
|
||||
store = NewImageStore(GinkgoT().TempDir())
|
||||
})
|
||||
|
||||
|
|
@ -41,9 +43,9 @@ var _ = Describe("Prune", func() {
|
|||
}
|
||||
|
||||
It("purges dangling item_artwork state for gone entities, summed across kinds", func() {
|
||||
Expect(awRepo.PutItemArtwork(&model.ItemArtwork{ItemKind: "al", ItemID: "gone-album", ImageType: model.ImageTypePrimary})).To(Succeed())
|
||||
Expect(awRepo.PutItemArtwork(&model.ItemArtwork{ItemKind: "ar", ItemID: "gone-artist", ImageType: model.ImageTypePrimary})).To(Succeed())
|
||||
Expect(awRepo.PutItemArtwork(&model.ItemArtwork{ItemKind: "ar", ItemID: "live-artist", ImageType: model.ImageTypePrimary})).To(Succeed())
|
||||
Expect(awRepo.PutItemArtwork(ctx, &model.ItemArtwork{ItemKind: "al", ItemID: "gone-album", ImageType: model.ImageTypePrimary})).To(Succeed())
|
||||
Expect(awRepo.PutItemArtwork(ctx, &model.ItemArtwork{ItemKind: "ar", ItemID: "gone-artist", ImageType: model.ImageTypePrimary})).To(Succeed())
|
||||
Expect(awRepo.PutItemArtwork(ctx, &model.ItemArtwork{ItemKind: "ar", ItemID: "live-artist", ImageType: model.ImageTypePrimary})).To(Succeed())
|
||||
awRepo.ExistingIDs = map[string]map[string]bool{
|
||||
"al": {},
|
||||
"ar": {"live-artist": true},
|
||||
|
|
@ -51,17 +53,17 @@ var _ = Describe("Prune", func() {
|
|||
|
||||
Expect(prune(context.Background(), ds, store)).To(Succeed())
|
||||
|
||||
_, err := awRepo.GetItemArtwork(model.KindAlbumArtwork, "gone-album", model.ImageTypePrimary)
|
||||
_, err := awRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "gone-album", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
_, err = awRepo.GetItemArtwork(model.KindArtistArtwork, "gone-artist", model.ImageTypePrimary)
|
||||
_, err = awRepo.GetItemArtwork(ctx, model.KindArtistArtwork, "gone-artist", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
_, err = awRepo.GetItemArtwork(model.KindArtistArtwork, "live-artist", model.ImageTypePrimary)
|
||||
_, err = awRepo.GetItemArtwork(ctx, model.KindArtistArtwork, "live-artist", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("purges dangling artwork_queue rows for gone entities", func() {
|
||||
queueRepo := tests.CreateMockArtworkQueueRepo()
|
||||
Expect(queueRepo.Enqueue(
|
||||
Expect(queueRepo.Enqueue(ctx,
|
||||
model.ArtworkQueueItem{ItemKind: "al", ItemID: "gone-album", ImageType: model.ImageTypePrimary},
|
||||
model.ArtworkQueueItem{ItemKind: "al", ItemID: "live-album", ImageType: model.ImageTypePrimary},
|
||||
)).To(Succeed())
|
||||
|
|
@ -80,17 +82,17 @@ var _ = Describe("Prune", func() {
|
|||
Expect(store.Write(h, "image/jpeg", bytes.NewReader(data))).To(Succeed())
|
||||
old := time.Now().Add(-2 * time.Hour)
|
||||
Expect(os.Chtimes(store.path(h, "image/jpeg"), old, old)).To(Succeed())
|
||||
Expect(awRepo.PutImage(&model.Artwork{Hash: h, Mime: "image/jpeg"})).To(Succeed())
|
||||
Expect(awRepo.PutImage(ctx, &model.Artwork{Hash: h, Mime: "image/jpeg"})).To(Succeed())
|
||||
ageArtwork(h, old)
|
||||
|
||||
kept := []byte("kept-bytes")
|
||||
hk, _ := hashImage(bytes.NewReader(kept))
|
||||
Expect(store.Write(hk, "image/jpeg", bytes.NewReader(kept))).To(Succeed())
|
||||
Expect(awRepo.PutImage(&model.Artwork{Hash: hk, Mime: "image/jpeg"})).To(Succeed())
|
||||
Expect(awRepo.PutImage(ctx, &model.Artwork{Hash: hk, Mime: "image/jpeg"})).To(Succeed())
|
||||
|
||||
Expect(prune(context.Background(), ds, store)).To(Succeed())
|
||||
|
||||
_, err := awRepo.GetImage(h)
|
||||
_, err := awRepo.GetImage(ctx, h)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
_, err = store.Open(h, "image/jpeg")
|
||||
Expect(os.IsNotExist(err)).To(BeTrue())
|
||||
|
|
@ -103,14 +105,14 @@ var _ = Describe("Prune", func() {
|
|||
data := []byte("reacquired-bytes")
|
||||
h, _ := hashImage(bytes.NewReader(data))
|
||||
Expect(store.Write(h, "image/jpeg", bytes.NewReader(data))).To(Succeed())
|
||||
Expect(awRepo.PutImage(&model.Artwork{Hash: h, Mime: "image/jpeg"})).To(Succeed())
|
||||
Expect(awRepo.PutImage(ctx, &model.Artwork{Hash: h, Mime: "image/jpeg"})).To(Succeed())
|
||||
ageArtwork(h, time.Now().Add(-2*time.Hour))
|
||||
Expect(awRepo.PutItemArtwork(&model.ItemArtwork{ItemKind: "al", ItemID: "a1",
|
||||
Expect(awRepo.PutItemArtwork(ctx, &model.ItemArtwork{ItemKind: "al", ItemID: "a1",
|
||||
ImageType: model.ImageTypePrimary, Hash: h, Source: "folder"})).To(Succeed())
|
||||
|
||||
Expect(prune(context.Background(), ds, store)).To(Succeed())
|
||||
|
||||
_, err := awRepo.GetImage(h)
|
||||
_, err := awRepo.GetImage(ctx, h)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
rc, err := store.Open(h, "image/jpeg")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -122,11 +124,11 @@ var _ = Describe("Prune", func() {
|
|||
h, _ := hashImage(bytes.NewReader(data))
|
||||
Expect(store.Write(h, "image/jpeg", bytes.NewReader(data))).To(Succeed())
|
||||
// Reacquisition refreshed created_at, so the row is unreferenced but too young to drop.
|
||||
Expect(awRepo.PutImage(&model.Artwork{Hash: h, Mime: "image/jpeg"})).To(Succeed())
|
||||
Expect(awRepo.PutImage(ctx, &model.Artwork{Hash: h, Mime: "image/jpeg"})).To(Succeed())
|
||||
|
||||
Expect(prune(context.Background(), ds, store)).To(Succeed())
|
||||
|
||||
_, err := awRepo.GetImage(h)
|
||||
_, err := awRepo.GetImage(ctx, h)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
rc, err := store.Open(h, "image/jpeg")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -137,7 +139,7 @@ var _ = Describe("Prune", func() {
|
|||
data := []byte("racing-bytes")
|
||||
h, _ := hashImage(bytes.NewReader(data))
|
||||
Expect(store.Write(h, "image/jpeg", bytes.NewReader(data))).To(Succeed())
|
||||
Expect(awRepo.PutImage(&model.Artwork{Hash: h, Mime: "image/jpeg"})).To(Succeed())
|
||||
Expect(awRepo.PutImage(ctx, &model.Artwork{Hash: h, Mime: "image/jpeg"})).To(Succeed())
|
||||
ageArtwork(h, time.Now().Add(-2*time.Hour))
|
||||
// The row is orphaned, but a concurrent acquisition just touched the file's mtime.
|
||||
|
||||
|
|
@ -170,7 +172,7 @@ var _ = Describe("Prune", func() {
|
|||
Expect(os.Chtimes(store.path(h, "image/png"), old, old)).To(Succeed())
|
||||
Expect(os.Chtimes(store.path(h, "image/jpeg"), old, old)).To(Succeed())
|
||||
// The row records the current mime; the .png file is a superseded variant.
|
||||
Expect(awRepo.PutImage(&model.Artwork{Hash: h, Mime: "image/jpeg"})).To(Succeed())
|
||||
Expect(awRepo.PutImage(ctx, &model.Artwork{Hash: h, Mime: "image/jpeg"})).To(Succeed())
|
||||
|
||||
Expect(prune(context.Background(), ds, store)).To(Succeed())
|
||||
|
||||
|
|
@ -192,14 +194,14 @@ var _ = Describe("Prune", func() {
|
|||
hb, _ := hashImage(bytes.NewReader(blocked))
|
||||
Expect(store.Write(hb, "image/jpeg", bytes.NewReader(blocked))).To(Succeed())
|
||||
Expect(os.Chtimes(store.path(hb, "image/jpeg"), old, old)).To(Succeed())
|
||||
Expect(awRepo.PutImage(&model.Artwork{Hash: hb, Mime: "image/jpeg"})).To(Succeed())
|
||||
Expect(awRepo.PutImage(ctx, &model.Artwork{Hash: hb, Mime: "image/jpeg"})).To(Succeed())
|
||||
ageArtwork(hb, old)
|
||||
|
||||
good := []byte("good-bytes")
|
||||
hg, _ := hashImage(bytes.NewReader(good))
|
||||
Expect(store.Write(hg, "image/jpeg", bytes.NewReader(good))).To(Succeed())
|
||||
Expect(os.Chtimes(store.path(hg, "image/jpeg"), old, old)).To(Succeed())
|
||||
Expect(awRepo.PutImage(&model.Artwork{Hash: hg, Mime: "image/jpeg"})).To(Succeed())
|
||||
Expect(awRepo.PutImage(ctx, &model.Artwork{Hash: hg, Mime: "image/jpeg"})).To(Succeed())
|
||||
ageArtwork(hg, old)
|
||||
|
||||
// A read-only shard directory makes os.Remove fail (EACCES) for hb's file only.
|
||||
|
|
@ -210,13 +212,13 @@ var _ = Describe("Prune", func() {
|
|||
|
||||
Expect(prune(context.Background(), ds, store)).To(Succeed())
|
||||
|
||||
_, err := awRepo.GetImage(hg)
|
||||
_, err := awRepo.GetImage(ctx, hg)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
_, err = store.Open(hg, "image/jpeg")
|
||||
Expect(os.IsNotExist(err)).To(BeTrue())
|
||||
|
||||
// The row purge does not depend on file removal, so only the file survives.
|
||||
_, err = awRepo.GetImage(hb)
|
||||
_, err = awRepo.GetImage(ctx, hb)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
rc, err := store.Open(hb, "image/jpeg")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ func (r *resolver) fetchExternalArtist(ctx context.Context, ar model.Artist) (io
|
|||
|
||||
// resolveAlbum walks conf.Server.CoverArtPriority over the folder, embedded and external sources.
|
||||
func (r *resolver) resolveAlbum(ctx context.Context, albumID string) (resolution, error) {
|
||||
al, err := r.ds.Album(ctx).Get(albumID)
|
||||
al, err := r.ds.Album().Get(ctx, albumID)
|
||||
if err != nil {
|
||||
return resolution{}, err
|
||||
}
|
||||
|
|
@ -243,7 +243,7 @@ func (r *resolver) resolveAlbum(ctx context.Context, albumID string) (resolution
|
|||
|
||||
// resolveArtist tries the uploaded image first, then walks conf.Server.ArtistArtPriority.
|
||||
func (r *resolver) resolveArtist(ctx context.Context, artistID string) (resolution, error) {
|
||||
ar, err := r.ds.Artist(ctx).Get(artistID)
|
||||
ar, err := r.ds.Artist().Get(ctx, artistID)
|
||||
if err != nil {
|
||||
return resolution{}, err
|
||||
}
|
||||
|
|
@ -259,7 +259,7 @@ func (r *resolver) resolveArtist(ctx context.Context, artistID string) (resoluti
|
|||
}
|
||||
|
||||
// Only consider albums where the artist is the sole album artist.
|
||||
als, err := r.ds.Album(ctx).GetAll(model.QueryOptions{Filters: persistence.SoleAlbumArtistFilter(artistID)})
|
||||
als, err := r.ds.Album().GetAll(ctx, model.QueryOptions{Filters: persistence.SoleAlbumArtistFilter(artistID)})
|
||||
if err != nil {
|
||||
return resolution{}, err
|
||||
}
|
||||
|
|
@ -328,7 +328,7 @@ const PlaylistGridSamples = 4
|
|||
|
||||
// resolvePlaylist tries the uploaded image, the sidecar and ExternalImageURL, then a generated grid.
|
||||
func (r *resolver) resolvePlaylist(ctx context.Context, playlistID string) (resolution, error) {
|
||||
pl, err := r.ds.Playlist(ctx).Get(playlistID)
|
||||
pl, err := r.ds.Playlist().Get(ctx, playlistID)
|
||||
if err != nil {
|
||||
return resolution{}, err
|
||||
}
|
||||
|
|
@ -374,8 +374,8 @@ func (r *resolver) resolvePlaylist(ctx context.Context, playlistID string) (reso
|
|||
}
|
||||
}
|
||||
|
||||
albumIDs, err := r.ds.Playlist(ctx).Tracks(pl.ID, false).
|
||||
GetAlbumIDs(model.QueryOptions{Max: PlaylistGridSamples, Sort: "random()"})
|
||||
albumIDs, err := r.ds.Playlist().Tracks(ctx, pl.ID, false).
|
||||
GetAlbumIDs(ctx, model.QueryOptions{Max: PlaylistGridSamples, Sort: "random()"})
|
||||
if err != nil {
|
||||
return resolution{}, err
|
||||
}
|
||||
|
|
@ -428,7 +428,7 @@ func (r *resolver) resolvePlaylist(ctx context.Context, playlistID string) (reso
|
|||
|
||||
// resolveRadio serves only an uploaded image; there is no fallback.
|
||||
func (r *resolver) resolveRadio(ctx context.Context, radioID string) (resolution, error) {
|
||||
radio, err := r.ds.Radio(ctx).Get(radioID)
|
||||
radio, err := r.ds.Radio().Get(ctx, radioID)
|
||||
if err != nil {
|
||||
return resolution{}, err
|
||||
}
|
||||
|
|
@ -439,7 +439,7 @@ func (r *resolver) resolveRadio(ctx context.Context, radioID string) (resolution
|
|||
// resolveMediaFile resolves a track's own embedded art only, so disabled or missing cover art
|
||||
// is a definitive absent.
|
||||
func (r *resolver) resolveMediaFile(ctx context.Context, id string) (resolution, error) {
|
||||
mf, err := r.ds.MediaFile(ctx).Get(id)
|
||||
mf, err := r.ds.MediaFile().Get(ctx, id)
|
||||
if err != nil {
|
||||
return resolution{}, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,14 @@ import (
|
|||
)
|
||||
|
||||
var _ = Describe("Uploader", func() {
|
||||
var ctx context.Context
|
||||
var svc Uploader
|
||||
var tmpDir string
|
||||
var artRepo *tests.MockArtworkRepo
|
||||
var queueRepo *tests.MockArtworkQueueRepo
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = GinkgoT().Context()
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
tmpDir = GinkgoT().TempDir()
|
||||
conf.Server.DataFolder = conf.NewDir(tmpDir)
|
||||
|
|
@ -33,7 +35,6 @@ var _ = Describe("Uploader", func() {
|
|||
|
||||
Describe("SetImage", func() {
|
||||
It("creates directory and saves image file", func() {
|
||||
ctx := context.Background()
|
||||
reader := strings.NewReader("fake image data")
|
||||
filename, err := svc.SetImage(ctx, consts.EntityArtist, "ar-1", "Pink Floyd", "", reader, ".jpg")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -46,7 +47,6 @@ var _ = Describe("Uploader", func() {
|
|||
})
|
||||
|
||||
It("falls back to ID-only filename when name cleans to empty", func() {
|
||||
ctx := context.Background()
|
||||
reader := strings.NewReader("data")
|
||||
filename, err := svc.SetImage(ctx, consts.EntityPlaylist, "pl-1", "!!!", "", reader, ".png")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -54,7 +54,6 @@ var _ = Describe("Uploader", func() {
|
|||
})
|
||||
|
||||
It("removes old image when replacing", func() {
|
||||
ctx := context.Background()
|
||||
oldDir := filepath.Join(tmpDir, "artwork", "artist")
|
||||
Expect(os.MkdirAll(oldDir, 0755)).To(Succeed())
|
||||
oldFile := filepath.Join(oldDir, "ar-1_old.png")
|
||||
|
|
@ -70,15 +69,13 @@ var _ = Describe("Uploader", func() {
|
|||
})
|
||||
|
||||
It("ignores missing old file without error", func() {
|
||||
ctx := context.Background()
|
||||
reader := strings.NewReader("data")
|
||||
_, err := svc.SetImage(ctx, consts.EntityArtist, "ar-1", "Name", "/nonexistent/path.jpg", reader, ".jpg")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("does not touch artwork state or the queue (that is EnqueueArtwork's job, post-Put)", func() {
|
||||
ctx := context.Background()
|
||||
Expect(artRepo.PutItemArtwork(&model.ItemArtwork{
|
||||
Expect(artRepo.PutItemArtwork(ctx, &model.ItemArtwork{
|
||||
ItemKind: "ar", ItemID: "ar-1", Hash: "oldhash", Source: "external",
|
||||
})).To(Succeed())
|
||||
|
||||
|
|
@ -87,25 +84,24 @@ var _ = Describe("Uploader", func() {
|
|||
|
||||
// SetImage only writes the file; the state row survives and nothing is queued until
|
||||
// the caller has persisted the new filename and called EnqueueArtwork.
|
||||
_, err = artRepo.GetItemArtwork(model.KindArtistArtwork, "ar-1", model.ImageTypePrimary)
|
||||
_, err = artRepo.GetItemArtwork(ctx, model.KindArtistArtwork, "ar-1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(queueRepo.DequeueBatch(1000)).To(BeEmpty())
|
||||
Expect(queueRepo.DequeueBatch(ctx, 1000)).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("EnqueueArtwork", func() {
|
||||
It("clears artwork state and enqueues a Bump", func() {
|
||||
ctx := context.Background()
|
||||
Expect(artRepo.PutItemArtwork(&model.ItemArtwork{
|
||||
Expect(artRepo.PutItemArtwork(ctx, &model.ItemArtwork{
|
||||
ItemKind: "ar", ItemID: "ar-1", Hash: "oldhash", Source: "external",
|
||||
})).To(Succeed())
|
||||
|
||||
svc.EnqueueArtwork(ctx, consts.EntityArtist, "ar-1")
|
||||
|
||||
_, err := artRepo.GetItemArtwork(model.KindArtistArtwork, "ar-1", model.ImageTypePrimary)
|
||||
_, err := artRepo.GetItemArtwork(ctx, model.KindArtistArtwork, "ar-1", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
|
||||
queued, err := queueRepo.DequeueBatch(1000)
|
||||
queued, err := queueRepo.DequeueBatch(ctx, 1000)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(queued).To(ContainElement(SatisfyAll(
|
||||
HaveField("ItemKind", "ar"),
|
||||
|
|
@ -116,13 +112,12 @@ var _ = Describe("Uploader", func() {
|
|||
|
||||
It("is a no-op for an unknown entity type", func() {
|
||||
svc.EnqueueArtwork(context.Background(), "unknown", "x-1")
|
||||
Expect(queueRepo.DequeueBatch(1000)).To(BeEmpty())
|
||||
Expect(queueRepo.DequeueBatch(ctx, 1000)).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("RemoveImage", func() {
|
||||
It("removes the file at the given path", func() {
|
||||
ctx := context.Background()
|
||||
dir := filepath.Join(tmpDir, "artwork", "artist")
|
||||
Expect(os.MkdirAll(dir, 0755)).To(Succeed())
|
||||
path := filepath.Join(dir, "ar-1_test.jpg")
|
||||
|
|
@ -134,13 +129,11 @@ var _ = Describe("Uploader", func() {
|
|||
})
|
||||
|
||||
It("succeeds when file does not exist", func() {
|
||||
ctx := context.Background()
|
||||
err := svc.RemoveImage(ctx, "/nonexistent/file.jpg")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("succeeds with empty path", func() {
|
||||
ctx := context.Background()
|
||||
err := svc.RemoveImage(ctx, "")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ type Worker struct {
|
|||
broker events.Broker
|
||||
pruneMu sync.RWMutex
|
||||
pools []*drainPool
|
||||
runCtx context.Context
|
||||
runCtx context.Context //nolint:containedctx // worker lifecycle ctx, set at Run
|
||||
paused func() bool
|
||||
|
||||
gatesMu sync.Mutex
|
||||
|
|
@ -155,7 +155,7 @@ func (w *Worker) drain(ctx context.Context, concurrency int, kinds ...string) (i
|
|||
}
|
||||
// Dequeue well past the pool size so a slow external lookup never idles the other slots.
|
||||
// DequeueBatch does not mark rows taken, so this is one query per pass, not per slot.
|
||||
items, err := w.proc.ds.ArtworkQueue(ctx).DequeueBatch(max(16, 4*concurrency), kinds...)
|
||||
items, err := w.proc.ds.ArtworkQueue().DequeueBatch(ctx, max(16, 4*concurrency), kinds...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
|
@ -246,12 +246,12 @@ func (w *Worker) process(ctx context.Context, item model.ArtworkQueueItem) (outc
|
|||
ctx = withTrace(ctx, trace)
|
||||
out, got, retryIn := w.proc.acquire(ctx, item)
|
||||
|
||||
queue := w.proc.ds.ArtworkQueue(ctx)
|
||||
queue := w.proc.ds.ArtworkQueue()
|
||||
switch out {
|
||||
case outcomeFound, outcomeAbsent:
|
||||
// A scan that re-enqueued this row mid-flight reset its retry_at, so the row survives
|
||||
// here and the next drain re-resolves it.
|
||||
if err := queue.DeleteIfUnchanged(item.ItemKind, item.ItemID, item.ImageType, item.RetryAt); err != nil {
|
||||
if err := queue.DeleteIfUnchanged(ctx, item.ItemKind, item.ItemID, item.ImageType, item.RetryAt); err != nil {
|
||||
log.Warn(ctx, "Artwork: Could not delete processed queue item", "kind", item.ItemKind, "id", item.ItemID, err)
|
||||
}
|
||||
case outcomeFoundStale, outcomeFailed:
|
||||
|
|
@ -260,7 +260,7 @@ func (w *Worker) process(ctx context.Context, item model.ArtworkQueueItem) (outc
|
|||
if retryAt.Before(item.EnqueuedAt.Add(giveUpAfter)) {
|
||||
// A mid-flight re-enqueue reset retry_at; stale backoff must not stomp its
|
||||
// fresh, immediate eligibility.
|
||||
if err := queue.MarkFailedIfUnchanged(item.ItemKind, item.ItemID, item.ImageType, item.RetryAt, retryAt, encoded); err != nil {
|
||||
if err := queue.MarkFailedIfUnchanged(ctx, item.ItemKind, item.ItemID, item.ImageType, item.RetryAt, retryAt, encoded); err != nil {
|
||||
log.Warn(ctx, "Artwork: Could not reschedule failed queue item", "kind", item.ItemKind, "id", item.ItemID, err)
|
||||
}
|
||||
log.Debug(ctx, "Artwork: Rescheduled item", "kind", item.ItemKind, "id", item.ItemID,
|
||||
|
|
@ -271,7 +271,7 @@ func (w *Worker) process(ctx context.Context, item model.ArtworkQueueItem) (outc
|
|||
// Art already being served is kept: exhaustion means unreachable, not removed.
|
||||
settled := "kept previous state"
|
||||
if out == outcomeFailed && settlesAbsentOnGiveUp(item.ItemKind) && !w.hasResolvedArtwork(ctx, item) {
|
||||
writeAbsent(ctx, w.proc.ds.Artwork(ctx), item)
|
||||
writeAbsent(ctx, w.proc.ds.Artwork(), item)
|
||||
settled = "recorded absent"
|
||||
}
|
||||
// The queue row is about to go, taking the only record of the failure with it. This write is
|
||||
|
|
@ -279,7 +279,7 @@ func (w *Worker) process(ctx context.Context, item model.ArtworkQueueItem) (outc
|
|||
w.recordGiveUp(ctx, item, encoded)
|
||||
log.Info(ctx, "Artwork: Retry budget exhausted, giving up", "kind", item.ItemKind, "id", item.ItemID,
|
||||
"outcome", out, "attempts", item.Attempts+1, "budget", giveUpAfter, "settled", settled)
|
||||
if err := queue.DeleteIfUnchanged(item.ItemKind, item.ItemID, item.ImageType, item.RetryAt); err != nil {
|
||||
if err := queue.DeleteIfUnchanged(ctx, item.ItemKind, item.ItemID, item.ImageType, item.RetryAt); err != nil {
|
||||
log.Warn(ctx, "Artwork: Could not remove exhausted queue item", "kind", item.ItemKind, "id", item.ItemID, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -293,7 +293,7 @@ func (w *Worker) recordGiveUp(ctx context.Context, item model.ArtworkQueueItem,
|
|||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := w.proc.ds.Artwork(ctx).PutLastFailure(kind, item.ItemID, item.ImageType, trace); err != nil {
|
||||
if err := w.proc.ds.Artwork().PutLastFailure(ctx, kind, item.ItemID, item.ImageType, trace); err != nil {
|
||||
log.Warn(ctx, "Artwork: Could not record the last failure", "kind", item.ItemKind, "id", item.ItemID, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -303,7 +303,7 @@ func (w *Worker) hasResolvedArtwork(ctx context.Context, item model.ArtworkQueue
|
|||
if !ok {
|
||||
return false
|
||||
}
|
||||
ia, err := w.proc.ds.Artwork(ctx).GetItemArtwork(kind, item.ItemID, item.ImageType)
|
||||
ia, err := w.proc.ds.Artwork().GetItemArtwork(ctx, kind, item.ItemID, item.ImageType)
|
||||
return err == nil && ia.Hash != ""
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@ import (
|
|||
const soakCycles = 2200
|
||||
|
||||
var _ = Describe("Worker soak", func() {
|
||||
var ctx context.Context
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = GinkgoT().Context()
|
||||
})
|
||||
|
||||
It("does not leak goroutines, heap, or fds over many acquisition cycles", func() {
|
||||
if testing.Short() {
|
||||
Skip("skipping soak test in short mode")
|
||||
|
|
@ -100,9 +106,9 @@ var _ = Describe("Worker soak", func() {
|
|||
// Read-back exercises the surfaces a caller would use after acquisition.
|
||||
if out == outcomeFound {
|
||||
kind, _ := model.ParseKind(it.ItemKind)
|
||||
ia, err := artRepo.GetItemArtwork(kind, it.ItemID, model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, kind, it.ItemID, model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred(), "cycle %d: GetItemArtwork", i)
|
||||
art, err := artRepo.GetImage(ia.Hash)
|
||||
art, err := artRepo.GetImage(ctx, ia.Hash)
|
||||
Expect(err).ToNot(HaveOccurred(), "cycle %d: GetImage", i)
|
||||
rc, err := store.Open(ia.Hash, art.Mime)
|
||||
switch {
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@ type reenqueueOnDequeue struct {
|
|||
done bool
|
||||
}
|
||||
|
||||
func (r *reenqueueOnDequeue) DequeueBatch(n int, kinds ...string) ([]model.ArtworkQueueItem, error) {
|
||||
items, err := r.MockArtworkQueueRepo.DequeueBatch(n, kinds...)
|
||||
func (r *reenqueueOnDequeue) DequeueBatch(ctx context.Context, n int, kinds ...string) ([]model.ArtworkQueueItem, error) {
|
||||
items, err := r.MockArtworkQueueRepo.DequeueBatch(ctx, n, kinds...)
|
||||
if !r.done && len(items) > 0 {
|
||||
r.done = true
|
||||
for k, it := range r.Data {
|
||||
|
|
@ -124,18 +124,27 @@ type visibilityPlaylistDS struct {
|
|||
tracks model.PlaylistTrackRepository
|
||||
}
|
||||
|
||||
func (v *visibilityPlaylistDS) Playlist(ctx context.Context) model.PlaylistRepository {
|
||||
func (v *visibilityPlaylistDS) Playlist() model.PlaylistRepository {
|
||||
repo := tests.CreateMockPlaylistRepo()
|
||||
repo.TracksRepo = v.tracks
|
||||
if u, ok := request.UserFrom(ctx); ok && u.IsAdmin {
|
||||
repo.SetData(model.Playlists{v.private})
|
||||
repo.SetData(model.Playlists{v.private})
|
||||
return &visibilityPlaylistRepo{MockPlaylistRepo: repo}
|
||||
}
|
||||
|
||||
type visibilityPlaylistRepo struct {
|
||||
*tests.MockPlaylistRepo
|
||||
}
|
||||
|
||||
func (v *visibilityPlaylistRepo) Get(ctx context.Context, id string) (*model.Playlist, error) {
|
||||
if u, ok := request.UserFrom(ctx); !ok || !u.IsAdmin {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
return repo
|
||||
return v.MockPlaylistRepo.Get(ctx, id)
|
||||
}
|
||||
|
||||
func adminUserRepo() *tests.MockedUserRepo {
|
||||
repo := tests.CreateMockUserRepo()
|
||||
Expect(repo.Put(&model.User{ID: "admin", UserName: "admin", IsAdmin: true})).To(Succeed())
|
||||
Expect(repo.Put(GinkgoT().Context(), &model.User{ID: "admin", UserName: "admin", IsAdmin: true})).To(Succeed())
|
||||
return repo
|
||||
}
|
||||
|
||||
|
|
@ -157,8 +166,8 @@ var _ = Describe("Worker", func() {
|
|||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = GinkgoT().Context()
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
ctx = context.Background()
|
||||
var err error
|
||||
repoRoot, err = os.Getwd()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -200,7 +209,7 @@ var _ = Describe("Worker", func() {
|
|||
ds.MockedAlbum.(*tests.MockAlbumRepo).SetData(model.Albums{
|
||||
{ID: "al1", Name: "Album", FolderIDs: []string{"f1"}},
|
||||
})
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{
|
||||
ItemKind: "al", ItemID: "al1", Priority: model.ArtworkPriorityScan,
|
||||
})).To(Succeed())
|
||||
|
||||
|
|
@ -208,11 +217,11 @@ var _ = Describe("Worker", func() {
|
|||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(n).To(Equal(1))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("folder"))
|
||||
|
||||
count, err := queueRepo.Count()
|
||||
count, err := queueRepo.Count(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(count).To(BeZero(), "a found item must be deleted from the queue")
|
||||
})
|
||||
|
|
@ -223,7 +232,7 @@ var _ = Describe("Worker", func() {
|
|||
ds.MockedMediaFile.(*tests.MockMediaFileRepo).SetData(model.MediaFiles{
|
||||
{ID: "mf1", LibraryID: 0, Path: "tests/fixtures/artist/an-album/test.mp3", HasCoverArt: true},
|
||||
})
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{
|
||||
ItemKind: "mf", ItemID: "mf1", Priority: model.ArtworkPriorityBump,
|
||||
})).To(Succeed())
|
||||
|
||||
|
|
@ -231,12 +240,12 @@ var _ = Describe("Worker", func() {
|
|||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(n).To(Equal(1))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindMediaFileArtwork, "mf1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindMediaFileArtwork, "mf1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("embedded"))
|
||||
Expect(ia.Hash).ToNot(BeEmpty())
|
||||
|
||||
art, err := artRepo.GetImage(ia.Hash)
|
||||
art, err := artRepo.GetImage(ctx, ia.Hash)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
r, err := store.Open(ia.Hash, art.Mime)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -245,7 +254,7 @@ var _ = Describe("Worker", func() {
|
|||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(data).ToNot(BeEmpty(), "embedded bytes must be written to the store")
|
||||
|
||||
count, err := queueRepo.Count()
|
||||
count, err := queueRepo.Count(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(count).To(BeZero())
|
||||
})
|
||||
|
|
@ -254,7 +263,7 @@ var _ = Describe("Worker", func() {
|
|||
conf.Server.CoverArtPriority = "external"
|
||||
ds.MockedAlbum.(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "al4", Name: "Album"}})
|
||||
imageAgents(&fakeImageAgent{name: "failAgent", err: errors.New("agent timed out")})
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "al", ItemID: "al4"})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al4"})).To(Succeed())
|
||||
|
||||
n, err := w.drain(ctx, 2)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -265,7 +274,7 @@ var _ = Describe("Worker", func() {
|
|||
Expect(it.Attempts).To(Equal(1))
|
||||
Expect(it.RetryAt).To(BeTemporally(">", time.Now()))
|
||||
|
||||
_, err = artRepo.GetItemArtwork(model.KindAlbumArtwork, "al4", model.ImageTypePrimary)
|
||||
_, err = artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al4", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound), "a timeout must never settle on absent")
|
||||
})
|
||||
|
||||
|
|
@ -275,7 +284,7 @@ var _ = Describe("Worker", func() {
|
|||
// Well above backoff(0)'s jittered ceiling, so only the hint can produce this retry_at.
|
||||
const askedFor = 90 * time.Minute
|
||||
imageAgents(&fakeImageAgent{name: "throttledAgent", err: &agents.RetryLaterError{RetryIn: askedFor}})
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "al", ItemID: "al9"})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al9"})).To(Succeed())
|
||||
|
||||
n, err := w.drain(ctx, 2)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -296,7 +305,7 @@ var _ = Describe("Worker", func() {
|
|||
{ID: "alstale", Name: "Album", FolderIDs: []string{"f1"}},
|
||||
})
|
||||
imageAgents(&fakeImageAgent{name: "failAgent", err: errors.New("agent timed out")})
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "al", ItemID: "alstale"})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "alstale"})).To(Succeed())
|
||||
|
||||
n, err := w.drain(ctx, 2)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -307,7 +316,7 @@ var _ = Describe("Worker", func() {
|
|||
Expect(it.Attempts).To(Equal(1))
|
||||
Expect(it.RetryAt).To(BeTemporally(">", time.Now()))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "alstale", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "alstale", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("folder"), "the fallback art is served meanwhile")
|
||||
|
||||
|
|
@ -327,7 +336,7 @@ var _ = Describe("Worker", func() {
|
|||
racing := &reenqueueOnDequeue{MockArtworkQueueRepo: queueRepo}
|
||||
ds.MockedArtworkQueue = racing
|
||||
w = NewWorker(ds, store, ag, ffm, broker, imgCache)
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{
|
||||
ItemKind: "al", ItemID: "al7", Priority: model.ArtworkPriorityScan,
|
||||
})).To(Succeed())
|
||||
|
||||
|
|
@ -337,7 +346,7 @@ var _ = Describe("Worker", func() {
|
|||
|
||||
// The concurrent re-enqueue changed retry_at, so the found-path delete was a no-op.
|
||||
Expect(findQueued(queueRepo, "al", "al7")).ToNot(BeNil())
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al7", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al7", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("folder"))
|
||||
})
|
||||
|
|
@ -349,7 +358,7 @@ var _ = Describe("Worker", func() {
|
|||
racing := &reenqueueOnDequeue{MockArtworkQueueRepo: queueRepo}
|
||||
ds.MockedArtworkQueue = racing
|
||||
w = NewWorker(ds, store, ag, ffm, broker, imgCache)
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "al", ItemID: "al8"})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al8"})).To(Succeed())
|
||||
dequeued := findQueued(queueRepo, "al", "al8").RetryAt
|
||||
|
||||
n, err := w.drain(ctx, 1)
|
||||
|
|
@ -368,7 +377,7 @@ var _ = Describe("Worker", func() {
|
|||
ds.MockedAlbum.(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "al9", Name: "Album"}})
|
||||
imageAgents(&fakeImageAgent{name: "failAgent", err: errors.New("agent timed out")})
|
||||
w = NewWorker(ds, store, ag, ffm, broker, imgCache)
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "al", ItemID: "al9"})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al9"})).To(Succeed())
|
||||
// Age the row past the retry budget.
|
||||
expireQueued(queueRepo, "al9")
|
||||
|
||||
|
|
@ -377,7 +386,7 @@ var _ = Describe("Worker", func() {
|
|||
Expect(n).To(Equal(1))
|
||||
|
||||
Expect(findQueued(queueRepo, "al", "al9")).To(BeNil())
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al9", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al9", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Hash).To(BeEmpty())
|
||||
})
|
||||
|
|
@ -385,13 +394,13 @@ var _ = Describe("Worker", func() {
|
|||
It("keeps already-served art when the retry budget is exhausted", func() {
|
||||
conf.Server.CoverArtPriority = "external"
|
||||
ds.MockedAlbum.(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "al10", Name: "Album"}})
|
||||
Expect(artRepo.PutItemArtwork(&model.ItemArtwork{
|
||||
Expect(artRepo.PutItemArtwork(ctx, &model.ItemArtwork{
|
||||
ItemKind: "al", ItemID: "al10", ImageType: model.ImageTypePrimary,
|
||||
Hash: "cafebabe", Source: "external:lastfm",
|
||||
})).To(Succeed())
|
||||
imageAgents(&fakeImageAgent{name: "failAgent", err: errors.New("agent timed out")})
|
||||
w = NewWorker(ds, store, ag, ffm, broker, imgCache)
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "al", ItemID: "al10"})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al10"})).To(Succeed())
|
||||
expireQueued(queueRepo, "al10")
|
||||
|
||||
n, err := w.drain(ctx, 1)
|
||||
|
|
@ -399,7 +408,7 @@ var _ = Describe("Worker", func() {
|
|||
Expect(n).To(Equal(1))
|
||||
|
||||
Expect(findQueued(queueRepo, "al", "al10")).To(BeNil())
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al10", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al10", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Hash).To(Equal("cafebabe"), "a persistent outage must not discard served art")
|
||||
})
|
||||
|
|
@ -409,7 +418,7 @@ var _ = Describe("Worker", func() {
|
|||
ds.MockedAlbum.(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "al11", Name: "Album"}})
|
||||
imageAgents(&fakeImageAgent{name: "failAgent", err: errors.New("agent timed out")})
|
||||
w = NewWorker(ds, store, ag, ffm, broker, imgCache)
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "al", ItemID: "al11"})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al11"})).To(Succeed())
|
||||
|
||||
_, err := w.drain(ctx, 1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -430,13 +439,13 @@ var _ = Describe("Worker", func() {
|
|||
ds.MockedAlbum.(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "al13", Name: "Album"}})
|
||||
imageAgents(&fakeImageAgent{name: "failAgent", err: errors.New("agent timed out")})
|
||||
w = NewWorker(ds, store, ag, ffm, broker, imgCache)
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "al", ItemID: "al13"})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al13"})).To(Succeed())
|
||||
expireQueued(queueRepo, "al13")
|
||||
|
||||
_, err := w.drain(ctx, 1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al13", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al13", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred(), "settling absent must create the row the failure is written to")
|
||||
Expect(ia.Hash).To(BeEmpty())
|
||||
Expect(DecodeTrace(ia.LastFailure, "")).ToNot(BeEmpty())
|
||||
|
|
@ -445,20 +454,20 @@ var _ = Describe("Worker", func() {
|
|||
It("keeps the failure on the state row after the queue row is deleted", func() {
|
||||
conf.Server.CoverArtPriority = "external"
|
||||
ds.MockedAlbum.(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "al12", Name: "Album"}})
|
||||
Expect(artRepo.PutItemArtwork(&model.ItemArtwork{
|
||||
Expect(artRepo.PutItemArtwork(ctx, &model.ItemArtwork{
|
||||
ItemKind: "al", ItemID: "al12", ImageType: model.ImageTypePrimary,
|
||||
Hash: "cafebabe", Source: "external:lastfm",
|
||||
})).To(Succeed())
|
||||
imageAgents(&fakeImageAgent{name: "failAgent", err: errors.New("agent timed out")})
|
||||
w = NewWorker(ds, store, ag, ffm, broker, imgCache)
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "al", ItemID: "al12"})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al12"})).To(Succeed())
|
||||
expireQueued(queueRepo, "al12")
|
||||
|
||||
_, err := w.drain(ctx, 1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
Expect(findQueued(queueRepo, "al", "al12")).To(BeNil())
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al12", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al12", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(DecodeTrace(ia.LastFailure, "")).ToNot(BeEmpty(),
|
||||
"the queue row is gone, so this is the only remaining record of the failure")
|
||||
|
|
@ -473,7 +482,7 @@ var _ = Describe("Worker", func() {
|
|||
ds.MockedMediaFile.(*tests.MockMediaFileRepo).SetData(model.MediaFiles{
|
||||
{ID: "mfX", LibraryID: 0, Path: "tests/fixtures/artist/an-album/gone.mp3", HasCoverArt: true},
|
||||
})
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "mf", ItemID: "mfX"})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "mf", ItemID: "mfX"})).To(Succeed())
|
||||
expireQueued(queueRepo, "mfX")
|
||||
|
||||
n, err := w.drain(ctx, 1)
|
||||
|
|
@ -481,7 +490,7 @@ var _ = Describe("Worker", func() {
|
|||
Expect(n).To(Equal(1))
|
||||
|
||||
Expect(findQueued(queueRepo, "mf", "mfX")).To(BeNil(), "the row must stop retrying")
|
||||
_, err = artRepo.GetItemArtwork(model.KindMediaFileArtwork, "mfX", model.ImageTypePrimary)
|
||||
_, err = artRepo.GetItemArtwork(ctx, model.KindMediaFileArtwork, "mfX", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound),
|
||||
"no row leaves the track unresolved, so a later view can still recover it")
|
||||
// Known gap: with no row and no absent settle, there is nowhere to keep the failure.
|
||||
|
|
@ -496,14 +505,14 @@ var _ = Describe("Worker", func() {
|
|||
tracks: &tests.MockPlaylistTrackRepo{},
|
||||
}
|
||||
w = NewWorker(vds, store, ag, ffm, broker, imgCache)
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "pl", ItemID: "plPriv"})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "pl", ItemID: "plPriv"})).To(Succeed())
|
||||
|
||||
n, err := w.drain(ctx, 1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(n).To(Equal(1))
|
||||
|
||||
Expect(findQueued(queueRepo, "pl", "plPriv")).To(BeNil())
|
||||
ia, err := artRepo.GetItemArtwork(model.KindPlaylistArtwork, "plPriv", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindPlaylistArtwork, "plPriv", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Hash).To(BeEmpty())
|
||||
})
|
||||
|
|
@ -523,9 +532,9 @@ var _ = Describe("Worker", func() {
|
|||
{ID: "al1", Name: "Album 1", FolderIDs: []string{"f1"}},
|
||||
{ID: "al2", Name: "Album 2", FolderIDs: []string{"f1"}},
|
||||
})
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "al", ItemID: "al1", Priority: model.ArtworkPriorityScan})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "al", ItemID: "al2", Priority: model.ArtworkPriorityScan})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "ar", ItemID: "ar1", Priority: model.ArtworkPriorityScan})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al1", Priority: model.ArtworkPriorityScan})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al2", Priority: model.ArtworkPriorityScan})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "ar", ItemID: "ar1", Priority: model.ArtworkPriorityScan})).To(Succeed())
|
||||
|
||||
n, err := w.drain(ctx, 3)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -572,7 +581,7 @@ var _ = Describe("Worker", func() {
|
|||
conf.Server.CoverArtPriority = "cover.*" // local-only; no folder image → absent
|
||||
ds.MockedAlbum.(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "al3", Name: "Artless"}})
|
||||
folderRepo.result = nil
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "al", ItemID: "al3", Priority: model.ArtworkPriorityScan})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al3", Priority: model.ArtworkPriorityScan})).To(Succeed())
|
||||
|
||||
n, err := w.drain(ctx, 2)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -582,7 +591,7 @@ var _ = Describe("Worker", func() {
|
|||
Expect(evts).To(HaveLen(1), "a removed cover must live-refresh clients so they drop it")
|
||||
Expect(evts[0].(*events.RefreshResource).Data(evts[0])).To(ContainSubstring("al3"))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al3", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al3", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Hash).To(BeEmpty(), "the outcome was absent, not found")
|
||||
})
|
||||
|
|
@ -591,7 +600,7 @@ var _ = Describe("Worker", func() {
|
|||
conf.Server.CoverArtPriority = "external"
|
||||
ds.MockedAlbum.(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "alx", Name: "Album"}})
|
||||
imageAgents(&fakeImageAgent{name: "failAgent", err: errors.New("agent timed out")})
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{ItemKind: "al", ItemID: "alx"})).To(Succeed())
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "alx"})).To(Succeed())
|
||||
|
||||
n, err := w.drain(ctx, 2)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -725,7 +734,7 @@ var _ = Describe("Worker", func() {
|
|||
{ID: "alpc", Name: "Album", FolderIDs: []string{"f1"}},
|
||||
})
|
||||
conf.Server.UICoverArtSize = 300
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{
|
||||
ItemKind: "al", ItemID: "alpc", Priority: model.ArtworkPriorityScan,
|
||||
})).To(Succeed())
|
||||
})
|
||||
|
|
@ -813,11 +822,11 @@ var _ = Describe("Worker", func() {
|
|||
|
||||
// Artists first, exactly as Backfill orders them.
|
||||
for _, a := range artists {
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{
|
||||
ItemKind: "ar", ItemID: a.ID, Priority: model.ArtworkPriorityBackfill,
|
||||
})).To(Succeed())
|
||||
}
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{
|
||||
ItemKind: "al", ItemID: "alx", Priority: model.ArtworkPriorityBackfill,
|
||||
})).To(Succeed())
|
||||
|
||||
|
|
@ -832,12 +841,12 @@ var _ = Describe("Worker", func() {
|
|||
})
|
||||
|
||||
Eventually(func() bool {
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "alx", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "alx", model.ImageTypePrimary)
|
||||
return err == nil && ia.Hash != ""
|
||||
}, 5*time.Second, 50*time.Millisecond).Should(BeTrue(),
|
||||
"a blocked external pool must not hold up local artwork")
|
||||
|
||||
_, err := artRepo.GetItemArtwork(model.KindArtistArtwork, "arx0", model.ImageTypePrimary)
|
||||
_, err := artRepo.GetItemArtwork(ctx, model.KindArtistArtwork, "arx0", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound), "artists are still blocked, as intended")
|
||||
})
|
||||
})
|
||||
|
|
@ -849,7 +858,7 @@ var _ = Describe("Worker", func() {
|
|||
for i := range 8 {
|
||||
id := fmt.Sprintf("alc%d", i)
|
||||
albums = append(albums, model.Album{ID: id, Name: "Album"})
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{
|
||||
ItemKind: "al", ItemID: id, Priority: model.ArtworkPriorityScan,
|
||||
})).To(Succeed())
|
||||
}
|
||||
|
|
@ -875,21 +884,21 @@ var _ = Describe("Worker", func() {
|
|||
for i := range 8 {
|
||||
id := fmt.Sprintf("alp%d", i)
|
||||
albums = append(albums, model.Album{ID: id, Name: "Album", FolderIDs: []string{"f1"}})
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{
|
||||
ItemKind: "al", ItemID: id, Priority: model.ArtworkPriorityScan,
|
||||
})).To(Succeed())
|
||||
}
|
||||
ds.MockedAlbum.(*tests.MockAlbumRepo).SetData(albums)
|
||||
// Pauses as soon as the first item has left the queue.
|
||||
w.PauseWhile(func() bool {
|
||||
n, _ := queueRepo.Count()
|
||||
n, _ := queueRepo.Count(ctx)
|
||||
return n < 8
|
||||
})
|
||||
|
||||
_, err := w.drain(ctx, 1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
count, err := queueRepo.Count()
|
||||
count, err := queueRepo.Count(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(count).To(Equal(int64(7)), "only the item dispatched before the pause may leave the queue")
|
||||
})
|
||||
|
|
@ -897,7 +906,7 @@ var _ = Describe("Worker", func() {
|
|||
It("dequeues past the worker pool so one drain covers many items", func() {
|
||||
for i := range 16 {
|
||||
ds.MockedAlbum.(*tests.MockAlbumRepo).SetData(model.Albums{{ID: fmt.Sprintf("alb%d", i), Name: "Album"}})
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{
|
||||
ItemKind: "al", ItemID: fmt.Sprintf("alb%d", i), Priority: model.ArtworkPriorityScan,
|
||||
})).To(Succeed())
|
||||
}
|
||||
|
|
@ -932,7 +941,7 @@ var _ = Describe("Worker", func() {
|
|||
ds.MockedAlbum.(*tests.MockAlbumRepo).SetData(model.Albums{
|
||||
{ID: "al1", Name: "Album", FolderIDs: []string{"f1"}},
|
||||
})
|
||||
Expect(queueRepo.Enqueue(model.ArtworkQueueItem{
|
||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{
|
||||
ItemKind: "al", ItemID: "al1", Priority: model.ArtworkPriorityScan,
|
||||
})).To(Succeed())
|
||||
w.PauseWhile(func() bool { return true })
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ func Init(ds model.DataStore) {
|
|||
}
|
||||
|
||||
func loadOrCreateSecret(ctx context.Context, ds model.DataStore, key string) string {
|
||||
secret, err := ds.Property(ctx).Get(key)
|
||||
secret, err := ds.Property().Get(ctx, key)
|
||||
if err != nil || secret == "" {
|
||||
log.Info(ctx, "Creating new JWT secret", "key", key)
|
||||
return createNewSecret(ctx, ds, key)
|
||||
|
|
@ -154,9 +154,9 @@ func CheckClaims(c Claims, usr model.User, audience string) error {
|
|||
}
|
||||
|
||||
func WithAdminUser(ctx context.Context, ds model.DataStore) context.Context {
|
||||
u, err := ds.User(ctx).FindFirstAdmin()
|
||||
u, err := ds.User().FindFirstAdmin(ctx)
|
||||
if err != nil {
|
||||
c, err := ds.User(ctx).CountAll()
|
||||
c, err := ds.User().CountAll(ctx)
|
||||
if c == 0 && err == nil {
|
||||
log.Debug(ctx, "No admin user yet!", err)
|
||||
} else {
|
||||
|
|
@ -176,7 +176,7 @@ func createNewSecret(ctx context.Context, ds model.DataStore, key string) string
|
|||
log.Error(ctx, "Could not encrypt JWT secret", err)
|
||||
return secret
|
||||
}
|
||||
if err := ds.Property(ctx).Put(key, encSecret); err != nil {
|
||||
if err := ds.Property().Put(ctx, key, encSecret); err != nil {
|
||||
log.Error(ctx, "Could not save JWT secret in DB", err)
|
||||
}
|
||||
return secret
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func userName(ctx context.Context) string {
|
|||
// BFR We should only access files through the `storage.Storage` interface. This will require changing how
|
||||
// TagLib and ffmpeg access files
|
||||
var AbsolutePath = func(ctx context.Context, ds model.DataStore, libId int, path string) string {
|
||||
libPath, err := ds.Library(ctx).GetPath(libId)
|
||||
libPath, err := ds.Library().GetPath(ctx, libId)
|
||||
if err != nil {
|
||||
return path
|
||||
}
|
||||
|
|
|
|||
18
core/external/extdata_helper_test.go
vendored
18
core/external/extdata_helper_test.go
vendored
|
|
@ -31,7 +31,7 @@ func (m *mockArtistRepo) SetData(artists model.Artists) {
|
|||
}
|
||||
|
||||
// Get implements model.ArtistRepository.
|
||||
func (m *mockArtistRepo) Get(id string) (*model.Artist, error) {
|
||||
func (m *mockArtistRepo) Get(_ context.Context, id string) (*model.Artist, error) {
|
||||
args := m.Called(id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
|
|
@ -40,7 +40,7 @@ func (m *mockArtistRepo) Get(id string) (*model.Artist, error) {
|
|||
}
|
||||
|
||||
// GetAll implements model.ArtistRepository.
|
||||
func (m *mockArtistRepo) GetAll(options ...model.QueryOptions) (model.Artists, error) {
|
||||
func (m *mockArtistRepo) GetAll(_ context.Context, options ...model.QueryOptions) (model.Artists, error) {
|
||||
argsSlice := make([]any, len(options))
|
||||
for i, v := range options {
|
||||
argsSlice[i] = v
|
||||
|
|
@ -85,7 +85,7 @@ func (m *mockMediaFileRepo) SetData(mediaFiles model.MediaFiles) {
|
|||
}
|
||||
|
||||
// Get implements model.MediaFileRepository.
|
||||
func (m *mockMediaFileRepo) Get(id string) (*model.MediaFile, error) {
|
||||
func (m *mockMediaFileRepo) Get(ctx context.Context, id string) (*model.MediaFile, error) {
|
||||
args := m.Called(id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
|
|
@ -94,12 +94,12 @@ func (m *mockMediaFileRepo) Get(id string) (*model.MediaFile, error) {
|
|||
}
|
||||
|
||||
// GetAllByTags implements model.MediaFileRepository.
|
||||
func (m *mockMediaFileRepo) GetAllByTags(_ model.TagName, _ []string, options ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
return m.GetAll(options...)
|
||||
func (m *mockMediaFileRepo) GetAllByTags(ctx context.Context, _ model.TagName, _ []string, options ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
return m.GetAll(ctx, options...)
|
||||
}
|
||||
|
||||
// GetAll implements model.MediaFileRepository.
|
||||
func (m *mockMediaFileRepo) GetAll(options ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
func (m *mockMediaFileRepo) GetAll(ctx context.Context, options ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
argsSlice := make([]any, len(options))
|
||||
for i, v := range options {
|
||||
argsSlice[i] = v
|
||||
|
|
@ -112,7 +112,7 @@ func (m *mockMediaFileRepo) GetAll(options ...model.QueryOptions) (model.MediaFi
|
|||
}
|
||||
|
||||
// GetRandom implements model.MediaFileRepository.
|
||||
func (m *mockMediaFileRepo) GetRandom(options ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
func (m *mockMediaFileRepo) GetRandom(ctx context.Context, options ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
argsSlice := make([]any, len(options))
|
||||
for i, v := range options {
|
||||
argsSlice[i] = v
|
||||
|
|
@ -156,7 +156,7 @@ func newMockAlbumRepo() *mockAlbumRepo {
|
|||
}
|
||||
|
||||
// Get implements model.AlbumRepository.
|
||||
func (m *mockAlbumRepo) Get(id string) (*model.Album, error) {
|
||||
func (m *mockAlbumRepo) Get(_ context.Context, id string) (*model.Album, error) {
|
||||
args := m.Called(id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
|
|
@ -165,7 +165,7 @@ func (m *mockAlbumRepo) Get(id string) (*model.Album, error) {
|
|||
}
|
||||
|
||||
// GetAll implements model.AlbumRepository.
|
||||
func (m *mockAlbumRepo) GetAll(options ...model.QueryOptions) (model.Albums, error) {
|
||||
func (m *mockAlbumRepo) GetAll(_ context.Context, options ...model.QueryOptions) (model.Albums, error) {
|
||||
argsSlice := make([]any, len(options))
|
||||
for i, v := range options {
|
||||
argsSlice[i] = v
|
||||
|
|
|
|||
16
core/external/provider.go
vendored
16
core/external/provider.go
vendored
|
|
@ -182,7 +182,7 @@ func (e *provider) populateAlbumInfo(ctx context.Context, album auxAlbum) (auxAl
|
|||
}
|
||||
}
|
||||
|
||||
err = e.ds.Album(ctx).UpdateExternalInfo(&album.Album)
|
||||
err = e.ds.Album().UpdateExternalInfo(ctx, &album.Album)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error trying to update album external information", "id", album.ID, "name", albumName,
|
||||
"elapsed", time.Since(start), err)
|
||||
|
|
@ -285,7 +285,7 @@ func (e *provider) populateArtistInfo(ctx context.Context, artist auxArtist) (au
|
|||
if !throttled {
|
||||
artist.ExternalInfoUpdatedAt = new(time.Now())
|
||||
}
|
||||
err := e.ds.Artist(ctx).UpdateExternalInfo(&artist.Artist)
|
||||
err := e.ds.Artist().UpdateExternalInfo(ctx, &artist.Artist)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error trying to update artist external information", "id", artist.ID, "name", artistName,
|
||||
"elapsed", time.Since(start), err)
|
||||
|
|
@ -548,7 +548,7 @@ func (e *provider) loadArtistsByID(ctx context.Context, similar []agents.Artist)
|
|||
if len(ids) == 0 {
|
||||
return matches, nil
|
||||
}
|
||||
res, err := e.ds.Artist(ctx).GetAll(model.QueryOptions{
|
||||
res, err := e.ds.Artist().GetAll(ctx, model.QueryOptions{
|
||||
Filters: squirrel.Eq{"artist.id": ids},
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -577,7 +577,7 @@ func (e *provider) loadArtistsByMBID(ctx context.Context, similar []agents.Artis
|
|||
if len(mbids) == 0 {
|
||||
return matches, nil
|
||||
}
|
||||
res, err := e.ds.Artist(ctx).GetAll(model.QueryOptions{
|
||||
res, err := e.ds.Artist().GetAll(ctx, model.QueryOptions{
|
||||
Filters: squirrel.Eq{"mbz_artist_id": mbids},
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -612,7 +612,7 @@ func (e *provider) loadArtistsByName(ctx context.Context, similar []agents.Artis
|
|||
clauses := slice.Map(names, func(name string) squirrel.Sqlizer {
|
||||
return squirrel.Like{"artist.name": name}
|
||||
})
|
||||
res, err := e.ds.Artist(ctx).GetAll(model.QueryOptions{
|
||||
res, err := e.ds.Artist().GetAll(ctx, model.QueryOptions{
|
||||
Filters: squirrel.Or(clauses),
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -628,7 +628,7 @@ func (e *provider) loadArtistsByName(ctx context.Context, similar []agents.Artis
|
|||
|
||||
func (e *provider) findArtist(ctx context.Context, artistName, id string) (*auxArtist, error) {
|
||||
if id != "" {
|
||||
artist, err := e.ds.Artist(ctx).Get(id)
|
||||
artist, err := e.ds.Artist().Get(ctx, id)
|
||||
if err == nil {
|
||||
return &auxArtist{Artist: *artist}, nil
|
||||
}
|
||||
|
|
@ -644,7 +644,7 @@ func (e *provider) findArtist(ctx context.Context, artistName, id string) (*auxA
|
|||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
artists, err := e.ds.Artist(ctx).GetAll(model.QueryOptions{
|
||||
artists, err := e.ds.Artist().GetAll(ctx, model.QueryOptions{
|
||||
Filters: squirrel.Like{"artist.name": artistName},
|
||||
Max: 1,
|
||||
})
|
||||
|
|
@ -666,7 +666,7 @@ func (e *provider) loadSimilar(ctx context.Context, artist *auxArtist, count int
|
|||
ids = append(ids, sa.ID)
|
||||
}
|
||||
|
||||
similar, err := e.ds.Artist(ctx).GetAll(model.QueryOptions{
|
||||
similar, err := e.ds.Artist().GetAll(ctx, model.QueryOptions{
|
||||
Filters: squirrel.Eq{"artist.id": ids},
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
|||
8
core/external/provider_refreshinfo_test.go
vendored
8
core/external/provider_refreshinfo_test.go
vendored
|
|
@ -71,8 +71,8 @@ var _ = Describe("Provider - RefreshInfo", func() {
|
|||
ag = new(mockAgents)
|
||||
broker = &fakeBroker{}
|
||||
p = external.NewProvider(ds, ag, matcher.New(ds), broker)
|
||||
mockArtistRepo = ds.Artist(ctx).(*tests.MockArtistRepo)
|
||||
mockAlbumRepo = ds.Album(ctx).(*tests.MockAlbumRepo)
|
||||
mockArtistRepo = ds.Artist().(*tests.MockArtistRepo)
|
||||
mockAlbumRepo = ds.Album().(*tests.MockAlbumRepo)
|
||||
})
|
||||
|
||||
It("repopulates an artist even when its info is fresh", func() {
|
||||
|
|
@ -84,7 +84,7 @@ var _ = Describe("Provider - RefreshInfo", func() {
|
|||
|
||||
Expect(p.RefreshInfo(ctx, model.KindArtistArtwork, "ar-1")).To(Succeed())
|
||||
|
||||
saved, err := mockArtistRepo.Get("ar-1")
|
||||
saved, err := mockArtistRepo.Get(ctx, "ar-1")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(saved.Biography).To(Equal("Fresh Bio"))
|
||||
})
|
||||
|
|
@ -99,7 +99,7 @@ var _ = Describe("Provider - RefreshInfo", func() {
|
|||
|
||||
Expect(p.RefreshInfo(ctx, model.KindAlbumArtwork, "al-1")).To(Succeed())
|
||||
|
||||
saved, err := mockAlbumRepo.Get("al-1")
|
||||
saved, err := mockAlbumRepo.Get(ctx, "al-1")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(saved.Description).To(Equal("Fresh Notes"))
|
||||
})
|
||||
|
|
|
|||
8
core/external/provider_similarsongs.go
vendored
8
core/external/provider_similarsongs.go
vendored
|
|
@ -37,7 +37,7 @@ func (e *provider) SimilarSongs(ctx context.Context, id string, count int) (mode
|
|||
if !errors.Is(err, model.ErrNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
genre, err := e.ds.Genre(ctx).Get(id)
|
||||
genre, err := e.ds.Genre().Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -178,13 +178,13 @@ func (e *provider) seedMix(ctx context.Context, count int, sample func() (model.
|
|||
func (e *provider) samplePlaylistTracks(ctx context.Context, playlistID string, n int) (model.MediaFiles, error) {
|
||||
// Refresh: a smart playlist materializes no tracks until it is evaluated, so skipping it would
|
||||
// mix an empty seed set. It is a no-op for regular playlists and inside the refresh delay.
|
||||
repo := e.ds.Playlist(ctx).Tracks(playlistID, true)
|
||||
repo := e.ds.Playlist().Tracks(ctx, playlistID, true)
|
||||
if repo == nil {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
// A playlist can hold the same file at several positions, so over-fetch and dedup: a repeated
|
||||
// seed wastes an agent call and can reach the mix twice through the seed fallback.
|
||||
tracks, err := repo.GetAll(model.QueryOptions{
|
||||
tracks, err := repo.GetAll(ctx, model.QueryOptions{
|
||||
Sort: "random",
|
||||
Max: n * 4,
|
||||
Filters: squirrel.Eq{"missing": false},
|
||||
|
|
@ -225,7 +225,7 @@ func (e *provider) sampleGenreTracks(ctx context.Context, genre *model.Genre, n
|
|||
// sampleTracks returns up to n random present tracks. Seeds can end up in the mix verbatim, so
|
||||
// missing files would surface as unplayable entries.
|
||||
func (e *provider) sampleTracks(ctx context.Context, filter squirrel.Sqlizer, n int) (model.MediaFiles, error) {
|
||||
return e.ds.MediaFile(ctx).GetRandom(model.QueryOptions{
|
||||
return e.ds.MediaFile().GetRandom(ctx, model.QueryOptions{
|
||||
Filters: squirrel.And{filter, squirrel.Eq{"missing": false}},
|
||||
Max: n,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ var _ = Describe("Provider - UpdateAlbumInfo", func() {
|
|||
ds = new(tests.MockDataStore)
|
||||
ag = new(mockAgents)
|
||||
p = external.NewProvider(ds, ag, matcher.New(ds), &fakeBroker{})
|
||||
mockAlbumRepo = ds.Album(ctx).(*tests.MockAlbumRepo)
|
||||
mockAlbumRepo = ds.Album().(*tests.MockAlbumRepo)
|
||||
conf.Server.DevAlbumInfoTimeToLive = 1 * time.Hour
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ var _ = Describe("Provider - UpdateArtistInfo", func() {
|
|||
ds = new(tests.MockDataStore)
|
||||
ag = new(mockAgents)
|
||||
p = external.NewProvider(ds, ag, matcher.New(ds), &fakeBroker{})
|
||||
mockArtistRepo = ds.Artist(ctx).(*tests.MockArtistRepo)
|
||||
mockArtistRepo = ds.Artist().(*tests.MockArtistRepo)
|
||||
})
|
||||
|
||||
It("returns error when artist is not found", func() {
|
||||
|
|
|
|||
140
core/library.go
140
core/library.go
|
|
@ -32,25 +32,28 @@ type Library interface {
|
|||
SetUserLibraries(ctx context.Context, userID string, libraryIDs []int) error
|
||||
ValidateLibraryAccess(ctx context.Context, userID string, libraryID int) error
|
||||
|
||||
NewRepository(ctx context.Context) rest.Repository
|
||||
Repository() rest.Repository[model.Library]
|
||||
}
|
||||
|
||||
type libraryService struct {
|
||||
ds model.DataStore
|
||||
scanner model.Scanner
|
||||
watcher Watcher
|
||||
broker events.Broker
|
||||
pluginManager PluginUnloader
|
||||
ds model.DataStore
|
||||
broker events.Broker
|
||||
repo *libraryRepositoryWrapper
|
||||
}
|
||||
|
||||
// NewLibrary creates a new Library service
|
||||
func NewLibrary(ds model.DataStore, scanner model.Scanner, watcher Watcher, broker events.Broker, pluginManager PluginUnloader) Library {
|
||||
return &libraryService{
|
||||
ds: ds,
|
||||
scanner: scanner,
|
||||
watcher: watcher,
|
||||
broker: broker,
|
||||
pluginManager: pluginManager,
|
||||
ds: ds,
|
||||
broker: broker,
|
||||
repo: &libraryRepositoryWrapper{
|
||||
LibraryRepository: ds.Library(),
|
||||
ds: ds,
|
||||
scanner: scanner,
|
||||
watcher: watcher,
|
||||
broker: broker,
|
||||
pluginManager: pluginManager,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -58,16 +61,16 @@ func NewLibrary(ds model.DataStore, scanner model.Scanner, watcher Watcher, brok
|
|||
|
||||
func (s *libraryService) GetUserLibraries(ctx context.Context, userID string) (model.Libraries, error) {
|
||||
// Verify user exists
|
||||
if _, err := s.ds.User(ctx).Get(userID); err != nil {
|
||||
if _, err := s.ds.User().Get(ctx, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.ds.User(ctx).GetUserLibraries(userID)
|
||||
return s.ds.User().GetUserLibraries(ctx, userID)
|
||||
}
|
||||
|
||||
func (s *libraryService) SetUserLibraries(ctx context.Context, userID string, libraryIDs []int) error {
|
||||
// Verify user exists
|
||||
user, err := s.ds.User(ctx).Get(userID)
|
||||
user, err := s.ds.User().Get(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -90,7 +93,7 @@ func (s *libraryService) SetUserLibraries(ctx context.Context, userID string, li
|
|||
}
|
||||
|
||||
// Set user libraries
|
||||
err = s.ds.User(ctx).SetUserLibraries(userID, libraryIDs)
|
||||
err = s.ds.User().SetUserLibraries(ctx, userID, libraryIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting user libraries: %w", err)
|
||||
}
|
||||
|
|
@ -115,7 +118,7 @@ func (s *libraryService) ValidateLibraryAccess(ctx context.Context, userID strin
|
|||
}
|
||||
|
||||
// Check if user has explicit access to this library
|
||||
libraries, err := s.ds.User(ctx).GetUserLibraries(userID)
|
||||
libraries, err := s.ds.User().GetUserLibraries(ctx, userID)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error checking library access", "userID", userID, "libraryID", libraryID, err)
|
||||
return fmt.Errorf("error checking library access: %w", err)
|
||||
|
|
@ -132,25 +135,14 @@ func (s *libraryService) ValidateLibraryAccess(ctx context.Context, userID strin
|
|||
|
||||
// REST repository wrapper
|
||||
|
||||
func (s *libraryService) NewRepository(ctx context.Context) rest.Repository {
|
||||
repo := s.ds.Library(ctx)
|
||||
wrapper := &libraryRepositoryWrapper{
|
||||
ctx: ctx,
|
||||
LibraryRepository: repo,
|
||||
Repository: repo.(rest.Repository),
|
||||
ds: s.ds,
|
||||
scanner: s.scanner,
|
||||
watcher: s.watcher,
|
||||
broker: s.broker,
|
||||
pluginManager: s.pluginManager,
|
||||
}
|
||||
return wrapper
|
||||
func (s *libraryService) Repository() rest.Repository[model.Library] {
|
||||
return s.repo
|
||||
}
|
||||
|
||||
var _ rest.Persistable[model.Library] = (*libraryRepositoryWrapper)(nil)
|
||||
|
||||
type libraryRepositoryWrapper struct {
|
||||
rest.Repository
|
||||
model.LibraryRepository
|
||||
ctx context.Context
|
||||
ds model.DataStore
|
||||
scanner model.Scanner
|
||||
watcher Watcher
|
||||
|
|
@ -158,59 +150,58 @@ type libraryRepositoryWrapper struct {
|
|||
pluginManager PluginUnloader
|
||||
}
|
||||
|
||||
func (r *libraryRepositoryWrapper) Save(entity any) (string, error) {
|
||||
lib := entity.(*model.Library)
|
||||
if err := r.validateLibrary(lib); err != nil {
|
||||
func (r *libraryRepositoryWrapper) Save(ctx context.Context, lib *model.Library) (string, error) {
|
||||
if err := r.validateLibrary(ctx, lib); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err := r.LibraryRepository.Put(lib)
|
||||
err := r.LibraryRepository.Put(ctx, lib)
|
||||
if err != nil {
|
||||
return "", r.mapError(err)
|
||||
}
|
||||
|
||||
// Start watcher and trigger scan after successful library creation
|
||||
if r.watcher != nil {
|
||||
if err := r.watcher.Watch(r.ctx, lib); err != nil {
|
||||
log.Warn(r.ctx, "Failed to start watcher for new library", "libraryID", lib.ID, "name", lib.Name, "path", lib.Path, err)
|
||||
if err := r.watcher.Watch(ctx, lib); err != nil {
|
||||
log.Warn(ctx, "Failed to start watcher for new library", "libraryID", lib.ID, "name", lib.Name, "path", lib.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
if r.scanner != nil {
|
||||
go r.triggerScan(lib, "new")
|
||||
go r.triggerScan(ctx, lib, "new")
|
||||
}
|
||||
|
||||
// Send library refresh event to all clients
|
||||
if r.broker != nil {
|
||||
event := &events.RefreshResource{}
|
||||
r.broker.SendBroadcastMessage(r.ctx, event.With("library", strconv.Itoa(lib.ID)))
|
||||
log.Debug(r.ctx, "Library created - sent refresh event", "libraryID", lib.ID, "name", lib.Name)
|
||||
r.broker.SendBroadcastMessage(ctx, event.With("library", strconv.Itoa(lib.ID)))
|
||||
log.Debug(ctx, "Library created - sent refresh event", "libraryID", lib.ID, "name", lib.Name)
|
||||
}
|
||||
|
||||
return strconv.Itoa(lib.ID), nil
|
||||
}
|
||||
|
||||
func (r *libraryRepositoryWrapper) Update(id string, entity any, cols ...string) error {
|
||||
lib := entity.(*model.Library)
|
||||
func (r *libraryRepositoryWrapper) Update(ctx context.Context, id string, entity model.Library, cols ...string) error {
|
||||
lib := &entity
|
||||
libID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid library ID: %s", id)
|
||||
}
|
||||
|
||||
lib.ID = libID
|
||||
if err := r.validateLibrary(lib); err != nil {
|
||||
if err := r.validateLibrary(ctx, lib); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the original library to check if path changed
|
||||
originalLib, err := r.Get(libID)
|
||||
originalLib, err := r.Get(ctx, libID)
|
||||
if err != nil {
|
||||
return r.mapError(err)
|
||||
}
|
||||
|
||||
pathChanged := originalLib.Path != lib.Path
|
||||
|
||||
err = r.LibraryRepository.Put(lib, cols...)
|
||||
err = r.LibraryRepository.Put(ctx, lib, cols...)
|
||||
if err != nil {
|
||||
return r.mapError(err)
|
||||
}
|
||||
|
|
@ -218,27 +209,36 @@ func (r *libraryRepositoryWrapper) Update(id string, entity any, cols ...string)
|
|||
// Restart watcher and trigger scan if path was updated
|
||||
if pathChanged {
|
||||
if r.watcher != nil {
|
||||
if err := r.watcher.Watch(r.ctx, lib); err != nil {
|
||||
log.Warn(r.ctx, "Failed to restart watcher for updated library", "libraryID", lib.ID, "name", lib.Name, "path", lib.Path, err)
|
||||
if err := r.watcher.Watch(ctx, lib); err != nil {
|
||||
log.Warn(ctx, "Failed to restart watcher for updated library", "libraryID", lib.ID, "name", lib.Name, "path", lib.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
if r.scanner != nil {
|
||||
go r.triggerScan(lib, "updated")
|
||||
go r.triggerScan(ctx, lib, "updated")
|
||||
}
|
||||
}
|
||||
|
||||
// Send library refresh event to all clients
|
||||
if r.broker != nil {
|
||||
event := &events.RefreshResource{}
|
||||
r.broker.SendBroadcastMessage(r.ctx, event.With("library", id))
|
||||
log.Debug(r.ctx, "Library updated - sent refresh event", "libraryID", libID, "name", lib.Name)
|
||||
r.broker.SendBroadcastMessage(ctx, event.With("library", id))
|
||||
log.Debug(ctx, "Library updated - sent refresh event", "libraryID", libID, "name", lib.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *libraryRepositoryWrapper) Delete(id string) error {
|
||||
func (r *libraryRepositoryWrapper) Delete(ctx context.Context, ids ...string) error {
|
||||
for _, id := range ids {
|
||||
if err := r.deleteOne(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *libraryRepositoryWrapper) deleteOne(ctx context.Context, id string) error {
|
||||
libID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return &rest.ValidationError{Errors: map[string]string{
|
||||
|
|
@ -247,7 +247,7 @@ func (r *libraryRepositoryWrapper) Delete(id string) error {
|
|||
}
|
||||
|
||||
// Get library info before deletion for logging
|
||||
lib, err := r.Get(libID)
|
||||
lib, err := r.Get(ctx, libID)
|
||||
if err != nil {
|
||||
return r.mapError(err)
|
||||
}
|
||||
|
|
@ -255,7 +255,7 @@ func (r *libraryRepositoryWrapper) Delete(id string) error {
|
|||
// Run the deletion in a transaction so the cascade delete and the orphaned-artist
|
||||
// reconciliation it triggers (see libraryRepository.Delete) commit atomically.
|
||||
err = r.ds.WithTx(func(tx model.DataStore) error {
|
||||
return tx.Library(r.ctx).Delete(libID)
|
||||
return tx.Library().Delete(ctx, libID)
|
||||
}, "delete library")
|
||||
if err != nil {
|
||||
return r.mapError(err)
|
||||
|
|
@ -263,25 +263,25 @@ func (r *libraryRepositoryWrapper) Delete(id string) error {
|
|||
|
||||
// Stop watcher and trigger scan after successful library deletion to clean up orphaned data
|
||||
if r.watcher != nil {
|
||||
if err := r.watcher.StopWatching(r.ctx, libID); err != nil {
|
||||
log.Warn(r.ctx, "Failed to stop watcher for deleted library", "libraryID", libID, "name", lib.Name, "path", lib.Path, err)
|
||||
if err := r.watcher.StopWatching(ctx, libID); err != nil {
|
||||
log.Warn(ctx, "Failed to stop watcher for deleted library", "libraryID", libID, "name", lib.Name, "path", lib.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
if r.scanner != nil {
|
||||
go r.triggerScan(lib, "deleted")
|
||||
go r.triggerScan(ctx, lib, "deleted")
|
||||
}
|
||||
|
||||
// Send library refresh event to all clients
|
||||
if r.broker != nil {
|
||||
event := &events.RefreshResource{}
|
||||
r.broker.SendBroadcastMessage(r.ctx, event.With("library", id))
|
||||
log.Debug(r.ctx, "Library deleted - sent refresh event", "libraryID", libID, "name", lib.Name)
|
||||
r.broker.SendBroadcastMessage(ctx, event.With("library", id))
|
||||
log.Debug(ctx, "Library deleted - sent refresh event", "libraryID", libID, "name", lib.Name)
|
||||
}
|
||||
|
||||
// After successful deletion, check if any plugins were auto-disabled
|
||||
// and need to be unloaded from memory
|
||||
r.pluginManager.UnloadDisabledPlugins(r.ctx)
|
||||
r.pluginManager.UnloadDisabledPlugins(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -309,7 +309,7 @@ func (r *libraryRepositoryWrapper) mapError(err error) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (r *libraryRepositoryWrapper) validateLibrary(library *model.Library) error {
|
||||
func (r *libraryRepositoryWrapper) validateLibrary(ctx context.Context, library *model.Library) error {
|
||||
validationErrors := make(map[string]string)
|
||||
|
||||
if library.Name == "" {
|
||||
|
|
@ -320,7 +320,7 @@ func (r *libraryRepositoryWrapper) validateLibrary(library *model.Library) error
|
|||
validationErrors["path"] = "ra.validation.required"
|
||||
} else {
|
||||
// Validate path format and accessibility
|
||||
if err := r.validateLibraryPath(library); err != nil {
|
||||
if err := r.validateLibraryPath(ctx, library); err != nil {
|
||||
validationErrors["path"] = err.Error()
|
||||
}
|
||||
}
|
||||
|
|
@ -332,7 +332,7 @@ func (r *libraryRepositoryWrapper) validateLibrary(library *model.Library) error
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *libraryRepositoryWrapper) validateLibraryPath(library *model.Library) error {
|
||||
func (r *libraryRepositoryWrapper) validateLibraryPath(ctx context.Context, library *model.Library) error {
|
||||
// Validate path format
|
||||
if !filepath.IsAbs(library.Path) {
|
||||
return fmt.Errorf("library path must be absolute")
|
||||
|
|
@ -350,7 +350,7 @@ func (r *libraryRepositoryWrapper) validateLibraryPath(library *model.Library) e
|
|||
|
||||
fsys, err := fileStore.FS()
|
||||
if err != nil {
|
||||
log.Warn(r.ctx, "Error validating library.path", "path", library.Path, err)
|
||||
log.Warn(ctx, "Error validating library.path", "path", library.Path, err)
|
||||
return fmt.Errorf("resources.library.validation.pathInvalid")
|
||||
}
|
||||
|
||||
|
|
@ -358,7 +358,7 @@ func (r *libraryRepositoryWrapper) validateLibraryPath(library *model.Library) e
|
|||
info, err := fs.Stat(fsys, ".")
|
||||
if err != nil {
|
||||
// Parse the error message to check for "not a directory"
|
||||
log.Warn(r.ctx, "Error stating library.path", "path", library.Path, err)
|
||||
log.Warn(ctx, "Error stating library.path", "path", library.Path, err)
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "not a directory") ||
|
||||
strings.Contains(errStr, "The directory name is invalid.") {
|
||||
|
|
@ -385,7 +385,7 @@ func (s *libraryService) validateLibraryIDs(ctx context.Context, libraryIDs []in
|
|||
}
|
||||
|
||||
// Use CountAll to efficiently validate library IDs exist
|
||||
count, err := s.ds.Library(ctx).CountAll(model.QueryOptions{
|
||||
count, err := s.ds.Library().CountAll(ctx, model.QueryOptions{
|
||||
Filters: squirrel.Eq{"id": libraryIDs},
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -399,13 +399,13 @@ func (s *libraryService) validateLibraryIDs(ctx context.Context, libraryIDs []in
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *libraryRepositoryWrapper) triggerScan(lib *model.Library, action string) {
|
||||
log.Info(r.ctx, fmt.Sprintf("Triggering scan for %s library", action), "libraryID", lib.ID, "name", lib.Name, "path", lib.Path)
|
||||
func (r *libraryRepositoryWrapper) triggerScan(ctx context.Context, lib *model.Library, action string) {
|
||||
log.Info(ctx, fmt.Sprintf("Triggering scan for %s library", action), "libraryID", lib.ID, "name", lib.Name, "path", lib.Path)
|
||||
start := time.Now()
|
||||
warnings, err := r.scanner.ScanAll(r.ctx, false) // Quick scan for new library
|
||||
warnings, err := r.scanner.ScanAll(ctx, false) // Quick scan for new library
|
||||
if err != nil {
|
||||
log.Error(r.ctx, fmt.Sprintf("Error scanning %s library", action), "libraryID", lib.ID, "name", lib.Name, err)
|
||||
log.Error(ctx, fmt.Sprintf("Error scanning %s library", action), "libraryID", lib.ID, "name", lib.Name, err)
|
||||
} else {
|
||||
log.Info(r.ctx, fmt.Sprintf("Scan completed for %s library", action), "libraryID", lib.ID, "name", lib.Name, "warnings", len(warnings), "elapsed", time.Since(start))
|
||||
log.Info(ctx, fmt.Sprintf("Scan completed for %s library", action), "libraryID", lib.ID, "name", lib.Name, "warnings", len(warnings), "elapsed", time.Since(start))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,18 +66,18 @@ var _ = Describe("Library Service", func() {
|
|||
})
|
||||
|
||||
Describe("Library CRUD Operations", func() {
|
||||
var repo rest.Persistable
|
||||
var repo rest.Persistable[model.Library]
|
||||
|
||||
BeforeEach(func() {
|
||||
r := service.NewRepository(ctx)
|
||||
repo = r.(rest.Persistable)
|
||||
r := service.Repository()
|
||||
repo = r.(rest.Persistable[model.Library])
|
||||
})
|
||||
|
||||
Describe("Create", func() {
|
||||
It("creates a new library successfully", func() {
|
||||
library := &model.Library{ID: 1, Name: "New Library", Path: tempDir}
|
||||
|
||||
_, err := repo.Save(library)
|
||||
_, err := repo.Save(ctx, library)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(libraryRepo.Data[1].Name).To(Equal("New Library"))
|
||||
|
|
@ -87,7 +87,7 @@ var _ = Describe("Library Service", func() {
|
|||
It("fails when library name is empty", func() {
|
||||
library := &model.Library{Path: tempDir}
|
||||
|
||||
_, err := repo.Save(library)
|
||||
_, err := repo.Save(ctx, library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("ra.validation.required"))
|
||||
|
|
@ -96,7 +96,7 @@ var _ = Describe("Library Service", func() {
|
|||
It("fails when library path is empty", func() {
|
||||
library := &model.Library{Name: "Test"}
|
||||
|
||||
_, err := repo.Save(library)
|
||||
_, err := repo.Save(ctx, library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("ra.validation.required"))
|
||||
|
|
@ -105,7 +105,7 @@ var _ = Describe("Library Service", func() {
|
|||
It("fails when library path is not absolute", func() {
|
||||
library := &model.Library{Name: "Test", Path: "relative/path"}
|
||||
|
||||
_, err := repo.Save(library)
|
||||
_, err := repo.Save(ctx, library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
var validationErr *rest.ValidationError
|
||||
|
|
@ -140,7 +140,7 @@ var _ = Describe("Library Service", func() {
|
|||
return errors.New("UNIQUE constraint failed: library.name")
|
||||
}
|
||||
|
||||
_, err = repo.Save(library)
|
||||
_, err = repo.Save(ctx, library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
var validationErr *rest.ValidationError
|
||||
|
|
@ -157,7 +157,7 @@ var _ = Describe("Library Service", func() {
|
|||
return errors.New("UNIQUE constraint failed: library.path")
|
||||
}
|
||||
|
||||
_, err := repo.Save(library)
|
||||
_, err := repo.Save(ctx, library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
var validationErr *rest.ValidationError
|
||||
|
|
@ -181,7 +181,7 @@ var _ = Describe("Library Service", func() {
|
|||
|
||||
library := &model.Library{ID: 1, Name: "Updated Library", Path: newTempDir}
|
||||
|
||||
err = repo.Update("1", library)
|
||||
err = repo.Update(ctx, "1", *library)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(libraryRepo.Data[1].Name).To(Equal("Updated Library"))
|
||||
|
|
@ -191,7 +191,7 @@ var _ = Describe("Library Service", func() {
|
|||
It("forwards the columns sent by the client to the repository", func() {
|
||||
library := &model.Library{ID: 1, Name: "Updated Library", Path: tempDir}
|
||||
|
||||
err := repo.Update("1", library, "name", "path")
|
||||
err := repo.Update(ctx, "1", *library, "name", "path")
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(libraryRepo.PutCols).To(Equal([]string{"name", "path"}))
|
||||
|
|
@ -205,7 +205,7 @@ var _ = Describe("Library Service", func() {
|
|||
|
||||
library := &model.Library{ID: 999, Name: "Non-existent", Path: uniqueTempDir}
|
||||
|
||||
err = repo.Update("999", library)
|
||||
err = repo.Update(ctx, "999", *library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(Equal(model.ErrNotFound))
|
||||
|
|
@ -214,7 +214,7 @@ var _ = Describe("Library Service", func() {
|
|||
It("fails when library name is empty", func() {
|
||||
library := &model.Library{ID: 1, Path: tempDir}
|
||||
|
||||
err := repo.Update("1", library)
|
||||
err := repo.Update(ctx, "1", *library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("ra.validation.required"))
|
||||
|
|
@ -224,7 +224,7 @@ var _ = Describe("Library Service", func() {
|
|||
unnormalizedPath := tempDir + "//../" + filepath.Base(tempDir)
|
||||
library := &model.Library{ID: 1, Name: "Updated Library", Path: unnormalizedPath}
|
||||
|
||||
err := repo.Update("1", library)
|
||||
err := repo.Update(ctx, "1", *library)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(libraryRepo.Data[1].Path).To(Equal(filepath.Clean(unnormalizedPath)))
|
||||
|
|
@ -239,7 +239,7 @@ var _ = Describe("Library Service", func() {
|
|||
// Update the library keeping the same name (should be allowed)
|
||||
library := &model.Library{ID: 1, Name: "Test Library", Path: tempDir}
|
||||
|
||||
err := repo.Update("1", library)
|
||||
err := repo.Update(ctx, "1", *library)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
|
@ -253,7 +253,7 @@ var _ = Describe("Library Service", func() {
|
|||
// Update the library keeping the same path (should be allowed)
|
||||
library := &model.Library{ID: 1, Name: "Test Library", Path: tempDir}
|
||||
|
||||
err := repo.Update("1", library)
|
||||
err := repo.Update(ctx, "1", *library)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
|
@ -284,7 +284,7 @@ var _ = Describe("Library Service", func() {
|
|||
// Try to update library 2 to have the same name as library 1
|
||||
library := &model.Library{ID: 2, Name: "Library One", Path: otherTempDir}
|
||||
|
||||
err = repo.Update("2", library)
|
||||
err = repo.Update(ctx, "2", *library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
var validationErr *rest.ValidationError
|
||||
|
|
@ -312,7 +312,7 @@ var _ = Describe("Library Service", func() {
|
|||
// Try to update library 2 to have the same path as library 1
|
||||
library := &model.Library{ID: 2, Name: "Library Two", Path: tempDir}
|
||||
|
||||
err = repo.Update("2", library)
|
||||
err = repo.Update(ctx, "2", *library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
var validationErr *rest.ValidationError
|
||||
|
|
@ -327,7 +327,7 @@ var _ = Describe("Library Service", func() {
|
|||
It("fails when path is not absolute", func() {
|
||||
library := &model.Library{Name: "Test", Path: "relative/path"}
|
||||
|
||||
_, err := repo.Save(library)
|
||||
_, err := repo.Save(ctx, library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
var validationErr *rest.ValidationError
|
||||
|
|
@ -339,7 +339,7 @@ var _ = Describe("Library Service", func() {
|
|||
nonExistentPath := filepath.Join(tempDir, "nonexistent")
|
||||
library := &model.Library{Name: "Test", Path: nonExistentPath}
|
||||
|
||||
_, err := repo.Save(library)
|
||||
_, err := repo.Save(ctx, library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
var validationErr *rest.ValidationError
|
||||
|
|
@ -354,7 +354,7 @@ var _ = Describe("Library Service", func() {
|
|||
|
||||
library := &model.Library{Name: "Test", Path: testFile}
|
||||
|
||||
_, err = repo.Save(library)
|
||||
_, err = repo.Save(ctx, library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
var validationErr *rest.ValidationError
|
||||
|
|
@ -371,7 +371,7 @@ var _ = Describe("Library Service", func() {
|
|||
It("handles multiple validation errors", func() {
|
||||
library := &model.Library{Name: "", Path: "relative/path"}
|
||||
|
||||
_, err := repo.Save(library)
|
||||
_, err := repo.Save(ctx, library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
var validationErr *rest.ValidationError
|
||||
|
|
@ -393,7 +393,7 @@ var _ = Describe("Library Service", func() {
|
|||
It("fails when updated path is not absolute", func() {
|
||||
library := &model.Library{ID: 1, Name: "Test", Path: "relative/path"}
|
||||
|
||||
err := repo.Update("1", library)
|
||||
err := repo.Update(ctx, "1", *library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
var validationErr *rest.ValidationError
|
||||
|
|
@ -410,7 +410,7 @@ var _ = Describe("Library Service", func() {
|
|||
// Update the library keeping the same name (should be allowed)
|
||||
library := &model.Library{ID: 1, Name: "Test Library", Path: tempDir}
|
||||
|
||||
err := repo.Update("1", library)
|
||||
err := repo.Update(ctx, "1", *library)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
|
@ -419,7 +419,7 @@ var _ = Describe("Library Service", func() {
|
|||
nonExistentPath := filepath.Join(tempDir, "nonexistent")
|
||||
library := &model.Library{ID: 1, Name: "Test", Path: nonExistentPath}
|
||||
|
||||
err := repo.Update("1", library)
|
||||
err := repo.Update(ctx, "1", *library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
var validationErr *rest.ValidationError
|
||||
|
|
@ -434,7 +434,7 @@ var _ = Describe("Library Service", func() {
|
|||
|
||||
library := &model.Library{ID: 1, Name: "Test", Path: testFile}
|
||||
|
||||
err = repo.Update("1", library)
|
||||
err = repo.Update(ctx, "1", *library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
var validationErr *rest.ValidationError
|
||||
|
|
@ -446,7 +446,7 @@ var _ = Describe("Library Service", func() {
|
|||
// Try to update with empty name and invalid path
|
||||
library := &model.Library{ID: 1, Name: "", Path: "relative/path"}
|
||||
|
||||
err := repo.Update("1", library)
|
||||
err := repo.Update(ctx, "1", *library)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
var validationErr *rest.ValidationError
|
||||
|
|
@ -467,14 +467,14 @@ var _ = Describe("Library Service", func() {
|
|||
})
|
||||
|
||||
It("deletes an existing library successfully", func() {
|
||||
err := repo.Delete("1")
|
||||
err := repo.Delete(ctx, "1")
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(libraryRepo.Data).To(HaveLen(0))
|
||||
})
|
||||
|
||||
It("fails when library doesn't exist", func() {
|
||||
err := repo.Delete("999")
|
||||
err := repo.Delete(ctx, "999")
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(Equal(model.ErrNotFound))
|
||||
|
|
@ -613,17 +613,17 @@ var _ = Describe("Library Service", func() {
|
|||
})
|
||||
|
||||
Describe("Scan Triggering", func() {
|
||||
var repo rest.Persistable
|
||||
var repo rest.Persistable[model.Library]
|
||||
|
||||
BeforeEach(func() {
|
||||
r := service.NewRepository(ctx)
|
||||
repo = r.(rest.Persistable)
|
||||
r := service.Repository()
|
||||
repo = r.(rest.Persistable[model.Library])
|
||||
})
|
||||
|
||||
It("triggers scan when creating a new library", func() {
|
||||
library := &model.Library{ID: 1, Name: "New Library", Path: tempDir}
|
||||
|
||||
_, err := repo.Save(library)
|
||||
_, err := repo.Save(ctx, library)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Wait briefly for the goroutine to complete
|
||||
|
|
@ -649,7 +649,7 @@ var _ = Describe("Library Service", func() {
|
|||
|
||||
// Update the library with a new path
|
||||
library := &model.Library{ID: 1, Name: "Updated Library", Path: newTempDir}
|
||||
err = repo.Update("1", library)
|
||||
err = repo.Update(ctx, "1", *library)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Wait briefly for the goroutine to complete
|
||||
|
|
@ -670,7 +670,7 @@ var _ = Describe("Library Service", func() {
|
|||
|
||||
// Update the library name only (same path)
|
||||
library := &model.Library{ID: 1, Name: "Updated Name", Path: tempDir}
|
||||
err := repo.Update("1", library)
|
||||
err := repo.Update(ctx, "1", *library)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Wait a bit to ensure no scan was triggered
|
||||
|
|
@ -683,7 +683,7 @@ var _ = Describe("Library Service", func() {
|
|||
// Try to create library with invalid data (empty name)
|
||||
library := &model.Library{Path: tempDir}
|
||||
|
||||
_, err := repo.Save(library)
|
||||
_, err := repo.Save(ctx, library)
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
// Ensure no scan was triggered since creation failed
|
||||
|
|
@ -700,7 +700,7 @@ var _ = Describe("Library Service", func() {
|
|||
|
||||
// Try to update with invalid data (empty name)
|
||||
library := &model.Library{ID: 1, Name: "", Path: tempDir}
|
||||
err := repo.Update("1", library)
|
||||
err := repo.Update(ctx, "1", *library)
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
// Ensure no scan was triggered since update failed
|
||||
|
|
@ -716,7 +716,7 @@ var _ = Describe("Library Service", func() {
|
|||
})
|
||||
|
||||
// Delete the library
|
||||
err := repo.Delete("1")
|
||||
err := repo.Delete(ctx, "1")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Wait briefly for the goroutine to complete
|
||||
|
|
@ -731,7 +731,7 @@ var _ = Describe("Library Service", func() {
|
|||
|
||||
It("does not trigger scan when library deletion fails", func() {
|
||||
// Try to delete a non-existent library
|
||||
err := repo.Delete("999")
|
||||
err := repo.Delete(ctx, "999")
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
// Ensure no scan was triggered since deletion failed
|
||||
|
|
@ -744,7 +744,7 @@ var _ = Describe("Library Service", func() {
|
|||
It("starts watcher when creating a new library", func() {
|
||||
library := &model.Library{ID: 1, Name: "New Library", Path: tempDir}
|
||||
|
||||
_, err := repo.Save(library)
|
||||
_, err := repo.Save(ctx, library)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Verify watcher was started
|
||||
|
|
@ -773,7 +773,7 @@ var _ = Describe("Library Service", func() {
|
|||
|
||||
// Update library with new path
|
||||
library := &model.Library{ID: 1, Name: "Updated Library", Path: newTempDir}
|
||||
err = repo.Update("1", library)
|
||||
err = repo.Update(ctx, "1", *library)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Verify watcher was restarted
|
||||
|
|
@ -793,7 +793,7 @@ var _ = Describe("Library Service", func() {
|
|||
|
||||
// Update library with same path but different name
|
||||
library := &model.Library{ID: 1, Name: "Updated Name", Path: tempDir}
|
||||
err := repo.Update("1", library)
|
||||
err := repo.Update(ctx, "1", *library)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Verify watcher was NOT restarted (since path didn't change)
|
||||
|
|
@ -808,7 +808,7 @@ var _ = Describe("Library Service", func() {
|
|||
{ID: 1, Name: "Test Library", Path: tempDir},
|
||||
})
|
||||
|
||||
err := repo.Delete("1")
|
||||
err := repo.Delete(ctx, "1")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Verify watcher was stopped
|
||||
|
|
@ -826,7 +826,7 @@ var _ = Describe("Library Service", func() {
|
|||
})
|
||||
|
||||
// Mock deletion to fail by trying to delete non-existent library
|
||||
err := repo.Delete("999")
|
||||
err := repo.Delete(ctx, "999")
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
// Verify watcher was NOT stopped since deletion failed
|
||||
|
|
@ -838,11 +838,11 @@ var _ = Describe("Library Service", func() {
|
|||
})
|
||||
|
||||
Describe("Event Broadcasting", func() {
|
||||
var repo rest.Persistable
|
||||
var repo rest.Persistable[model.Library]
|
||||
|
||||
BeforeEach(func() {
|
||||
r := service.NewRepository(ctx)
|
||||
repo = r.(rest.Persistable)
|
||||
r := service.Repository()
|
||||
repo = r.(rest.Persistable[model.Library])
|
||||
// Clear any events from broker
|
||||
broker.Events = []events.Event{}
|
||||
})
|
||||
|
|
@ -850,7 +850,7 @@ var _ = Describe("Library Service", func() {
|
|||
It("sends refresh event when creating a library", func() {
|
||||
library := &model.Library{ID: 1, Name: "New Library", Path: tempDir}
|
||||
|
||||
_, err := repo.Save(library)
|
||||
_, err := repo.Save(ctx, library)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(broker.Events).To(HaveLen(1))
|
||||
|
|
@ -863,7 +863,7 @@ var _ = Describe("Library Service", func() {
|
|||
})
|
||||
|
||||
library := &model.Library{ID: 1, Name: "Updated Library", Path: tempDir}
|
||||
err := repo.Update("1", library)
|
||||
err := repo.Update(ctx, "1", *library)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(broker.Events).To(HaveLen(1))
|
||||
|
|
@ -875,7 +875,7 @@ var _ = Describe("Library Service", func() {
|
|||
{ID: 2, Name: "Library to Delete", Path: tempDir},
|
||||
})
|
||||
|
||||
err := repo.Delete("2")
|
||||
err := repo.Delete(ctx, "2")
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(broker.Events).To(HaveLen(1))
|
||||
|
|
@ -883,13 +883,13 @@ var _ = Describe("Library Service", func() {
|
|||
})
|
||||
|
||||
Describe("Plugin Manager Integration", func() {
|
||||
var repo rest.Persistable
|
||||
var repo rest.Persistable[model.Library]
|
||||
|
||||
BeforeEach(func() {
|
||||
// Reset the call count for each test
|
||||
pluginManager.unloadCalls = 0
|
||||
r := service.NewRepository(ctx)
|
||||
repo = r.(rest.Persistable)
|
||||
r := service.Repository()
|
||||
repo = r.(rest.Persistable[model.Library])
|
||||
})
|
||||
|
||||
It("calls UnloadDisabledPlugins after successful library deletion", func() {
|
||||
|
|
@ -897,14 +897,14 @@ var _ = Describe("Library Service", func() {
|
|||
{ID: 2, Name: "Library to Delete", Path: tempDir},
|
||||
})
|
||||
|
||||
err := repo.Delete("2")
|
||||
err := repo.Delete(ctx, "2")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(pluginManager.unloadCalls).To(Equal(1))
|
||||
})
|
||||
|
||||
It("does not call UnloadDisabledPlugins when library deletion fails", func() {
|
||||
// Try to delete non-existent library
|
||||
err := repo.Delete("999")
|
||||
err := repo.Delete(ctx, "999")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(pluginManager.unloadCalls).To(Equal(0))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ func (l *lyricsService) GetLyrics(ctx context.Context, mf *model.MediaFile) (mod
|
|||
func (l *lyricsService) GetLyricsByArtistTitle(ctx context.Context, artist, title string) (model.LyricList, error) {
|
||||
opts := songsByArtistTitleWithLyricsFirst(artist, title)
|
||||
opts.Max = maxLegacyLyricsCandidates
|
||||
mediaFiles, err := l.ds.MediaFile(ctx).GetAll(opts)
|
||||
mediaFiles, err := l.ds.MediaFile().GetAll(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ func (s *maintenanceService) RemapMissingFile(ctx context.Context, missingID, ta
|
|||
return fmt.Errorf("%w: %q", ErrSameFile, missingID)
|
||||
}
|
||||
|
||||
missing, err := s.ds.MediaFile(ctx).Get(missingID)
|
||||
missing, err := s.ds.MediaFile().Get(ctx, missingID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading missing file %q: %w", missingID, err)
|
||||
}
|
||||
|
|
@ -66,7 +66,7 @@ func (s *maintenanceService) RemapMissingFile(ctx context.Context, missingID, ta
|
|||
return fmt.Errorf("%w: %q", ErrNotMissing, missingID)
|
||||
}
|
||||
|
||||
target, err := s.ds.MediaFile(ctx).GetWithParticipants(targetID)
|
||||
target, err := s.ds.MediaFile().GetWithParticipants(ctx, targetID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading target file %q: %w", targetID, err)
|
||||
}
|
||||
|
|
@ -82,27 +82,27 @@ func (s *maintenanceService) RemapMissingFile(ctx context.Context, missingID, ta
|
|||
// Preserve the original created_at so the remapped track doesn't resurface in "Recently Added"
|
||||
target.CreatedAt = missing.CreatedAt
|
||||
target.ID = missing.ID
|
||||
if err := tx.MediaFile(ctx).Put(target); err != nil {
|
||||
if err := tx.MediaFile().Put(ctx, target); err != nil {
|
||||
return fmt.Errorf("update matched track: %w", err)
|
||||
}
|
||||
// Unlike the scanner's freshly-imported target, this one may carry history of its own
|
||||
if err := tx.MediaFile(ctx).ReassignReferences(discardedID, missing.ID); err != nil {
|
||||
if err := tx.MediaFile().ReassignReferences(ctx, discardedID, missing.ID); err != nil {
|
||||
return fmt.Errorf("reassign target references: %w", err)
|
||||
}
|
||||
if err := tx.MediaFile(ctx).Delete(discardedID); err != nil {
|
||||
if err := tx.MediaFile().Delete(ctx, discardedID); err != nil {
|
||||
return fmt.Errorf("delete discarded track: %w", err)
|
||||
}
|
||||
|
||||
if oldAlbumID != newAlbumID {
|
||||
oldAlbumTracks, err := tx.MediaFile(ctx).CountAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": oldAlbumID}})
|
||||
oldAlbumTracks, err := tx.MediaFile().CountAll(ctx, model.QueryOptions{Filters: squirrel.Eq{"album_id": oldAlbumID}})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get old album tracks: %w", err)
|
||||
}
|
||||
if oldAlbumTracks == 0 {
|
||||
if err := tx.Album(ctx).ReassignAnnotation(oldAlbumID, newAlbumID); err != nil {
|
||||
if err := tx.Album().ReassignAnnotation(ctx, oldAlbumID, newAlbumID); err != nil {
|
||||
return fmt.Errorf("reassign album annotations: %w", err)
|
||||
}
|
||||
if err := tx.Album(ctx).CopyAttributes(oldAlbumID, newAlbumID, "created_at"); err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||
if err := tx.Album().CopyAttributes(ctx, oldAlbumID, newAlbumID, "created_at"); err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||
return fmt.Errorf("copy album attributes: %w", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -121,7 +121,7 @@ func (s *maintenanceService) RemapMissingFile(ctx context.Context, missingID, ta
|
|||
|
||||
// Stats are refreshed synchronously, unlike deleteMissing, so the CLI sees them before it exits.
|
||||
// album/artist play count aggregates are not recalculated here; they are refreshed by the next scan.
|
||||
if _, err := s.ds.Artist(ctx).RefreshStats(true); err != nil {
|
||||
if _, err := s.ds.Artist().RefreshStats(ctx, true); err != nil {
|
||||
log.Error(ctx, "Error refreshing artist stats after remapping missing file", err)
|
||||
}
|
||||
affectedAlbumIDs := []string{newAlbumID}
|
||||
|
|
@ -146,10 +146,10 @@ func (s *maintenanceService) deleteMissing(ctx context.Context, ids []string) er
|
|||
// Delete missing files within a transaction
|
||||
err = s.ds.WithTx(func(tx model.DataStore) error {
|
||||
if len(ids) == 0 {
|
||||
_, err := tx.MediaFile(ctx).DeleteAllMissing()
|
||||
_, err := tx.MediaFile().DeleteAllMissing(ctx)
|
||||
return err
|
||||
}
|
||||
return tx.MediaFile(ctx).DeleteMissing(ids)
|
||||
return tx.MediaFile().DeleteMissing(ctx, ids)
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error deleting missing tracks from DB", "ids", ids, err)
|
||||
|
|
@ -192,11 +192,11 @@ func (s *maintenanceService) refreshAlbums(ctx context.Context, albumIDs []strin
|
|||
|
||||
// refreshAlbumChunk processes a single chunk of album IDs
|
||||
func (s *maintenanceService) refreshAlbumChunk(ctx context.Context, albumIDs []string) error {
|
||||
albumRepo := s.ds.Album(ctx)
|
||||
mfRepo := s.ds.MediaFile(ctx)
|
||||
albumRepo := s.ds.Album()
|
||||
mfRepo := s.ds.MediaFile()
|
||||
|
||||
// Batch load existing albums
|
||||
albums, err := albumRepo.GetAll(model.QueryOptions{
|
||||
albums, err := albumRepo.GetAll(ctx, model.QueryOptions{
|
||||
Filters: squirrel.Eq{"album.id": albumIDs},
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -210,7 +210,7 @@ func (s *maintenanceService) refreshAlbumChunk(ctx context.Context, albumIDs []s
|
|||
}
|
||||
|
||||
// Batch load all media files for these albums
|
||||
mediaFiles, err := mfRepo.GetAll(model.QueryOptions{
|
||||
mediaFiles, err := mfRepo.GetAll(ctx, model.QueryOptions{
|
||||
Filters: squirrel.Eq{"album_id": albumIDs},
|
||||
Sort: "album_id, path",
|
||||
})
|
||||
|
|
@ -243,7 +243,7 @@ func (s *maintenanceService) refreshAlbumChunk(ctx context.Context, albumIDs []s
|
|||
newAlbum.UpdatedAt = time.Now()
|
||||
newAlbum.CreatedAt = oldAlbum.CreatedAt
|
||||
|
||||
if err := albumRepo.Put(&newAlbum); err != nil {
|
||||
if err := albumRepo.Put(ctx, &newAlbum); err != nil {
|
||||
log.Error(ctx, "Error updating album during refresh", "albumID", albumID, err)
|
||||
// Continue with other albums instead of failing entirely
|
||||
continue
|
||||
|
|
@ -265,7 +265,7 @@ func (s *maintenanceService) getAffectedAlbumIDs(ctx context.Context, ids []stri
|
|||
}
|
||||
}
|
||||
|
||||
mfs, err := s.ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
mfs, err := s.ds.MediaFile().GetAll(ctx, model.QueryOptions{
|
||||
Filters: filters,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -293,7 +293,7 @@ func (s *maintenanceService) refreshStatsAsync(ctx context.Context, affectedAlbu
|
|||
// Refresh artist stats in background
|
||||
s.wg.Go(func() {
|
||||
bgCtx := request.AddValues(context.Background(), ctx)
|
||||
if _, err := s.ds.Artist(bgCtx).RefreshStats(true); err != nil {
|
||||
if _, err := s.ds.Artist().RefreshStats(bgCtx, true); err != nil {
|
||||
log.Error(bgCtx, "Error refreshing artist stats after deleting missing files", err)
|
||||
} else {
|
||||
log.Debug(bgCtx, "Successfully refreshed artist stats after deleting missing files")
|
||||
|
|
|
|||
|
|
@ -262,12 +262,12 @@ var _ = Describe("Maintenance", func() {
|
|||
|
||||
Expect(service.RemapMissingFile(ctx, "m1", "t1")).To(Succeed())
|
||||
|
||||
got, err := mfRepo.Get("m1")
|
||||
got, err := mfRepo.Get(ctx, "m1")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(got.Path).To(Equal("new/song.mp3")) // moved to target's location
|
||||
Expect(got.Missing).To(BeFalse())
|
||||
Expect(got.CreatedAt).To(BeTemporally("==", created)) // created_at preserved
|
||||
exists, _ := mfRepo.Exists("t1")
|
||||
exists, _ := mfRepo.Exists(ctx, "t1")
|
||||
Expect(exists).To(BeFalse()) // discarded row removed
|
||||
Expect(ds.GCCalled).To(BeTrue())
|
||||
})
|
||||
|
|
@ -369,14 +369,14 @@ var _ = Describe("Maintenance", func() {
|
|||
Expect(artistRepo.IsRefreshStatsCalled()).To(BeTrue(), "Artist stats should be refreshed")
|
||||
|
||||
// The old album lost the remapped track, so its stats are recalculated from the remaining one
|
||||
oldAlbum, err := albumRepo.Get("album1")
|
||||
oldAlbum, err := albumRepo.Get(ctx, "album1")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(oldAlbum.SongCount).To(Equal(1))
|
||||
Expect(oldAlbum.Size).To(Equal(int64(1000)))
|
||||
Expect(oldAlbum.Duration).To(BeNumerically("==", 100))
|
||||
|
||||
// The target album keeps the track, now under the missing file's ID
|
||||
newAlbum, err := albumRepo.Get("album2")
|
||||
newAlbum, err := albumRepo.Get(ctx, "album2")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(newAlbum.SongCount).To(Equal(1))
|
||||
Expect(newAlbum.Size).To(Equal(int64(2000)))
|
||||
|
|
@ -407,7 +407,7 @@ var _ = Describe("Maintenance", func() {
|
|||
Expect(service.RemapMissingFile(ctx, "m1", "t1")).To(Succeed())
|
||||
|
||||
// The surviving row is the missing file's ID, holding the target's data
|
||||
got, err := mfRepo.GetWithParticipants("m1")
|
||||
got, err := mfRepo.GetWithParticipants(ctx, "m1")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(got.Participants).To(HaveKeyWithValue(model.RoleArtist, model.ParticipantList{participant}))
|
||||
})
|
||||
|
|
@ -447,7 +447,7 @@ type extendedMediaFileRepo struct {
|
|||
deleteMissingError error
|
||||
}
|
||||
|
||||
func (m *extendedMediaFileRepo) DeleteMissing(ids []string) error {
|
||||
func (m *extendedMediaFileRepo) DeleteMissing(ctx context.Context, ids []string) error {
|
||||
m.deleteMissingCalled = true
|
||||
m.deletedIDs = ids
|
||||
if m.deleteMissingError != nil {
|
||||
|
|
@ -470,7 +470,7 @@ type extendedAlbumRepo struct {
|
|||
failOnce bool
|
||||
}
|
||||
|
||||
func (m *extendedAlbumRepo) Put(album *model.Album) error {
|
||||
func (m *extendedAlbumRepo) Put(ctx context.Context, album *model.Album) error {
|
||||
m.mu.Lock()
|
||||
m.putCallCount++
|
||||
m.lastPutData = album
|
||||
|
|
@ -490,7 +490,7 @@ func (m *extendedAlbumRepo) Put(album *model.Album) error {
|
|||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
return m.MockAlbumRepo.Put(album)
|
||||
return m.MockAlbumRepo.Put(ctx, album)
|
||||
}
|
||||
|
||||
func (m *extendedAlbumRepo) GetPutCallCount() int {
|
||||
|
|
@ -507,7 +507,7 @@ type extendedArtistRepo struct {
|
|||
refreshStatsError error
|
||||
}
|
||||
|
||||
func (m *extendedArtistRepo) RefreshStats(allArtists bool) (int64, error) {
|
||||
func (m *extendedArtistRepo) RefreshStats(ctx context.Context, allArtists bool) (int64, error) {
|
||||
m.mu.Lock()
|
||||
m.refreshStatsCalled = true
|
||||
err := m.refreshStatsError
|
||||
|
|
@ -516,7 +516,7 @@ func (m *extendedArtistRepo) RefreshStats(allArtists bool) (int64, error) {
|
|||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return m.MockArtistRepo.RefreshStats(allArtists)
|
||||
return m.MockArtistRepo.RefreshStats(ctx, allArtists)
|
||||
}
|
||||
|
||||
func (m *extendedArtistRepo) IsRefreshStatsCalled() bool {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ func (m *Matcher) matchByID(ctx context.Context, songs []agents.Song, result map
|
|||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
res, err := m.ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
res, err := m.ds.MediaFile().GetAll(ctx, model.QueryOptions{
|
||||
Filters: squirrel.And{
|
||||
squirrel.Eq{"media_file.id": ids},
|
||||
squirrel.Eq{"missing": false},
|
||||
|
|
@ -134,7 +134,7 @@ func (m *Matcher) matchByMBID(ctx context.Context, songs []agents.Song, result m
|
|||
if len(mbids) == 0 {
|
||||
return nil
|
||||
}
|
||||
res, err := m.ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
res, err := m.ds.MediaFile().GetAll(ctx, model.QueryOptions{
|
||||
Filters: squirrel.And{
|
||||
squirrel.Eq{"mbz_recording_id": mbids},
|
||||
squirrel.Eq{"missing": false},
|
||||
|
|
@ -180,7 +180,7 @@ func (m *Matcher) matchByISRC(ctx context.Context, songs []agents.Song, result m
|
|||
if len(isrcs) == 0 {
|
||||
return nil
|
||||
}
|
||||
res, err := m.ds.MediaFile(ctx).GetAllByTags(model.TagISRC, isrcs, model.QueryOptions{
|
||||
res, err := m.ds.MediaFile().GetAllByTags(ctx, model.TagISRC, isrcs, model.QueryOptions{
|
||||
Filters: squirrel.Eq{"missing": false},
|
||||
Sort: "starred desc, rating desc, year asc, compilation asc",
|
||||
})
|
||||
|
|
@ -442,7 +442,7 @@ func (m *Matcher) resolveArtists(ctx context.Context, queries []indexedQuery) (r
|
|||
filter = append(filter, squirrel.Eq{"id": slices.Collect(maps.Keys(allIDs))})
|
||||
}
|
||||
if len(filter) > 0 {
|
||||
artists, err := m.ds.Artist(ctx).GetAll(model.QueryOptions{Filters: filter})
|
||||
artists, err := m.ds.Artist().GetAll(ctx, model.QueryOptions{Filters: filter})
|
||||
if err != nil {
|
||||
return resolvedArtists{}, err
|
||||
}
|
||||
|
|
@ -543,7 +543,7 @@ func (m *Matcher) fetchTracksCreditedTo(ctx context.Context, artistIDs []string)
|
|||
return nil, nil
|
||||
}
|
||||
args := slice.Map(artistIDs, func(id string) any { return id })
|
||||
return m.ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
return m.ds.MediaFile().GetAll(ctx, model.QueryOptions{
|
||||
Filters: squirrel.And{
|
||||
squirrel.Expr(
|
||||
"media_file.id IN (SELECT media_file_id FROM media_file_artists "+
|
||||
|
|
|
|||
|
|
@ -1267,7 +1267,7 @@ func newMockMediaFileRepo() *mockMediaFileRepo {
|
|||
return &mockMediaFileRepo{}
|
||||
}
|
||||
|
||||
func (m *mockMediaFileRepo) GetAll(options ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
func (m *mockMediaFileRepo) GetAll(ctx context.Context, options ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
argsSlice := make([]any, len(options))
|
||||
for i, v := range options {
|
||||
argsSlice[i] = v
|
||||
|
|
@ -1279,8 +1279,8 @@ func (m *mockMediaFileRepo) GetAll(options ...model.QueryOptions) (model.MediaFi
|
|||
return args.Get(0).(model.MediaFiles), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockMediaFileRepo) GetAllByTags(_ model.TagName, _ []string, options ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
return m.GetAll(options...)
|
||||
func (m *mockMediaFileRepo) GetAllByTags(ctx context.Context, _ model.TagName, _ []string, options ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
return m.GetAll(ctx, options...)
|
||||
}
|
||||
|
||||
func (m *mockMediaFileRepo) SetError(hasError bool) {
|
||||
|
|
@ -1298,7 +1298,7 @@ func newMockArtistRepo() *mockArtistRepo {
|
|||
return &mockArtistRepo{}
|
||||
}
|
||||
|
||||
func (m *mockArtistRepo) GetAll(options ...model.QueryOptions) (model.Artists, error) {
|
||||
func (m *mockArtistRepo) GetAll(_ context.Context, options ...model.QueryOptions) (model.Artists, error) {
|
||||
argsSlice := make([]any, len(options))
|
||||
for i, v := range options {
|
||||
argsSlice[i] = v
|
||||
|
|
|
|||
|
|
@ -47,11 +47,11 @@ type insightsCollector struct {
|
|||
|
||||
func GetInstance(ds model.DataStore) Insights {
|
||||
return singleton.GetInstance(func() *insightsCollector {
|
||||
id, err := ds.Property(context.TODO()).Get(consts.InsightsIDKey)
|
||||
id, err := ds.Property().Get(context.TODO(), consts.InsightsIDKey)
|
||||
if err != nil {
|
||||
log.Trace("Could not get Insights ID from DB. Creating one", err)
|
||||
id = uuid.NewString()
|
||||
err = ds.Property(context.TODO()).Put(consts.InsightsIDKey, id)
|
||||
err = ds.Property().Put(context.TODO(), consts.InsightsIDKey, id)
|
||||
if err != nil {
|
||||
log.Trace("Could not save Insights ID to DB", err)
|
||||
}
|
||||
|
|
@ -87,7 +87,7 @@ func (c *insightsCollector) LastRun(context.Context) (timestamp time.Time, succe
|
|||
}
|
||||
|
||||
func (c *insightsCollector) sendInsights(ctx context.Context) {
|
||||
count, err := c.ds.User(ctx).CountAll(model.QueryOptions{})
|
||||
count, err := c.ds.User().CountAll(ctx, model.QueryOptions{})
|
||||
if err != nil {
|
||||
log.Trace(ctx, "Could not check user count", err)
|
||||
return
|
||||
|
|
@ -245,41 +245,41 @@ func (c *insightsCollector) collect(ctx context.Context) []byte {
|
|||
|
||||
// Library info
|
||||
var err error
|
||||
data.Library.Tracks, err = c.ds.MediaFile(ctx).CountAll()
|
||||
data.Library.Tracks, err = c.ds.MediaFile().CountAll(ctx)
|
||||
if err != nil {
|
||||
log.Trace(ctx, "Error reading tracks count", err)
|
||||
}
|
||||
data.Library.Albums, err = c.ds.Album(ctx).CountAll()
|
||||
data.Library.Albums, err = c.ds.Album().CountAll(ctx)
|
||||
if err != nil {
|
||||
log.Trace(ctx, "Error reading albums count", err)
|
||||
}
|
||||
data.Library.Artists, err = c.ds.Artist(ctx).CountAll()
|
||||
data.Library.Artists, err = c.ds.Artist().CountAll(ctx)
|
||||
if err != nil {
|
||||
log.Trace(ctx, "Error reading artists count", err)
|
||||
}
|
||||
data.Library.Playlists, err = c.ds.Playlist(ctx).CountAll()
|
||||
data.Library.Playlists, err = c.ds.Playlist().CountAll(ctx)
|
||||
if err != nil {
|
||||
log.Trace(ctx, "Error reading playlists count", err)
|
||||
}
|
||||
data.Library.Shares, err = c.ds.Share(ctx).CountAll()
|
||||
data.Library.Shares, err = c.ds.Share().CountAll(ctx)
|
||||
if err != nil {
|
||||
log.Trace(ctx, "Error reading shares count", err)
|
||||
}
|
||||
data.Library.Radios, err = c.ds.Radio(ctx).Count()
|
||||
data.Library.Radios, err = c.ds.Radio().CountAll(ctx)
|
||||
if err != nil {
|
||||
log.Trace(ctx, "Error reading radios count", err)
|
||||
}
|
||||
data.Library.Libraries, err = c.ds.Library(ctx).CountAll()
|
||||
data.Library.Libraries, err = c.ds.Library().CountAll(ctx)
|
||||
if err != nil {
|
||||
log.Trace(ctx, "Error reading libraries count", err)
|
||||
}
|
||||
data.Library.ActiveUsers, err = c.ds.User(ctx).CountAll(model.QueryOptions{
|
||||
data.Library.ActiveUsers, err = c.ds.User().CountAll(ctx, model.QueryOptions{
|
||||
Filters: squirrel.Gt{"last_access_at": time.Now().Add(-7 * 24 * time.Hour)},
|
||||
})
|
||||
if err != nil {
|
||||
log.Trace(ctx, "Error reading active users count", err)
|
||||
}
|
||||
data.Library.FileSuffixes, err = c.ds.MediaFile(ctx).CountBySuffix()
|
||||
data.Library.FileSuffixes, err = c.ds.MediaFile().CountBySuffix(ctx)
|
||||
if err != nil {
|
||||
log.Trace(ctx, "Error reading file suffixes count", err)
|
||||
}
|
||||
|
|
@ -297,7 +297,7 @@ func (c *insightsCollector) collect(ctx context.Context) []byte {
|
|||
|
||||
// Collect active players if permitted
|
||||
if conf.Server.DevEnablePlayerInsights {
|
||||
data.Library.ActivePlayers, err = c.ds.Player(ctx).CountByClient(model.QueryOptions{
|
||||
data.Library.ActivePlayers, err = c.ds.Player().CountByClient(ctx, model.QueryOptions{
|
||||
Filters: squirrel.Gt{"last_seen": time.Now().Add(-7 * 24 * time.Hour)},
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -324,7 +324,7 @@ func (c *insightsCollector) collect(ctx context.Context) []byte {
|
|||
|
||||
// hasSmartPlaylists checks if there are any smart playlists (playlists with rules)
|
||||
func (c *insightsCollector) hasSmartPlaylists(ctx context.Context) (bool, error) {
|
||||
count, err := c.ds.Playlist(ctx).CountAll(model.QueryOptions{
|
||||
count, err := c.ds.Playlist().CountAll(ctx, model.QueryOptions{
|
||||
Filters: squirrel.And{squirrel.NotEq{"rules": ""}, squirrel.NotEq{"rules": nil}},
|
||||
})
|
||||
return count > 0, err
|
||||
|
|
|
|||
|
|
@ -197,28 +197,28 @@ var getPrometheusMetrics = sync.OnceValue(func() *prometheusMetrics {
|
|||
})
|
||||
|
||||
func processSqlAggregateMetrics(ctx context.Context, ds model.DataStore, targetGauge *prometheus.GaugeVec) {
|
||||
albumsCount, err := ds.Album(ctx).CountAll()
|
||||
albumsCount, err := ds.Album().CountAll(ctx)
|
||||
if err != nil {
|
||||
log.Warn("album CountAll error", err)
|
||||
return
|
||||
}
|
||||
targetGauge.With(prometheus.Labels{"model": "album"}).Set(float64(albumsCount))
|
||||
|
||||
artistCount, err := ds.Artist(ctx).CountAll()
|
||||
artistCount, err := ds.Artist().CountAll(ctx)
|
||||
if err != nil {
|
||||
log.Warn("artist CountAll error", err)
|
||||
return
|
||||
}
|
||||
targetGauge.With(prometheus.Labels{"model": "artist"}).Set(float64(artistCount))
|
||||
|
||||
songsCount, err := ds.MediaFile(ctx).CountAll()
|
||||
songsCount, err := ds.MediaFile().CountAll(ctx)
|
||||
if err != nil {
|
||||
log.Warn("media CountAll error", err)
|
||||
return
|
||||
}
|
||||
targetGauge.With(prometheus.Labels{"model": "media"}).Set(float64(songsCount))
|
||||
|
||||
usersCount, err := ds.User(ctx).CountAll()
|
||||
usersCount, err := ds.User().CountAll(ctx)
|
||||
if err != nil {
|
||||
log.Warn("user CountAll error", err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ type Track interface {
|
|||
}
|
||||
|
||||
type playbackDevice struct {
|
||||
serviceCtx context.Context
|
||||
serviceCtx context.Context //nolint:containedctx // playback service lifecycle ctx
|
||||
ParentPlaybackServer PlaybackServer
|
||||
Default bool
|
||||
User string
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ func (ps *playbackServer) getDefaultDevice() (*playbackDevice, error) {
|
|||
|
||||
// GetMediaFile retrieves the MediaFile given by the id parameter
|
||||
func (ps *playbackServer) GetMediaFile(id string) (*model.MediaFile, error) {
|
||||
return ps.datastore.MediaFile(*ps.ctx).Get(id)
|
||||
return ps.datastore.MediaFile().Get(*ps.ctx, id)
|
||||
}
|
||||
|
||||
// GetDeviceForUser returns the audio playback device for the given user. As of now this is but only the default device.
|
||||
|
|
|
|||
|
|
@ -37,14 +37,14 @@ func (p *players) Register(ctx context.Context, playerID, client, userAgent, ip
|
|||
var err error
|
||||
user, _ := request.UserFrom(ctx)
|
||||
if playerID != "" {
|
||||
plr, err = p.ds.Player(ctx).Get(playerID)
|
||||
plr, err = p.ds.Player().Get(ctx, playerID)
|
||||
if err == nil && (plr.Client != client || plr.UserId != user.ID) {
|
||||
playerID = ""
|
||||
}
|
||||
}
|
||||
username := userName(ctx)
|
||||
if err != nil || playerID == "" {
|
||||
plr, err = p.ds.Player(ctx).FindMatch(user.ID, client, userAgent)
|
||||
plr, err = p.ds.Player().FindMatch(ctx, user.ID, client, userAgent)
|
||||
if err == nil {
|
||||
log.Debug(ctx, "Found matching player", "id", plr.ID, "client", client, "username", username, "type", userAgent)
|
||||
} else {
|
||||
|
|
@ -66,17 +66,17 @@ func (p *players) Register(ctx context.Context, playerID, client, userAgent, ip
|
|||
ctx, cancel := context.WithTimeout(ctx, time.Second)
|
||||
defer cancel()
|
||||
|
||||
err = p.ds.Player(ctx).Put(plr)
|
||||
err = p.ds.Player().Put(ctx, plr)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Could not save player", "id", plr.ID, "client", client, "username", username, "type", userAgent, err)
|
||||
}
|
||||
})
|
||||
if plr.TranscodingId != "" {
|
||||
trc, err = p.ds.Transcoding(ctx).Get(plr.TranscodingId)
|
||||
trc, err = p.ds.Transcoding().Get(ctx, plr.TranscodingId)
|
||||
}
|
||||
return plr, trc, err
|
||||
}
|
||||
|
||||
func (p *players) Get(ctx context.Context, playerId string) (*model.Player, error) {
|
||||
return p.ds.Player(ctx).Get(playerId)
|
||||
return p.ds.Player().Get(ctx, playerId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,14 +145,14 @@ func (m *mockPlayerRepository) add(p *model.Player) {
|
|||
m.data[p.ID] = *p
|
||||
}
|
||||
|
||||
func (m *mockPlayerRepository) Get(id string) (*model.Player, error) {
|
||||
func (m *mockPlayerRepository) Get(_ context.Context, id string) (*model.Player, error) {
|
||||
if p, ok := m.data[id]; ok {
|
||||
return &p, nil
|
||||
}
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockPlayerRepository) FindMatch(userId, client, userAgent string) (*model.Player, error) {
|
||||
func (m *mockPlayerRepository) FindMatch(_ context.Context, userId, client, userAgent string) (*model.Player, error) {
|
||||
for _, p := range m.data {
|
||||
if p.Client == client && p.UserId == userId && p.UserAgent == userAgent {
|
||||
return &p, nil
|
||||
|
|
@ -161,7 +161,7 @@ func (m *mockPlayerRepository) FindMatch(userId, client, userAgent string) (*mod
|
|||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockPlayerRepository) Put(p *model.Player) error {
|
||||
func (m *mockPlayerRepository) Put(_ context.Context, p *model.Player) error {
|
||||
m.lastSaved = p
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ func (s *playlists) ImportFile(ctx context.Context, absolutePath string, sync bo
|
|||
}
|
||||
if pls.ID != "" && pls.Sync != sync {
|
||||
pls.Sync = sync
|
||||
if putErr := s.ds.Playlist(ctx).Put(pls); putErr != nil {
|
||||
if putErr := s.ds.Playlist().Put(ctx, pls); putErr != nil {
|
||||
return nil, putErr
|
||||
}
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ func (s *playlists) ImportFile(ctx context.Context, absolutePath string, sync bo
|
|||
var errNotInLibrary = fmt.Errorf("path not in any library")
|
||||
|
||||
func (s *playlists) resolveFolder(ctx context.Context, dir string) (*model.Folder, error) {
|
||||
libs, err := s.ds.Library(ctx).GetAll()
|
||||
libs, err := s.ds.Library().GetAll(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -84,7 +84,7 @@ func (s *playlists) resolveFolder(ctx context.Context, dir string) (*model.Folde
|
|||
return nil, fmt.Errorf("%w: %s", errNotInLibrary, dir)
|
||||
}
|
||||
|
||||
folder, err := s.ds.Folder(ctx).GetByPath(lib, dir)
|
||||
folder, err := s.ds.Folder().GetByPath(ctx, lib, dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolving folder for path %s: %w", dir, err)
|
||||
}
|
||||
|
|
@ -122,7 +122,7 @@ func (s *playlists) ImportM3U(ctx context.Context, reader io.Reader) (*model.Pla
|
|||
log.Error(ctx, "Error parsing playlist", err)
|
||||
return nil, err
|
||||
}
|
||||
err = s.ds.Playlist(ctx).Put(pls)
|
||||
err = s.ds.Playlist().Put(ctx, pls)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error saving playlist", err)
|
||||
return nil, err
|
||||
|
|
@ -166,14 +166,14 @@ func fingerprint(h *xxh3.Hasher) string {
|
|||
// findByPathNormalized looks up a playlist by path, trying both NFC and NFD Unicode
|
||||
// normalization forms to handle cross-platform filesystem differences.
|
||||
func (s *playlists) findByPathNormalized(ctx context.Context, path string) (*model.Playlist, error) {
|
||||
pls, err := s.ds.Playlist(ctx).FindByPath(path)
|
||||
pls, err := s.ds.Playlist().FindByPath(ctx, path)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
altPath := norm.NFD.String(path)
|
||||
if altPath == path {
|
||||
altPath = norm.NFC.String(path)
|
||||
}
|
||||
if altPath != path {
|
||||
pls, err = s.ds.Playlist(ctx).FindByPath(altPath)
|
||||
pls, err = s.ds.Playlist().FindByPath(ctx, altPath)
|
||||
}
|
||||
}
|
||||
return pls, err
|
||||
|
|
@ -221,5 +221,5 @@ func (s *playlists) updatePlaylist(ctx context.Context, newPls *model.Playlist,
|
|||
newPls.Public = conf.Server.DefaultPlaylistPublicVisibility
|
||||
}
|
||||
}
|
||||
return s.ds.Playlist(ctx).Put(newPls)
|
||||
return s.ds.Playlist().Put(ctx, newPls)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1172,7 +1172,7 @@ type mockedMediaFileRepo struct {
|
|||
data map[string]model.MediaFile
|
||||
}
|
||||
|
||||
func (r *mockedMediaFileRepo) FindByPaths(paths []string) (model.MediaFiles, error) {
|
||||
func (r *mockedMediaFileRepo) FindByPaths(ctx context.Context, paths []string) (model.MediaFiles, error) {
|
||||
var mfs model.MediaFiles
|
||||
|
||||
// If data map provided, look up files
|
||||
|
|
@ -1212,7 +1212,7 @@ type mockedMediaFileFromListRepo struct {
|
|||
data []string
|
||||
}
|
||||
|
||||
func (r *mockedMediaFileFromListRepo) FindByPaths(paths []string) (model.MediaFiles, error) {
|
||||
func (r *mockedMediaFileFromListRepo) FindByPaths(ctx context.Context, paths []string) (model.MediaFiles, error) {
|
||||
var mfs model.MediaFiles
|
||||
|
||||
for idx, dataPath := range r.data {
|
||||
|
|
@ -1247,7 +1247,7 @@ type mockFolderRepoForImport struct {
|
|||
folder *model.Folder
|
||||
}
|
||||
|
||||
func (m *mockFolderRepoForImport) GetByPath(_ model.Library, _ string) (*model.Folder, error) {
|
||||
func (m *mockFolderRepoForImport) GetByPath(_ context.Context, _ model.Library, _ string) (*model.Folder, error) {
|
||||
if m.folder != nil {
|
||||
return m.folder, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import (
|
|||
)
|
||||
|
||||
func (s *playlists) parseM3U(ctx context.Context, pls *model.Playlist, folder *model.Folder, reader io.Reader) error {
|
||||
mediaFileRepository := s.ds.MediaFile(ctx)
|
||||
mediaFileRepository := s.ds.MediaFile()
|
||||
resolver, err := newPathResolver(ctx, s.ds)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -96,7 +96,7 @@ func (s *playlists) parseM3U(ctx context.Context, pls *model.Playlist, folder *m
|
|||
}
|
||||
}
|
||||
|
||||
found, err := mediaFileRepository.FindByPaths(lookupCandidates)
|
||||
found, err := mediaFileRepository.FindByPaths(ctx, lookupCandidates)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Error reading files from DB", "playlist", pls.Name, err)
|
||||
continue
|
||||
|
|
@ -215,7 +215,7 @@ type pathResolver struct {
|
|||
|
||||
// newPathResolver creates a pathResolver with libraries loaded from the datastore.
|
||||
func newPathResolver(ctx context.Context, ds model.DataStore) (*pathResolver, error) {
|
||||
libs, err := ds.Library(ctx).GetAll()
|
||||
libs, err := ds.Library().GetAll(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ var _ = Describe("libraryMatcher", func() {
|
|||
|
||||
// Helper function to create a libraryMatcher from the mock datastore
|
||||
createMatcher := func(ds model.DataStore) *libraryMatcher {
|
||||
libs, err := ds.Library(ctx).GetAll()
|
||||
libs, err := ds.Library().GetAll(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
return newLibraryMatcher(libs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@ type Playlists interface {
|
|||
ImportM3U(ctx context.Context, reader io.Reader) (*model.Playlist, error)
|
||||
|
||||
// REST adapters
|
||||
NewRepository(ctx context.Context) rest.Repository
|
||||
TracksRepository(ctx context.Context, playlistId string, refreshSmartPlaylist bool) rest.Repository
|
||||
Repository() rest.Repository[model.Playlist]
|
||||
TracksRepository(ctx context.Context, playlistId string, refreshSmartPlaylist bool) rest.Repository[model.PlaylistTrack]
|
||||
}
|
||||
|
||||
// ImageUploadService is a local interface satisfied by artwork.Uploader.
|
||||
|
|
@ -64,10 +64,13 @@ type ImageUploadService interface {
|
|||
type playlists struct {
|
||||
ds model.DataStore
|
||||
imgUpload ImageUploadService
|
||||
repo *playlistRepositoryWrapper
|
||||
}
|
||||
|
||||
func NewPlaylists(ds model.DataStore, imgUpload ImageUploadService) Playlists {
|
||||
return &playlists{ds: ds, imgUpload: imgUpload}
|
||||
s := &playlists{ds: ds, imgUpload: imgUpload}
|
||||
s.repo = &playlistRepositoryWrapper{PlaylistRepository: ds.Playlist(), service: s}
|
||||
return s
|
||||
}
|
||||
|
||||
func InPath(folder model.Folder) bool {
|
||||
|
|
@ -86,30 +89,30 @@ func InPath(folder model.Folder) bool {
|
|||
// --- Read operations ---
|
||||
|
||||
func (s *playlists) GetAll(ctx context.Context, options ...model.QueryOptions) (model.Playlists, error) {
|
||||
return s.ds.Playlist(ctx).GetAll(options...)
|
||||
return s.ds.Playlist().GetAll(ctx, options...)
|
||||
}
|
||||
|
||||
func (s *playlists) Get(ctx context.Context, id string) (*model.Playlist, error) {
|
||||
return s.ds.Playlist(ctx).Get(id)
|
||||
return s.ds.Playlist().Get(ctx, id)
|
||||
}
|
||||
|
||||
func (s *playlists) GetWithTracks(ctx context.Context, id string) (*model.Playlist, error) {
|
||||
return s.ds.Playlist(ctx).GetWithTracks(id, true, false)
|
||||
return s.ds.Playlist().GetWithTracks(ctx, id, true, false)
|
||||
}
|
||||
|
||||
func (s *playlists) GetPlaylists(ctx context.Context, mediaFileId string) (model.Playlists, error) {
|
||||
return s.ds.Playlist(ctx).GetPlaylists(mediaFileId)
|
||||
return s.ds.Playlist().GetPlaylists(ctx, mediaFileId)
|
||||
}
|
||||
|
||||
// Tracks scopes a repository to one playlist's tracks, for callers that page or stream them rather
|
||||
// than loading every one like GetWithTracks. Gets first because PlaylistRepository.Tracks discards
|
||||
// its error behind a nil (and warns), and this is probed with ids that are usually not playlists.
|
||||
func (s *playlists) Tracks(ctx context.Context, id string) (model.PlaylistTrackRepository, error) {
|
||||
repo := s.ds.Playlist(ctx)
|
||||
if _, err := repo.Get(id); err != nil {
|
||||
repo := s.ds.Playlist()
|
||||
if _, err := repo.Get(ctx, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tracks := repo.Tracks(id, true)
|
||||
tracks := repo.Tracks(ctx, id, true)
|
||||
if tracks == nil {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
|
@ -127,7 +130,7 @@ func (s *playlists) Create(ctx context.Context, playlistId string, name string,
|
|||
var err error
|
||||
|
||||
if playlistId != "" {
|
||||
pls, err = tx.Playlist(ctx).Get(playlistId)
|
||||
pls, err = tx.Playlist().Get(ctx, playlistId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -145,7 +148,7 @@ func (s *playlists) Create(ctx context.Context, playlistId string, name string,
|
|||
pls.Tracks = nil
|
||||
pls.AddMediaFilesByID(ids)
|
||||
|
||||
err = tx.Playlist(ctx).Put(pls)
|
||||
err = tx.Playlist().Put(ctx, pls)
|
||||
playlistId = pls.ID
|
||||
return err
|
||||
})
|
||||
|
|
@ -165,7 +168,7 @@ func (s *playlists) Delete(ctx context.Context, id string) error {
|
|||
}
|
||||
}
|
||||
|
||||
return s.ds.Playlist(ctx).Delete(id)
|
||||
return s.ds.Playlist().Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (s *playlists) Update(ctx context.Context, playlistID string,
|
||||
|
|
@ -183,21 +186,21 @@ func (s *playlists) Update(ctx context.Context, playlistID string,
|
|||
return err
|
||||
}
|
||||
return s.ds.WithTxImmediate(func(tx model.DataStore) error {
|
||||
repo := tx.Playlist(ctx)
|
||||
repo := tx.Playlist()
|
||||
|
||||
if len(idxToRemove) > 0 {
|
||||
tracksRepo := repo.Tracks(playlistID, false)
|
||||
tracksRepo := repo.Tracks(ctx, playlistID, false)
|
||||
// Convert 0-based indices to 1-based position IDs and delete them directly,
|
||||
// avoiding the need to load all tracks into memory.
|
||||
positions := make([]string, len(idxToRemove))
|
||||
for i, idx := range idxToRemove {
|
||||
positions[i] = strconv.Itoa(idx + 1)
|
||||
}
|
||||
if err := tracksRepo.Delete(positions...); err != nil {
|
||||
if err := tracksRepo.Delete(ctx, positions...); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(idsToAdd) > 0 {
|
||||
if _, err := tracksRepo.Add(idsToAdd); err != nil {
|
||||
if _, err := tracksRepo.Add(ctx, idsToAdd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -205,7 +208,7 @@ func (s *playlists) Update(ctx context.Context, playlistID string,
|
|||
}
|
||||
|
||||
if len(idsToAdd) > 0 {
|
||||
if _, err := repo.Tracks(playlistID, false).Add(idsToAdd); err != nil {
|
||||
if _, err := repo.Tracks(ctx, playlistID, false).Add(ctx, idsToAdd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -221,7 +224,7 @@ func (s *playlists) Update(ctx context.Context, playlistID string,
|
|||
|
||||
// checkWritable fetches the playlist and verifies the current user can modify it.
|
||||
func (s *playlists) checkWritable(ctx context.Context, id string) (*model.Playlist, error) {
|
||||
pls, err := s.ds.Playlist(ctx).Get(id)
|
||||
pls, err := s.ds.Playlist().Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -257,7 +260,7 @@ func (s *playlists) updateMetadata(ctx context.Context, ds model.DataStore, pls
|
|||
if public != nil {
|
||||
pls.Public = *public
|
||||
}
|
||||
return ds.Playlist(ctx).Put(pls)
|
||||
return ds.Playlist().Put(ctx, pls)
|
||||
}
|
||||
|
||||
// --- Track management operations ---
|
||||
|
|
@ -266,7 +269,7 @@ func (s *playlists) AddTracks(ctx context.Context, playlistID string, ids []stri
|
|||
if _, err := s.checkTracksEditable(ctx, playlistID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return s.ds.Playlist(ctx).Tracks(playlistID, false).Add(ids)
|
||||
return s.ds.Playlist().Tracks(ctx, playlistID, false).Add(ctx, ids)
|
||||
}
|
||||
|
||||
// InsertTracks adds tracks before the 1-based position pos; a position past the end appends.
|
||||
|
|
@ -279,7 +282,7 @@ func (s *playlists) InsertTracks(ctx context.Context, playlistID string, ids []s
|
|||
// concurrent writers instead of waiting for the lock.
|
||||
err := s.ds.WithTxImmediate(func(tx model.DataStore) error {
|
||||
var err error
|
||||
count, err = tx.Playlist(ctx).Tracks(playlistID, false).Insert(ids, pos)
|
||||
count, err = tx.Playlist().Tracks(ctx, playlistID, false).Insert(ctx, ids, pos)
|
||||
return err
|
||||
})
|
||||
return count, err
|
||||
|
|
@ -289,21 +292,21 @@ func (s *playlists) AddAlbums(ctx context.Context, playlistID string, albumIds [
|
|||
if _, err := s.checkTracksEditable(ctx, playlistID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return s.ds.Playlist(ctx).Tracks(playlistID, false).AddAlbums(albumIds)
|
||||
return s.ds.Playlist().Tracks(ctx, playlistID, false).AddAlbums(ctx, albumIds)
|
||||
}
|
||||
|
||||
func (s *playlists) AddArtists(ctx context.Context, playlistID string, artistIds []string) (int, error) {
|
||||
if _, err := s.checkTracksEditable(ctx, playlistID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return s.ds.Playlist(ctx).Tracks(playlistID, false).AddArtists(artistIds)
|
||||
return s.ds.Playlist().Tracks(ctx, playlistID, false).AddArtists(ctx, artistIds)
|
||||
}
|
||||
|
||||
func (s *playlists) AddDiscs(ctx context.Context, playlistID string, discs []model.DiscID) (int, error) {
|
||||
if _, err := s.checkTracksEditable(ctx, playlistID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return s.ds.Playlist(ctx).Tracks(playlistID, false).AddDiscs(discs)
|
||||
return s.ds.Playlist().Tracks(ctx, playlistID, false).AddDiscs(ctx, discs)
|
||||
}
|
||||
|
||||
func (s *playlists) RemoveTracks(ctx context.Context, playlistID string, trackIds []string) error {
|
||||
|
|
@ -311,7 +314,7 @@ func (s *playlists) RemoveTracks(ctx context.Context, playlistID string, trackId
|
|||
return err
|
||||
}
|
||||
return s.ds.WithTx(func(tx model.DataStore) error {
|
||||
return tx.Playlist(ctx).Tracks(playlistID, false).Delete(trackIds...)
|
||||
return tx.Playlist().Tracks(ctx, playlistID, false).Delete(ctx, trackIds...)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -320,7 +323,7 @@ func (s *playlists) ReorderTrack(ctx context.Context, playlistID string, pos int
|
|||
return err
|
||||
}
|
||||
return s.ds.WithTxImmediate(func(tx model.DataStore) error {
|
||||
return tx.Playlist(ctx).Tracks(playlistID, false).Reorder(pos, newPos)
|
||||
return tx.Playlist().Tracks(ctx, playlistID, false).Reorder(ctx, pos, newPos)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -339,7 +342,7 @@ func (s *playlists) SetImage(ctx context.Context, playlistID string, reader io.R
|
|||
}
|
||||
|
||||
pls.UploadedImage = filename
|
||||
if err := s.ds.Playlist(ctx).Put(pls); err != nil {
|
||||
if err := s.ds.Playlist().Put(ctx, pls); err != nil {
|
||||
return err
|
||||
}
|
||||
s.imgUpload.EnqueueArtwork(ctx, consts.EntityPlaylist, pls.ID)
|
||||
|
|
@ -357,7 +360,7 @@ func (s *playlists) RemoveImage(ctx context.Context, playlistID string) error {
|
|||
}
|
||||
|
||||
pls.UploadedImage = ""
|
||||
if err := s.ds.Playlist(ctx).Put(pls); err != nil {
|
||||
if err := s.ds.Playlist().Put(ctx, pls); err != nil {
|
||||
return err
|
||||
}
|
||||
s.imgUpload.EnqueueArtwork(ctx, consts.EntityPlaylist, pls.ID)
|
||||
|
|
|
|||
|
|
@ -493,15 +493,15 @@ var _ = Describe("Playlists", func() {
|
|||
|
||||
It("clears the resolved artwork state and re-queues after removing an upload", func() {
|
||||
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
||||
Expect(ds.Artwork(ctx).PutItemArtwork(&model.ItemArtwork{
|
||||
Expect(ds.Artwork().PutItemArtwork(ctx, &model.ItemArtwork{
|
||||
ItemKind: "pl", ItemID: "pls-1", Hash: "oldhash", Source: "upload",
|
||||
})).To(Succeed())
|
||||
|
||||
Expect(ps.RemoveImage(ctx, "pls-1")).To(Succeed())
|
||||
|
||||
_, err := ds.Artwork(ctx).GetItemArtwork(model.KindPlaylistArtwork, "pls-1", model.ImageTypePrimary)
|
||||
_, err := ds.Artwork().GetItemArtwork(ctx, model.KindPlaylistArtwork, "pls-1", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
queued, _ := ds.ArtworkQueue(ctx).DequeueBatch(100)
|
||||
queued, _ := ds.ArtworkQueue().DequeueBatch(ctx, 100)
|
||||
Expect(queued).To(ContainElement(SatisfyAll(
|
||||
HaveField("ItemKind", "pl"),
|
||||
HaveField("ItemID", "pls-1"),
|
||||
|
|
|
|||
|
|
@ -14,42 +14,43 @@ import (
|
|||
|
||||
// --- REST adapter (follows Share/Library pattern) ---
|
||||
|
||||
func (s *playlists) NewRepository(ctx context.Context) rest.Repository {
|
||||
return &playlistRepositoryWrapper{
|
||||
ctx: ctx,
|
||||
PlaylistRepository: s.ds.Playlist(ctx),
|
||||
service: s,
|
||||
}
|
||||
func (s *playlists) Repository() rest.Repository[model.Playlist] {
|
||||
return s.repo
|
||||
}
|
||||
|
||||
// playlistRepositoryWrapper wraps the playlist repository as a thin REST-to-service adapter.
|
||||
// It satisfies rest.Repository through the embedded PlaylistRepository (via ResourceRepository),
|
||||
// and rest.Persistable by delegating to service methods for all mutations.
|
||||
// playlistRepositoryWrapper wraps the playlist repository as a thin REST-to-service adapter,
|
||||
// delegating to service methods for all mutations.
|
||||
type playlistRepositoryWrapper struct {
|
||||
model.PlaylistRepository
|
||||
ctx context.Context
|
||||
service *playlists
|
||||
}
|
||||
|
||||
func (r *playlistRepositoryWrapper) Save(entity any) (string, error) {
|
||||
return r.service.savePlaylist(r.ctx, entity.(*model.Playlist))
|
||||
var _ rest.Persistable[model.Playlist] = (*playlistRepositoryWrapper)(nil)
|
||||
|
||||
func (r *playlistRepositoryWrapper) Save(ctx context.Context, entity *model.Playlist) (string, error) {
|
||||
return r.service.savePlaylist(ctx, entity)
|
||||
}
|
||||
|
||||
func (r *playlistRepositoryWrapper) Update(id string, entity any, cols ...string) error {
|
||||
return r.service.updatePlaylistEntity(r.ctx, id, entity.(*model.Playlist), cols...)
|
||||
func (r *playlistRepositoryWrapper) Update(ctx context.Context, id string, entity model.Playlist, cols ...string) error {
|
||||
return r.service.updatePlaylistEntity(ctx, id, &entity, cols...)
|
||||
}
|
||||
|
||||
func (r *playlistRepositoryWrapper) Delete(id string) error {
|
||||
return r.service.Delete(r.ctx, id)
|
||||
func (r *playlistRepositoryWrapper) Delete(ctx context.Context, ids ...string) error {
|
||||
for _, id := range ids {
|
||||
if err := r.service.Delete(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *playlists) TracksRepository(ctx context.Context, playlistId string, refreshSmartPlaylist bool) rest.Repository {
|
||||
repo := s.ds.Playlist(ctx)
|
||||
tracks := repo.Tracks(playlistId, refreshSmartPlaylist)
|
||||
func (s *playlists) TracksRepository(ctx context.Context, playlistId string, refreshSmartPlaylist bool) rest.Repository[model.PlaylistTrack] {
|
||||
repo := s.ds.Playlist()
|
||||
tracks := repo.Tracks(ctx, playlistId, refreshSmartPlaylist)
|
||||
if tracks == nil {
|
||||
return nil
|
||||
}
|
||||
return tracks.(rest.Repository)
|
||||
return tracks
|
||||
}
|
||||
|
||||
// savePlaylist creates a new playlist, assigning the owner from context.
|
||||
|
|
@ -63,7 +64,7 @@ func (s *playlists) savePlaylist(ctx context.Context, pls *model.Playlist) (stri
|
|||
pls.UploadedImage = "" // Managed by image upload endpoint
|
||||
pls.ExternalImageURL = "" // Managed by M3U import / plugins only
|
||||
pls.EvaluatedAt = nil // Server-managed
|
||||
err := s.ds.Playlist(ctx).Put(pls)
|
||||
err := s.ds.Playlist().Put(ctx, pls)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -156,7 +157,7 @@ func (s *playlists) applyFlagsOnly(ctx context.Context, current, entity *model.P
|
|||
if len(updateCols) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.ds.Playlist(ctx).Put(current, updateCols...)
|
||||
return s.ds.Playlist().Put(ctx, current, updateCols...)
|
||||
}
|
||||
|
||||
// sentFields returns a predicate that reports whether a JSON field was present
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ var _ = Describe("REST Adapter", func() {
|
|||
})
|
||||
|
||||
Describe("NewRepository", func() {
|
||||
var repo rest.Persistable
|
||||
var repo rest.Persistable[model.Playlist]
|
||||
|
||||
BeforeEach(func() {
|
||||
mockPlsRepo.Data = map[string]*model.Playlist{
|
||||
|
|
@ -43,9 +43,9 @@ var _ = Describe("REST Adapter", func() {
|
|||
Describe("Save", func() {
|
||||
It("sets the owner from the context user", func() {
|
||||
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
pls := &model.Playlist{Name: "New Playlist"}
|
||||
id, err := repo.Save(pls)
|
||||
id, err := repo.Save(ctx, pls)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(id).ToNot(BeEmpty())
|
||||
Expect(pls.OwnerID).To(Equal("user-1"))
|
||||
|
|
@ -53,16 +53,16 @@ var _ = Describe("REST Adapter", func() {
|
|||
|
||||
It("forces a new creation by clearing ID", func() {
|
||||
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
pls := &model.Playlist{ID: "should-be-cleared", Name: "New"}
|
||||
_, err := repo.Save(pls)
|
||||
_, err := repo.Save(ctx, pls)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(pls.ID).ToNot(Equal("should-be-cleared"))
|
||||
})
|
||||
|
||||
It("clears server-managed fields to prevent injection via REST API", func() {
|
||||
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
pls := &model.Playlist{
|
||||
Name: "Legit Playlist",
|
||||
Comment: "A comment",
|
||||
|
|
@ -74,7 +74,7 @@ var _ = Describe("REST Adapter", func() {
|
|||
ExternalImageURL: "http://evil.example.com/ssrf",
|
||||
EvaluatedAt: new(time.Now()),
|
||||
}
|
||||
_, err := repo.Save(pls)
|
||||
_, err := repo.Save(ctx, pls)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
saved := mockPlsRepo.Last
|
||||
|
|
@ -95,33 +95,33 @@ var _ = Describe("REST Adapter", func() {
|
|||
Describe("Update", func() {
|
||||
It("allows owner to update their playlist", func() {
|
||||
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
pls := &model.Playlist{Name: "Updated"}
|
||||
err := repo.Update("pls-1", pls)
|
||||
err := repo.Update(ctx, "pls-1", *pls)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("allows admin to update any playlist", func() {
|
||||
ctx = request.WithUser(ctx, model.User{ID: "admin-1", IsAdmin: true})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
pls := &model.Playlist{Name: "Updated"}
|
||||
err := repo.Update("pls-1", pls)
|
||||
err := repo.Update(ctx, "pls-1", *pls)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("denies non-owner, non-admin", func() {
|
||||
ctx = request.WithUser(ctx, model.User{ID: "other-user", IsAdmin: false})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
pls := &model.Playlist{Name: "Updated"}
|
||||
err := repo.Update("pls-1", pls)
|
||||
err := repo.Update(ctx, "pls-1", *pls)
|
||||
Expect(err).To(Equal(rest.ErrPermissionDenied))
|
||||
})
|
||||
|
||||
It("denies regular user from changing ownership", func() {
|
||||
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
pls := &model.Playlist{Name: "Updated", OwnerID: "other-user"}
|
||||
err := repo.Update("pls-1", pls)
|
||||
err := repo.Update(ctx, "pls-1", *pls)
|
||||
Expect(err).To(Equal(rest.ErrPermissionDenied))
|
||||
})
|
||||
|
||||
|
|
@ -133,9 +133,9 @@ var _ = Describe("REST Adapter", func() {
|
|||
// entity.OwnerID. sentFields normalizes both sides so the
|
||||
// permission gate fires regardless of casing.
|
||||
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
pls := &model.Playlist{OwnerID: "other-user"}
|
||||
err := repo.Update("pls-1", pls, colName)
|
||||
err := repo.Update(ctx, "pls-1", *pls, colName)
|
||||
Expect(err).To(Equal(rest.ErrPermissionDenied))
|
||||
},
|
||||
Entry("canonical camelCase", "ownerId"),
|
||||
|
|
@ -152,10 +152,10 @@ var _ = Describe("REST Adapter", func() {
|
|||
Rules: &criteria.Criteria{Expression: criteria.Contains{"title": "old"}},
|
||||
}
|
||||
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
newRules := &criteria.Criteria{Expression: criteria.Contains{"title": "new"}}
|
||||
pls := &model.Playlist{Name: "Smart Playlist", Rules: newRules}
|
||||
err := repo.Update("smart-1", pls)
|
||||
err := repo.Update(ctx, "smart-1", *pls)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last.Rules).To(Equal(newRules))
|
||||
})
|
||||
|
|
@ -171,10 +171,10 @@ var _ = Describe("REST Adapter", func() {
|
|||
Rules: &criteria.Criteria{Expression: criteria.Contains{"title": "old"}},
|
||||
}
|
||||
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
newRules := &criteria.Criteria{Expression: criteria.Contains{"title": "new"}}
|
||||
pls := &model.Playlist{Rules: newRules}
|
||||
err := repo.Update("smart-1", pls, "rules")
|
||||
err := repo.Update(ctx, "smart-1", *pls, "rules")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last.ImportedHash).To(BeEmpty())
|
||||
})
|
||||
|
|
@ -190,9 +190,9 @@ var _ = Describe("REST Adapter", func() {
|
|||
UpdatedAt: originalTime,
|
||||
}
|
||||
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
pls := &model.Playlist{Name: "File Playlist", Sync: false}
|
||||
err := repo.Update("file-pls", pls)
|
||||
err := repo.Update(ctx, "file-pls", *pls)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last.Sync).To(BeFalse())
|
||||
Expect(mockPlsRepo.Last.UpdatedAt).To(Equal(originalTime))
|
||||
|
|
@ -207,9 +207,9 @@ var _ = Describe("REST Adapter", func() {
|
|||
Sync: false,
|
||||
}
|
||||
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
pls := &model.Playlist{Name: "Manual Playlist", Sync: true}
|
||||
err := repo.Update("manual-pls", pls)
|
||||
err := repo.Update(ctx, "manual-pls", *pls)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last).To(BeNil())
|
||||
})
|
||||
|
|
@ -224,9 +224,9 @@ var _ = Describe("REST Adapter", func() {
|
|||
UpdatedAt: originalTime,
|
||||
}
|
||||
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
pls := &model.Playlist{Name: "My Playlist", Public: true}
|
||||
err := repo.Update("pls-pub", pls)
|
||||
err := repo.Update(ctx, "pls-pub", *pls)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last.Public).To(BeTrue())
|
||||
Expect(mockPlsRepo.Last.UpdatedAt).To(Equal(originalTime))
|
||||
|
|
@ -241,9 +241,9 @@ var _ = Describe("REST Adapter", func() {
|
|||
Sync: true,
|
||||
}
|
||||
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
pls := &model.Playlist{Name: "New Name", Sync: false}
|
||||
err := repo.Update("file-pls2", pls)
|
||||
err := repo.Update(ctx, "file-pls2", *pls)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last.Name).To(Equal("New Name"))
|
||||
Expect(mockPlsRepo.Last.Sync).To(BeFalse())
|
||||
|
|
@ -251,9 +251,9 @@ var _ = Describe("REST Adapter", func() {
|
|||
|
||||
It("returns rest.ErrNotFound when playlist doesn't exist", func() {
|
||||
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
pls := &model.Playlist{Name: "Updated"}
|
||||
err := repo.Update("nonexistent", pls)
|
||||
err := repo.Update(ctx, "nonexistent", *pls)
|
||||
Expect(err).To(Equal(rest.ErrNotFound))
|
||||
})
|
||||
|
||||
|
|
@ -274,8 +274,8 @@ var _ = Describe("REST Adapter", func() {
|
|||
})
|
||||
|
||||
It("preserves name and comment when only public is sent (bulk Make Public)", func() {
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
err := repo.Update("partial", &model.Playlist{Public: true}, "public")
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
err := repo.Update(ctx, "partial", model.Playlist{Public: true}, "public")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last.Name).To(Equal("Original Name"))
|
||||
Expect(mockPlsRepo.Last.Comment).To(Equal("Original comment"))
|
||||
|
|
@ -290,16 +290,16 @@ var _ = Describe("REST Adapter", func() {
|
|||
Path: "/music/p.m3u",
|
||||
Sync: true,
|
||||
}
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
err := repo.Update("file-partial", &model.Playlist{Sync: false}, "sync")
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
err := repo.Update(ctx, "file-partial", model.Playlist{Sync: false}, "sync")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last.Name).To(Equal("Keep Me"))
|
||||
Expect(mockPlsRepo.Last.Sync).To(BeFalse())
|
||||
})
|
||||
|
||||
It("renames the playlist when only name is sent", func() {
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
err := repo.Update("partial", &model.Playlist{Name: "Renamed"}, "name")
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
err := repo.Update(ctx, "partial", model.Playlist{Name: "Renamed"}, "name")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last.Name).To(Equal("Renamed"))
|
||||
Expect(mockPlsRepo.Last.Comment).To(Equal("Original comment"))
|
||||
|
|
@ -307,8 +307,8 @@ var _ = Describe("REST Adapter", func() {
|
|||
})
|
||||
|
||||
It("clears the comment when an empty comment is sent explicitly", func() {
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
err := repo.Update("partial", &model.Playlist{Comment: ""}, "comment")
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
err := repo.Update(ctx, "partial", model.Playlist{Comment: ""}, "comment")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last.Comment).To(BeEmpty())
|
||||
Expect(mockPlsRepo.Last.Name).To(Equal("Original Name"))
|
||||
|
|
@ -323,9 +323,9 @@ var _ = Describe("REST Adapter", func() {
|
|||
Public: true,
|
||||
Rules: &criteria.Criteria{Expression: criteria.Is{"genre": "Rock"}},
|
||||
}
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
newRules := &criteria.Criteria{Expression: criteria.Is{"genre": "Jazz"}, Sort: "year DESC"}
|
||||
err := repo.Update("smart-partial", &model.Playlist{Rules: newRules}, "rules")
|
||||
err := repo.Update(ctx, "smart-partial", model.Playlist{Rules: newRules}, "rules")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last.Rules).To(Equal(newRules))
|
||||
Expect(mockPlsRepo.Last.Name).To(Equal("Smart Original"))
|
||||
|
|
@ -342,9 +342,9 @@ var _ = Describe("REST Adapter", func() {
|
|||
Rules: &criteria.Criteria{Expression: criteria.Is{"genre": "Rock"}},
|
||||
EvaluatedAt: &evaluatedAt,
|
||||
}
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
newRules := &criteria.Criteria{Expression: criteria.Is{"genre": "Jazz"}}
|
||||
err := repo.Update("smart-reset", &model.Playlist{Rules: newRules}, "rules")
|
||||
err := repo.Update(ctx, "smart-reset", model.Playlist{Rules: newRules}, "rules")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last.EvaluatedAt).To(BeNil())
|
||||
})
|
||||
|
|
@ -358,8 +358,8 @@ var _ = Describe("REST Adapter", func() {
|
|||
Rules: &criteria.Criteria{Expression: criteria.Is{"genre": "Rock"}},
|
||||
EvaluatedAt: &evaluatedAt,
|
||||
}
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
err := repo.Update("smart-keep", &model.Playlist{Name: "Renamed Smart"}, "name")
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
err := repo.Update(ctx, "smart-keep", model.Playlist{Name: "Renamed Smart"}, "name")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last.EvaluatedAt).ToNot(BeNil())
|
||||
Expect(*mockPlsRepo.Last.EvaluatedAt).To(BeTemporally("~", evaluatedAt, time.Second))
|
||||
|
|
@ -373,10 +373,10 @@ var _ = Describe("REST Adapter", func() {
|
|||
OwnerID: "user-1",
|
||||
Rules: &criteria.Criteria{Expression: criteria.Is{"genre": "Rock"}},
|
||||
}
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
newRules := &criteria.Criteria{Expression: criteria.Is{"artist": "Miles Davis"}, Sort: "album"}
|
||||
err := repo.Update("smart-edit",
|
||||
&model.Playlist{Name: "Smart Renamed", Rules: newRules},
|
||||
err := repo.Update(ctx, "smart-edit",
|
||||
model.Playlist{Name: "Smart Renamed", Rules: newRules},
|
||||
"name", "rules")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last.Name).To(Equal("Smart Renamed"))
|
||||
|
|
@ -392,11 +392,11 @@ var _ = Describe("REST Adapter", func() {
|
|||
OwnerID: "user-1",
|
||||
Rules: rules,
|
||||
}
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
// Same rules sent back — rulesEqual should report no change and
|
||||
// the request should no-op (no Put call).
|
||||
sameRules := &criteria.Criteria{Expression: criteria.Is{"genre": "Rock"}}
|
||||
err := repo.Update("smart-idempotent", &model.Playlist{Rules: sameRules}, "rules")
|
||||
err := repo.Update(ctx, "smart-idempotent", model.Playlist{Rules: sameRules}, "rules")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last).To(BeNil()) // no Put happened
|
||||
})
|
||||
|
|
@ -410,8 +410,8 @@ var _ = Describe("REST Adapter", func() {
|
|||
Public: false,
|
||||
Rules: rules,
|
||||
}
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
err := repo.Update("smart-public", &model.Playlist{Public: true}, "public")
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
err := repo.Update(ctx, "smart-public", model.Playlist{Public: true}, "public")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last.Public).To(BeTrue())
|
||||
Expect(mockPlsRepo.Last.Rules).To(Equal(rules))
|
||||
|
|
@ -421,8 +421,8 @@ var _ = Describe("REST Adapter", func() {
|
|||
It("does not treat a missing ownerId as an ownership transfer attempt", func() {
|
||||
// A non-admin user sending only {public:true} should not be blocked
|
||||
// just because OwnerID is the zero value in the deserialized entity.
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
err := repo.Update("partial", &model.Playlist{Public: true}, "public")
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
err := repo.Update(ctx, "partial", model.Playlist{Public: true}, "public")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
|
|
@ -431,8 +431,8 @@ var _ = Describe("REST Adapter", func() {
|
|||
// like {"Name":"x"}, but rest.Put's field-name extraction is
|
||||
// case-sensitive. sentFields normalizes both sides so a request
|
||||
// with {"Name":"Renamed"} is honored, not silently ignored.
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
err := repo.Update("partial", &model.Playlist{Name: "Renamed"}, "Name")
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
err := repo.Update(ctx, "partial", model.Playlist{Name: "Renamed"}, "Name")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Last.Name).To(Equal("Renamed"))
|
||||
Expect(mockPlsRepo.Last.Comment).To(Equal("Original comment"))
|
||||
|
|
@ -443,16 +443,16 @@ var _ = Describe("REST Adapter", func() {
|
|||
Describe("Delete", func() {
|
||||
It("delegates to service Delete with permission checks", func() {
|
||||
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
err := repo.Delete("pls-1")
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
err := repo.Delete(ctx, "pls-1")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockPlsRepo.Deleted).To(ContainElement("pls-1"))
|
||||
})
|
||||
|
||||
It("denies non-owner", func() {
|
||||
ctx = request.WithUser(ctx, model.User{ID: "other-user", IsAdmin: false})
|
||||
repo = ps.NewRepository(ctx).(rest.Persistable)
|
||||
err := repo.Delete("pls-1")
|
||||
repo = ps.Repository().(rest.Persistable[model.Playlist])
|
||||
err := repo.Delete(ctx, "pls-1")
|
||||
Expect(err).To(Equal(rest.ErrPermissionDenied))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ type bufferedScrobbler struct {
|
|||
loader Loader
|
||||
service string
|
||||
wakeSignal chan struct{}
|
||||
ctx context.Context
|
||||
ctx context.Context //nolint:containedctx // scrobbler lifecycle ctx, cancelled by Stop
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ func (b *bufferedScrobbler) NowPlaying(ctx context.Context, userId string, track
|
|||
}
|
||||
|
||||
func (b *bufferedScrobbler) Scrobble(ctx context.Context, userId string, s Scrobble) error {
|
||||
err := b.ds.ScrobbleBuffer(ctx).Enqueue(b.service, userId, s.ID, s.TimeStamp)
|
||||
err := b.ds.ScrobbleBuffer().Enqueue(ctx, b.service, userId, s.ID, s.TimeStamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -154,8 +154,8 @@ func (b *bufferedScrobbler) run(ctx context.Context) {
|
|||
}
|
||||
|
||||
func (b *bufferedScrobbler) processQueue(ctx context.Context) (bool, time.Duration) {
|
||||
buffer := b.ds.ScrobbleBuffer(ctx)
|
||||
userIds, err := buffer.UserIDs(b.service)
|
||||
buffer := b.ds.ScrobbleBuffer()
|
||||
userIds, err := buffer.UserIDs(ctx, b.service)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error retrieving userIds from scrobble buffer", "scrobbler", b.service, err)
|
||||
return false, 0
|
||||
|
|
@ -176,14 +176,14 @@ func (b *bufferedScrobbler) processUserQueue(ctx context.Context, userId string)
|
|||
// Scrobbles are drained on a background context that no longer carries the
|
||||
// request's authenticated user. Restore it from the buffered userId so that
|
||||
// scrobblers relying on the user in the context (e.g. plugins) still get it.
|
||||
if user, err := b.ds.User(ctx).Get(userId); err != nil {
|
||||
if user, err := b.ds.User().Get(ctx, userId); err != nil {
|
||||
log.Warn(ctx, "Could not load user for buffered scrobble", "userId", userId, "scrobbler", b.service, err)
|
||||
} else {
|
||||
ctx = request.WithUser(ctx, *user)
|
||||
}
|
||||
buffer := b.ds.ScrobbleBuffer(ctx)
|
||||
buffer := b.ds.ScrobbleBuffer()
|
||||
for {
|
||||
entry, err := buffer.Next(b.service, userId)
|
||||
entry, err := buffer.Next(ctx, b.service, userId)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error reading from scrobble buffer", "scrobbler", b.service, err)
|
||||
return false, 0
|
||||
|
|
@ -210,7 +210,7 @@ func (b *bufferedScrobbler) processUserQueue(ctx context.Context, userId string)
|
|||
log.Error(ctx, "Error sending scrobble to service. Discarding", "scrobbler", b.service,
|
||||
"userId", entry.UserID, "artist", entry.Artist, "track", entry.Title, err)
|
||||
}
|
||||
err = buffer.Dequeue(entry)
|
||||
err = buffer.Dequeue(ctx, entry)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error removing entry from scrobble buffer", "userId", entry.UserID,
|
||||
"track", entry.Title, "artist", entry.Artist, "scrobbler", b.service, err)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ var _ = Describe("BufferedScrobbler", func() {
|
|||
ctx = context.Background()
|
||||
buffer = tests.CreateMockedScrobbleBufferRepo()
|
||||
userRepo := tests.CreateMockUserRepo()
|
||||
Expect(userRepo.Put(&model.User{ID: "user1", UserName: "alice"})).To(Succeed())
|
||||
Expect(userRepo.Put(ctx, &model.User{ID: "user1", UserName: "alice"})).To(Succeed())
|
||||
ds = &tests.MockDataStore{
|
||||
MockedScrobbleBuffer: buffer,
|
||||
MockedUser: userRepo,
|
||||
|
|
@ -55,7 +55,7 @@ var _ = Describe("BufferedScrobbler", func() {
|
|||
track := model.MediaFile{ID: "123", Title: "Test Track"}
|
||||
now := time.Now()
|
||||
scrobble := Scrobble{MediaFile: track, TimeStamp: now}
|
||||
Expect(buffer.Length()).To(Equal(int64(0)))
|
||||
Expect(buffer.Length(ctx)).To(Equal(int64(0)))
|
||||
Expect(scr.ScrobbleCalled.Load()).To(BeFalse())
|
||||
|
||||
Expect(bs.Scrobble(ctx, "user1", scrobble)).To(Succeed())
|
||||
|
|
@ -131,7 +131,7 @@ func TestBufferedScrobblerBackoffSchedule(t *testing.T) {
|
|||
g := NewWithT(t)
|
||||
buffer := tests.CreateMockedScrobbleBufferRepo()
|
||||
userRepo := tests.CreateMockUserRepo()
|
||||
g.Expect(userRepo.Put(&model.User{ID: "user1", UserName: "alice"})).To(Succeed())
|
||||
g.Expect(userRepo.Put(t.Context(), &model.User{ID: "user1", UserName: "alice"})).To(Succeed())
|
||||
ds := &tests.MockDataStore{MockedScrobbleBuffer: buffer, MockedUser: userRepo}
|
||||
|
||||
flaky := &recoveringScrobbler{}
|
||||
|
|
@ -147,7 +147,7 @@ func TestBufferedScrobblerBackoffSchedule(t *testing.T) {
|
|||
// First attempt fires immediately on the enqueue wake and is left buffered.
|
||||
synctest.Wait()
|
||||
g.Expect(flaky.count.Load()).To(Equal(int32(1)))
|
||||
g.Expect(buffer.Length()).To(Equal(int64(1)))
|
||||
g.Expect(buffer.Length(t.Context())).To(Equal(int64(1)))
|
||||
|
||||
// Each subsequent retry waits exactly double the previous: 5s, 10s, 20s, 40s.
|
||||
for i, gap := range []time.Duration{5 * time.Second, 10 * time.Second, 20 * time.Second, 40 * time.Second} {
|
||||
|
|
@ -165,10 +165,10 @@ func TestBufferedScrobblerBackoffSchedule(t *testing.T) {
|
|||
flaky.succeed()
|
||||
bs.sendWakeSignal()
|
||||
synctest.Wait()
|
||||
g.Expect(buffer.Length()).To(Equal(int64(1)), "wake during backoff drained early")
|
||||
g.Expect(buffer.Length(t.Context())).To(Equal(int64(1)), "wake during backoff drained early")
|
||||
time.Sleep(80 * time.Second)
|
||||
synctest.Wait()
|
||||
g.Expect(buffer.Length()).To(Equal(int64(0)))
|
||||
g.Expect(buffer.Length(t.Context())).To(Equal(int64(0)))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -176,7 +176,7 @@ func TestBufferedScrobblerBackoffWindow(t *testing.T) {
|
|||
synctest.Test(t, func(t *testing.T) {
|
||||
buffer := tests.CreateMockedScrobbleBufferRepo()
|
||||
userRepo := tests.CreateMockUserRepo()
|
||||
_ = userRepo.Put(&model.User{ID: "user1", UserName: "alice"})
|
||||
_ = userRepo.Put(t.Context(), &model.User{ID: "user1", UserName: "alice"})
|
||||
ds := &tests.MockDataStore{MockedScrobbleBuffer: buffer, MockedUser: userRepo}
|
||||
scr := &fakeScrobbler{Authorized: true}
|
||||
scr.SetError(errors.Join(errors.New("boom"), ErrRetryLater))
|
||||
|
|
@ -211,7 +211,7 @@ func TestBufferedScrobblerHonorsServerDelay(t *testing.T) {
|
|||
synctest.Test(t, func(t *testing.T) {
|
||||
buffer := tests.CreateMockedScrobbleBufferRepo()
|
||||
userRepo := tests.CreateMockUserRepo()
|
||||
_ = userRepo.Put(&model.User{ID: "user1", UserName: "alice"})
|
||||
_ = userRepo.Put(t.Context(), &model.User{ID: "user1", UserName: "alice"})
|
||||
ds := &tests.MockDataStore{MockedScrobbleBuffer: buffer, MockedUser: userRepo}
|
||||
scr := &fakeScrobbler{Authorized: true}
|
||||
scr.SetError(errors.Join(errors.New("429"), &agents.RetryLaterError{RetryIn: 30 * time.Second}))
|
||||
|
|
@ -244,8 +244,8 @@ func TestBufferedScrobblerTakesTheLongestServerDelayAcrossUsers(t *testing.T) {
|
|||
synctest.Test(t, func(t *testing.T) {
|
||||
buffer := tests.CreateMockedScrobbleBufferRepo()
|
||||
userRepo := tests.CreateMockUserRepo()
|
||||
_ = userRepo.Put(&model.User{ID: "user1", UserName: "alice"})
|
||||
_ = userRepo.Put(&model.User{ID: "user2", UserName: "bob"})
|
||||
_ = userRepo.Put(t.Context(), &model.User{ID: "user1", UserName: "alice"})
|
||||
_ = userRepo.Put(t.Context(), &model.User{ID: "user2", UserName: "bob"})
|
||||
ds := &tests.MockDataStore{MockedScrobbleBuffer: buffer, MockedUser: userRepo}
|
||||
scr := &recoveringScrobbler{delays: map[string]time.Duration{
|
||||
"user1": 10 * time.Second,
|
||||
|
|
@ -253,8 +253,8 @@ func TestBufferedScrobblerTakesTheLongestServerDelayAcrossUsers(t *testing.T) {
|
|||
}}
|
||||
// Both are buffered before the drain goroutine exists: it drains once on startup, and
|
||||
// seeing only one user there would park it on that user's delay, ignoring the other.
|
||||
_ = buffer.Enqueue("test", "user1", "1", time.Now())
|
||||
_ = buffer.Enqueue("test", "user2", "2", time.Now())
|
||||
_ = buffer.Enqueue(t.Context(), "test", "user1", "1", time.Now())
|
||||
_ = buffer.Enqueue(t.Context(), "test", "user2", "2", time.Now())
|
||||
bs := newBufferedScrobbler(ds, scr, "test")
|
||||
defer bs.Stop()
|
||||
|
||||
|
|
|
|||
|
|
@ -68,14 +68,14 @@ type ReportPlaybackParams struct {
|
|||
}
|
||||
|
||||
type nowPlayingEntry struct {
|
||||
ctx context.Context
|
||||
ctx context.Context //nolint:containedctx // queued work item carries the request ctx to the worker
|
||||
userId string
|
||||
track *model.MediaFile
|
||||
position int
|
||||
}
|
||||
|
||||
type playbackReportEntry struct {
|
||||
ctx context.Context
|
||||
ctx context.Context //nolint:containedctx // queued work item carries the request ctx to the worker
|
||||
info PlaybackSession
|
||||
filtered bool
|
||||
}
|
||||
|
|
@ -293,7 +293,7 @@ func (p *playTracker) ReportPlayback(ctx context.Context, params ReportPlaybackP
|
|||
log.Trace(ctx, "Ignoring out-of-order starting report for playing session", "clientId", clientId, "mediaId", params.MediaId)
|
||||
return nil
|
||||
}
|
||||
mf, err := p.ds.MediaFile(ctx).GetWithParticipants(params.MediaId)
|
||||
mf, err := p.ds.MediaFile().GetWithParticipants(ctx, params.MediaId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -328,7 +328,7 @@ func (p *playTracker) ReportPlayback(ctx context.Context, params ReportPlaybackP
|
|||
case StatePlaying, StatePaused:
|
||||
info, getErr := p.playMap.Get(clientId)
|
||||
if getErr != nil || info.MediaFile.ID != params.MediaId {
|
||||
mf, err := p.ds.MediaFile(ctx).GetWithParticipants(params.MediaId)
|
||||
mf, err := p.ds.MediaFile().GetWithParticipants(ctx, params.MediaId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -364,7 +364,7 @@ func (p *playTracker) ReportPlayback(ctx context.Context, params ReportPlaybackP
|
|||
var loadedMF *model.MediaFile
|
||||
haveVerdict := false
|
||||
if !params.IgnoreScrobble && player.ScrobbleEnabled {
|
||||
mf, err := p.ds.MediaFile(ctx).GetWithParticipants(params.MediaId)
|
||||
mf, err := p.ds.MediaFile().GetWithParticipants(ctx, params.MediaId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -409,7 +409,7 @@ func (p *playTracker) ReportPlayback(ctx context.Context, params ReportPlaybackP
|
|||
mf := loadedMF
|
||||
if mf == nil {
|
||||
var mfErr error
|
||||
mf, mfErr = p.ds.MediaFile(ctx).GetWithParticipants(params.MediaId)
|
||||
mf, mfErr = p.ds.MediaFile().GetWithParticipants(ctx, params.MediaId)
|
||||
if mfErr != nil {
|
||||
return mfErr
|
||||
}
|
||||
|
|
@ -477,7 +477,7 @@ func (p *playTracker) Submit(ctx context.Context, submissions []Submission) erro
|
|||
success := 0
|
||||
|
||||
for _, s := range submissions {
|
||||
mf, err := p.ds.MediaFile(ctx).GetWithParticipants(s.TrackID)
|
||||
mf, err := p.ds.MediaFile().GetWithParticipants(ctx, s.TrackID)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Cannot find track for scrobbling", "id", s.TrackID, "user", username, err)
|
||||
continue
|
||||
|
|
@ -504,22 +504,22 @@ func (p *playTracker) Submit(ctx context.Context, submissions []Submission) erro
|
|||
|
||||
func (p *playTracker) incPlay(ctx context.Context, track *model.MediaFile, timestamp time.Time) error {
|
||||
return p.ds.WithTx(func(tx model.DataStore) error {
|
||||
err := tx.MediaFile(ctx).IncPlayCount(track.ID, timestamp)
|
||||
err := tx.MediaFile().IncPlayCount(ctx, track.ID, timestamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tx.Album(ctx).IncPlayCount(track.AlbumID, timestamp)
|
||||
err = tx.Album().IncPlayCount(ctx, track.AlbumID, timestamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, artist := range track.Participants[model.RoleArtist] {
|
||||
err = tx.Artist(ctx).IncPlayCount(artist.ID, timestamp)
|
||||
err = tx.Artist().IncPlayCount(ctx, artist.ID, timestamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if conf.Server.EnableScrobbleHistory {
|
||||
return tx.Scrobble(ctx).RecordScrobble(track.ID, timestamp)
|
||||
return tx.Scrobble().RecordScrobble(ctx, track.ID, timestamp)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
|
@ -538,7 +538,7 @@ func (p *playTracker) isFilteredOut(ctx context.Context, t *model.MediaFile) boo
|
|||
log.Warn(ctx, "Invalid scrobble filter, ignoring", "user", u.UserName, err)
|
||||
return false
|
||||
}
|
||||
match, err := p.ds.MediaFile(ctx).MatchesCriteria(t.ID, c)
|
||||
match, err := p.ds.MediaFile().MatchesCriteria(ctx, t.ID, c)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Error evaluating scrobble filter, ignoring", "user", u.UserName, "track", t.Title, err)
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -55,12 +55,12 @@ type flipOnPlayRepo struct {
|
|||
played atomic.Bool
|
||||
}
|
||||
|
||||
func (r *flipOnPlayRepo) IncPlayCount(id string, ts time.Time) error {
|
||||
func (r *flipOnPlayRepo) IncPlayCount(ctx context.Context, id string, ts time.Time) error {
|
||||
r.played.Store(true)
|
||||
return r.MediaFileRepository.IncPlayCount(id, ts)
|
||||
return r.MediaFileRepository.IncPlayCount(ctx, id, ts)
|
||||
}
|
||||
|
||||
func (r *flipOnPlayRepo) MatchesCriteria(string, criteria.Criteria) (bool, error) {
|
||||
func (r *flipOnPlayRepo) MatchesCriteria(context.Context, string, criteria.Criteria) (bool, error) {
|
||||
if r.played.Load() {
|
||||
return r.after, nil
|
||||
}
|
||||
|
|
@ -73,9 +73,9 @@ type slowMediaFileRepo struct {
|
|||
model.MediaFileRepository
|
||||
}
|
||||
|
||||
func (s *slowMediaFileRepo) GetWithParticipants(id string) (*model.MediaFile, error) {
|
||||
func (s *slowMediaFileRepo) GetWithParticipants(ctx context.Context, id string) (*model.MediaFile, error) {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
return s.MediaFileRepository.GetWithParticipants(id)
|
||||
return s.MediaFileRepository.GetWithParticipants(ctx, id)
|
||||
}
|
||||
|
||||
var _ = Describe("PlayTracker", func() {
|
||||
|
|
@ -119,13 +119,13 @@ var _ = Describe("PlayTracker", func() {
|
|||
model.RoleArtist: []model.Participant{_p("ar-1", "Artist 1"), _p("ar-2", "Artist 2")},
|
||||
},
|
||||
}
|
||||
_ = ds.MediaFile(ctx).Put(&track)
|
||||
_ = ds.MediaFile().Put(ctx, &track)
|
||||
artist1 = model.Artist{ID: "ar-1"}
|
||||
_ = ds.Artist(ctx).Put(&artist1)
|
||||
_ = ds.Artist().Put(ctx, &artist1)
|
||||
artist2 = model.Artist{ID: "ar-2"}
|
||||
_ = ds.Artist(ctx).Put(&artist2)
|
||||
_ = ds.Artist().Put(ctx, &artist2)
|
||||
album = model.Album{ID: "al-1"}
|
||||
_ = ds.Album(ctx).(*tests.MockAlbumRepo).Put(&album)
|
||||
_ = ds.Album().(*tests.MockAlbumRepo).Put(ctx, &album)
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
|
|
@ -149,7 +149,7 @@ var _ = Describe("PlayTracker", func() {
|
|||
It("returns current playing music", func() {
|
||||
track2 := track
|
||||
track2.ID = "456"
|
||||
_ = ds.MediaFile(ctx).Put(&track2)
|
||||
_ = ds.MediaFile().Put(ctx, &track2)
|
||||
ctx1 := request.WithUser(GinkgoT().Context(), model.User{UserName: "user-1"})
|
||||
ctx1 = request.WithPlayer(ctx1, model.Player{ScrobbleEnabled: true})
|
||||
_ = tracker.ReportPlayback(ctx1, ReportPlaybackParams{
|
||||
|
|
@ -180,7 +180,7 @@ var _ = Describe("PlayTracker", func() {
|
|||
hidden := track
|
||||
hidden.ID = "789"
|
||||
hidden.LibraryID = 2
|
||||
_ = ds.MediaFile(ctx).Put(&hidden)
|
||||
_ = ds.MediaFile().Put(ctx, &hidden)
|
||||
reporter := request.WithPlayer(
|
||||
request.WithUser(GinkgoT().Context(), model.User{ID: "u-2", UserName: "user-2"}),
|
||||
model.Player{ScrobbleEnabled: true},
|
||||
|
|
@ -199,7 +199,7 @@ var _ = Describe("PlayTracker", func() {
|
|||
hidden := track
|
||||
hidden.ID = "789"
|
||||
hidden.LibraryID = 2
|
||||
_ = ds.MediaFile(ctx).Put(&hidden)
|
||||
_ = ds.MediaFile().Put(ctx, &hidden)
|
||||
reporter := request.WithPlayer(
|
||||
request.WithUser(GinkgoT().Context(), model.User{ID: "u-2", UserName: "user-2"}),
|
||||
model.Player{ScrobbleEnabled: true},
|
||||
|
|
@ -360,7 +360,7 @@ var _ = Describe("PlayTracker", func() {
|
|||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
mockDS := ds.(*tests.MockDataStore)
|
||||
mockScrobble := mockDS.Scrobble(ctx).(*tests.MockScrobbleRepo)
|
||||
mockScrobble := mockDS.Scrobble().(*tests.MockScrobbleRepo)
|
||||
Expect(mockScrobble.RecordedScrobbles).To(HaveLen(1))
|
||||
Expect(mockScrobble.RecordedScrobbles[0].MediaFileID).To(Equal("123"))
|
||||
Expect(mockScrobble.RecordedScrobbles[0].UserID).To(Equal("u-1"))
|
||||
|
|
@ -376,7 +376,7 @@ var _ = Describe("PlayTracker", func() {
|
|||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
mockDS := ds.(*tests.MockDataStore)
|
||||
mockScrobble := mockDS.Scrobble(ctx).(*tests.MockScrobbleRepo)
|
||||
mockScrobble := mockDS.Scrobble().(*tests.MockScrobbleRepo)
|
||||
Expect(mockScrobble.RecordedScrobbles).To(HaveLen(0))
|
||||
})
|
||||
})
|
||||
|
|
@ -388,7 +388,7 @@ var _ = Describe("PlayTracker", func() {
|
|||
BeforeEach(func() {
|
||||
ctx = request.WithUser(ctx, model.User{ID: "u-1", UserName: "user-1",
|
||||
ScrobbleFilter: `{"all":[{"contains":{"title":"Track"}}]}`})
|
||||
repo = ds.MediaFile(ctx).(*tests.MockMediaFileRepo)
|
||||
repo = ds.MediaFile().(*tests.MockMediaFileRepo)
|
||||
})
|
||||
|
||||
It("does not send a matching track to the agent", func() {
|
||||
|
|
@ -489,7 +489,7 @@ var _ = Describe("PlayTracker", func() {
|
|||
var flip *flipOnPlayRepo
|
||||
|
||||
install := func(before, after bool) {
|
||||
flip = &flipOnPlayRepo{MediaFileRepository: ds.MediaFile(ctx), before: before, after: after}
|
||||
flip = &flipOnPlayRepo{MediaFileRepository: ds.MediaFile(), before: before, after: after}
|
||||
ds.(*tests.MockDataStore).MockedMediaFile = flip
|
||||
}
|
||||
|
||||
|
|
@ -632,7 +632,7 @@ var _ = Describe("PlayTracker", func() {
|
|||
It("starting replaces existing entry when switching tracks on same player", func() {
|
||||
track2 := track
|
||||
track2.ID = "456"
|
||||
_ = ds.MediaFile(ctx).Put(&track2)
|
||||
_ = ds.MediaFile().Put(ctx, &track2)
|
||||
|
||||
err := tracker.ReportPlayback(ctx, ReportPlaybackParams{
|
||||
MediaId: "123", PositionMs: 50000, State: "playing", PlaybackRate: 1.0, ClientId: defaultClientId,
|
||||
|
|
@ -659,7 +659,7 @@ var _ = Describe("PlayTracker", func() {
|
|||
|
||||
track2 := track
|
||||
track2.ID = "456"
|
||||
_ = ds.MediaFile(ctx).Put(&track2)
|
||||
_ = ds.MediaFile().Put(ctx, &track2)
|
||||
|
||||
err := tracker.ReportPlayback(ctx1, ReportPlaybackParams{
|
||||
MediaId: "123", PositionMs: 0, State: "playing", PlaybackRate: 1.0, ClientId: "client-1",
|
||||
|
|
@ -760,7 +760,7 @@ var _ = Describe("PlayTracker", func() {
|
|||
model.RoleArtist: []model.Participant{_p("ar-1", "Artist 1")},
|
||||
},
|
||||
}
|
||||
_ = ds.MediaFile(ctx).Put(&longTrack)
|
||||
_ = ds.MediaFile().Put(ctx, &longTrack)
|
||||
|
||||
err := tracker.ReportPlayback(ctx, ReportPlaybackParams{
|
||||
MediaId: "long", PositionMs: 0, State: "starting", PlaybackRate: 1.0, ClientId: defaultClientId,
|
||||
|
|
@ -958,7 +958,7 @@ var _ = Describe("PlayTracker", func() {
|
|||
BeforeEach(func() {
|
||||
track2 := track
|
||||
track2.ID = "456"
|
||||
_ = ds.MediaFile(ctx).Put(&track2)
|
||||
_ = ds.MediaFile().Put(ctx, &track2)
|
||||
})
|
||||
|
||||
It("does not downgrade an actively playing session when a late starting report arrives for the same track", func() {
|
||||
|
|
@ -1023,7 +1023,7 @@ var _ = Describe("PlayTracker", func() {
|
|||
})
|
||||
|
||||
It("never lets a concurrent starting report downgrade the playing session", func() {
|
||||
ds.(*tests.MockDataStore).MockedMediaFile = &slowMediaFileRepo{MediaFileRepository: ds.MediaFile(ctx)}
|
||||
ds.(*tests.MockDataStore).MockedMediaFile = &slowMediaFileRepo{MediaFileRepository: ds.MediaFile()}
|
||||
for i := range 20 {
|
||||
raceClientId := fmt.Sprintf("race-client-%d", i)
|
||||
var wg sync.WaitGroup
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/deluan/rest"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
|
|
@ -21,22 +20,24 @@ import (
|
|||
|
||||
type Share interface {
|
||||
Load(ctx context.Context, id string) (*model.Share, error)
|
||||
NewRepository(ctx context.Context) rest.Repository
|
||||
Repository() model.ShareRepository
|
||||
}
|
||||
|
||||
func NewShare(ds model.DataStore) Share {
|
||||
return &shareService{
|
||||
ds: ds,
|
||||
ds: ds,
|
||||
repo: &shareRepositoryWrapper{ShareRepository: ds.Share(), ds: ds},
|
||||
}
|
||||
}
|
||||
|
||||
type shareService struct {
|
||||
ds model.DataStore
|
||||
ds model.DataStore
|
||||
repo *shareRepositoryWrapper
|
||||
}
|
||||
|
||||
func (s *shareService) Load(ctx context.Context, id string) (*model.Share, error) {
|
||||
repo := s.ds.Share(ctx)
|
||||
share, err := repo.Get(id)
|
||||
repo := s.ds.Share()
|
||||
share, err := repo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -47,40 +48,29 @@ func (s *shareService) Load(ctx context.Context, id string) (*model.Share, error
|
|||
share.LastVisitedAt = new(time.Now())
|
||||
share.VisitCount++
|
||||
|
||||
err = repo.(rest.Persistable).Update(id, share, "last_visited_at", "visit_count")
|
||||
err = repo.Update(ctx, id, *share, "last_visited_at", "visit_count")
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Could not increment visit count for share", "share", share.ID)
|
||||
}
|
||||
return share, nil
|
||||
}
|
||||
|
||||
func (s *shareService) NewRepository(ctx context.Context) rest.Repository {
|
||||
repo := s.ds.Share(ctx)
|
||||
wrapper := &shareRepositoryWrapper{
|
||||
ctx: ctx,
|
||||
ShareRepository: repo,
|
||||
Repository: repo.(rest.Repository),
|
||||
Persistable: repo.(rest.Persistable),
|
||||
ds: s.ds,
|
||||
}
|
||||
return wrapper
|
||||
func (s *shareService) Repository() model.ShareRepository {
|
||||
return s.repo
|
||||
}
|
||||
|
||||
type shareRepositoryWrapper struct {
|
||||
model.ShareRepository
|
||||
rest.Repository
|
||||
rest.Persistable
|
||||
ctx context.Context
|
||||
ds model.DataStore
|
||||
ds model.DataStore
|
||||
}
|
||||
|
||||
func (r *shareRepositoryWrapper) newId() (string, error) {
|
||||
func (r *shareRepositoryWrapper) newId(ctx context.Context) (string, error) {
|
||||
for {
|
||||
id, err := nanoid.Generate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 10)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
exists, err := r.Exists(id)
|
||||
exists, err := r.Exists(ctx, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -90,14 +80,13 @@ func (r *shareRepositoryWrapper) newId() (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *shareRepositoryWrapper) Save(entity any) (string, error) {
|
||||
s := entity.(*model.Share)
|
||||
func (r *shareRepositoryWrapper) Save(ctx context.Context, s *model.Share) (string, error) {
|
||||
// Owner is always the caller; never trust a client-supplied UserID, as it
|
||||
// determines the library-access context used to resolve the share contents.
|
||||
if user, ok := request.UserFrom(r.ctx); ok {
|
||||
if user, ok := request.UserFrom(ctx); ok {
|
||||
s.UserID = user.ID
|
||||
}
|
||||
id, err := r.newId()
|
||||
id, err := r.newId(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -106,39 +95,39 @@ func (r *shareRepositoryWrapper) Save(entity any) (string, error) {
|
|||
s.ExpiresAt = new(time.Now().Add(conf.Server.DefaultShareExpiration))
|
||||
}
|
||||
|
||||
s.ResourceType, err = r.resourceType(s.ResourceIDs)
|
||||
s.ResourceType, err = r.resourceType(ctx, s.ResourceIDs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch s.ResourceType {
|
||||
case "artist":
|
||||
s.Contents = r.contentsLabelFromArtist(s.ID, s.ResourceIDs)
|
||||
s.Contents = r.contentsLabelFromArtist(ctx, s.ID, s.ResourceIDs)
|
||||
case "album":
|
||||
s.Contents = r.contentsLabelFromAlbums(s.ID, s.ResourceIDs)
|
||||
s.Contents = r.contentsLabelFromAlbums(ctx, s.ID, s.ResourceIDs)
|
||||
case "playlist":
|
||||
s.Contents = r.contentsLabelFromPlaylist(s.ID, s.ResourceIDs)
|
||||
s.Contents = r.contentsLabelFromPlaylist(ctx, s.ID, s.ResourceIDs)
|
||||
case "media_file":
|
||||
s.Contents = r.contentsLabelFromMediaFiles(s.ID, s.ResourceIDs)
|
||||
s.Contents = r.contentsLabelFromMediaFiles(ctx, s.ID, s.ResourceIDs)
|
||||
}
|
||||
|
||||
s.Contents = str.TruncateRunes(s.Contents, 30, "...")
|
||||
|
||||
return r.Persistable.Save(s)
|
||||
return r.ShareRepository.Save(ctx, s)
|
||||
}
|
||||
|
||||
var shareableKinds = []model.Kind{model.KindArtistArtwork, model.KindAlbumArtwork, model.KindPlaylistArtwork, model.KindMediaFileArtwork}
|
||||
|
||||
// resourceType resolves every ID as the current user, so an entity they cannot see cannot
|
||||
// ride along behind a valid first one, and requires all IDs to be of the same kind.
|
||||
func (r *shareRepositoryWrapper) resourceType(resourceIDs string) (string, error) {
|
||||
func (r *shareRepositoryWrapper) resourceType(ctx context.Context, resourceIDs string) (string, error) {
|
||||
resourceType := ""
|
||||
for _, id := range strings.Split(resourceIDs, ",") {
|
||||
kind, err := model.GetEntityKindByID(r.ctx, r.ds, id)
|
||||
kind, err := model.GetEntityKindByID(ctx, r.ds, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !slices.Contains(shareableKinds, kind) {
|
||||
log.Error(r.ctx, "Invalid Resource ID", "id", id)
|
||||
log.Error(ctx, "Invalid Resource ID", "id", id)
|
||||
return "", model.ErrNotFound
|
||||
}
|
||||
if resourceType != "" && kind.String() != resourceType {
|
||||
|
|
@ -149,53 +138,53 @@ func (r *shareRepositoryWrapper) resourceType(resourceIDs string) (string, error
|
|||
return resourceType, nil
|
||||
}
|
||||
|
||||
func (r *shareRepositoryWrapper) Update(id string, entity any, _ ...string) error {
|
||||
func (r *shareRepositoryWrapper) Update(ctx context.Context, id string, entity model.Share, _ ...string) error {
|
||||
cols := []string{"description", "downloadable"}
|
||||
|
||||
// TODO Better handling of Share expiration
|
||||
if !V(entity.(*model.Share).ExpiresAt).IsZero() {
|
||||
if !V(entity.ExpiresAt).IsZero() {
|
||||
cols = append(cols, "expires_at")
|
||||
}
|
||||
return r.Persistable.Update(id, entity, cols...)
|
||||
return r.ShareRepository.Update(ctx, id, entity, cols...)
|
||||
}
|
||||
|
||||
func (r *shareRepositoryWrapper) contentsLabelFromArtist(shareID string, ids string) string {
|
||||
func (r *shareRepositoryWrapper) contentsLabelFromArtist(ctx context.Context, shareID string, ids string) string {
|
||||
idList := strings.SplitN(ids, ",", 2)
|
||||
a, err := r.ds.Artist(r.ctx).Get(idList[0])
|
||||
a, err := r.ds.Artist().Get(ctx, idList[0])
|
||||
if err != nil {
|
||||
log.Error(r.ctx, "Error retrieving artist name for share", "share", shareID, err)
|
||||
log.Error(ctx, "Error retrieving artist name for share", "share", shareID, err)
|
||||
return ""
|
||||
}
|
||||
return a.Name
|
||||
}
|
||||
|
||||
func (r *shareRepositoryWrapper) contentsLabelFromAlbums(shareID string, ids string) string {
|
||||
func (r *shareRepositoryWrapper) contentsLabelFromAlbums(ctx context.Context, shareID string, ids string) string {
|
||||
idList := strings.Split(ids, ",")
|
||||
all, err := r.ds.Album(r.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album.id": idList}})
|
||||
all, err := r.ds.Album().GetAll(ctx, model.QueryOptions{Filters: squirrel.Eq{"album.id": idList}})
|
||||
if err != nil {
|
||||
log.Error(r.ctx, "Error retrieving album names for share", "share", shareID, err)
|
||||
log.Error(ctx, "Error retrieving album names for share", "share", shareID, err)
|
||||
return ""
|
||||
}
|
||||
names := slice.Map(all, func(a model.Album) string { return a.Name })
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
func (r *shareRepositoryWrapper) contentsLabelFromPlaylist(shareID string, id string) string {
|
||||
pls, err := r.ds.Playlist(r.ctx).Get(id)
|
||||
func (r *shareRepositoryWrapper) contentsLabelFromPlaylist(ctx context.Context, shareID string, id string) string {
|
||||
pls, err := r.ds.Playlist().Get(ctx, id)
|
||||
if err != nil {
|
||||
log.Error(r.ctx, "Error retrieving album names for share", "share", shareID, err)
|
||||
log.Error(ctx, "Error retrieving album names for share", "share", shareID, err)
|
||||
return ""
|
||||
}
|
||||
return pls.Name
|
||||
}
|
||||
|
||||
func (r *shareRepositoryWrapper) contentsLabelFromMediaFiles(shareID string, ids string) string {
|
||||
func (r *shareRepositoryWrapper) contentsLabelFromMediaFiles(ctx context.Context, shareID string, ids string) string {
|
||||
idList := strings.Split(ids, ",")
|
||||
mfs, err := r.ds.MediaFile(r.ctx).GetAll(model.QueryOptions{Filters: squirrel.And{
|
||||
mfs, err := r.ds.MediaFile().GetAll(ctx, model.QueryOptions{Filters: squirrel.And{
|
||||
squirrel.Eq{"media_file.id": idList},
|
||||
squirrel.Eq{"missing": false},
|
||||
}})
|
||||
if err != nil {
|
||||
log.Error(r.ctx, "Error retrieving media files for share", "share", shareID, err)
|
||||
log.Error(ctx, "Error retrieving media files for share", "share", shareID, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,27 +14,27 @@ import (
|
|||
var _ = Describe("Share", func() {
|
||||
var ds model.DataStore
|
||||
var share Share
|
||||
var mockedRepo rest.Persistable
|
||||
var mockedRepo rest.Persistable[model.Share]
|
||||
ctx := context.Background()
|
||||
|
||||
BeforeEach(func() {
|
||||
ds = &tests.MockDataStore{}
|
||||
mockedRepo = ds.Share(ctx).(rest.Persistable)
|
||||
mockedRepo = ds.Share().(rest.Persistable[model.Share])
|
||||
share = NewShare(ds)
|
||||
})
|
||||
|
||||
Describe("NewRepository", func() {
|
||||
var repo rest.Persistable
|
||||
var repo rest.Persistable[model.Share]
|
||||
|
||||
BeforeEach(func() {
|
||||
repo = share.NewRepository(ctx).(rest.Persistable)
|
||||
_ = ds.Album(ctx).Put(&model.Album{ID: "123", Name: "Album"})
|
||||
repo = share.Repository().(rest.Persistable[model.Share])
|
||||
_ = ds.Album().Put(ctx, &model.Album{ID: "123", Name: "Album"})
|
||||
})
|
||||
|
||||
Describe("Save", func() {
|
||||
It("it sets a random ID", func() {
|
||||
entity := &model.Share{Description: "test", ResourceIDs: "123"}
|
||||
id, err := repo.Save(entity)
|
||||
id, err := repo.Save(ctx, entity)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(id).ToNot(BeEmpty())
|
||||
Expect(entity.ID).To(Equal(id))
|
||||
|
|
@ -42,63 +42,62 @@ var _ = Describe("Share", func() {
|
|||
|
||||
It("assigns the logged-in user as owner, ignoring a client-supplied UserID", func() {
|
||||
loggedInCtx := request.WithUser(context.Background(), model.User{ID: "logged-in-user"})
|
||||
repo := share.NewRepository(loggedInCtx).(rest.Persistable)
|
||||
repo := share.Repository().(rest.Persistable[model.Share])
|
||||
entity := &model.Share{Description: "test", ResourceIDs: "123", UserID: "victim-user"}
|
||||
_, err := repo.Save(entity)
|
||||
_, err := repo.Save(loggedInCtx, entity)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(entity.UserID).To(Equal("logged-in-user"))
|
||||
})
|
||||
|
||||
It("does not truncate ASCII labels shorter than 30 characters", func() {
|
||||
_ = ds.MediaFile(ctx).Put(&model.MediaFile{ID: "456", Title: "Example Media File"})
|
||||
_ = ds.MediaFile().Put(ctx, &model.MediaFile{ID: "456", Title: "Example Media File"})
|
||||
entity := &model.Share{Description: "test", ResourceIDs: "456"}
|
||||
_, err := repo.Save(entity)
|
||||
_, err := repo.Save(ctx, entity)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(entity.Contents).To(Equal("Example Media File"))
|
||||
})
|
||||
|
||||
It("truncates ASCII labels longer than 30 characters", func() {
|
||||
_ = ds.MediaFile(ctx).Put(&model.MediaFile{ID: "789", Title: "Example Media File But The Title Is Really Long For Testing Purposes"})
|
||||
_ = ds.MediaFile().Put(ctx, &model.MediaFile{ID: "789", Title: "Example Media File But The Title Is Really Long For Testing Purposes"})
|
||||
entity := &model.Share{Description: "test", ResourceIDs: "789"}
|
||||
_, err := repo.Save(entity)
|
||||
_, err := repo.Save(ctx, entity)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(entity.Contents).To(Equal("Example Media File But The ..."))
|
||||
})
|
||||
|
||||
It("does not truncate CJK labels shorter than 30 runes", func() {
|
||||
_ = ds.MediaFile(ctx).Put(&model.MediaFile{ID: "456", Title: "青春コンプレックス"})
|
||||
_ = ds.MediaFile().Put(ctx, &model.MediaFile{ID: "456", Title: "青春コンプレックス"})
|
||||
entity := &model.Share{Description: "test", ResourceIDs: "456"}
|
||||
_, err := repo.Save(entity)
|
||||
_, err := repo.Save(ctx, entity)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(entity.Contents).To(Equal("青春コンプレックス"))
|
||||
})
|
||||
|
||||
It("truncates CJK labels longer than 30 runes", func() {
|
||||
_ = ds.MediaFile(ctx).Put(&model.MediaFile{ID: "789", Title: "私の中の幻想的世界観及びその顕現を想起させたある現実での出来事に関する一考察"})
|
||||
_ = ds.MediaFile().Put(ctx, &model.MediaFile{ID: "789", Title: "私の中の幻想的世界観及びその顕現を想起させたある現実での出来事に関する一考察"})
|
||||
entity := &model.Share{Description: "test", ResourceIDs: "789"}
|
||||
_, err := repo.Save(entity)
|
||||
_, err := repo.Save(ctx, entity)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(entity.Contents).To(Equal("私の中の幻想的世界観及びその顕現を想起させたある現実で..."))
|
||||
})
|
||||
|
||||
It("fails when any of the resource IDs does not exist", func() {
|
||||
entity := &model.Share{Description: "test", ResourceIDs: "123,missing"}
|
||||
_, err := repo.Save(entity)
|
||||
_, err := repo.Save(ctx, entity)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
It("fails when the resource IDs are of mixed types", func() {
|
||||
_ = ds.MediaFile(ctx).Put(&model.MediaFile{ID: "456", Title: "Example Media File"})
|
||||
_ = ds.MediaFile().Put(ctx, &model.MediaFile{ID: "456", Title: "Example Media File"})
|
||||
entity := &model.Share{Description: "test", ResourceIDs: "123,456"}
|
||||
_, err := repo.Save(entity)
|
||||
_, err := repo.Save(ctx, entity)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Update", func() {
|
||||
It("filters out read-only fields", func() {
|
||||
entity := &model.Share{}
|
||||
err := repo.Update("id", entity)
|
||||
err := repo.Update(ctx, "id", model.Share{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mockedRepo.(*tests.MockShareRepo).Cols).To(ConsistOf("description", "downloadable"))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ func (s *Sonic) GetSonicSimilarTracks(ctx context.Context, id string, count int)
|
|||
return nil, err
|
||||
}
|
||||
|
||||
mf, err := s.ds.MediaFile(ctx).Get(id)
|
||||
mf, err := s.ds.MediaFile().Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting media file %s: %w", id, err)
|
||||
}
|
||||
|
|
@ -120,11 +120,11 @@ func (s *Sonic) FindSonicPath(ctx context.Context, startID, endID string, count
|
|||
return nil, err
|
||||
}
|
||||
|
||||
startMF, err := s.ds.MediaFile(ctx).Get(startID)
|
||||
startMF, err := s.ds.MediaFile().Get(ctx, startID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting start media file %s: %w", startID, err)
|
||||
}
|
||||
endMF, err := s.ds.MediaFile(ctx).Get(endID)
|
||||
endMF, err := s.ds.MediaFile().Get(ctx, endID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting end media file %s: %w", endID, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ func (s *deciderService) computeTranscodedStream(ctx context.Context, src *Detai
|
|||
// It checks the DB first (for user-customized values), then falls back to
|
||||
// the built-in defaults, and finally to fallbackBitrate.
|
||||
func lookupDefaultBitrate(ctx context.Context, ds model.DataStore, format string) int {
|
||||
if t, err := ds.Transcoding(ctx).FindByFormat(format); err == nil && t.DefaultBitRate > 0 {
|
||||
if t, err := ds.Transcoding().FindByFormat(ctx, format); err == nil && t.DefaultBitRate > 0 {
|
||||
return t.DefaultBitRate
|
||||
}
|
||||
for _, dt := range consts.DefaultTranscodings {
|
||||
|
|
@ -327,7 +327,7 @@ func lookupDefaultBitrate(ctx context.Context, ds model.DataStore, format string
|
|||
// It checks the DB first (for user-customized commands), then falls back to
|
||||
// the built-in default command. Returns "" if the format is unknown.
|
||||
func LookupTranscodeCommand(ctx context.Context, ds model.DataStore, format string) string {
|
||||
t, err := ds.Transcoding(ctx).FindByFormat(format)
|
||||
t, err := ds.Transcoding().FindByFormat(ctx, format)
|
||||
if err == nil && t.Command != "" {
|
||||
return t.Command
|
||||
}
|
||||
|
|
@ -447,7 +447,7 @@ func (s *deciderService) ensureProbed(ctx context.Context, mf *model.MediaFile)
|
|||
}
|
||||
mf.ProbeData = string(data)
|
||||
|
||||
if err := s.ds.MediaFile(ctx).UpdateProbeData(mf.ID, mf.ProbeData); err != nil {
|
||||
if err := s.ds.MediaFile().UpdateProbeData(ctx, mf.ID, mf.ProbeData); err != nil {
|
||||
log.Error(ctx, "Failed to persist probe data", "mediaID", mf.ID, err)
|
||||
// Don't fail the decision — we have the data in memory
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ func (ms *mediaStreamer) NewStream(ctx context.Context, mf *model.MediaFile, req
|
|||
}
|
||||
|
||||
type Stream struct {
|
||||
ctx context.Context
|
||||
ctx context.Context //nolint:containedctx // stream outlives the call that built it; Read has no ctx
|
||||
mf *model.MediaFile
|
||||
bitRate int
|
||||
format string
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ var _ = Describe("MediaStreamer", func() {
|
|||
conf.Server.CacheFolder = conf.NewDir(cacheDir)
|
||||
conf.Server.TranscodingCacheSize = "100MB"
|
||||
ds = &tests.MockDataStore{MockedTranscoding: &tests.MockTranscodingRepo{}}
|
||||
ds.MediaFile(ctx).(*tests.MockMediaFileRepo).SetData(model.MediaFiles{
|
||||
ds.MediaFile().(*tests.MockMediaFileRepo).SetData(model.MediaFiles{
|
||||
{ID: "123", Path: "tests/fixtures/test.mp3", Suffix: "mp3", BitRate: 128, Duration: 257.0},
|
||||
})
|
||||
testCache := stream.NewTranscodingCache()
|
||||
|
|
@ -51,7 +51,7 @@ var _ = Describe("MediaStreamer", func() {
|
|||
var mf *model.MediaFile
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
mf, err = ds.MediaFile(ctx).Get("123")
|
||||
mf, err = ds.MediaFile().Get(ctx, "123")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
It("returns a seekable stream if format is 'raw'", func() {
|
||||
|
|
@ -151,7 +151,7 @@ var _ = Describe("MediaStreamer", func() {
|
|||
var mf *model.MediaFile
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
mf, err = ds.MediaFile(ctx).Get("123")
|
||||
mf, err = ds.MediaFile().Get(ctx, "123")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
|
|
|
|||
56
core/user.go
56
core/user.go
|
|
@ -15,62 +15,40 @@ type PluginUnloader interface {
|
|||
|
||||
// User provides business logic for user management with plugin coordination.
|
||||
type User interface {
|
||||
NewRepository(ctx context.Context) rest.Repository
|
||||
Repository() rest.Repository[model.User]
|
||||
}
|
||||
|
||||
type userService struct {
|
||||
ds model.DataStore
|
||||
pluginManager PluginUnloader
|
||||
repo *userRepositoryWrapper
|
||||
}
|
||||
|
||||
// NewUser creates a new User service
|
||||
func NewUser(ds model.DataStore, pluginManager PluginUnloader) User {
|
||||
return &userService{
|
||||
ds: ds,
|
||||
pluginManager: pluginManager,
|
||||
repo: &userRepositoryWrapper{
|
||||
UserRepository: ds.User(),
|
||||
pluginManager: pluginManager,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewRepository returns a REST repository wrapper for user operations.
|
||||
// Repository returns a REST repository wrapper for user operations.
|
||||
// The wrapper intercepts Delete operations to coordinate plugin unloading.
|
||||
func (s *userService) NewRepository(ctx context.Context) rest.Repository {
|
||||
repo := s.ds.User(ctx)
|
||||
wrapper := &userRepositoryWrapper{
|
||||
ctx: ctx,
|
||||
UserRepository: repo,
|
||||
pluginManager: s.pluginManager,
|
||||
}
|
||||
return wrapper
|
||||
func (s *userService) Repository() rest.Repository[model.User] {
|
||||
return s.repo
|
||||
}
|
||||
|
||||
type userRepositoryWrapper struct {
|
||||
model.UserRepository
|
||||
ctx context.Context
|
||||
pluginManager PluginUnloader
|
||||
}
|
||||
|
||||
// Save implements rest.Persistable by delegating to the underlying repository.
|
||||
func (r *userRepositoryWrapper) Save(entity any) (string, error) {
|
||||
return r.UserRepository.(rest.Persistable).Save(entity)
|
||||
}
|
||||
|
||||
// Update implements rest.Persistable by delegating to the underlying repository.
|
||||
func (r *userRepositoryWrapper) Update(id string, entity any, cols ...string) error {
|
||||
return r.UserRepository.(rest.Persistable).Update(id, entity, cols...)
|
||||
}
|
||||
|
||||
// Delete implements rest.Persistable and coordinates plugin unloading.
|
||||
func (r *userRepositoryWrapper) Delete(id string) error {
|
||||
// The underlying repository Delete handles the database cleanup
|
||||
// including calling cleanupPluginUserReferences
|
||||
err := r.UserRepository.(rest.Persistable).Delete(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// After successful deletion, check if any plugins were auto-disabled
|
||||
// and need to be unloaded from memory
|
||||
r.pluginManager.UnloadDisabledPlugins(r.ctx)
|
||||
|
||||
return nil
|
||||
var _ rest.Persistable[model.User] = (*userRepositoryWrapper)(nil)
|
||||
|
||||
// Delete unloads plugins even on error: a bulk delete can fail after earlier users were removed
|
||||
// and their plugins auto-disabled.
|
||||
func (r *userRepositoryWrapper) Delete(ctx context.Context, ids ...string) error {
|
||||
err := r.UserRepository.Delete(ctx, ids...)
|
||||
r.pluginManager.UnloadDisabledPlugins(ctx)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,19 +29,19 @@ var _ = Describe("User Service", func() {
|
|||
})
|
||||
|
||||
Describe("NewRepository", func() {
|
||||
It("returns a rest.Persistable", func() {
|
||||
repo := service.NewRepository(ctx)
|
||||
_, ok := repo.(rest.Persistable)
|
||||
It("returns a rest.Persistable[model.User]", func() {
|
||||
repo := service.Repository()
|
||||
_, ok := repo.(rest.Persistable[model.User])
|
||||
Expect(ok).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Delete", func() {
|
||||
var repo rest.Persistable
|
||||
var repo rest.Persistable[model.User]
|
||||
|
||||
BeforeEach(func() {
|
||||
r := service.NewRepository(ctx)
|
||||
repo = r.(rest.Persistable)
|
||||
r := service.Repository()
|
||||
repo = r.(rest.Persistable[model.User])
|
||||
|
||||
// Add a test user
|
||||
user := &model.User{
|
||||
|
|
@ -50,37 +50,45 @@ var _ = Describe("User Service", func() {
|
|||
IsAdmin: false,
|
||||
}
|
||||
user.NewPassword = "password"
|
||||
Expect(userRepo.Put(user)).To(Succeed())
|
||||
Expect(userRepo.Put(ctx, user)).To(Succeed())
|
||||
})
|
||||
|
||||
It("deletes the user successfully", func() {
|
||||
err := repo.Delete("user-123")
|
||||
err := repo.Delete(ctx, "user-123")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Verify user is deleted
|
||||
_, err = userRepo.Get("user-123")
|
||||
_, err = userRepo.Get(ctx, "user-123")
|
||||
Expect(err).To(Equal(model.ErrNotFound))
|
||||
})
|
||||
|
||||
It("calls UnloadDisabledPlugins after successful deletion", func() {
|
||||
err := repo.Delete("user-123")
|
||||
err := repo.Delete(ctx, "user-123")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(pluginManager.unloadCalls).To(Equal(1))
|
||||
})
|
||||
|
||||
It("does not call UnloadDisabledPlugins when deletion fails", func() {
|
||||
// Try to delete non-existent user
|
||||
err := repo.Delete("non-existent")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(pluginManager.unloadCalls).To(Equal(0))
|
||||
It("still calls UnloadDisabledPlugins when deletion fails", func() {
|
||||
err := repo.Delete(ctx, "non-existent")
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
Expect(pluginManager.unloadCalls).To(Equal(1))
|
||||
})
|
||||
|
||||
It("unloads plugins when a bulk delete fails after removing earlier users", func() {
|
||||
err := repo.Delete(ctx, "user-123", "non-existent")
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
|
||||
_, err = userRepo.Get(ctx, "user-123")
|
||||
Expect(err).To(Equal(model.ErrNotFound))
|
||||
Expect(pluginManager.unloadCalls).To(Equal(1))
|
||||
})
|
||||
|
||||
It("returns error when repository fails", func() {
|
||||
userRepo.Error = errors.New("database error")
|
||||
err := repo.Delete("user-123")
|
||||
err := repo.Delete(ctx, "user-123")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("database error"))
|
||||
Expect(pluginManager.unloadCalls).To(Equal(0))
|
||||
Expect(pluginManager.unloadCalls).To(Equal(1))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
2
db/db.go
2
db/db.go
|
|
@ -184,7 +184,7 @@ func isSchemaEmpty(ctx context.Context, db *sql.DB) bool {
|
|||
}
|
||||
|
||||
type logAdapter struct {
|
||||
ctx context.Context
|
||||
ctx context.Context //nolint:containedctx // goose logger interface has no ctx
|
||||
silent bool
|
||||
}
|
||||
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -9,7 +9,7 @@ require (
|
|||
github.com/Masterminds/squirrel v1.5.4
|
||||
github.com/andybalholm/cascadia v1.3.5
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0
|
||||
github.com/deluan/rest v0.0.0-20260913134927-47b21f30cc12
|
||||
github.com/deluan/rest v1.0.1
|
||||
github.com/deluan/sanitize v0.0.0-20241120162836-fdfd8fdfaa55
|
||||
github.com/dexterlb/mpvipc v0.0.0-20260722094525-0cf47d745b36
|
||||
github.com/djherbis/atime v1.1.0
|
||||
|
|
|
|||
18
go.sum
18
go.sum
|
|
@ -31,8 +31,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3h
|
|||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/deluan/go-taglib v0.0.0-20260913142955-d55e0c9353cb h1:CGVY6RtDsqaleUFogGP03m3a/9OKi3ZMDr5nhm51Emk=
|
||||
github.com/deluan/go-taglib v0.0.0-20260913142955-d55e0c9353cb/go.mod h1:QGxQ4Z1IWyY9w56xNEFjYAaWE8uSxA/gneQ7RPcFJrY=
|
||||
github.com/deluan/rest v0.0.0-20260913134927-47b21f30cc12 h1:x4N/tx0XC9zcnipk5rpdI4qaG9JmovHW5HL97XYnO1I=
|
||||
github.com/deluan/rest v0.0.0-20260913134927-47b21f30cc12/go.mod h1:tSgDythFsl0QgS/PFWfIZqcJKnkADWneY80jaVRlqK8=
|
||||
github.com/deluan/rest v1.0.1 h1:Enuzzfd88C1/lG6Jqr2NgRreJoClaFuWcOuspeKWHZ8=
|
||||
github.com/deluan/rest v1.0.1/go.mod h1:r0yO0VgBWOb5Xb7aCPIedtcOuwmA2Oe5lGailC5pvc4=
|
||||
github.com/deluan/sanitize v0.0.0-20241120162836-fdfd8fdfaa55 h1:wSCnggTs2f2ji6nFwQmfwgINcmSMj0xF0oHnoyRSPe4=
|
||||
github.com/deluan/sanitize v0.0.0-20241120162836-fdfd8fdfaa55/go.mod h1:ZNCLJfehvEf34B7BbLKjgpsL9lyW7q938w/GY1XgV4E=
|
||||
github.com/dexterlb/mpvipc v0.0.0-20260722094525-0cf47d745b36 h1:KtPfdSST6e0vJbMzMmVqPa5mO1u8vMBlybRCW2ieXpA=
|
||||
|
|
@ -109,8 +109,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
|
|
@ -128,8 +126,6 @@ github.com/jellydator/ttlcache/v3 v3.4.1 h1:bOdXmXiycyK6E6Qjyuj5vl+/vU3SCOoDs8a8
|
|||
github.com/jellydator/ttlcache/v3 v3.4.1/go.mod h1:j7LO12PNghFg5+0v9budMAT4rDK4JY969jb9vOdOBBk=
|
||||
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
|
||||
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kardianos/service v1.3.0 h1:/LGy+xPP2TM+GLTiCZ2di7cy0Jd/qrawlTUfqKYFdTI=
|
||||
github.com/kardianos/service v1.3.0/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
|
|
@ -138,7 +134,6 @@ github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi
|
|||
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
|
|
@ -231,13 +226,8 @@ github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
|||
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sethvargo/go-retry v0.4.0 h1:9qy1OoIAxBL+gBYnkTnTnWle5wlfsXQlwRzIbbpdqPw=
|
||||
github.com/sethvargo/go-retry v0.4.0/go.mod h1:tvsjdKG6xfiCx4LSiUZ06kcv38xvdVQwv8R6/VnnVWg=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
|
||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
||||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
|
|
@ -252,7 +242,6 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
|
|||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
|
|
@ -310,7 +299,6 @@ golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
|
|||
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
|
|
@ -318,7 +306,6 @@ golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
|||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
|
|
@ -333,7 +320,6 @@ golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
|||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"iter"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
|
||||
"github.com/deluan/rest"
|
||||
"github.com/gohugoio/hashstructure"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
)
|
||||
|
||||
type Album struct {
|
||||
|
|
@ -137,24 +138,25 @@ type Albums []Album
|
|||
type AlbumCursor iter.Seq2[Album, error]
|
||||
|
||||
type AlbumRepository interface {
|
||||
CountAll(...QueryOptions) (int64, error)
|
||||
Exists(id string) (bool, error)
|
||||
Put(*Album) error
|
||||
UpdateExternalInfo(*Album) error
|
||||
Get(id string) (*Album, error)
|
||||
GetAll(...QueryOptions) (Albums, error)
|
||||
rest.Repository[Album]
|
||||
CountAll(ctx context.Context, options ...QueryOptions) (int64, error)
|
||||
Exists(ctx context.Context, id string) (bool, error)
|
||||
Put(ctx context.Context, m *Album) error
|
||||
UpdateExternalInfo(ctx context.Context, m *Album) error
|
||||
Get(ctx context.Context, id string) (*Album, error)
|
||||
GetAll(ctx context.Context, options ...QueryOptions) (Albums, error)
|
||||
// GetSoleAlbumArtistIDsInSubtrees returns the sole album artists of the albums with folders in
|
||||
// any of the given library-relative subtrees.
|
||||
GetSoleAlbumArtistIDsInSubtrees(lib Library, paths ...string) ([]string, error)
|
||||
GetCursor(...QueryOptions) (AlbumCursor, error)
|
||||
GetYears(libraryIDs ...int) ([]int, error)
|
||||
GetSoleAlbumArtistIDsInSubtrees(ctx context.Context, lib Library, paths ...string) ([]string, error)
|
||||
GetCursor(ctx context.Context, options ...QueryOptions) (AlbumCursor, error)
|
||||
GetYears(ctx context.Context, libraryIDs ...int) ([]int, error)
|
||||
|
||||
// The following methods are used exclusively by the scanner:
|
||||
Touch(ids ...string) error
|
||||
TouchByMissingFolder() (int64, error)
|
||||
GetTouchedAlbums(libID int) (AlbumCursor, error)
|
||||
RefreshPlayCounts() (int64, error)
|
||||
CopyAttributes(fromID, toID string, columns ...string) error
|
||||
Touch(ctx context.Context, ids ...string) error
|
||||
TouchByMissingFolder(ctx context.Context) (int64, error)
|
||||
GetTouchedAlbums(ctx context.Context, libID int) (AlbumCursor, error)
|
||||
RefreshPlayCounts(ctx context.Context) (int64, error)
|
||||
CopyAttributes(ctx context.Context, fromID, toID string, columns ...string) error
|
||||
|
||||
AnnotatedRepository
|
||||
SearchableRepository[Albums]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package model
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Annotations struct {
|
||||
PlayCount int64 `structs:"play_count" json:"playCount,omitempty"`
|
||||
|
|
@ -13,8 +16,8 @@ type Annotations struct {
|
|||
}
|
||||
|
||||
type AnnotatedRepository interface {
|
||||
IncPlayCount(itemID string, ts time.Time) error
|
||||
SetStar(starred bool, itemIDs ...string) error
|
||||
SetRating(rating int, itemID string) error
|
||||
ReassignAnnotation(prevID string, newID string) error
|
||||
IncPlayCount(ctx context.Context, itemID string, ts time.Time) error
|
||||
SetStar(ctx context.Context, starred bool, itemIDs ...string) error
|
||||
SetRating(ctx context.Context, rating int, itemID string) error
|
||||
ReassignAnnotation(ctx context.Context, prevID string, newID string) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"iter"
|
||||
"maps"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/deluan/rest"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
)
|
||||
|
||||
|
|
@ -84,18 +86,19 @@ type ArtistIndexes []ArtistIndex
|
|||
type ArtistCursor iter.Seq2[Artist, error]
|
||||
|
||||
type ArtistRepository interface {
|
||||
CountAll(options ...QueryOptions) (int64, error)
|
||||
Exists(id string) (bool, error)
|
||||
Put(m *Artist, colsToUpdate ...string) error
|
||||
UpdateExternalInfo(a *Artist) error
|
||||
Get(id string) (*Artist, error)
|
||||
GetAll(options ...QueryOptions) (Artists, error)
|
||||
GetCursor(options ...QueryOptions) (ArtistCursor, error)
|
||||
GetIndex(includeMissing bool, libraryIds []int, roles ...Role) (ArtistIndexes, error)
|
||||
rest.Repository[Artist]
|
||||
CountAll(ctx context.Context, options ...QueryOptions) (int64, error)
|
||||
Exists(ctx context.Context, id string) (bool, error)
|
||||
Put(ctx context.Context, m *Artist, colsToUpdate ...string) error
|
||||
UpdateExternalInfo(ctx context.Context, a *Artist) error
|
||||
Get(ctx context.Context, id string) (*Artist, error)
|
||||
GetAll(ctx context.Context, options ...QueryOptions) (Artists, error)
|
||||
GetCursor(ctx context.Context, options ...QueryOptions) (ArtistCursor, error)
|
||||
GetIndex(ctx context.Context, includeMissing bool, libraryIds []int, roles ...Role) (ArtistIndexes, error)
|
||||
|
||||
// The following methods are used exclusively by the scanner:
|
||||
RefreshPlayCounts() (int64, error)
|
||||
RefreshStats(allArtists bool) (int64, error)
|
||||
RefreshPlayCounts(ctx context.Context) (int64, error)
|
||||
RefreshStats(ctx context.Context, allArtists bool) (int64, error)
|
||||
|
||||
AnnotatedRepository
|
||||
SearchableRepository[Artists]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package model
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Artwork is one unique image, identified by the XXH3-64 hash of its bytes.
|
||||
type Artwork struct {
|
||||
|
|
@ -116,60 +119,60 @@ const (
|
|||
|
||||
// Delete* takes the rows to remove; Purge* finds them itself and reports how many went.
|
||||
type ArtworkRepository interface {
|
||||
GetImage(hash string) (*Artwork, error)
|
||||
PutImage(a *Artwork) error
|
||||
GetImage(ctx context.Context, hash string) (*Artwork, error)
|
||||
PutImage(ctx context.Context, a *Artwork) error
|
||||
// PurgeOrphans deletes rows referenced by no item_artwork row and older than cutoff.
|
||||
PurgeOrphans(createdBefore time.Time) (int64, error)
|
||||
GetItemArtwork(kind Kind, id, imageType string) (*ItemArtwork, error)
|
||||
PutItemArtwork(ia *ItemArtwork) error
|
||||
PurgeOrphans(ctx context.Context, createdBefore time.Time) (int64, error)
|
||||
GetItemArtwork(ctx context.Context, kind Kind, id, imageType string) (*ItemArtwork, error)
|
||||
PutItemArtwork(ctx context.Context, ia *ItemArtwork) error
|
||||
// PutLastFailure records the trace of the attempt that exhausted the retry budget.
|
||||
PutLastFailure(kind Kind, id, imageType, trace string) error
|
||||
DeleteForItems(kind Kind, ids []string) error
|
||||
PutLastFailure(ctx context.Context, kind Kind, id, imageType, trace string) error
|
||||
DeleteForItems(ctx context.Context, kind Kind, ids []string) error
|
||||
// GetInfoForItems hydrates a page in one batched query.
|
||||
GetInfoForItems(kind Kind, ids []string) (map[string]ItemArtworkInfo, error)
|
||||
GetInfoForItems(ctx context.Context, kind Kind, ids []string) (map[string]ItemArtworkInfo, error)
|
||||
// GetMimeByHash returns hash -> current mime for every stored artwork.
|
||||
GetMimeByHash() (map[string]string, error)
|
||||
GetMimeByHash(ctx context.Context) (map[string]string, error)
|
||||
// PurgeDanglingItems removes state rows whose entity no longer exists.
|
||||
PurgeDanglingItems() (int64, error)
|
||||
PurgeDanglingItems(ctx context.Context) (int64, error)
|
||||
}
|
||||
|
||||
type ArtworkQueueRepository interface {
|
||||
// Get returns the pending row for an item, or ErrNotFound when it is not queued.
|
||||
Get(kind Kind, id, imageType string) (*ArtworkQueueItem, error)
|
||||
Get(ctx context.Context, kind Kind, id, imageType string) (*ArtworkQueueItem, error)
|
||||
// Enqueue upserts; an existing row keeps the higher priority and has its retry_at reset.
|
||||
Enqueue(items ...ArtworkQueueItem) error
|
||||
Enqueue(ctx context.Context, items ...ArtworkQueueItem) error
|
||||
// EnqueuePreservingBackoff upserts like Enqueue but preserves an existing row's retry_at, so a
|
||||
// request-triggered read-through never resets a failed resolution's backoff.
|
||||
EnqueuePreservingBackoff(items ...ArtworkQueueItem) error
|
||||
EnqueuePreservingBackoff(ctx context.Context, items ...ArtworkQueueItem) error
|
||||
// EnqueueAllMissing inserts queue rows for all entities with no item_artwork row, at the given priority.
|
||||
EnqueueAllMissing(kind Kind, priority int) (int64, error)
|
||||
EnqueueAllMissing(ctx context.Context, kind Kind, priority int) (int64, error)
|
||||
// EnqueueIfMissing inserts only for items with no item_artwork row yet.
|
||||
EnqueueIfMissing(items ...ArtworkQueueItem) error
|
||||
EnqueueIfMissing(ctx context.Context, items ...ArtworkQueueItem) error
|
||||
// CountBySource reports how many items of a kind currently resolve from the given sources.
|
||||
// An empty sources slice means every source; "" matches absent state, and the pseudo-source
|
||||
// ArtworkSourceFailed matches the absent states that gave up.
|
||||
CountBySource(kind Kind, sources []string) (int64, error)
|
||||
CountBySource(ctx context.Context, kind Kind, sources []string) (int64, error)
|
||||
// SourcesInUse lists the distinct sources items of a kind currently resolve from, "" included.
|
||||
SourcesInUse(kind Kind) ([]string, error)
|
||||
SourcesInUse(ctx context.Context, kind Kind) ([]string, error)
|
||||
// EnqueueBySource inserts queue rows for items of a kind whose current source matches.
|
||||
// It does not clear existing artwork state: the current image stays until it is replaced.
|
||||
EnqueueBySource(kind Kind, sources []string, priority int) (int64, error)
|
||||
EnqueueBySource(ctx context.Context, kind Kind, sources []string, priority int) (int64, error)
|
||||
// DequeueBatch returns up to n items with retry_at <= now, priority desc, enqueued_at asc.
|
||||
// Restricted to the given kinds when any are passed, so one kind cannot block another's drain.
|
||||
DequeueBatch(n int, kinds ...string) ([]ArtworkQueueItem, error)
|
||||
DequeueBatch(ctx context.Context, n int, kinds ...string) ([]ArtworkQueueItem, error)
|
||||
// MarkFailedIfUnchanged applies the failure backoff only while retry_at still matches
|
||||
// seenRetryAt, so a concurrent re-enqueue keeps its fresh eligibility.
|
||||
MarkFailedIfUnchanged(kind, id, imageType string, seenRetryAt, retryAt time.Time, trace string) error
|
||||
MarkFailedIfUnchanged(ctx context.Context, kind, id, imageType string, seenRetryAt, retryAt time.Time, trace string) error
|
||||
// DeleteIfUnchanged deletes only while retry_at still matches, sparing a concurrent re-enqueue.
|
||||
DeleteIfUnchanged(kind, id, imageType string, retryAt time.Time) error
|
||||
Count() (int64, error)
|
||||
DeleteIfUnchanged(ctx context.Context, kind, id, imageType string, retryAt time.Time) error
|
||||
Count(ctx context.Context) (int64, error)
|
||||
// CountQueued reports the pending rows matching the kinds and priorities, grouped by both;
|
||||
// an empty filter means every one.
|
||||
CountQueued(kinds []Kind, priorities []int) ([]ArtworkQueueStat, error)
|
||||
CountQueued(ctx context.Context, kinds []Kind, priorities []int) ([]ArtworkQueueStat, error)
|
||||
// PurgeDangling removes queue rows whose entity no longer exists.
|
||||
PurgeDangling() (int64, error)
|
||||
PurgeDangling(ctx context.Context) (int64, error)
|
||||
// PurgeQueued removes pending rows matching the kinds and priorities; an empty filter means every one.
|
||||
PurgeQueued(kinds []Kind, priorities []int) (int64, error)
|
||||
PurgeQueued(ctx context.Context, kinds []Kind, priorities []int) (int64, error)
|
||||
}
|
||||
|
||||
type ArtworkQueueStat struct {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
package model
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Bookmarkable struct {
|
||||
BookmarkPosition int64 `structs:"-" json:"bookmarkPosition"`
|
||||
}
|
||||
|
||||
type BookmarkableRepository interface {
|
||||
AddBookmark(id, comment string, position int64) error
|
||||
DeleteBookmark(id string) error
|
||||
GetBookmarks() (Bookmarks, error)
|
||||
AddBookmark(ctx context.Context, id, comment string, position int64) error
|
||||
DeleteBookmark(ctx context.Context, id string) error
|
||||
GetBookmarks(ctx context.Context) (Bookmarks, error)
|
||||
}
|
||||
|
||||
type Bookmark struct {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/deluan/rest"
|
||||
)
|
||||
|
||||
type QueryOptions struct {
|
||||
|
|
@ -16,34 +15,28 @@ type QueryOptions struct {
|
|||
Seed string // for random sorting
|
||||
}
|
||||
|
||||
type ResourceRepository interface {
|
||||
rest.Repository
|
||||
}
|
||||
|
||||
type DataStore interface {
|
||||
Library(ctx context.Context) LibraryRepository
|
||||
Folder(ctx context.Context) FolderRepository
|
||||
Album(ctx context.Context) AlbumRepository
|
||||
Artist(ctx context.Context) ArtistRepository
|
||||
MediaFile(ctx context.Context) MediaFileRepository
|
||||
Genre(ctx context.Context) GenreRepository
|
||||
Tag(ctx context.Context) TagRepository
|
||||
Playlist(ctx context.Context) PlaylistRepository
|
||||
PlayQueue(ctx context.Context) PlayQueueRepository
|
||||
Transcoding(ctx context.Context) TranscodingRepository
|
||||
Player(ctx context.Context) PlayerRepository
|
||||
Radio(ctx context.Context) RadioRepository
|
||||
Share(ctx context.Context) ShareRepository
|
||||
Property(ctx context.Context) PropertyRepository
|
||||
User(ctx context.Context) UserRepository
|
||||
UserProps(ctx context.Context) UserPropsRepository
|
||||
ScrobbleBuffer(ctx context.Context) ScrobbleBufferRepository
|
||||
Scrobble(ctx context.Context) ScrobbleRepository
|
||||
Plugin(ctx context.Context) PluginRepository
|
||||
Artwork(ctx context.Context) ArtworkRepository
|
||||
ArtworkQueue(ctx context.Context) ArtworkQueueRepository
|
||||
|
||||
Resource(ctx context.Context, model any) ResourceRepository
|
||||
Library() LibraryRepository
|
||||
Folder() FolderRepository
|
||||
Album() AlbumRepository
|
||||
Artist() ArtistRepository
|
||||
MediaFile() MediaFileRepository
|
||||
Genre() GenreRepository
|
||||
Tag() TagRepository
|
||||
Playlist() PlaylistRepository
|
||||
PlayQueue() PlayQueueRepository
|
||||
Transcoding() TranscodingRepository
|
||||
Player() PlayerRepository
|
||||
Radio() RadioRepository
|
||||
Share() ShareRepository
|
||||
Property() PropertyRepository
|
||||
User() UserRepository
|
||||
UserProps() UserPropsRepository
|
||||
ScrobbleBuffer() ScrobbleBufferRepository
|
||||
Scrobble() ScrobbleRepository
|
||||
Plugin() PluginRepository
|
||||
Artwork() ArtworkRepository
|
||||
ArtworkQueue() ArtworkQueueRepository
|
||||
|
||||
WithTx(block func(tx DataStore) error, scope ...string) error
|
||||
WithTxImmediate(block func(tx DataStore) error, scope ...string) error
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
"os"
|
||||
|
|
@ -83,19 +84,19 @@ type FolderUpdateInfo struct {
|
|||
}
|
||||
|
||||
type FolderRepository interface {
|
||||
Get(id string) (*Folder, error)
|
||||
GetByPath(lib Library, path string) (*Folder, error)
|
||||
GetAll(...QueryOptions) ([]Folder, error)
|
||||
CountAll(...QueryOptions) (int64, error)
|
||||
GetFolderUpdateInfo(lib Library, targetPaths ...string) (map[string]FolderUpdateInfo, error)
|
||||
Get(ctx context.Context, id string) (*Folder, error)
|
||||
GetByPath(ctx context.Context, lib Library, path string) (*Folder, error)
|
||||
GetAll(ctx context.Context, options ...QueryOptions) ([]Folder, error)
|
||||
CountAll(ctx context.Context, options ...QueryOptions) (int64, error)
|
||||
GetFolderUpdateInfo(ctx context.Context, lib Library, targetPaths ...string) (map[string]FolderUpdateInfo, error)
|
||||
// HasAudioOutsideFolders reports whether any folder in parent's subtree
|
||||
// (including parent itself) contains audio files and is not one of the
|
||||
// given folder IDs.
|
||||
HasAudioOutsideFolders(parent Folder, excludeFolderIDs []string) (bool, error)
|
||||
Put(*Folder) error
|
||||
MarkMissing(missing bool, ids ...string) error
|
||||
GetTouchedWithPlaylists() (FolderCursor, error)
|
||||
HasAudioOutsideFolders(ctx context.Context, parent Folder, excludeFolderIDs []string) (bool, error)
|
||||
Put(ctx context.Context, f *Folder) error
|
||||
MarkMissing(ctx context.Context, missing bool, ids ...string) error
|
||||
GetTouchedWithPlaylists(ctx context.Context) (FolderCursor, error)
|
||||
// GetAllWithPlaylists returns all non-missing folders with playlists, ignoring
|
||||
// the scan-timestamp gate used by GetTouchedWithPlaylists.
|
||||
GetAllWithPlaylists() (FolderCursor, error)
|
||||
GetAllWithPlaylists(ctx context.Context) (FolderCursor, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/deluan/rest"
|
||||
)
|
||||
|
||||
type Genre struct {
|
||||
ID string `structs:"id" json:"id,omitempty" toml:"id,omitempty" yaml:"id,omitempty"`
|
||||
Name string `structs:"name" json:"name"`
|
||||
|
|
@ -10,6 +16,7 @@ type Genre struct {
|
|||
type Genres []Genre
|
||||
|
||||
type GenreRepository interface {
|
||||
GetAll(...QueryOptions) (Genres, error)
|
||||
Get(id string) (*Genre, error)
|
||||
rest.Repository[Genre]
|
||||
GetAll(ctx context.Context, options ...QueryOptions) (Genres, error)
|
||||
Get(ctx context.Context, id string) (*Genre, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ func getEntity(ctx context.Context, ds DataStore, id string) (any, Kind, error)
|
|||
kind Kind
|
||||
get func() (any, error)
|
||||
}{
|
||||
{KindArtistArtwork, func() (any, error) { return ds.Artist(ctx).Get(id) }},
|
||||
{KindAlbumArtwork, func() (any, error) { return ds.Album(ctx).Get(id) }},
|
||||
{KindPlaylistArtwork, func() (any, error) { return ds.Playlist(ctx).Get(id) }},
|
||||
{KindMediaFileArtwork, func() (any, error) { return ds.MediaFile(ctx).Get(id) }},
|
||||
{KindRadioArtwork, func() (any, error) { return ds.Radio(ctx).Get(id) }},
|
||||
{KindArtistArtwork, func() (any, error) { return ds.Artist().Get(ctx, id) }},
|
||||
{KindAlbumArtwork, func() (any, error) { return ds.Album().Get(ctx, id) }},
|
||||
{KindPlaylistArtwork, func() (any, error) { return ds.Playlist().Get(ctx, id) }},
|
||||
{KindMediaFileArtwork, func() (any, error) { return ds.MediaFile().Get(ctx, id) }},
|
||||
{KindRadioArtwork, func() (any, error) { return ds.Radio().Get(ctx, id) }},
|
||||
}
|
||||
for _, g := range getters {
|
||||
entity, err := g.get()
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ var _ = Describe("GetEntityByID", func() {
|
|||
})
|
||||
|
||||
It("returns the entity matching the id", func() {
|
||||
ds.Album(ctx).(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "a1", Name: "One"}})
|
||||
ds.Album().(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "a1", Name: "One"}})
|
||||
entity, err := model.GetEntityByID(ctx, ds, "a1")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(entity).To(BeAssignableToTypeOf(&model.Album{}))
|
||||
|
|
@ -32,7 +32,7 @@ var _ = Describe("GetEntityByID", func() {
|
|||
})
|
||||
|
||||
It("propagates unexpected repository errors instead of reporting not-found", func() {
|
||||
ds.Album(ctx).(*tests.MockAlbumRepo).SetError(true)
|
||||
ds.Album().(*tests.MockAlbumRepo).SetError(true)
|
||||
_, err := model.GetEntityByID(ctx, ds, "a1")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).ToNot(MatchError(model.ErrNotFound))
|
||||
|
|
@ -49,7 +49,7 @@ var _ = Describe("GetEntityKindByID", func() {
|
|||
})
|
||||
|
||||
It("returns the artwork kind for the matching id", func() {
|
||||
ds.Album(ctx).(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "a1"}})
|
||||
ds.Album().(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "a1"}})
|
||||
kind, err := model.GetEntityKindByID(ctx, ds, "a1")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(kind).To(Equal(model.KindAlbumArtwork))
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/deluan/rest"
|
||||
"github.com/navidrome/navidrome/utils/slice"
|
||||
)
|
||||
|
||||
|
|
@ -39,23 +41,24 @@ func (l Libraries) IDs() []int {
|
|||
}
|
||||
|
||||
type LibraryRepository interface {
|
||||
Get(id int) (*Library, error)
|
||||
rest.Repository[Library]
|
||||
Get(ctx context.Context, id int) (*Library, error)
|
||||
// GetPath returns the path of the library with the given ID.
|
||||
// Its implementation must be optimized to avoid unnecessary queries.
|
||||
GetPath(id int) (string, error)
|
||||
GetAll(...QueryOptions) (Libraries, error)
|
||||
CountAll(...QueryOptions) (int64, error)
|
||||
Put(l *Library, colsToUpdate ...string) error
|
||||
Delete(id int) error
|
||||
StoreMusicFolder() error
|
||||
AddArtist(id int, artistID string) error
|
||||
GetPath(ctx context.Context, id int) (string, error)
|
||||
GetAll(ctx context.Context, options ...QueryOptions) (Libraries, error)
|
||||
CountAll(ctx context.Context, options ...QueryOptions) (int64, error)
|
||||
Put(ctx context.Context, l *Library, colsToUpdate ...string) error
|
||||
Delete(ctx context.Context, id int) error
|
||||
StoreMusicFolder(ctx context.Context) error
|
||||
AddArtist(ctx context.Context, id int, artistID string) error
|
||||
|
||||
// User-library association methods
|
||||
GetUsersWithLibraryAccess(libraryID int) (Users, error)
|
||||
GetUsersWithLibraryAccess(ctx context.Context, libraryID int) (Users, error)
|
||||
|
||||
// TODO These methods should be moved to a core service
|
||||
ScanBegin(id int, fullScan bool) error
|
||||
ScanEnd(id int) error
|
||||
ScanInProgress() (bool, error)
|
||||
RefreshStats(id int) error
|
||||
ScanBegin(ctx context.Context, id int, fullScan bool) error
|
||||
ScanEnd(ctx context.Context, id int) error
|
||||
ScanInProgress(ctx context.Context) (bool, error)
|
||||
RefreshStats(ctx context.Context, id int) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package model
|
|||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
|
@ -11,6 +12,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/deluan/rest"
|
||||
"github.com/gohugoio/hashstructure"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
|
|
@ -537,42 +539,43 @@ func (mfs MediaFiles) ToM3U8(title string, absolutePaths bool) string {
|
|||
type MediaFileCursor iter.Seq2[MediaFile, error]
|
||||
|
||||
type MediaFileRepository interface {
|
||||
CountAll(options ...QueryOptions) (int64, error)
|
||||
CountBySuffix(options ...QueryOptions) (map[string]int64, error)
|
||||
Exists(id string) (bool, error)
|
||||
Put(m *MediaFile) error
|
||||
UpdateProbeData(id string, data string) error
|
||||
Get(id string) (*MediaFile, error)
|
||||
GetWithParticipants(id string) (*MediaFile, error)
|
||||
GetAll(options ...QueryOptions) (MediaFiles, error)
|
||||
rest.Repository[MediaFile]
|
||||
CountAll(ctx context.Context, options ...QueryOptions) (int64, error)
|
||||
CountBySuffix(ctx context.Context, options ...QueryOptions) (map[string]int64, error)
|
||||
Exists(ctx context.Context, id string) (bool, error)
|
||||
Put(ctx context.Context, m *MediaFile) error
|
||||
UpdateProbeData(ctx context.Context, id string, data string) error
|
||||
Get(ctx context.Context, id string) (*MediaFile, error)
|
||||
GetWithParticipants(ctx context.Context, id string) (*MediaFile, error)
|
||||
GetAll(ctx context.Context, options ...QueryOptions) (MediaFiles, error)
|
||||
// GetRandom returns up to options.Max media files in random order, applying the same
|
||||
// filters as GetAll. Sort/Order are ignored.
|
||||
GetRandom(options ...QueryOptions) (MediaFiles, error)
|
||||
GetAllByTags(tag TagName, values []string, options ...QueryOptions) (MediaFiles, error)
|
||||
GetRandom(ctx context.Context, options ...QueryOptions) (MediaFiles, error)
|
||||
GetAllByTags(ctx context.Context, tag TagName, values []string, options ...QueryOptions) (MediaFiles, error)
|
||||
// MatchesCriteria reports whether the media file matches the criteria's rule
|
||||
// expression, using the logged user's annotations. Limit and offset are ignored.
|
||||
MatchesCriteria(id string, c criteria.Criteria) (bool, error)
|
||||
GetCursor(options ...QueryOptions) (MediaFileCursor, error)
|
||||
MatchesCriteria(ctx context.Context, id string, c criteria.Criteria) (bool, error)
|
||||
GetCursor(ctx context.Context, options ...QueryOptions) (MediaFileCursor, error)
|
||||
// GetAlbumIDsByFolder returns the distinct IDs of albums with non-missing tracks in the given
|
||||
// folders or their direct children.
|
||||
GetAlbumIDsByFolder(lib Library, folderIDs ...string) ([]string, error)
|
||||
GetAlbumIDsByFolder(ctx context.Context, lib Library, folderIDs ...string) ([]string, error)
|
||||
// GetCursorWithArtwork streams like GetCursor, hydrated, so callers that render images don't
|
||||
// pay the scanner's per-row cost; it uses the same id pre-pass as the other cursors.
|
||||
GetCursorWithArtwork(options ...QueryOptions) (MediaFileCursor, error)
|
||||
Delete(id string) error
|
||||
DeleteMissing(ids []string) error
|
||||
DeleteAllMissing() (int64, error)
|
||||
FindByPaths(paths []string) (MediaFiles, error)
|
||||
GetCursorWithArtwork(ctx context.Context, options ...QueryOptions) (MediaFileCursor, error)
|
||||
Delete(ctx context.Context, id string) error
|
||||
DeleteMissing(ctx context.Context, ids []string) error
|
||||
DeleteAllMissing(ctx context.Context) (int64, error)
|
||||
FindByPaths(ctx context.Context, paths []string) (MediaFiles, error)
|
||||
// ReassignReferences moves annotations, bookmarks and playlist entries from prevID to newID,
|
||||
// keeping newID's own row wherever a user has both.
|
||||
ReassignReferences(prevID, newID string) error
|
||||
ReassignReferences(ctx context.Context, prevID, newID string) error
|
||||
|
||||
// The following methods are used exclusively by the scanner:
|
||||
MarkMissing(bool, ...*MediaFile) error
|
||||
MarkMissingByFolder(missing bool, folderIDs ...string) error
|
||||
GetMissingAndMatching(libId int) (MediaFileCursor, error)
|
||||
FindRecentFilesByMBZTrackID(missing MediaFile, since time.Time) (MediaFiles, error)
|
||||
FindRecentFilesByProperties(missing MediaFile, since time.Time) (MediaFiles, error)
|
||||
MarkMissing(ctx context.Context, missing bool, mfs ...*MediaFile) error
|
||||
MarkMissingByFolder(ctx context.Context, missing bool, folderIDs ...string) error
|
||||
GetMissingAndMatching(ctx context.Context, libId int) (MediaFileCursor, error)
|
||||
FindRecentFilesByMBZTrackID(ctx context.Context, missing MediaFile, since time.Time) (MediaFiles, error)
|
||||
FindRecentFilesByProperties(ctx context.Context, missing MediaFile, since time.Time) (MediaFiles, error)
|
||||
|
||||
AnnotatedRepository
|
||||
BookmarkableRepository
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/deluan/rest"
|
||||
)
|
||||
|
||||
type Player struct {
|
||||
|
|
@ -23,9 +26,11 @@ type Player struct {
|
|||
type Players []Player
|
||||
|
||||
type PlayerRepository interface {
|
||||
Get(id string) (*Player, error)
|
||||
FindMatch(userId, client, userAgent string) (*Player, error)
|
||||
Put(p *Player) error
|
||||
CountAll(...QueryOptions) (int64, error)
|
||||
CountByClient(...QueryOptions) (map[string]int64, error)
|
||||
rest.Repository[Player]
|
||||
rest.Persistable[Player]
|
||||
Get(ctx context.Context, id string) (*Player, error)
|
||||
FindMatch(ctx context.Context, userId, client, userAgent string) (*Player, error)
|
||||
Put(ctx context.Context, p *Player) error
|
||||
CountAll(ctx context.Context, options ...QueryOptions) (int64, error)
|
||||
CountByClient(ctx context.Context, options ...QueryOptions) (map[string]int64, error)
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue