Migrates ~74 call sites away from the playerStore re-export shims that
were kept during M0–E.41 to avoid touching 30+ imports per PR. Now
that the bigger refactor work is done, each helper goes back to its
real home:
- `initAudioListeners`, `installQueueUndoHotkey`, `flushPlayQueuePosition`
→ from their own store modules
- `getPlaybackProgressSnapshot`, `subscribePlaybackProgress`,
`PlaybackProgressSnapshot` → from `playbackProgress`
- `resolveReplayGainDb`, `shuffleArray`, `songToTrack`
→ from `utils/*`
- `_resetQueueUndoStacksForTest`, `consumePendingQueueListScrollTop`,
`registerQueueListScrollTopReader` → from `queueUndo`
- `PlayerState`, `Track` types → from `playerStoreTypes`
Drops the corresponding 13 re-export stubs from `playerStore.ts` and
the now-unused imports. Also drops dead section banners + per-wrapper
comments above one-line action delegates. Trims one stale "(separate
PR)" note in `transportLightActions.ts` since that follow-up landed
in E.39.
`playerStore.ts`: 180 → 112 LOC (−68). Down from Phase E's starting
3732 LOC.
`bootstrap.test.ts` mock target updated from `../store/playerStore`
to `../store/queueUndoHotkey` to keep the spy reachable after the
import change.
* fix(home): swap Because-you-listened rail to AlbumRow under 696 px
The hero-style BecauseCards are tuned for full-rail widths (3 cards at
1052 px+, 2 cards at 696-1051 px). Below that the cards stretched
full-width with a fixed 160 px cover stuck on the left and centred text
floating in a wide empty area — looked like three over-sized banners
stacked vertically instead of a compact recommendation rail.
A `ResizeObserver` on the rail wrapper now watches the container width
and below 696 px renders a standard `AlbumRow` (which is already
perf-tuned for narrow rails: artwork budget, viewport windowing, scroll
paging). Wide layouts keep the unchanged hero card layout, so the
mainstage view at full width is identical to before.
* docs(changelog): add Because-you-listened narrow-layout fix entry
Two reports rolled into one pass:
1. Rail occasionally rendered nothing on Home open. Cause: the rotated
anchor sometimes had no Last.fm similar-artists or no library matches
among the sampled set, and the old code gave up after one try and
stored that dud anchor as the rotation cursor — so the next mount
started from the same dud's neighbour and could fail again.
2. Recommendations felt repetitive. Cause: pool of 12 anchors walked
round-robin made each anchor recur every 12 mounts, similar-artist
sample of 6 from 12 had heavy overlap visit-to-visit, and per-artist
single-album random pick meant artists with one library album always
surfaced the same record.
Anchor selection: random pick from pool with a per-server cooldown
buffer (last 5 anchors excluded, capped at floor(pool/2) so small
libraries don't soft-lock). Up to 4 anchors are tried in a shuffled
candidates list before giving up; the localStorage cursor only advances
on a successful anchor so duds don't poison future mounts.
Picks variety: similar-artist fetch raised from 12 to 25 (same
getArtistInfo call, larger response — Last.fm typically returns up to
~50). Per-server ring buffer of the last 30 shown album ids; per-similar
-artist album choice prefers an album not in that buffer, falling back
to any album when the artist's whole catalogue is stale so the slot is
never lost.
Pool cap raised 12 -> 20 to give the cooldown buffer room to breathe in
libraries with varied listening history.
Storage: legacy `psysonic_because_anchor:` single-id keys from the
round-robin era are stripped on module load (one-shot localStorage
sweep, the new `..._anchor_history:` prefix has a different colon
position so no false matches).
API budget unchanged in the hot path: 1 getArtistInfo + 6 getArtist
per Home mount. Worst case (3 dud anchors, 4th succeeds) is 4
getArtistInfo + 6 getArtist.
* feat(home): mix recently-played + starred into Because-you-like anchor pool
Anchor pool was sourced only from getAlbumList(frequent), so the rotation
cursor walked the same eight top-played artists no matter how varied the
rest of the listening history was. Round-robin merge of mostPlayed,
recentlyPlayed and starred (dedup by artistId) means each mount can land
on a different listening *mode* — heavy rotation, current focus, or
explicit favorites — instead of stepping through the same top-played
sequence.
Pool size 8 -> 12 to let the cursor visit all three modes before
wrapping. Visibility guard widened so the rail still renders when the
server has no frequent-play data yet but starred or recent items exist.
Zero new API calls — all three lists are already in Home's initial
fetch.
* fix(home): drop orphan 3rd Because-card in 2-col range, keep all 3 stacked on mobile
auto-fit grid wraps to 2 cols between 696-1051px container width, which
left the third card alone on a second row at 1080p. Container query
hides the 3rd card only inside that 2-col band; on wider screens the
full 3-up row stays, on narrow viewports (single column) all three
cards stack vertically as expected.
* docs(changelog): Because-you-listened seed pool + 1080p layout polish (PR #493)
* docs(changelog): fold PR #493 refinements into the existing Because-you-listened entry
Drop the separate Changed section entry — the feature is in the same
1.46.0 release window as PR #489, so readers want a single description
of the final behaviour, not "added X, then changed X" for the same
release. PR reference becomes "PRs #489, #493".
At 1080p with three cards per row (~370 px wide) the previous layout
broke down:
- 200×200 cover left only ~150 px of text width after gap + padding,
so titles truncated mid-word and the meta-pill wrapped vertically
into a stack instead of staying on one line.
- The "·" separators in the meta vanished as soon as the pill wrapped,
because the ::after lives inside the wrapping flex line.
- Albums without cover-art rendered the placeholder as an empty grey
rect — visually broken next to neighbours that did have art.
Adjustments:
- Cover wrap 200 → 160 px. Buys 40 px of text width per card and brings
cover/text proportions into balance.
- Meta-pill: inline-flex with width: max-content + max-width: 100% so
the pill is content-fit when the meta line fits, capped at the
available text width otherwise. Font 12 → 10 px, column-gap and
padding tightened, flex-wrap kept (wraps to a second row if a server
ever returns a really long meta string instead of clipping).
- Cover-art placeholder shows a centred Lucide Music icon at 30 %
--text-primary alpha — same visual weight as a faded thumbnail
instead of an empty rect.
* feat(home): "Because you listened" recommendation rail
New Home rail (under Recently Added, default on, toggleable in Settings →
Personalisation → Home Page) that surfaces 3 albums from artists similar
to one of your top-played artists. Anchor rotates per Home mount so a
different top-artist seeds the recommendations each visit; within each
anchor, both the similar-artist subset and the chosen album per artist
are randomised, so the same anchor returns different picks on subsequent
visits.
Card layout matches the regular Album cards' surface (--bg-card with
accent-tinted border + 1px inset top highlight) and gets the same
Play / Enqueue hover overlay buttons. Cover and meta scale via CSS only
— no infinite animations, no filter/blur/transform, no compositing
layers. Grid wraps below 3-up at <400px card width instead of shrinking.
API budget: one getArtistInfo2 + 6 parallel getArtist calls per Home
mount, both reusing the existing mostPlayed payload to derive the anchor
pool (no extra API call to find top artists). All 8 locales seeded.
* fix(home): ru plurals + per-server anchor + narrower card grid
- ru: add _few / _many for becauseYouLikeTracks (CLDR Russian needs
4 forms — 3 треков was wrong, now 3 трека).
- Anchor rotation memory is now per-server. The localStorage key
becomes psysonic_because_anchor:<serverId>; switching servers no
longer aliases server A's rotation onto server B's pool.
- because-card grid minmax(400px, 1fr) -> minmax(340px, 1fr) so two
cards fit side by side at typical sidebar-expanded widths instead
of collapsing to a single card per row.
* docs: CHANGELOG + Contributors entry for Because-you-listened rail (PR #489)
* style(home): blurred cover backdrop + centred layout for Because-cards
- Each Because-card renders the album cover as a blurred, low-opacity
full-bleed background layer behind the existing cover thumb and text.
Resolved through useCachedUrl so the cache layer feeds it (same key
as the thumbnail) instead of a fresh salted URL on every render.
- Card content (cover thumb + text block) now centred horizontally and
vertically within the card; the meta line lives in a small pill that
sits centred under the artist row.
- Text contrast halo and meta-pill background are theme-aware via
color-mix on var(--bg-card) / var(--text-primary), so the same rules
read on dark and light themes (was hard-coded rgba black before and
smudged the type on Latte / Nord Snowstorm).