diff --git a/cmd/artwork.go b/cmd/artwork.go index 5d4c8a04b..34797d188 100644 --- a/cmd/artwork.go +++ b/cmd/artwork.go @@ -8,7 +8,6 @@ import ( "io" "os" "slices" - "strconv" "strings" "time" @@ -264,7 +263,7 @@ func configState(rep statusReport) string { func printQueueStats(w io.Writer, stats []model.ArtworkQueueStat, total int64, countHeader, indent string) { fmt.Fprintf(w, "%sKIND\tPRIORITY\t%s\n", indent, countHeader) for _, s := range stats { - fmt.Fprintf(w, "%s%s\t%s\t%d\n", indent, kindName(s.ItemKind), priorityName(s.Priority), s.Count) + fmt.Fprintf(w, "%s%s\t%s\t%d\n", indent, kindName(s.ItemKind), artwork.PriorityName(s.Priority), s.Count) } fmt.Fprintf(w, "%sTOTAL\t\t%d\n", indent, total) } @@ -276,37 +275,14 @@ func kindName(prefix string) string { return prefix } -type artworkPriority struct { - name string - value int -} - -// knownPriorities is the one listing behind both the name and the parse, so they cannot drift. -var knownPriorities = []artworkPriority{ - {"bump", model.ArtworkPriorityBump}, - {"scan", model.ArtworkPriorityScan}, - {"recheck", model.ArtworkPriorityRecheck}, - {"backfill", model.ArtworkPriorityBackfill}, -} - -// priorityName falls back to the number: a row written by a newer version still has to print. -func priorityName(p int) string { - for _, ap := range knownPriorities { - if ap.value == p { - return ap.name - } - } - return strconv.Itoa(p) -} - func priorityNames() string { - return strings.Join(slice.Map(knownPriorities, func(ap artworkPriority) string { return ap.name }), ", ") + return strings.Join(slice.Map(artwork.KnownPriorities, func(ap artwork.Priority) string { return ap.Name }), ", ") } func parseArtworkPriority(s string) (int, error) { - for _, ap := range knownPriorities { - if ap.name == s { - return ap.value, nil + for _, ap := range artwork.KnownPriorities { + if ap.Name == s { + return ap.Value, nil } } return 0, fmt.Errorf("invalid priority %q, expected one of: %s", s, priorityNames()) @@ -801,7 +777,7 @@ func formatExplain(rep artwork.ExplainReport) string { case rep.Queued == nil: fmt.Fprintln(w, " (not queued)") default: - fmt.Fprintf(w, " Priority:\t%s (%d)\n", priorityName(rep.Queued.Priority), rep.Queued.Priority) + fmt.Fprintf(w, " Priority:\t%s (%d)\n", artwork.PriorityName(rep.Queued.Priority), rep.Queued.Priority) fmt.Fprintf(w, " Attempts:\t%d\n", rep.Queued.Attempts) fmt.Fprintf(w, " Retry at:\t%s\n", formatTime(rep.Queued.RetryAt)) } diff --git a/cmd/artwork_test.go b/cmd/artwork_test.go index 0be9d85ec..88ec47007 100644 --- a/cmd/artwork_test.go +++ b/cmd/artwork_test.go @@ -940,7 +940,7 @@ var _ = Describe("parseArtworkPriority", func() { It("accepts every name status prints", func() { for _, p := range []int{model.ArtworkPriorityRecheck, model.ArtworkPriorityBackfill, model.ArtworkPriorityScan, model.ArtworkPriorityBump} { - Expect(parseArtworkPriority(priorityName(p))).To(Equal(p)) + Expect(parseArtworkPriority(artwork.PriorityName(p))).To(Equal(p)) } }) diff --git a/core/artwork/explain.go b/core/artwork/explain.go index 9001c5cb6..afea165da 100644 --- a/core/artwork/explain.go +++ b/core/artwork/explain.go @@ -132,7 +132,7 @@ func Explain(ctx context.Context, ds model.DataStore, ag *agents.Agents, kind mo } } // Set even for a kind the chain never walks, so a caller that asked to walk (e.g. --live on a - // playlist) is reported as having tried, matching the pre-refactor CLI's unconditional flag. + // playlist) is reported as having tried. rep.Walked = opts.Walk != nil if !Explainable(kind) { return rep, nil @@ -192,3 +192,27 @@ func (r ExplainReport) GaveUpAfter() []TraceStep { } return DecodeTrace(r.Stored.LastFailure, "") } + +// Priority names one of the queue's fixed priority levels. +type Priority struct { + Name string + Value int +} + +// KnownPriorities is the one listing behind both the name and the parse, so they cannot drift. +var KnownPriorities = []Priority{ + {"bump", model.ArtworkPriorityBump}, + {"scan", model.ArtworkPriorityScan}, + {"recheck", model.ArtworkPriorityRecheck}, + {"backfill", model.ArtworkPriorityBackfill}, +} + +// PriorityName falls back to the number: a row written by a newer version still has to print. +func PriorityName(p int) string { + for _, ap := range KnownPriorities { + if ap.Value == p { + return ap.Name + } + } + return strconv.Itoa(p) +} diff --git a/core/artwork/explain_test.go b/core/artwork/explain_test.go index 676f626d0..f9fa41f69 100644 --- a/core/artwork/explain_test.go +++ b/core/artwork/explain_test.go @@ -92,6 +92,16 @@ var _ = Describe("FormatAgents", func() { }) }) +var _ = Describe("PriorityName", func() { + It("names a known priority", func() { + Expect(artwork.PriorityName(model.ArtworkPriorityScan)).To(Equal("scan")) + }) + + It("falls back to the number for an unknown priority", func() { + Expect(artwork.PriorityName(999)).To(Equal("999")) + }) +}) + var _ = Describe("Explain", func() { var ds *tests.MockDataStore var artRepo *tests.MockArtworkRepo diff --git a/server/nativeapi/artwork_explain.go b/server/nativeapi/artwork_explain.go index ef3ab3987..3ec8e66d2 100644 --- a/server/nativeapi/artwork_explain.go +++ b/server/nativeapi/artwork_explain.go @@ -28,15 +28,15 @@ type traceStepDTO struct { type storedDTO struct { Source string `json:"source"` - Hash string `json:"hash,omitempty"` SourcePath string `json:"sourcePath,omitempty"` AttemptedAt string `json:"attemptedAt,omitempty"` } type queuedDTO struct { - Priority int `json:"priority"` - Attempts int `json:"attempts"` - RetryAt string `json:"retryAt,omitempty"` + Priority int `json:"priority"` + PriorityName string `json:"priorityName"` + Attempts int `json:"attempts"` + RetryAt string `json:"retryAt,omitempty"` } type configDTO struct { @@ -121,16 +121,16 @@ func toExplainDTO(rep artwork.ExplainReport) explainDTO { if rep.Stored != nil { dto.Stored = &storedDTO{ Source: rep.Stored.Source, - Hash: rep.Stored.Hash, SourcePath: rep.Stored.SourcePath, AttemptedAt: rfc3339(rep.Stored.AttemptedAt), } } if rep.Queued != nil { dto.Queued = &queuedDTO{ - Priority: rep.Queued.Priority, - Attempts: rep.Queued.Attempts, - RetryAt: rfc3339(rep.Queued.RetryAt), + Priority: rep.Queued.Priority, + PriorityName: artwork.PriorityName(rep.Queued.Priority), + Attempts: rep.Queued.Attempts, + RetryAt: rfc3339(rep.Queued.RetryAt), } } if steps := rep.LastAttemptFailed(); len(steps) > 0 { diff --git a/server/nativeapi/artwork_explain_test.go b/server/nativeapi/artwork_explain_test.go index e830254cf..782dc05b1 100644 --- a/server/nativeapi/artwork_explain_test.go +++ b/server/nativeapi/artwork_explain_test.go @@ -95,7 +95,6 @@ var _ = Describe("GET /artwork/explain", func() { Expect(got["stored"]).To(Equal(map[string]any{ "source": "external:deezer", - "hash": "abc", "sourcePath": "/music/Radiohead/folder.jpg", "attemptedAt": attemptedAt.Format(time.RFC3339), })) @@ -128,9 +127,10 @@ var _ = Describe("GET /artwork/explain", func() { var got map[string]any Expect(json.Unmarshal(w.Body.Bytes(), &got)).To(Succeed()) Expect(got["queued"]).To(Equal(map[string]any{ - "priority": float64(model.ArtworkPriorityScan), - "attempts": float64(1), - "retryAt": retryAt.Format(time.RFC3339), + "priority": float64(model.ArtworkPriorityScan), + "priorityName": "scan", + "attempts": float64(1), + "retryAt": retryAt.Format(time.RFC3339), })) Expect(got["lastAttemptFailed"]).To(Equal([]any{ map[string]any{"candidate": "external:deezer", "outcome": "error", "detail": "timeout"}, diff --git a/ui/src/common/ArtworkInfo.jsx b/ui/src/common/ArtworkInfo.jsx index b83fc71b6..1531b9a25 100644 --- a/ui/src/common/ArtworkInfo.jsx +++ b/ui/src/common/ArtworkInfo.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import { Chip, Link, TableCell, TableRow } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' import { useDataProvider, usePermissions, useTranslate } from 'react-admin' +import { DateField } from './DateField' const OUTCOME_COLORS = { hit: '#4caf50', @@ -96,7 +97,7 @@ export const ArtworkInfo = ({ resource, id }) => { if (!isAdmin || !report) return null - const recorded = report.chainOrigin !== 'not recorded' + const recorded = !!report.stored return ( <> @@ -109,7 +110,7 @@ export const ArtworkInfo = ({ resource, id }) => { <> {report.stored?.source} - {report.stored?.attemptedAt} + ) : ( @@ -141,16 +142,21 @@ export const ArtworkInfo = ({ resource, id }) => { title={translate('artwork.gaveUpAfter')} steps={report.gaveUpAfter} /> + {report.stored?.sourcePath && ( + + {report.stored.sourcePath} + + )} {report.queued && ( <> - {report.queued.priority} + {report.queued.priorityName} {report.queued.attempts} - {report.queued.retryAt} + )} diff --git a/ui/src/common/ArtworkInfo.test.jsx b/ui/src/common/ArtworkInfo.test.jsx index 862881939..40064d5e0 100644 --- a/ui/src/common/ArtworkInfo.test.jsx +++ b/ui/src/common/ArtworkInfo.test.jsx @@ -22,7 +22,11 @@ vi.mock('react-admin', async (importOriginal) => { const report = { result: 'resolved from external:deezer', chainOrigin: 'recorded 2026-09-01T10:00:00Z', - stored: { source: 'external:deezer', attemptedAt: '2026-09-01T10:00:00Z' }, + stored: { + source: 'external:deezer', + sourcePath: '/music/Radiohead/folder.jpg', + attemptedAt: '2026-09-01T10:00:00Z', + }, steps: [ { candidate: 'cover.*', outcome: 'miss' }, { @@ -31,6 +35,7 @@ const report = { detail: 'https://cdn/x.jpg', }, ], + queued: { priority: 20, priorityName: 'scan', attempts: 1, retryAt: '' }, config: { setting: 'ArtistArtPriority', value: 'external' }, } @@ -74,6 +79,8 @@ describe('', () => { await userEvent.click(screen.getByText('artwork.showDetails')) expect(screen.getByText('cover.*')).toBeInTheDocument() expect(screen.getByText('ArtistArtPriority:')).toBeInTheDocument() + expect(screen.getByText('/music/Radiohead/folder.jpg')).toBeInTheDocument() + expect(screen.getByText('scan')).toBeInTheDocument() }) it('shows the not-recorded state', async () => { diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json index e141e6785..d96067df9 100644 --- a/ui/src/i18n/en.json +++ b/ui/src/i18n/en.json @@ -656,6 +656,7 @@ "result": "Result", "source": "Source", "attemptedAt": "Attempted at", + "sourcePath": "Source path", "showDetails": "Show details", "hideDetails": "Hide details", "priority": "Priority",