Commit Graph
10 Commits
Author SHA1 Message Date
bvandeusen 366692a1fc fix: stop the sync feed hiding missing files from clients — #2704
test-go / test (push) Successful in 2m3s
test-go / integration (push) Successful in 5m8s
android / Build + lint + test (push) Failing after 4m24s
#2523 filtered missing tracks out of every path that CHOOSES music, but
the client sync feed was never touched: GetTracksByIDs has no filter and
the wire had no field for it. So every Android client held a cached
library containing tracks whose files are gone, with no way to tell, and
could queue them from any cache-first path -- the exact failure #2523
existed to prevent, reached by a different route.

Ships the state rather than filtering the feed, of the two options the
ticket weighed. A missing file is expected to come back: the scanner
clears the mark, and adopts the row if it returns renamed (#2528).
Withholding the row would mean a delete-and-recreate on every client for
what is usually a transient unmount, churning caches and throwing away
the identity #2528 works to preserve.

Room goes to v8. No hand-written migration: the pre-v1 destructive
fallback rebuilds from sync, which repopulates every row with the new
column -- exactly the case that policy exists for.

The interesting part was working out what "missing" means to a client,
and it is NOT "unplayable". Two findings shaped the fix:

Server search and album detail never filtered missing tracks either, and
that turns out to be right rather than an oversight. The consistent rule
the codebase already follows is that Minstrel never PICKS a missing
track for you -- recommendation, discover, mixes and browse all exclude
them -- but it does not hide one you went looking for by name or opened
an album to find. Hiding track 4 makes an album look wrong. So the fix
is to mark and to keep it out of queues, not to hide it.

And a track whose server file is missing still plays perfectly if its
audio is already in the device cache. ShuffleSource's offline pools
filter to exactly those residents, so it now clears the mark on the way
out: the bytes are local and the server's loss is irrelevant. Without
that, the queue filter below would have thrown away tracks that work,
turning a fix into an offline regression.

The queue protection is one choke point rather than five call sites.
setQueue is where playlists, album play-all, search, radio and cold-boot
resume all converge. dropUnavailable is pure so the index arithmetic is
pinned by tests -- removing entries ahead of the requested position
would otherwise start playback on the wrong track, and asking to start
on a missing track now starts the next playable one, which is the
"gets skipped" behaviour the operator asked for. An entirely missing
queue returns empty and the caller leaves the player alone rather than
replacing what is playing with silence.
2026-08-17 12:56:31 -04:00
bvandeusen a9ca49dc4e feat(library): genre + year quick-jumps on album and artist detail — #367
test-web / test (push) Successful in 44s
test-go / test (push) Successful in 1m1s
test-go / integration (push) Successful in 4m59s
Last bullet of #367. From an album you like, one click to everything else from
that year or in that genre.

Year was free — AlbumRef already carried it. Genre was not: AlbumDetail is
AlbumRef + tracks and neither carried genre, because genre lives on TRACKS. So
both detail responses gained a derived `genres` array, computed from the
entity's tracks rather than stored, since an album's tracks can legitimately
disagree about genre.

Split and trimmed identically to the browse index. That's the invariant this
whole task turned on: if the chip's matching diverged from the index's
splitting, a chip would lead to a page that doesn't contain the album you
clicked from.

No year link on artist detail. An artist spans many years, so a single one
would be a lie about the discography — genres only there.

Genre lookup failure is logged and degrades to no chips rather than failing the
request; a navigation nicety must not 404 a detail page that otherwise loaded.
`genres` is always an array at JSON, never null, matching how every other list
field in this package is emitted.

## Type widening, and the TypeScript version of a lesson from earlier today

Adding a required field to AlbumDetail/ArtistDetail breaks every typed fixture
that constructs one. Six of them across three test files. That's the same shape
as the Go signature changes that cost three CI rounds in #2453 — change a type,
then go find everything that builds it — so I searched for the constructions
before pushing instead of after. All six updated.

Tests: the encoded href for a slash-bearing genre ("Rock/Pop" →
?g=Rock%2FPop), the year href, and the no-tags case rendering no chips at all.
gofmt verified clean via docker rather than guessed.
2026-08-05 13:50:29 -04:00
bvandeusenandClaude Opus 4.8 fdd14ef04c feat(server): "You might like" album/artist Home rows (#790)
test-go / test (push) Successful in 39s
test-go / integration (push) Failing after 4m39s
Surface in-library albums/artists the listener doesn't actively spin but
is predicted to enjoy, derived from the same similarity + like-weighted
candidate engine that powers For-You — rolled up from track scores to
album/artist granularity. Built in the daily 3am BuildSystemPlaylists
pass, atomic-replaced alongside the system playlists, and read back by
/api/home (+ /api/home/index).

Cold-start gate: skips generation entirely below 20 distinct unskipped
tracks AND 5 distinct artists, so a thin profile ships empty rows rather
than near-random tiles.

- migration 0034: you_might_like_albums / you_might_like_artists (id+rank,
  CASCADE, per-user rank index).
- playlists/you_might_like.go: cold-start gate + similarity roll-up
  (sum-of-top-3 aggregation, per-artist album cap, daily-rotating via the
  same userIDHash jitter as For-You) + atomic-replace persist in the tx.
- recommendation/home.go: two new HomePayload sections with read-time
  cross-section dedup vs Most Played / Rediscover / Last Played, trimmed
  to 10 each.
- api: you_might_like_albums / you_might_like_artists on /api/home and
  /api/home/index, reusing albumRefFrom / artistRefFromCovered.
- tests: pure roll-up/aggregation/cap unit tests + DB-backed gate,
  sufficiency, and atomic-replace tests (all green vs real Postgres).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 19:33:46 -04:00
bvandeusen 0119eacf14 feat(api): GET /api/home/index for per-item rendering (Slice B)
Sibling to /api/home that returns the same five sections (recently
added, rediscover albums, rediscover artists, most played, last
played) but as flat slices of entity ID strings instead of
denormalized objects. The Flutter client uses this to drive its
per-item rendering pass — small discovery response then per-tile
hydration via the existing /api/albums/:id, /api/artists/:id,
/api/tracks/:id endpoints.

Reuses recommendation.HomeData so the DB cost is identical to
/api/home. JSON payload shrinks roughly an order of magnitude on
populated libraries (no embedded title / artist / cover URL fields).

Old /api/home stays untouched so the web client and older Flutter
builds keep working — no min-client-version bump needed until both
clients have migrated.
2026-05-13 20:41:44 -04:00
bvandeusen 1691290875 fix(server): forward-fix CI — gofmt convert/types + rename unused r in enricher_test 2026-05-04 17:41:16 -04:00
bvandeusenandClaude Sonnet 4.6 4e3bd46d69 fix(server,web/m7-353): post-review fixes (test compilation, AlbumRef field, bulk handler, design system)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:21:24 -04:00
bvandeusenandClaude Sonnet 4.6 fc608fb36e feat(server/m7-365): GET /api/me/history handler + integration test
Implements the listening-history endpoint for M7 #365: returns a user's
play events newest-first, excluding skipped events and quarantined tracks,
with offset/limit pagination and a has_more heuristic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 05:52:17 -04:00
bvandeusen 1abaf15051 feat(api): extend ArtistRef/AlbumRef + add HomePayload for M6a
- ArtistRef gains SortName and CoverURL fields
- AlbumRef gains SortTitle field
- albumRefFrom now populates SortTitle from dbq.Album.SortTitle
- artistRefFrom now populates SortName from dbq.Artist.SortName
- New artistRefFromCovered helper builds CoverURL from nullable cover_album_id
- New HomePayload view type for GET /api/home response body
2026-05-01 17:56:19 -04:00
bvandeusen c5af7bb53a feat(api): add DTOs for library reads and search 2026-04-21 00:04:51 -04:00
bvandeusenandClaude Opus 4.7 2f2dfa86df feat(api): POST /api/auth/login
Verifies bcrypt password hash, mints a session token, stores its
sha256 in the sessions table, and returns the token in both the
response body (for Flutter) and an httpOnly/SameSite=Strict
cookie (for the web SPA).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 21:25:52 -04:00