From 98966ab43e5da80a1210005f99c5a2f819a8d38a Mon Sep 17 00:00:00 2001 From: Vynull App Date: Mon, 20 Jul 2026 18:53:18 +0000 Subject: [PATCH] api + web: pull album art on first view, not first CDJ load Newly added tracks showed no artwork in the web UI until a CDJ loaded them. The add path only reads the tag-embedded picture, which misses art ffmpeg can see, and the artwork endpoint's lazy extraction (one probe on first request, then cached for good) was unreachable from the UI: every render site gated the on art_id > 0, so the request that would have triggered extraction was never made. Loading the track on a deck ran analysis, whose artwork write-back set art_id, and only then did the UI show art - a chicken-and-egg the CDJ happened to break. The track payload now carries art_checked, and a shared artHTML helper renders the optimistically whenever the file has never been probed: the first view of a new track fires the artwork request, the endpoint extracts synchronously, and the same response delivers the image. Tracks probed and found artless go straight to the placeholder (no repeated 404 traffic) and an onerror fallback covers the artless and concurrent-probe cases. Verified in the browser against a fresh library: a never-probed track with embedded art shows it on first open of the detail drawer with art_id and art_checked persisted, an artless track degrades to the placeholder, and after its probe it makes no further artwork requests. --- api/api.go | 2 ++ api/web/index.html | 34 +++++++++++++++++++++++++--------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/api/api.go b/api/api.go index 07f4aab..eece652 100644 --- a/api/api.go +++ b/api/api.go @@ -690,6 +690,7 @@ type TrackInfo struct { PlayCount int `json:"play_count,omitempty"` Tags []string `json:"tags,omitempty"` ArtID uint32 `json:"art_id,omitempty"` + ArtChecked bool `json:"art_checked,omitempty"` // false with art_id 0 = never probed; the web UI requests art optimistically to trigger lazy extraction FileMissing bool `json:"file_missing,omitempty"` } @@ -744,6 +745,7 @@ func (s *Server) libTrackToInfo(t *library.Track) TrackInfo { ColorName: trackColorNames[t.ColorID], PlayCount: t.PlayCount, ArtID: t.ArtID, + ArtChecked: t.ArtChecked, FileMissing: t.FileMissing, } if !t.DateAdded.IsZero() { diff --git a/api/web/index.html b/api/web/index.html index 0291e44..28d251e 100644 --- a/api/web/index.html +++ b/api/web/index.html @@ -5586,12 +5586,32 @@ } // `sort` is the accessor used by the comparator. `sortable: false` opts a // column out of header-click sorting (e.g. the waveform thumbnail). +// artHTML renders a track's artwork . The request is made optimistically +// for tracks whose file has never been probed (art_id 0 with art_checked +// unset): GET /api/artwork/{id} is what triggers the server's lazy ffmpeg +// extraction, so the first view of a newly added track pulls the art out of +// the file instead of waiting for a CDJ load to analyze it. Tracks already +// probed and found artless (art_checked true) go straight to the placeholder, +// so there is no repeated 404 traffic; onerror degrades to the placeholder +// for the artless-probe and concurrent-probe cases. +function artHTML(t, imgClass, fbClass, fbText) { + if (t.art_id > 0 || !t.art_checked) { + return ``; + } + return `
${fbText}
`; +} +function artFallback(img) { + const d = document.createElement('div'); + d.className = img.getAttribute('data-fb-class') || 'lib-art empty'; + d.textContent = img.getAttribute('data-fb-text') || ''; + img.replaceWith(d); +} + const LIBRARY_COLUMNS = [ { key: 'art', label: 'ART', default: false, thClass: '', tdClass: 'art-cell', sortable: false, - cell: t => t.art_id > 0 - ? `` - : `
` }, + cell: t => artHTML(t, 'lib-art', 'lib-art empty', '') }, { key: 'waveform', label: 'WV', default: true, thClass: '', tdClass: 'wave-cell', sortable: false, cell: t => `
${cueMarkerOverlay(t.id, t.duration)}
` }, @@ -6191,9 +6211,7 @@ mobileMQ.addEventListener('change', placeMobileSidebar); function libCardHtml(t) { - const art = t.art_id > 0 - ? `` - : `
`; + const art = artHTML(t, 'lib-art', 'lib-art placeholder', '♪'); const facts = []; if (t.bpm) facts.push((+t.bpm).toFixed(1)); if (t.key) facts.push(t.key); @@ -7654,9 +7672,7 @@ // Tags section is re-rendered into
after mutations. const tagsHTML = '
'; - const detailArt = t.art_id > 0 - ? `` - : `
`; + const detailArt = artHTML(t, 'detail-art', 'detail-art empty', ''); document.getElementById('detail-body').innerHTML = `
${detailArt}