refactor: use stdlib slices/maps and utils helpers in artwork code

Mechanical cleanups, no behavior change:

- 15 copies of the same id-extraction loop collapse to slice.Map (5 repo
  mocks, the scanner's track sweep, 4 wantIDs assertions) and slice.ToMap
  (6 index-by-id loops in the hydration specs).
- disc.go built a map[string]bool purely to dedup folder ids and then
  walked it back into a slice; slice.Unique says that directly.
- folders_artist.go's image filter is slice.Filter over model.IsImageFile.
- mock_artwork_repo deleted from a map while ranging it; maps.DeleteFunc
  states the intent.
- sort.Slice -> slices.SortFunc + cmp.Or; math.Min/Max -> builtin min/max;
  make+copy -> bytes.Clone; strings.Split -> SplitSeq on a per-request
  path; three-clause pixel loops -> for range.
- Reuse utils.BaseName where a stem was recomputed by hand. Not at
  playlist_cover.go:27: that path is a full OS path and utils.BaseName
  uses path.Base, which does not split backslashes.
- Drop a dead nil-guard in agents.go: getAgent returns a bare nil
  interface, and a type assertion on nil already yields ok == false.

cmp.Or was rejected for the gate fallback (func types are not comparable,
does not compile) and for ItemArtwork.AttemptedAt (cmp.Or compares
time.Time with ==, which includes loc; IsZero does not).
This commit is contained in:
Deluan 2026-07-27 21:56:44 -04:00
parent 13659f2c30
commit 8c65931ba7
31 changed files with 93 additions and 175 deletions

View File

@ -141,11 +141,7 @@ type AlbumImageAgent struct {
func (a *Agents) ArtistImageAgents() []ArtistImageAgent {
var result []ArtistImageAgent
for _, ea := range a.getEnabledAgentNames() {
ag := a.getAgent(ea)
if ag == nil {
continue
}
if retriever, ok := ag.(ArtistImageRetriever); ok {
if retriever, ok := a.getAgent(ea).(ArtistImageRetriever); ok {
result = append(result, ArtistImageAgent{Name: ea.name, Retriever: retriever})
}
}
@ -157,11 +153,7 @@ func (a *Agents) ArtistImageAgents() []ArtistImageAgent {
func (a *Agents) AlbumImageAgents() []AlbumImageAgent {
var result []AlbumImageAgent
for _, ea := range a.getEnabledAgentNames() {
ag := a.getAgent(ea)
if ag == nil {
continue
}
if retriever, ok := ag.(AlbumImageRetriever); ok {
if retriever, ok := a.getAgent(ea).(AlbumImageRetriever); ok {
result = append(result, AlbumImageAgent{Name: ea.name, Retriever: retriever})
}
}

View File

@ -57,13 +57,13 @@ func Encode(img image.Image, xComp, yComp int) (string, error) {
lin := srgbToLinearTable()
factors := make([][3]float64, xComp*yComp)
for y := 0; y < h; y++ {
for y := range h {
row := rgba.Pix[y*rgba.Stride:]
for x := 0; x < w; x++ {
for x := range w {
p := x * 4
lr, lg, lb := lin[row[p]], lin[row[p+1]], lin[row[p+2]]
for j := 0; j < yComp; j++ {
for i := 0; i < xComp; i++ {
for j := range yComp {
for i := range xComp {
basis := cosX[i][x] * cosY[j][y]
f := &factors[j*xComp+i]
f[0] += basis * lr
@ -94,7 +94,7 @@ func Encode(img image.Image, xComp, yComp int) (string, error) {
for _, f := range ac {
actualMax = max(actualMax, math.Abs(f[0]), math.Abs(f[1]), math.Abs(f[2]))
}
quantMax := int(math.Max(0, math.Min(82, math.Floor(actualMax*166-0.5))))
quantMax := int(max(0, min(82, math.Floor(actualMax*166-0.5))))
maxVal = float64(quantMax+1) / 166
sb.WriteString(Encode83(quantMax, 1))
} else {
@ -141,7 +141,7 @@ func downscale(img image.Image) image.Image {
}
func quantAC(v, maxVal float64) int {
return int(math.Max(0, math.Min(18, math.Floor(signPow(v/maxVal, 0.5)*9+9.5))))
return int(max(0, min(18, math.Floor(signPow(v/maxVal, 0.5)*9+9.5))))
}
func signPow(v, exp float64) float64 {
@ -157,7 +157,7 @@ func srgbToLinear(v int) float64 {
}
func linearToSRGB(v float64) int {
v = math.Min(math.Max(0, v), 1)
v = min(max(0, v), 1)
if v <= 0.0031308 {
return int(v*12.92*255 + 0.5)
}

View File

@ -22,8 +22,8 @@ func decode83(s string) int {
func solidImage(w, h int, c color.NRGBA) image.Image {
img := image.NewNRGBA(image.Rect(0, 0, w, h))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
for y := range h {
for x := range w {
img.SetNRGBA(x, y, c)
}
}
@ -32,8 +32,8 @@ func solidImage(w, h int, c color.NRGBA) image.Image {
func gradientImage(w, h int) image.Image {
img := image.NewNRGBA(image.Rect(0, 0, w, h))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
for y := range h {
for x := range w {
img.SetNRGBA(x, y, color.NRGBA{R: uint8(255 * x / w), G: uint8(255 * y / h), B: 128, A: 255})
}
}

View File

@ -15,6 +15,7 @@ import (
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/slice"
)
// discArtworkReader resolves disc-level artwork from a library's folder images
@ -76,21 +77,17 @@ func newDiscArtworkReader(ctx context.Context, ds model.DataStore, artID model.A
// Build disc folder set and find first track. mf.Path is already library-relative.
var firstTrackRel string
allFolderIDs := make(map[string]bool)
for _, mf := range mfs {
allFolderIDs[mf.FolderID] = true
if firstTrackRel == "" {
if mf.Path != "" {
firstTrackRel = filepath.ToSlash(mf.Path)
break
}
}
folderIDs := slice.Unique(slice.Map(mfs, func(mf model.MediaFile) string { return mf.FolderID }))
// Resolve folder IDs to library-relative paths
discFoldersRel := make(map[string]bool)
if len(allFolderIDs) > 0 {
folderIDs := make([]string, 0, len(allFolderIDs))
for id := range allFolderIDs {
folderIDs = append(folderIDs, id)
}
if len(folderIDs) > 0 {
folders, err := ds.Folder(ctx).GetAll(model.QueryOptions{
Filters: squirrel.Eq{"folder.id": folderIDs},
})
@ -144,8 +141,7 @@ func (d *discArtworkReader) fromDiscArtPriority(ctx context.Context, ffmpeg ffmp
func (d *discArtworkReader) fromDiscSubtitle(ctx context.Context, subtitle string) sourceFunc {
return func() (io.ReadCloser, string, error) {
for _, file := range d.imgFiles {
name := path.Base(file)
stem := strings.TrimSuffix(name, path.Ext(name))
stem := utils.BaseName(file)
if !strings.EqualFold(stem, subtitle) {
continue
}

View File

@ -10,6 +10,7 @@ import (
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/slice"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@ -182,10 +183,7 @@ var _ = Describe("Playlist artwork resolution", func() {
mfs, err := rds.MediaFile(rctx).GetAll(model.QueryOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(mfs).To(HaveLen(4))
ids := make([]string, 0, len(mfs))
for _, mf := range mfs {
ids = append(ids, mf.ID)
}
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)

View File

@ -9,6 +9,7 @@ import (
"image/color"
"image/png"
"io"
"maps"
"os"
"path/filepath"
"strings"
@ -295,9 +296,7 @@ func pngBytes(label string) []byte {
func trackFile(num int, title string, extra ...map[string]any) *fstest.MapFile {
tags := storagetest.Track(num, title)
for _, e := range extra {
for k, v := range e {
tags[k] = v
}
maps.Copy(tags, e)
}
return storagetest.MP3(tags)
}

View File

@ -17,6 +17,8 @@ import (
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/slice"
"github.com/navidrome/navidrome/utils/str"
)
@ -72,13 +74,7 @@ func findImageInFolder(ctx context.Context, libFS fs.FS, relFolder, absFolder, p
return nil, "", err
}
var imagePaths []string
for _, m := range matches {
if !model.IsImageFile(m) {
continue
}
imagePaths = append(imagePaths, m)
}
imagePaths := slice.Filter(matches, model.IsImageFile)
// Prefer base filenames over numeric-suffixed ones (artist.jpg before artist.1.jpg)
slices.SortFunc(imagePaths, compareImageFiles)
@ -157,7 +153,7 @@ func findImageInArtistFolder(folder, mbzArtistID, artistName string) string {
continue
}
name := entry.Name()
base := strings.TrimSuffix(name, filepath.Ext(name))
base := utils.BaseName(name)
if strings.EqualFold(base, candidate) && model.IsImageFile(name) {
return filepath.Join(folder, name)
}

View File

@ -13,6 +13,7 @@ import (
"github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/slice"
)
const staleAbsentAge = 24 * time.Hour
@ -85,12 +86,11 @@ func enqueueBackfillKind(ctx context.Context, ds model.DataStore, kind model.Kin
if len(ids) == 0 {
return nil
}
items := make([]model.ArtworkQueueItem, len(ids))
for i, id := range ids {
items[i] = model.ArtworkQueueItem{
items := slice.Map(ids, func(id string) model.ArtworkQueueItem {
return model.ArtworkQueueItem{
ItemKind: kind.Prefix(), ItemID: id, ImageType: model.ImageTypePrimary, Priority: model.ArtworkPriorityBackfill,
}
}
})
return ds.ArtworkQueue(ctx).Enqueue(items...)
}

View File

@ -13,6 +13,7 @@ import (
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
xdraw "golang.org/x/image/draw"
)
@ -33,7 +34,7 @@ func findPlaylistSidecarPath(ctx context.Context, plsPath string) string {
}
for _, entry := range entries {
name := entry.Name()
nameBase := strings.TrimSuffix(name, filepath.Ext(name))
nameBase := utils.BaseName(name)
if !entry.IsDir() && strings.EqualFold(nameBase, base) && model.IsImageFile(name) {
return filepath.Join(dir, name)
}

View File

@ -129,8 +129,7 @@ func resizeStaticImage(data []byte, size int, square bool) (io.Reader, int, erro
return nil, originalSize, err
}
// Copy bytes before returning buffer to pool (pool may reuse the buffer)
encoded := make([]byte, buf.Len())
copy(encoded, buf.Bytes())
encoded := bytes.Clone(buf.Bytes())
bufPool.Put(buf)
return bytes.NewReader(encoded), originalSize, nil
}

View File

@ -2,6 +2,7 @@ package artwork
import (
"bytes"
"cmp"
"context"
"io"
"math"
@ -253,9 +254,7 @@ func (w *Worker) broadcastRefresh(ctx context.Context, found []model.ArtworkQueu
}
func (w *Worker) process(ctx context.Context, item model.ArtworkQueueItem) (outcome, *acquired) {
if item.ImageType == "" {
item.ImageType = model.ImageTypePrimary
}
item.ImageType = cmp.Or(item.ImageType, model.ImageTypePrimary)
out, got := w.proc.acquire(ctx, item)
queue := w.proc.ds.ArtworkQueue(ctx)
@ -333,7 +332,7 @@ func (w *Worker) precache(ctx context.Context, got *acquired) {
// backoffFor returns min(5s×4^n, giveUpAfter) scaled by (1+jitter), with jitter in [-0.4, 0.4].
func backoffFor(attempts int, jitter float64) time.Duration {
d := math.Min(float64(backoffBase)*math.Pow(4, float64(attempts)), float64(giveUpAfter))
d := min(float64(backoffBase)*math.Pow(4, float64(attempts)), float64(giveUpAfter))
return time.Duration(d * (1 + jitter))
}

View File

@ -7,6 +7,7 @@ import (
"io"
"net/http"
"os"
"slices"
"sync"
"time"
@ -43,7 +44,7 @@ func (c *recordingCache) Get(ctx context.Context, arg cache.Item) (*cache.Cached
func (c *recordingCache) getKeys() []string {
c.mu.Lock()
defer c.mu.Unlock()
return append([]string(nil), c.keys...)
return slices.Clone(c.keys)
}
// Simulates a concurrent Enqueue between DequeueBatch and the worker's delete, so
@ -88,7 +89,7 @@ func (f *fakeEventBroker) SendBroadcastMessage(_ context.Context, event events.E
func (f *fakeEventBroker) getEvents() []events.Event {
f.mu.Lock()
defer f.mu.Unlock()
return f.events
return slices.Clone(f.events)
}
var _ events.Broker = (*fakeEventBroker)(nil)

View File

@ -144,9 +144,7 @@ func (mf MediaFile) CoverArtID() ArtworkID {
// otherwise it returns the album artwork ID.
func (mf MediaFile) DiscCoverArtID() ArtworkID {
if mf.DiscNumber > 0 {
id := NewArtworkID(KindDiscArtwork, DiscArtworkID(mf.AlbumID, mf.DiscNumber), nil)
id.Hash = mf.ImageHash
return id
return ArtworkID{Kind: KindDiscArtwork, ID: DiscArtworkID(mf.AlbumID, mf.DiscNumber), Hash: mf.ImageHash}
}
return mf.AlbumCoverArtID()
}

View File

@ -13,6 +13,7 @@ import (
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/id"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/utils/slice"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@ -89,13 +90,9 @@ var _ = Describe("AlbumRepository", func() {
want, err := albumRepo.GetAll()
Expect(err).ToNot(HaveOccurred())
Expect(want).ToNot(BeEmpty())
wantIDs := make([]string, 0, len(want))
for _, a := range want {
wantIDs = append(wantIDs, a.ID)
}
ids, err := albumRepo.GetAllIDs()
Expect(err).ToNot(HaveOccurred())
Expect(ids).To(ConsistOf(wantIDs))
Expect(ids).To(ConsistOf(slice.Map(want, func(a model.Album) string { return a.ID })))
})
})

View File

@ -15,6 +15,7 @@ import (
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/slice"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@ -289,13 +290,9 @@ var _ = Describe("ArtistRepository", func() {
want, err := repo.GetAll()
Expect(err).ToNot(HaveOccurred())
Expect(want).ToNot(BeEmpty())
wantIDs := make([]string, 0, len(want))
for _, a := range want {
wantIDs = append(wantIDs, a.ID)
}
ids, err := repo.GetAllIDs()
Expect(err).ToNot(HaveOccurred())
Expect(ids).To(ConsistOf(wantIDs))
Expect(ids).To(ConsistOf(slice.Map(want, func(a model.Artist) string { return a.ID })))
})
})

View File

@ -66,12 +66,9 @@ var _ = Describe("Artwork hydration", func() {
putInfo("al", albumAbbeyRoad.ID, "")
// albumRadioactivity: no row -> unresolved
byID := map[string]model.Album{}
all, err := repo.GetAll()
Expect(err).ToNot(HaveOccurred())
for _, a := range all {
byID[a.ID] = a
}
byID := slice.ToMap(all, func(a model.Album) (string, model.Album) { return a.ID, a })
Expect(byID[albumSgtPeppers.ID].ImageHash).To(Equal("althash11111111"))
Expect(byID[albumSgtPeppers.ID].ImageAbsent).To(BeFalse())
@ -118,12 +115,9 @@ var _ = Describe("Artwork hydration", func() {
putInfo("ar", artistKraftwerk.ID, "")
// artistCJK: no row -> unresolved
byID := map[string]model.Artist{}
all, err := repo.GetAll()
Expect(err).ToNot(HaveOccurred())
for _, a := range all {
byID[a.ID] = a
}
byID := slice.ToMap(all, func(a model.Artist) (string, model.Artist) { return a.ID, a })
Expect(byID[artistBeatles.ID].ImageHash).To(Equal("arhash444444444"))
Expect(byID[artistBeatles.ID].ImageAbsent).To(BeFalse())
@ -157,12 +151,9 @@ var _ = Describe("Artwork hydration", func() {
putInfo("pl", plsBest.ID, "plhash777777777")
putInfo("pl", plsCool.ID, "")
byID := map[string]model.Playlist{}
all, err := repo.GetAll()
Expect(err).ToNot(HaveOccurred())
for _, p := range all {
byID[p.ID] = p
}
byID := slice.ToMap(all, func(p model.Playlist) (string, model.Playlist) { return p.ID, p })
Expect(byID[plsBest.ID].ImageHash).To(Equal("plhash777777777"))
Expect(byID[plsBest.ID].ImageAbsent).To(BeFalse())
@ -185,10 +176,7 @@ var _ = Describe("Artwork hydration", func() {
Expect(err).ToNot(HaveOccurred())
tracks := pls.Tracks
Expect(tracks).ToNot(BeEmpty())
byID := map[string]model.PlaylistTrack{}
for _, t := range tracks {
byID[t.MediaFile.ID] = t
}
byID := slice.ToMap(tracks, func(t model.PlaylistTrack) (string, model.PlaylistTrack) { return t.MediaFile.ID, t })
Expect(byID).To(HaveKey(songDayInALife.ID))
Expect(byID[songDayInALife.ID].AlbumImage.ImageHash).To(Equal("pltrackhash1234"))
Expect(byID[songDayInALife.ID].BlurHash).To(Equal("LPLBLURhash"))
@ -216,12 +204,9 @@ var _ = Describe("Artwork hydration", func() {
putInfo("ra", radioWithHomePage.ID, "rahash999999999")
putInfo("ra", radioWithoutHomePage.ID, "")
byID := map[string]model.Radio{}
all, err := repo.GetAll()
Expect(err).ToNot(HaveOccurred())
for _, rd := range all {
byID[rd.ID] = rd
}
byID := slice.ToMap(all, func(rd model.Radio) (string, model.Radio) { return rd.ID, rd })
Expect(byID[radioWithHomePage.ID].ImageHash).To(Equal("rahash999999999"))
Expect(byID[radioWithHomePage.ID].ImageAbsent).To(BeFalse())
@ -247,13 +232,9 @@ var _ = Describe("Artwork hydration", func() {
}
getByID := func() map[string]model.MediaFile {
byID := map[string]model.MediaFile{}
all, err := repo.GetAll()
Expect(err).ToNot(HaveOccurred())
for _, mf := range all {
byID[mf.ID] = mf
}
return byID
return slice.ToMap(all, func(mf model.MediaFile) (string, model.MediaFile) { return mf.ID, mf })
}
BeforeEach(func() {

View File

@ -1,6 +1,7 @@
package persistence
import (
"cmp"
"context"
"fmt"
"slices"
@ -43,10 +44,7 @@ func (r *artworkQueueRepository) enqueue(conflict string, items []model.ArtworkQ
for chunk := range slices.Chunk(items, enqueueChunkSize) {
ins := Insert(r.tableName).Columns("item_kind", "item_id", "image_type", "priority", "attempts", "retry_at", "enqueued_at")
for _, it := range chunk {
if it.ImageType == "" {
it.ImageType = model.ImageTypePrimary
}
ins = ins.Values(it.ItemKind, it.ItemID, it.ImageType, it.Priority, 0, now, now)
ins = ins.Values(it.ItemKind, it.ItemID, cmp.Or(it.ImageType, model.ImageTypePrimary), it.Priority, 0, now, now)
}
ins = ins.Suffix(conflict)
if _, err := r.executeSQL(ins); err != nil {

View File

@ -6,6 +6,7 @@ import (
"github.com/Masterminds/squirrel"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/slice"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/pocketbase/dbx"
@ -193,10 +194,7 @@ var _ = Describe("ArtworkQueueRepository", func() {
Expect(purged).To(Equal(int64(5)))
got, _ := repo.DequeueBatch(100)
ids := make([]string, 0, len(got))
for _, it := range got {
ids = append(ids, it.ItemID)
}
ids := slice.Map(got, func(it model.ArtworkQueueItem) string { return it.ItemID })
Expect(ids).To(ConsistOf(albumSgtPeppers.ID, artistKraftwerk.ID, plsBest.ID, radioWithHomePage.ID, songDayInALife.ID))
})

View File

@ -1,6 +1,7 @@
package persistence
import (
"cmp"
"context"
"slices"
"time"
@ -155,9 +156,7 @@ func (r *artworkRepository) GetItemArtwork(kind model.Kind, id, imageType string
}
func (r *artworkRepository) PutItemArtwork(ia *model.ItemArtwork) error {
if ia.ImageType == "" {
ia.ImageType = model.ImageTypePrimary
}
ia.ImageType = cmp.Or(ia.ImageType, model.ImageTypePrimary)
ia.UpdatedAt = time.Now()
// PutItemArtwork records the outcome of an attempt, so an unset attempted_at is now.
if ia.AttemptedAt.IsZero() {

View File

@ -8,6 +8,7 @@ import (
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/utils/slice"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/pocketbase/dbx"
@ -42,13 +43,9 @@ var _ = Describe("PlaylistRepository", func() {
want, err := repo.GetAll()
Expect(err).ToNot(HaveOccurred())
Expect(want).ToNot(BeEmpty())
wantIDs := make([]string, 0, len(want))
for _, p := range want {
wantIDs = append(wantIDs, p.ID)
}
ids, err := repo.GetAllIDs()
Expect(err).ToNot(HaveOccurred())
Expect(ids).To(ConsistOf(wantIDs))
Expect(ids).To(ConsistOf(slice.Map(want, func(p model.Playlist) string { return p.ID })))
})
})

View File

@ -7,6 +7,7 @@ import (
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/utils/slice"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@ -83,13 +84,9 @@ var _ = Describe("RadioRepository", func() {
want, err := repo.GetAll()
Expect(err).To(BeNil())
Expect(want).ToNot(BeEmpty())
wantIDs := make([]string, 0, len(want))
for _, r := range want {
wantIDs = append(wantIDs, r.ID)
}
ids, err := repo.GetAllIDs()
Expect(err).To(BeNil())
Expect(ids).To(ConsistOf(wantIDs))
Expect(ids).To(ConsistOf(slice.Map(want, func(r model.Radio) string { return r.ID })))
})
})

View File

@ -401,10 +401,7 @@ func (p *phaseFolders) persistChanges(entry *folderEntry) (*folderEntry, error)
// A re-imported track returns to unresolved so new embedded art is picked up lazily.
if len(entry.tracks) > 0 {
trackIDs := make([]string, len(entry.tracks))
for i := range entry.tracks {
trackIDs[i] = entry.tracks[i].ID
}
trackIDs := slice.Map(entry.tracks, func(t model.MediaFile) string { return t.ID })
if err := tx.Artwork(p.ctx).DeleteForItems(model.KindMediaFileArtwork, trackIDs); err != nil {
log.Warn(p.ctx, "Scanner: could not invalidate media_file artwork", "folder", entry.path, err)
}

View File

@ -56,7 +56,7 @@ func ifNoneMatch(header, hash string) bool {
if header == "*" {
return true
}
for _, tag := range strings.Split(header, ",") {
for tag := range strings.SplitSeq(header, ",") {
tag = strings.TrimSpace(tag)
tag = strings.TrimPrefix(tag, "W/")
if strings.Trim(tag, `"`) == hash {

View File

@ -2,6 +2,7 @@ package nativeapi
import (
"net/http"
"slices"
"github.com/go-chi/chi/v5"
"github.com/navidrome/navidrome/core/artwork"
@ -9,12 +10,12 @@ import (
"github.com/navidrome/navidrome/model"
)
var refreshableArtworkKinds = map[model.Kind]bool{
model.KindAlbumArtwork: true,
model.KindArtistArtwork: true,
model.KindPlaylistArtwork: true,
model.KindRadioArtwork: true,
model.KindMediaFileArtwork: true,
var refreshableArtworkKinds = []model.Kind{
model.KindAlbumArtwork,
model.KindArtistArtwork,
model.KindPlaylistArtwork,
model.KindRadioArtwork,
model.KindMediaFileArtwork,
}
func (api *Router) addArtworkRoute(r chi.Router) {
@ -27,7 +28,7 @@ func (api *Router) refreshArtwork() http.HandlerFunc {
ctx := r.Context()
kind, _ := model.ParseKind(chi.URLParam(r, "kind"))
id := chi.URLParam(r, "id")
if !refreshableArtworkKinds[kind] {
if !slices.Contains(refreshableArtworkKinds, kind) {
http.Error(w, "invalid artwork kind", http.StatusBadRequest)
return
}

View File

@ -7,6 +7,7 @@ import (
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/id"
"github.com/navidrome/navidrome/utils/slice"
)
func CreateMockAlbumRepo() *MockAlbumRepo {
@ -86,11 +87,7 @@ func (m *MockAlbumRepo) GetAllIDs(qo ...model.QueryOptions) ([]string, error) {
if err != nil {
return nil, err
}
ids := make([]string, len(all))
for i, a := range all {
ids[i] = a.ID
}
return ids, nil
return slice.Map(all, func(a model.Album) string { return a.ID }), nil
}
func (m *MockAlbumRepo) GetCursor(qo ...model.QueryOptions) (model.AlbumCursor, error) {

View File

@ -6,6 +6,7 @@ import (
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/id"
"github.com/navidrome/navidrome/utils/slice"
)
func CreateMockArtistRepo() *MockArtistRepo {
@ -118,11 +119,7 @@ func (m *MockArtistRepo) GetAllIDs(options ...model.QueryOptions) ([]string, err
if err != nil {
return nil, err
}
ids := make([]string, len(all))
for i, a := range all {
ids[i] = a.ID
}
return ids, nil
return slice.Map(all, func(a model.Artist) string { return a.ID }), nil
}
func (m *MockArtistRepo) GetCursor(options ...model.QueryOptions) (model.ArtistCursor, error) {

View File

@ -1,8 +1,8 @@
package tests
import (
"cmp"
"slices"
"sort"
"sync"
"time"
@ -67,11 +67,8 @@ func (m *MockArtworkQueueRepo) DequeueBatch(n int, kinds ...string) ([]model.Art
res = append(res, it)
}
}
sort.Slice(res, func(i, j int) bool {
if res[i].Priority != res[j].Priority {
return res[i].Priority > res[j].Priority
}
return res[i].EnqueuedAt.Before(res[j].EnqueuedAt)
slices.SortFunc(res, func(a, b model.ArtworkQueueItem) int {
return cmp.Or(cmp.Compare(b.Priority, a.Priority), a.EnqueuedAt.Compare(b.EnqueuedAt))
})
if len(res) > n {
res = res[:n]

View File

@ -1,10 +1,12 @@
package tests
import (
"maps"
"sync"
"time"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/slice"
)
type MockArtworkRepo struct {
@ -167,11 +169,9 @@ func (m *MockArtworkRepo) DeleteForItem(kind model.Kind, id string) error {
if m.Err != nil {
return m.Err
}
for k, ia := range m.ItemData {
if ia.ItemKind == kind.Prefix() && ia.ItemID == id {
delete(m.ItemData, k)
}
}
maps.DeleteFunc(m.ItemData, func(_ string, ia model.ItemArtwork) bool {
return ia.ItemKind == kind.Prefix() && ia.ItemID == id
})
return nil
}
@ -181,15 +181,11 @@ func (m *MockArtworkRepo) DeleteForItems(kind model.Kind, ids []string) error {
if m.Err != nil {
return m.Err
}
idSet := make(map[string]bool, len(ids))
for _, id := range ids {
idSet[id] = true
}
for k, ia := range m.ItemData {
if ia.ItemKind == kind.Prefix() && idSet[ia.ItemID] {
delete(m.ItemData, k)
}
}
idSet := slice.ToSet(ids)
maps.DeleteFunc(m.ItemData, func(_ string, ia model.ItemArtwork) bool {
_, ok := idSet[ia.ItemID]
return ok && ia.ItemKind == kind.Prefix()
})
return nil
}

View File

@ -132,11 +132,7 @@ func (m *MockMediaFileRepo) GetAllIDs(qo ...model.QueryOptions) ([]string, error
if err != nil {
return nil, err
}
ids := make([]string, len(all))
for i, mf := range all {
ids[i] = mf.ID
}
return ids, nil
return slice.Map(all, func(mf model.MediaFile) string { return mf.ID }), nil
}
func (m *MockMediaFileRepo) Put(mf *model.MediaFile) error {

View File

@ -7,6 +7,7 @@ import (
"github.com/deluan/rest"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/id"
"github.com/navidrome/navidrome/utils/slice"
)
func CreateMockPlaylistRepo() *MockPlaylistRepo {
@ -57,11 +58,7 @@ func (m *MockPlaylistRepo) GetAllIDs(options ...model.QueryOptions) ([]string, e
if err != nil {
return nil, err
}
ids := make([]string, len(all))
for i, p := range all {
ids[i] = p.ID
}
return ids, nil
return slice.Map(all, func(p model.Playlist) string { return p.ID }), nil
}
func (m *MockPlaylistRepo) GetCursor(options ...model.QueryOptions) (model.PlaylistCursor, error) {

View File

@ -5,6 +5,7 @@ import (
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/id"
"github.com/navidrome/navidrome/utils/slice"
)
type MockedRadioRepo struct {
@ -78,11 +79,7 @@ func (m *MockedRadioRepo) GetAllIDs(qo ...model.QueryOptions) ([]string, error)
if err != nil {
return nil, err
}
ids := make([]string, len(all))
for i, r := range all {
ids[i] = r.ID
}
return ids, nil
return slice.Map(all, func(r model.Radio) string { return r.ID }), nil
}
func (m *MockedRadioRepo) Put(radio *model.Radio, _ ...string) error {