mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
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:
parent
13659f2c30
commit
8c65931ba7
@ -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})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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...)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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 })))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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 })))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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))
|
||||
})
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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 })))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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 })))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user