Kyle: "this is going to be a soulseek menu now — essentially the soulseek
app in our app." Approved against research/soulseek-search/hub2.html, a
one-to-one mock in the real frame, then built in three layers.
THE AREA. Search and Downloads leave the sidebar; one Soulseek entry
(globe) opens src/views/Soulseek.svelte, whose tab row is SoulseekQt's own
anatomy: Transfers, Search, Browse, Rooms, Chat, Users. The old view ids
stay routable — App.svelte aliases "search" and "downloads" to the hub and
the component syncs its tab off them — so the title bar's download ring,
"get on soulseek" hand-offs, history entries and restored sessions all
land on the right tab. The glass bar rides the hub with the connection
line on its right (online as X · sharing N files), the page takes Home's
below-the-bar inset, and the bar arrows join the chrome spec (16px glyphs)
alongside the sidebar toggle's disc. The replaced Search.svelte and
Downloads.svelte are deleted; Transfers carries their data forward in Qt's
own columns (User/File/Progress/Status/ETA/Speed/Size/Place, cancel and
play/reveal/add on the rows, clear-complete and open-folder in the
toolbar, live speed totals in the footer) and Search re-skins the peer
search as the Qt table: results regrouped user -> folder -> file,
slot/queue chips, format filter chips that actually filter, fold-out
files with stream-and-download on hover.
THE WIRE. soulseek-proto learns the rest of the protocol: rooms 13-17
(say/join/leave/user-joined/user-left), private messages 22/23, the
watch/status/stats family 5/6/7/36, the room list 64, and the peer-side
browse pair 4/5 with Directory::read to decode what our own responder
already encoded. Parsers are tolerant where the wire is baroque — roster
sections consume their own declared counts even when they disagree with
the name list, RoomList ignores the private-room tail, truncated stats
default instead of failing the frame. Codec tests ride along, including a
browse round-trip against our own encoder.
soulseek-client turns those into Events (RoomList/RoomJoined/RoomMessage/
PrivateMessage/UserWatched/...), auto-acks every private message the
moment it lands — unacked DMs are redelivered on every login — and grows
a spawn-shaped browse(): the returned future owns an Arc of the internals
so the caller drops the client lock before awaiting the 45s worst case.
THE APP. The event pump forwards everything as soulseek:* events; new
commands cover join/leave/say/message/room-list/browse plus a buddy list
persisted in config and re-watched on every login so status and stats
stream while connected. The frontend slsk store drains it all into the
four tabs: Rooms (joined + popular lists, live roster column, per-user
colored feed, say box — the server echoes our own lines so nothing is
appended locally), Chat (recency-sorted conversations, unread badges, DM
bubbles — our half appended locally since DMs are NOT echoed), Users
(add/remove buddies, status chips, one-click Browse/Chat cross-wiring),
and Browse (type a name, walk their tree, stream or download any file).
And the sharing side finally taps the shoulder: UploadToast announces the
first event of every new upload in the shared toast slot, folding a
same-user burst into one counting pill ("nikac is taking 12 files")
instead of twelve entrances.
Live-verified against the real network: the server handed 703 rooms after
login, and a browse of a live peer returned 3,796 directories / 34,773
files in 929ms with full paths, sizes and attributes. Join/say and DMs
are wired but deliberately unexercised — the first hello in a real room
is Kyle's to say, not a probe's.
Kyle put four crops side by side and asked why the top chrome disagrees
with itself. The audit found five variants across three panel tops: the
glass bar's arrows (28px circle, white, 17px glyph, 0.12 white-veil
hover), the sidebar toggle and both Now Playing chevrons (28px 6px-radius
squares, grey, 16px, opaque --bg-highlight hover that slabs under glass
and a --glass-hover override that reads as nothing up here), the
add-to-playlist trigger (grey square, 15px glyph), and the save button
(34px 8px-radius square, 18px glyph).
One spec now, the arrows' language: 28x28 circle, 16px glyph, #fff at
rest, rgba(255,255,255,.12) hover in BOTH modes. This commit carries the
Now Playing header set — np-icon (head collapse + rail expand), the
plm-trigger override, and a new np-head override pulling SaveMenu's
trigger down from its 34px playbar size (SaveMenu grows an iconSize prop;
the playbar keeps 18, the head passes 16). The glass-hover overrides for
these buttons are gone — the base veil is mode-independent — except one
glass-scoped rule kept solely to outrank SaveMenu's own glass hover on
specificity. The bar arrows' 17->16 and the sidebar toggle's disc ride the
hub commit; their files carry that rework and split poorly.
Verified by computed style across all five controls: identical to the
pixel, and the disabled arrows stay dimmed white rather than grey so even
the off state is the same hue family.
The auto-sourcing resolver used to narrate from the right side of the
glass bar — a pulsing line while it searched, and an error that then sat
in the header indefinitely. It is now ResolverToast, in the exact slot,
pill and motion FollowToast and ImportToast already share: bottom-center
above the player bar, dark glass pill, toast-in/out, reduced-motion
respected, pointer-events none.
Three moments: while resolving, an accent dot pulses beside
"{label} — finding the best source…" for as long as the search runs; on
success the pill just leaves — the playbar coming alive IS the
confirmation, the same rule the bar version followed by disappearing; on
failure it swaps to the error in the bar's own red, holds 3.8s so it can
actually be read, then leaves. The label is copied locally so the success
exit never flashes an empty "— finding the best source…" while animating
out (the resolver clears its label in the same breath as active).
GlassBar no longer renders or imports any of it; the rstatus markup and
styles are gone with a pointer comment left in their place.
Verified by driving the resolver state through all four phases after a
clean reload: searching shows, success dismisses, a re-search re-shows,
an error holds then auto-dismisses — with the bar confirmed empty.
Kyle photographed the artist page interleaved with itself: the identity
block parked over the Popular rows with every scroll-driven paint effect
at its rest pose — wordmark unshrunk at full size, no fade on the rows
passing under it — while the sticky geometry carried on correctly. It
healed before it could be probed (no remount: a late measurement or
observer delivery snapped it back), and a controlled reload-restore came
up perfectly measured under a 14s watch, so the trigger was never pinned.
Two answers shipped:
THE GATE (backlog #3). The rotation TICK already skipped while the app
scrolled recently, but that gate was start-only: a cache-miss payload —
multi-MB raw-RGBA IPC + decode + texture/twin bake, a measured ~150ms
main-thread task — used to COMMIT whenever its promise resolved, an
arbitrary moment relative to input. All four commit sites (GPU ready,
preview paint, 4K finish crossfade, rotation/final) now route through
deferHeroCommit, which waits for the same 600ms calm the tick waits for.
The OLD banner and its twin stay mounted for the whole wait, so the mono
overlay never sits empty mid-scroll; first paints are exempt (an empty
hero is worse than a busy frame); a newer render supersedes a parked older
one so it paints once; switching artists drops any parked commit; and the
expensive snapshotGpuTwins bake rides INSIDE the deferred commit — it is
the very task the gate exists to keep out of a scroll. gpuHeroReady clears
heroLoadInFlight immediately (the load IS done; only the paint waits), so
a superseded unpainted commit can never wedge the rotation machinery.
THE TRIPWIRE (dev builds, dark glass only). Once a second, at scroll calm
so a mid-flick frame gap never false-trips: compare the gfade ride's clock
against scrollTop, and check --travel for the pre-measure sentinel plus
dead or missing timelines. Faults land in globalThis.__heroTripwire and
console.error as one latched snapshot per episode. Self-tested by
injecting the sentinel with a 120px scroll: tripped exactly
travel-sentinel within one tick while correctly NOT flagging the healthy
ride (ct 4.29% = 120/2797). Next time the page breaks in someone's hands
it hands over the culprit instead of a screenshot mystery.
Verified live: a natural rotation completed through the calm path
(heroGpuMs restamped 10.70 -> 29.60 during a fronted 26s watch).
Three rounds with Kyle's eyes on it, one rule each:
DETACH. The dropdown's top edge used to ride the exact line where the
content pane begins ("completely lined up with the main window") — and the
first fix, 38px, left the two edges within 4px, a near-miss that read
sloppier than either aligned or apart. 46px, tuned live over the album
page (38/42/46, OS captures), lands the panel ~9px below the pane line
with a 15px seam of bar under the pill.
FROST, not smoke. Plain rgba(0,0,0,0.72) ghosted the page's display-size
heading through razor-sharp; blur(18) blurred it but left it READABLE — a
blur only defeats text once its radius reaches the glyph scale, and page
headings here are ~40px tall. Now blur(40px) saturate(1.3) contrast(0.85)
over 0.55 smoke: the contrast compression is the other half of why real
acrylic is unreadable.
Why backdrop-filter is legal on THIS surface when the bars had to grow
periscopes: the filter black-flattens only the frost fraction of its
backdrop, and this panel is deliberately dark — where page content sits
behind it the content blurs into proper fog, and where bare frost shows
the flatten lands within a couple points of the plain-smoke look it
replaced. The bars failed because they wanted to SHOW the frost; this
panel wants to bury it.
NOISE + SHEEN. Over the hero's dark side the blur had nothing to re-emit
and the panel sat flat dead next to its lively half ("only half of it
seems fine"). Real acrylic never dies over black because blur is only half
its recipe — a 4.5% monochrome turbulence and a faint top sheen finish it,
so both halves read as one brushed material whatever is behind.
Interior chrome joins the glass vocabulary (hover rows, count pill,
chips, hairlines, SAVED tag, cover placeholders), and the old
keep-menus-opaque call is overridden for this menu alone —
Playlists/Playlist keep theirs.
Kyle: "when clicking and dragging the window the window gets light." It
did, by design that no longer had a reason: WM_ENTERSIZEMOVE parked the
accent at plain BLURBEHIND for EVERY modal loop, move drags included, on
the old assumption that a suspended acrylic punches through on moves the
same way it does on resizes. Plain blur ignores the gradient tint and has
none of the acrylic's material dimming — measured over a striped backdrop,
every glass surface stepped ~15% brighter (band luma 45.5 -> 52.2) for the
whole of every titlebar drag. And no compensation was possible: move loops
emit no resize-drag event, the frontend never hears about them.
Measured on this machine (research/glass-drag harness, SC_MOVE posted so
no synthetic mouse goes near the UI, GUI_INMOVESIZE as ground truth): DWM
composites the acrylic straight through a move loop. Re-asserted mid-loop
it restored rest luma EXACTLY (45.5/31.8/29.4), stationary and through 28
frames of real mouse-speed motion — no punch-through, no trailing.
So the swap is now gated on SIZING_LATCH: a titlebar move keeps the
acrylic, a latched resize parks at Blur exactly as before (DWM genuinely
suspends acrylic in live resizes — that half is load-bearing and stays).
The WM_SIZING belt-and-braces arm swaps the accent too, so a resize the
hit-test latch misses still degrades one mouse-move late instead of never.
Verified after: entering a move loop changes nothing to the decimal;
entering a resize loop still swaps (+6.7 band luma, the intended trade)
and restores on release.
THE HEADLINE, and it invalidates a lot of earlier reasoning about the glass
bar: all three periscopes were gated on an idle callback granting a >=15ms
slice, and with any frame continuously pending this 60Hz laptop caps
timeRemaining at ~13.5ms. The gate NEVER passed — traced hundreds of
consecutive refusals — so the fog never built, on any pane, all session. What
the bar had been showing all along was no fog at all: a thin tint over the raw
page. That is why Kyle read the top bar as "so light compared to the left and
right sidebar" (on album pages the raw page there is the banner, deliberately
bled up bright), and why the Now Playing blur "goes away whenever the next
track plays and its glitchy" — every track change clears that pane's fog for
rebuild, and the rebuild starved forever. The skip is now bounded: after eight
starved retries (~1-2s of honest waiting) it builds anyway. Same three lines in
GlassBar, Sidebar and NowPlaying — the pattern is copied, so a fix to one is
not a fix.
THE BAND'S TINT, once there was a fog to judge. The bar wore
rgba(20,20,24,0.28): a non-black colour that adds ~6 luma of its own light, at
a net 0.676 against the panes' 0.7075, so it read lighter than the panes by
construction. It now wears the panes' exact frost — pure black 0.35 over the
0.55 canvas — applied as .peri::after, because the bar's own background paints
UNDER .peri and cannot tint the fog (measured: an 0.85 background moved the
band 34.6 -> 33.5). Same material as the sidebar and Now Playing bands;
brightness then follows the wallpaper for all of them alike, which is why they
measure 20-36 across one screen and no absolute number could have matched.
THE FOG RIDES THE COMPOSITOR NOW. It chased scroll events on the main thread
and landed frames late ("the content behind the glass bar is delayed with
scrolling"). .pane-shell publishes the scroller as a named timeline
(--pane-scroll) and the clone animates against it, so the ride equals -scrollTop
with the main thread nowhere in the loop. Verified as a live ScrollTimeline
animation tracking scrollTop exactly. The rules live in global.css on purpose:
Svelte's -global- keyframes prefix stripping desynced animation-name from
@keyframes when they lived in the component, and getAnimations() showed it dead.
The old event sync stays as the no-timeline-support fallback.
THE SEARCH WASH reaches the top of the bar again. Its contract is that "the bar
frosts over colour rather than over black", but the frost veil dimmed the whole
fog and left a dark step where bright wash met the band; and the blur kernel at
the band's top edge sampled past the wash's own top edge into nothing, fading
the colour out over the first ~20px — the dark strip Kyle photographed twice.
The cloned wash is boosted to compensate for the veil exactly, and bled into the
fog's 48px overscan so the kernel always has colour to sample. The album banner
never showed this because its box already bleeds -70px above the page.
Judged on OS-level captures throughout (CopyFromScreen): CDP screenshots cannot
see the acrylic and led every wrong turn in this file's history. 98/98 tests.
Two bugs Kyle reported, both in what a session restore does NOT bring back.
THE LANE AND THE STREAM BADGE. Kyle: after the machine sleeps, "the playbar
waveform goes away and the track says streming even though its cashed". The
trigger is not sleep — it is the frontend restarting with a track loaded, which
WebView2 does on its own under memory pressure (and which a page reload does
too). Neither survives it: the waveform is measured out of the audio bytes and
was never re-requested, so the lane rested as dots while ALSO flagged complete;
and streamReceived/streamTotal reset to 0, which is what the Now Playing header
picks between "transferring", "Cached · size" and "Streaming from <peer>" with —
both at zero fell through to the last, for a file that finished downloading
sessions ago. His read was exactly right: it WAS cached and the label was lying.
Both are now rebuilt from the BACKEND's truth rather than the snapshot. The
restore fires the same final-quality waveform request the stream-completion path
uses (it answers from whatever the cache holds — full, partial or nothing, each
rendering honestly) and asks stream_cache_len for the real cached byte count,
setting the counters only when the cache is COMPLETE so "Cached" is never
claimed for a prefix. Local tracks get their waveform re-requested too; they had
the same dotted-lane bug. Note the guard compares path+peer, not object
identity: player.queue is a $state proxy, so the item read back is never === the
snapshot object the closure holds.
THE BREADCRUMB'S ARTIST PORTRAIT. Reaching an album directly (search -> album,
skipping the artist page) left a bare glyph where the artist's picture belongs:
trail() synthesises that crumb from the record's own artist NAME, and
metadata::CatalogAlbum carries only the name — no id, no picture. openCatalogAlbum
now resolves the artist seed after the page lands and re-records the crumb in
place (same key), so trail() reads artistArt/artistId out of it. The resolver
shares the album header's own cache and in-flight dedupe, so this is the lookup
the page mounting already pays for. It also heals the crumb's CLICK, which had
artistId null on every direct arrival.
Verified live: reloading the app in its broken state restored a 2000-peak
waveform and flipped the header to "Cached · 14.6 MB"; Daft Punk's portrait
shows on a direct-from-search album arrival. 98/98 tests.
Kyle asked for the popularity bars to match the search page "to keep a
consistent design across the board". They were two designs, and one of them had
quietly drifted into a third.
THE SEARCH PAGE'S PILL IS NOW THE APP-WIDE DESIGN: a visible
rgba(255,255,255,0.07) track, a solid var(--accent) fill scaled by the shared
curve, the full comma-formatted count standing centred on top, 17px tall with
a 4px radius. The album page, the artist page's open-in-place album panel, the
discography's record sections and the artist page's Popular list all draw it.
WHAT WENT AWAY IS THE HEAT WASH. Those three surfaces drove the fill's COLOUR
and ALPHA from the count (--t on the row, alpha ramping 0.30-0.85 via a double
color-mix, digits ramping with it), so length and colour told one story twice
and every page read as a different design. Length is the only encoding now, and
listenerHeat is deleted from listenerBars.js — nothing computes a temperature
any more. The dark-glass CONTRACT's deliberate exclusion for these bars ("their
alpha IS the data encoding") is moot and marked so.
THE DISCOGRAPHY HAD DRIFTED FURTHEST and is the reason this mattered: it
computed peak/heat inline with a LINEAR share, a 6% floor and abbreviated
labels ("12K"), against everyone else's COUNT_GAMMA 0.42 curve, browser-measured
max-content floor and full figures. It now imports listenerBars.js like the
rest, so no two surfaces can disagree about how big a hit is — the reason the
module exists.
THE POPULAR LIST joins them: it was plain right-aligned muted text, deliberately
not accent so the row's own `.now` accent stayed distinct (the ♫ rank and title
still carry that). Its column widened 62px -> 96px, the pill's width, and its
peak is taken over the WHOLE list rather than the visible five so the bars do
not re-scale on Show more.
Kept throughout: unknown counts render an em dash with NO bar (a floor-width
pill behind a dash reads as a measured tiny value), and a column drops entirely
when Last.fm knew nothing.
Verified live against the running app — computed styles on the album and artist
pages match the search bar value-for-value. 98/98 tests.
Kyle reported the blur "being masked by just going opeque" on maximize, and
separately the desktop punching through a maximized window. Both were the same
transition, and the old guard covered neither honestly.
THE GUARD'S SIGNAL IS NOW THE MODAL LOOP ITSELF. thumbbar.rs already
subclasses the main window for the taskbar transport, so the size loop is three
arms on that existing proc: WM_NCLBUTTONDOWN carrying a sizing hit-test (or
SC_SIZE, the keyboard path) latches "this loop is a RESIZE", WM_ENTERSIZEMOVE
arms, WM_EXITSIZEMOVE releases, and WM_SIZING arms as a second net because this
window is undecorated and tao does its own NCHITTEST. Emitted as
soulseek:resize-drag, POSTED to ourselves rather than emitted inline: emitting
from inside DefWindowProc's modal loop reenters the wry runtime's window store
while that loop holds it.
The signal and the failure are then the same object, which is what the two
previous attempts were not. The first grounded on any DOM `resize` — one event
fires for a maximize, so the settled window sat flat black for ~70ms hiding
glass that was already back. The second discriminated on event CADENCE, and
that is the one worth remembering: on this 4-core laptop, re-rasterising every
backdrop-filter at each new size pushes `resize` deliveries far apart, and
measured 2026-08-28 events 150ms apart never grounded AT ALL — a real drag
stayed see-through, shipping the exact artifact the guard exists to prevent.
Cadence is not a proxy for the loop.
DWM SUSPENDS THE ACRYLIC INSIDE THE LOOP but keeps compositing the plain
BLURBEHIND, so the accent swaps down to Blur for the gesture and back on
release (window_glass refactored into apply_accent + an Accent enum; GLASS_ON
tracks the main window so the subclass can re-assert). The window stays glassy
through a drag instead of going black — which is what Kyle actually wanted, and
GROUND_DRAG is off on his verification by hand.
THE MAXIMIZE ANIMATION SCALES A SNAPSHOT the accent cannot ride: measured at
43ms/frame, desktop icons read sharply THROUGH the maximized window until
~613ms after the call. Disabling the DWM zoom animation removed the
punch-through outright and every frame of a ground-suppressed maximize showed
the blur soft and present — but that also removed the animation, and Kyle chose
the animation back with the canvas grounded across the recomposition. So
transitions are forced ON, the subclass brackets the jump (active at WM_SIZE, a
re-assert immediately and again at 650ms, release with the second), and
GROUND_DISCRETE paints the canvas for exactly that bracket. Both policies are
one-line JS constants in glass.js — flipping between "animation + dark bracket"
and "instant snap + live glass" needs no rebuild.
The DOM resize listener survives as the fallback for a build with no subclass,
reverting to the original behaviour; it stands down for good the moment one
native event is heard. Measured clean: 3 maximize/restore gestures, 3 brackets,
canvas never opaque outside them; a 9-second modal loop held with zero resize
events in it.
98/98 tests.
SURFACES: panes add black, surfaces INSIDE panes add white. Every pane stands
on 0.7075 black over the acrylic (.content 0.35 over .app 0.55), so a
translucent black card nets 0.81 - darker than its own pane and inside the
~0.85 band already rejected once on the artist page. Solid mode's ladder is
measured UPWARD from #121212 (+6/+18/+24), so glass elevates with WHITE: new
--glass-surface/-hover/-press/-hairline/-well/-ring under :root.glass
(0.03/0.08/0.11/0.10/0.18), which is independently the vocabulary
.btn-ghost:hover and .ax-ghost:hover already shipped. The four solid tokens are
NEVER redefined - they are consumed inside color-mix() and gradient stops where
an alpha multiplies through and kills the call site (0.74 x 0.03 = an invisible
chip; the album row's text-overflow fade works by BEING opaque), and they paint
modal chrome that has to stay solid. 129 additive :global(:root.glass)
declarations across 18 files, all 129 verified inside a glass block by walking
the diff - solid mode byte-identical, confirmed structurally, by computed style
with the class off, and visually.
Three of them are not colour swaps: the album row's .rowbtns shim was OCCLUDING
the last ~19px of the listeners bar and nothing translucent can occlude, so
.tcount masks out just before the buttons instead; Playlist's banner scrim and
the discography's pinned ambience both landed on opaque --bg-base and now
dissolve like the album banner; and the 0.9 black hero tint on
Home/CatalogSearch/Discography paints straight onto the pane when there is no
artwork, so :first-child (the {#if} renders no element) flips it to white.
Menus, dialogs, toasts and the import surfaces stay opaque deliberately, and
the Settings copy for the mode now says so instead of claiming cards do.
Measured with a known ground substituted behind the transparent window, since
CDP never sees the acrylic: library toolbar chroma +2 as shipped, +16 with a
white fill AND its backdrop-filter dropped, +30 for the bare pane beside it.
RESIZE: Windows drops the legacy accent-policy acrylic for the whole of a live
resize and the window stays transparent through it, so the desktop punches
through sharp - measured by toggling window_glass alone, desktop icon labels
readable through the titlebar. The blur cannot be kept (documented since Win10
2004, and the modern DWM backdrop was already rejected for the
overlay-promotion blink), so armGlassResizeGuard grounds the canvas to
--bg-sidebar for the length of the gesture.
STALLS: "choppy but the FPS says 144" was 200-750ms MAIN-THREAD TASKS, not
frame pacing - idle-scheduled work fires at scroll pauses by definition. Twin
bake moved to display resolution and off the ready task (was a same-task 4K
WebGL readback plus software blur, 100-170ms per rotation AND per artist
entry); gl.finish() -> gl.flush() in buildMaster, which had been draining the
whole 4K EASU/RCAS/grade chain on the main thread; copyCanvas realloc guarded;
one app-wide scroll clock in scrollbar.js (markAppScroll/appScrollIdleFor) so
all three periscopes stop cloning mid-gesture on ANY pane rather than only
their own; bounded reclone with the querySelectorAll census deleted, the gate
widened 250->1000ms, and the build/attach split across an idle callback and a
rAF behind a generation guard; the hero rotation tick gated on scroll recency
and its payload cache sized to the working set (2->4 entries, 24->32MiB,
32->64MiB); sessionTick no longer force-rewrites every 12s while merely PAUSED.
Scroll storm went from 109-616ms long tasks to zero. Hover recalc and GC were
measured innocent - do not re-chase them.
98/98 tests throughout.
Two threads from one session, both of them the same shape: something was
being decided from the wrong source.
THE PROVIDERS WERE ASKED ABOUT A FOLDER. The picker sent the directory name
to Deezer and MusicBrainz — the ripper's naming, carrying catalogue numbers,
years and initials. "[WARP CD 156] Battles - Mirrored - 2007" matches
nothing anywhere; "Mirrored" matches instantly. Across the reference library
501 of 515 records carry a tag album and 315 disagree with their folder, and
no amount of cleaning turns "ACPP" into "Prospect Park, Brooklyn NY". The
record's own claim now leads and the folder is the fallback, in one place
(src/lib/import-title.js) that apply and the picker both read — apply
already resolved it this way, and only the picker did not. The verdict judge
had the same split: it compared candidates against the folder too, so an
exact answer scored as mere containment and a clean match wore a warning.
Identification also stops searching when searching cannot work. A folder
named "1989 Pauls Boutique" is a hopeless query and an easy comparison, so
after Deezer and MusicBrainz come back empty the artist's own catalogue is
ranked by title similarity. Scoring rather than stripping is the point: 1989,
1984 and 2001 are real albums, and any cleaner aggressive enough to rescue
Paul's Boutique would have destroyed them. Words split on whitespace with
punctuation folded inside them, so "Paul's" stays one token; pressing
vocabulary is ignored for scoring only; coverage is measured from both sides.
15 assertions pin the cases that would break it.
Cached verdicts now carry the engine that produced them. A cached match is
good forever, a cached "nothing found" only as good as the thing that
looked — and nothing ever re-asked. Negatives from an older engine, or from
a query that has since changed, are dropped and asked again.
Soulseek was asked the same wrong question, with the artist said twice:
"battles battles - mirrored - 2007". The server matches on ALL terms, so
every leftover token narrows the search — that copy was only found because
the peer happened to have 2007 in its path. The ladder now leads with the
record's name and keeps the folder as the narrower rung behind it.
AVAILABILITY OUTRANKS ENCODING. Candidate copies were ranked with avgBitrate
above hasFreeSlot, and queue depth — captured on every candidate — was never
read at all. A peer with no free slot and 277 people ahead beat a free peer
with a queue of two, because its files reported a bitrate and the others
reported none, and `?? 0` scores "didn't say" as the worst possible answer.
Bitrate is now ignored between two fully-lossless copies, where it describes
the material rather than the quality.
And nothing ever gave up. Track 1 is requested alone so it is not queued
behind eleven of our own files, the rest follow once the peer proves it will
serve us, and a copy that makes no progress for 45s is abandoned for the next
ranked one — cancelling the transfers it leaves, which used to trickle on
forever. A refusal falls through to the next peer instead of ending the
record. "Try another peer" is reachable while one is loading, which it never
was.
RE-IMPORTING BUILT A DUPLICATE FACTORY. Apply decided a track was already
filed by comparing the destination's size to its source — but apply itself
rewrites that file, embedding the chosen cover, so it is 582 KB larger from
the moment it is written and the sizes can never match again. Every run laid
down another " (n)" copy with its own library row: three runs left three of
everything, 2.16 GB. A file at the destination that this run did not write is
now simply an earlier import; within one run a repeated name is still a
collision, which is what `kept` is for.
ARTWORK FOLLOWS THE RECORD, ONLY THE AUDIO FOLLOWS THE CHOICE. Apply read
art from whichever folder the audio came from, which for a kept found copy is
the pen — and a downloaded album often carries no picture, so three records
imported with no cover while their own files held a 1500px one. It also only
ever looked at tags, never at the Folder.jpg the review had just offered as
"yours".
COVERS ARE RESAMPLED BY RUST, NOT BY THE COMPOSITOR. The webview was handed
2000px masters to paint 132px tiles — a 15x single-step reduction that reads
as aliasing on photographic covers, and carries megabytes of base64 to draw a
thumbnail. Artist portraits already had this exact fix; album covers never
got it. getArt is now bounded by default and getArtMaster is the deliberate
act, because ~60 places paint a cover and only two want real pixels: with the
polarity the other way round, every surface anyone forgets is silently wrong.
The fullscreen viewer also framed non-square art inside a square element, so
its rounded corners and shadow wrapped 96px of nothing; it takes the
artwork's own ratio now, bounded on both axes.
Smaller, from the same passes: the preview play button is a real transport
rather than a restart, and leaving a record silences its own preview without
touching music started elsewhere; bucket chips navigate with per-lens memory
instead of overwriting the saved resume position; Next counts from the
record's real place in a queue that re-sorts under it, which was skipping one
record for every one reviewed under a filter; a cover URL with nothing behind
it is shown as the empty answer it is rather than a broken image in a
pickable frame; write_tags returns early when there is nothing to change,
instead of rewriting the whole file to write values it already had; and the
import window has a Start over, which says what it costs before it costs it.