25 Commits

Author SHA1 Message Date
Deluan
d4399d9492 refactor(artwork): derive blurhash components inside Encode
Components had a single caller, which only ever fed it the bounds of the image
it then passed to Encode. Exporting it gave callers two ways to get it wrong —
components mismatched with the image, or out of the 1..9 range — in exchange for
a knob nobody turned.

Derive them from img.Bounds() at the top of Encode and unexport the helper. The
counts must come from the pre-downscale bounds: downscale's integer rounding can
shift the ratio across a component boundary, and the hash is a client-side cache
key. Verified byte-identical over 18 hashes spanning 9 aspect ratios.

The out-of-range validation goes with it, being unreachable once the counts are
always derived. The aspect-ratio table now asserts through Encode's size flag,
which encodes (x-1)+(y-1)*9.
2026-07-28 21:39:13 -04:00
Deluan
02fcf8bea7 style(artwork): trim verbose comments to the 1-2 line budget
Comments only; no executable code changed. Verified by comparing the Go
token stream of every touched file before and after: identical.

Removes 375 of the 1104 comment lines this branch added, targeting content
that belongs in a commit message or PR body rather than in the code:
rejected alternatives ("DeleteIfUnchanged, not Delete", "Waking all beats
routing by kind"), refactor history ("as the legacy reader did"), issue
references (#5798, #5597, #5376), benchmark numbers (~400ms, ~16k allocs),
and four persistence doc comments that duplicated the interface godoc in
model/artwork.go verbatim.

Comments predating this branch are left untouched.

The ASCII fixture trees in the e2e suites are deliberately kept above the
line budget: they diagram the fixture layout with its expected outcomes,
and every pre-existing block in those files carries one.
2026-07-27 18:57:31 -04:00
Deluan
e646ce5065 refactor(artwork): log decoded dimensions as fields, not a formatted string
fmt.Sprintf ran on every newly-decoded image even with Debug off, since
Go evaluates log arguments regardless of level. Separate width/height
fields also query better than a "300x300" string.

Correction to e0f1acd1a's message: it said "all 40" messages carry the
prefix. The package has 68 log call sites and 62 distinct messages; 40
was only what the test suite happened to exercise. The sweep itself was
complete -- zero unprefixed messages remain.
2026-07-27 14:51:44 -04:00
Deluan
e0f1acd1a2 feat(artwork): prefix every log message and time the slow steps
Prefix: 22 messages still logged unprefixed, so a line from this package
was indistinguishable from any other subsystem's. All 40 now carry
"Artwork: ", matching Scanner:/API:/Watcher: -- which earns its place
because DevLogSourceLine is off by default.

Timing on what can actually be slow: total per acquisition (on every
exit, failures included), the read that also covers the provider
download, hashing, decode+blurhash, resize, drain batch, precache,
prune, backfill, and the external agent call -- with the rate-limiter
wait counted separately, since a throttled agent and a slow one look
identical from the drain.

Debug coverage for states that were previously silent: dedup hit vs
decode, settling absent, serving a lower-priority source after an
external failure, retry scheduling with attempts and budget left, giving
up when the budget runs out, breaker open/close per agent, provisional
read-through, dangling state rows, and the mtime mismatch that makes art
appear to vanish. outcome gained a String() so it reads as a name.
2026-07-27 14:18:27 -04:00
Deluan
3a0190dff3 refactor(artwork): unexport artwork.HashImage function 2026-07-27 10:33:13 -04:00
Deluan
eecb7434d3 refactor(artwork): make acquisition a processor with its own receiver
workerDeps was a parameter bag threaded into two free functions that
nothing outside the worker calls. It becomes the processor type, with
processItem and persist as acquire and persist methods on it, and Worker
holds one collaborator instead of reaching through a bag.

Kept as a separate type rather than folding onto Worker: acquisition
takes a queue item and returns bytes, while Worker.process settles the
queue row around it. That boundary is what keeps retry policy out of the
image pipeline, and what lets the acquisition specs build a three-field
value instead of a Worker with drain pools, gates, a broker and a
real on-disk cache.
2026-07-26 21:46:06 -04:00
Deluan
f893690cb6 refactor(artwork): give workerDeps only what the processor uses
The bag carried cache, which processItem never reads and only the
worker's precache uses, and carried agents/ffmpeg/gate solely to
reconstruct a resolver on every queue item. cache and ffmpeg move to
Worker, where precache actually uses them, and the resolver is built
once in NewWorker.

persist's hash parameter was redundant: decodeArtwork sets Hash and
GetImage selects it, so art.Hash already holds it on both paths.

The type itself now lives beside Worker, which owns it, rather than in
the file of the function it is passed to.
2026-07-26 21:28:15 -04:00
Deluan
b71a40f654 style(artwork): use one log prefix, spelled the way the codebase does
The package logged under three spellings of its own name -- "artwork: "
lowercase, "Prune: " and one "Artwork: " -- and the lowercase ones
carried lowercase message text, against 568 capitalized to 48 lowercase
elsewhere.

Prefixing itself is the convention here (Scanner:, API:, Watcher:) and
it earns its place: DevLogSourceLine is off by default, so without it a
line does not say which subsystem emitted it. So this normalizes the
spelling rather than dropping the prefix. Error strings stay lowercase
and unprefixed per Go convention.
2026-07-26 21:11:44 -04:00
Deluan
1e054cdd28 refactor(artwork): give the resolver a receiver and one capability field
The resolve* chain walkers each took seven parameters -- ds, agents,
ffmpeg, gate, localOnly -- while the package already had workerDeps
bundling the same collaborators for processItem. They are now methods on
a resolver.

The external capability is one nilable field instead of three values
that had to agree. Previously a local-only resolution passed agents=nil,
gate=denyGate and localOnly=true, and only the localOnly check actually
protected anything: the external branch dereferences agents in the loop
header, before the gate closure runs, so denyGate could never fire. It
is deleted. A nil ext now both marks the resolution local-only and
removes the agents there were to dereference, and newLocalResolver takes
no parameter that could supply one.

The playlist tile loop hardcoded localOnly=false, safe only because an
early return 22 lines above it made that unreachable; it now inherits
the resolver's capability.
2026-07-26 20:57:22 -04:00
Deluan
c9c363523b refactor(artwork): narrow the prune lock and drop redundant in-flight tracking
The prune read-lock wrapped all of processItem, including external fetches under
their own timeout. Since a pending RWMutex writer blocks new readers, one prune
arriving behind a slow provider stalled every subsequent item in both drain
pools. Extract persist() so the lock covers only the window it protects: store
placement plus the two row writes.

The in-flight set guarded against a queue row appearing twice in one batch, but
artwork_queue's primary key makes that impossible, drains are serial per pool,
and the pools' kind lists are disjoint. Removing it also retires the cancellation
unwind loop that existed only to release those claims.
2026-07-26 20:07:44 -04:00
Deluan
19d89143f7 fix(artwork): compare the pixel cap without multiplying
Defence in depth rather than a live hole: the reported crafted PNG
(0xffffffff square) never reaches the multiplication, because
image/png rejects it at DecodeConfig, and the largest dimensions any
supported format can declare — 2^30-1 for PNG, 16-bit for JPEG and
GIF, 14-bit for WebP — cannot overflow the int64 product.

decodeCapped is format-agnostic though, so the guard should not depend
on a decoder's own limits staying where they are. Comparing by division
holds for any dimensions a decoder might report, and non-positive ones
are now rejected outright.
2026-07-25 15:22:22 -04:00
Deluan
cf0264412b perf(artwork): precache from the bytes just acquired
Warming the resize cache re-read the two rows and the file the
acquisition had just written, so every acquired image cost two extra
queries and a second full read of a file whose bytes were still in
memory. processItem now hands back what it persisted and precache warms
from that, under the same cache key the serving path computes.

Resolving the admin user also moves behind the empty-queue check: it is
needed only to resolve private playlists, so an idle server no longer
runs a user lookup on every poll.
2026-07-25 11:47:40 -04:00
Deluan
381dce0363 fix(artwork): never record absent after a local I/O failure
Local sources swallowed their open errors, so a stale NFS/SMB mount was
indistinguishable from "this entity has no artwork": the chain returned
no reader, processItem took the absent branch, and the upsert replaced a
good content hash with the empty string. Clients then saw a placeholder
until the 1h request recheck or the 24h stale-absent sweep, and the
orphaned bytes became eligible for the next prune.

A candidate the resolver knows about — a file in the folder listing, a
track's own audio file — failing to open is not evidence of absence, so
it now forces a retry the same way an external agent error does.
2026-07-25 10:06:51 -04:00
Deluan
d7385a9f68 fix(artwork): register the GIF decoder in core/artwork
The deleted artwork.go carried blank imports for image/gif and
x/image/webp. WebP came back via resize.go's gen2brain/webp, which
self-registers, but GIF did not: core/artwork claims GIF support in
mimeForFormat and extForMime while relying on an unrelated server
package to have imported the decoder.

The guard lives in the e2e suite because that test binary has no other
image/gif importer; a spec in core/artwork would pass regardless, since
animation_test.go imports the package non-blank.
2026-07-25 10:02:18 -04:00
Deluan
0d1df1648e feat(artwork): precache on acquisition, bump on upload/radio changes, manual re-resolve API 2026-07-23 14:08:08 -04:00
Deluan
190c291e61 feat(artwork): worker fetches agent images directly with per-agent rate limits and breakers 2026-07-23 14:07:52 -04:00
Deluan
b172ce4296 refactor(artwork): reuse auth.WithAdminUser and dedupe image cap guards 2026-07-22 18:02:54 -04:00
Deluan
f614850ff0 fix(artwork): store backing-file provenance per item, not per hash 2026-07-22 15:54:08 -04:00
Deluan
d6434b9929 fix(artwork): reject decompression-bomb dimensions before decoding 2026-07-22 15:53:32 -04:00
Deluan
6afcb93a9b fix(artwork): retry higher-priority external art after fallback hit
With CoverArtPriority="external,cover.jpg", a transient external failure
followed by a folder hit dropped the external error: the worker recorded
found and deleted the queue row, so the configured higher-priority external
art was never retried. Carry extError onto the fallback resolution and add
an outcomeFoundStale that persists+serves the art but reschedules via
MarkFailed, giving the external source another chance. When external later
answers definitively-not-found, the hit is not stale and the row is deleted.
2026-07-22 15:53:32 -04:00
Deluan
67f6d8aee8 fix(artwork): cap resolved image reads
A user-editable ExternalImageURL can point at an arbitrarily large endpoint;
a fast server could make the worker buffer hundreds of MB inside the 5s HTTP
timeout. Bound the read to a fixed 20MB cap (no config knob) via io.LimitReader
and fail the item if it is exceeded.
2026-07-22 15:53:32 -04:00
Deluan
87095fab08 refactor(artwork): deduplicate purge loop, backfill table, and extGate alias 2026-07-22 15:53:32 -04:00
Deluan
0fbbd01357 style(artwork): fix comment accuracy and budget; fingerprint ArtistImageFolder
Correct the inverted workerDeps.extGate comment, trim over-budget doc comments, and add conf.Server.ArtistImageFolder to the resolution fingerprint so an image-folder change re-resolves artist artwork.
2026-07-22 15:53:32 -04:00
Deluan
d6fc829f84 style(artwork): tighten processor comments to budget 2026-07-22 15:53:32 -04:00
Deluan
e0655dc882 feat(artwork): add acquisition processor
Resolves one queue item end to end: hash/dedup, decode + 128px thumbnail
blurhash, place bytes (store vs source file), and persist found/absent/
failed state for the worker (Task 4) to act on.
2026-07-22 15:53:32 -04:00