Last.fm returns error 27 "Deprecated" for user.getArtistTracks, so the per-artist
plays modal was 502ing for every artist. Pivot to filtering the user's supported
all-time top tracks + top albums (which carry real per-item play counts) to the
artist — exact personal counts, cheaper, and the clicked artists are the user's
top artists so their items cluster near the front. Adds topTracks(); route + modal
return shape unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an optional lastfmName prop to ArtistModal. When set (only on
/lastfm) it fetches /api/lastfm/artist-plays independently of the
existing preview fetch and renders a most-played-songs list plus a
most-played-albums list above the release grid. Album Want resolves
via /api/mb/release-group then POSTs /api/discover/want, mirroring
the existing want(r) flow. Own-state chips name-match Last.fm albums
against the already-loaded release grid. Other ArtistModal callers
(discover-client.tsx) pass no lastfmName, so their behavior is
unchanged.
searchReleaseGroup took the first MusicBrainz hit, so a title MB ranks with the
single first (e.g. "Thriller" ties single+album at score 100) resolved to the
single — the Last.fm album modal and the Want flow then showed a 1-track single.
Prefer a clean Album > EP > Single > album-with-secondary, keeping MB order as the
in-tier tiebreak; single-only titles still resolve.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds LastfmSource, resolving Last.fm's often-empty artist mbid via the
injected MB browser (name->mbid cached) so every emitted SimilarArtist.mbid
is a real MusicBrainz artist MBID. Registered in build_similarity_sources,
gated on lastfm.api_key so the ListenBrainz-only path is unchanged when
unset.
Two row-leak bugs found in whole-branch review:
- discovery.run_discovery now prunes DiscoverySeedContribution rows whose
seedMbid is no longer a WatchedArtist (unfollow/removal has no FK cascade)
and recomputes the touched candidates' scores this sweep, instead of
letting a dead seed's contribution inflate the aggregate forever.
- maybe_run_scan's exception handler around scan_chunk now calls
clear_worklist and clears scan.id, matching the drain path. Previously
an exception mid-chunk left the already-done rows plus the remaining
worklist for that scan_id orphaned forever, since the next scan mints a
fresh uuid. Also clear scan.id in the build_worklist failure handler for
consistency.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace scan_chunk's O(N^2) tree re-walk-and-skip-to-cursor with a
persisted ScanWorkItem worklist: build_worklist walks the tree once at
scan start and inserts one row per album (idempotent via ON CONFLICT),
scan_chunk pops up to `limit` not-done rows by indexed cursor and marks
them done, and clear_worklist drops the scan's rows once drained.
scan_library is reimplemented on top of the worklist (one-shot scan_id,
drain, clear) with an unchanged signature/behavior. main.py's
maybe_run_scan now drives a generated scan.id through Config
(scan.id/scan.inProgress/scan.progress/scan.result) instead of a
scan.cursor string, so a mid-scan library change no longer risks
skipping or re-walking albums.
The worker never reads the global monitor.autoMonitorFuture Config key;
auto-monitor-future is governed solely by the per-artist
WatchedArtist.autoMonitorFuture toggle on the Artists page. Removes the
misleading no-op checkbox and its DEFAULTS/test entries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconciled the pre-existing test_config_from_config_parses_and_defaults
test in test_discovery.py, which referenced max_seeds/maxSeeds and would
otherwise have broken.
Adds a crash-recovery preamble that restores `final` from `.old` when a
prior swap was interrupted after rename(final, old) but before
rename(tmp, final), and always drops stale `.old`/`.importing` orphans.
Also guards the swap: if nothing new was staged into tmp (empty/missing
staging) and `final` already exists, leave it untouched instead of
swapping in an empty copy. This is required beyond the literal preamble
because the pre-existing swap logic unconditionally replaced `final`
with `tmp` regardless of whether anything was actually imported, which
would otherwise destroy a just-recovered (or any existing) copy whenever
staging is empty.
Two concurrent Wants for the same release-group both pass the
findUnique existence check as absent; the loser's create() then hits
the rgMbid unique constraint and threw an uncaught P2002, 500ing the
request. Wrap the create in try/catch and fall back to the same
update-and-return-200 path on P2002, converging instead of erroring.
Non-P2002 errors are rethrown unchanged.
A `"` or `\` inside artist/album terminates the quoted Lucene phrase
and corrupts the MusicBrainz search query. Escape both terms before
interpolation.
FIELDS[field] walked the prototype chain, so field=__proto__ (or
constructor, hasOwnProperty, etc.) resolved to a truthy inherited value
whose .key was undefined. Prisma drops undefined filter values, so
deleteMany({ where: { key: undefined } }) collapsed to deleteMany({}),
wiping the entire Config table. Guard with an own-property check before
indexing FIELDS so only real allowlist entries pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement GET and PATCH endpoints for monitor configuration, matching the existing discover config pattern. GET returns stored or default values; PATCH upserts known keys with allowlist filtering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Staging is no longer forced under the library. The worker reads STAGING_ROOT
(container path, default /music/.staging) and stages per-job downloads there;
docker-compose mounts ${STAGING_DIR:-${MUSIC_DIR}/.staging} at /staging with
STAGING_ROOT=/staging. Set STAGING_DIR to a fast local disk when the library
(MUSIC_DIR) is a network share so temp download I/O stays off the share.
Safe cross-volume: import_album already assembles into a temp dir on the
library volume and swaps atomically there, so partial downloads never touch
the library and the final swap stays atomic. clear_staging_root now clears the
root's contents (mount-safe) rather than removing the root. Documented in
.env.example; new tests cover the separate-root path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The MusicBrainz search+follow moves into a modal opened by an "Add artist"
button; the main view gets a filter that narrows the watched roster (with a
"N of N" count). Follow keeps the modal open (toast per follow) so several can
be added in a row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Worker stamps a throttled worker.heartbeat; GET /api/worker reports online
from its updatedAt recency (DB-time); masthead shows a live listening/offline
dot instead of a static decoration.
- /api/requests enriched with rgMbid; Recently Pressed shows cover thumbs.
- Library album count moved from the filter row into a section header.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The standalone /discover/artist/[mbid] preview page was an inconsistent
orphan (no art, no tabs, old inline TRACKS toggle). Rebuild it to mirror the
artist-detail page: category tabs, cover thumbnails, and the album modal for
tracks, with per-release Want. Extract shared categoryOf/TAB_ORDER util.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a vinyl-record favicon (icon.svg) — fixes the favicon.ico 404 console
error. Lightweight toast system (ToastHost + toast()) fired on request, wanted
add, search, follow, and want actions so submits give explicit feedback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wanted API returns rgMbid/type/year; Wanted rows show a cover thumb and open
the album modal. Discover suggested-album rows get a cover thumb too. Completes
cover art across the app (artist detail, Library, modals, Wanted, Discover).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Split the long Settings page into tabs so each service stands alone, instead
of one long mixed scroll. Same handlers/aria-labels; credential save still
PUTs the full config.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clicking a suggested (or find-similar) artist opens an ArtistModal: Follow +
a cover-art grid of their releases with per-album Want (reuses the preview
endpoint), plus a "Full page" link to the deep-linkable preview route. Clicking
a suggested album opens the AlbumModal (tracklist + Want). Lighter than jumping
to a full preview page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New /library: GET /api/library lists LibraryItems enriched with rgMbid + year
from the matching MonitoredRelease. Client renders a cover-art grid with a live
filter; clicking an album opens the AlbumModal (tracklist). Added Library to the
nav. The album-only overview to see everything downloaded.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Discography now has category tabs (All/Albums/Singles/EPs/Live/Comps/Other
with counts, defaults to Albums), cover-art thumbnails, and clicking an album
opens the AlbumModal with its tracklist. Detail GET gains ?all=true so the
client filters by tab (studio-only default still governs the artists-list
counts). Replaces the old flat date-sorted list + show-all toggle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CoverArt loads album art from the Cover Art Archive CDN by release-group MBID
(browser-side, cached, graceful placeholder fallback). AlbumModal shows cover +
tracklist (fetched from MB) with slots for a status chip and context actions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wanted releases pressed while the monitor is off never got reconciled, so
they lingered on the wanted list. Extract finish_job() and call it for every
finished job (not just when monitor.enabled) so the MonitoredRelease
transitions to fulfilled. Add fulfill_owned_releases(), run once at startup,
to backfill releases already owned at/above the quality cutoff (self-heals
items stuck from before this fix). Both idempotent + tested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Settings onto the Pressing Plant system: credentials with clear set/unset
affordances, a Library section that shows the configured /music path with a
"change MUSIC_DIR in docker-compose + rebuild" note (honest about the mount
being non-remountable from the UI), restyled scan trigger/result, and the
discovery section. All logic and aria-labels preserved. Completes the
frontend redesign (all pages + shell on one system, both themes).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>