-
v2026.08.03 — Discover requests: taste-aware, rotating, snoozable, tag-targeted
Stabletest-web / test (push) Successful in 1m5stest-go / test (push) Successful in 1m30sandroid / Build + lint + test (push) Successful in 5m1stest-go / integration (push) Successful in 5m29srelease / Build signed APK (tag releases only) (push) Successful in 4m21srelease / Build + push container image (push) Successful in 17sreleased this
2026-08-03 08:46:50 -04:00 | 346 commits to main since this releasePer-day CalVer. Milestone #268 — the Discover request surface, rebuilt. Server + web + Android, with schema migrations. Merged via PR #116;
mainCI green on324059b2(Go, web, Android incl. debug assemble, and the image build).The Discover request surface was the last recommendation surface still running its early-May M5c implementation. Reported symptoms: "shows the same artists until you 'request' one" and "once it has a strong signal of your taste seems to go stale." Both were real, with separate causes — and the second got worse the more you listened, by construction.
Why it went stale
Four mechanisms, all fixed:
- No rotation. A deterministic top-N over a score ordering shows the same faces every day until one is requested away — the tail of the ranking was simply unreachable.
- Unbounded signal.
signal = 5×liked + Σexp(-age/halflife)grew without limit, so a few heavily-played artists monopolised every slot and entrenched harder as listening accumulated. - Skips counted as engagement. No
was_skippedfilter, so skipping an artist repeatedly increased its signal and pushed more of its neighbours at you. - No diversity cap. All twelve slots could be neighbours of one artist.
Seeded from your taste profile, not raw play counts
Seeds now come from
taste_profile_artists.weight— already decayed, damped and signed, so an artist you've drifted away from stops contributing instead of accumulating forever, and can contribute negatively. Log-damped so one heavy artist can't monopolise. Skips no longer read as affinity.While the profile is still empty (new account, or before the first nightly recompute) it falls back to likes + non-skipped plays, so the surface never goes blank.
The deck rotates daily
Top scorers always lead; the remaining slots rotate on
md5(mbid || date)— stable within a day so pull-to-refresh doesn't reshuffle under you, different tomorrow, and it needs no stored state. A per-seed cap stops one artist owning the deck, with a floor: diversity is a preference with score-order top-up, never a quota that leaves you with a three-card surface."Not right now" — a snooze, not a dislike
You can park a suggestion for ~90 days. It expires on its own, records no verdict on the music, and never touches your taste profile — this is acquisition triage, not negative feedback. That's enforced structurally, not by convention: the filter sits at the candidate stage rather than in the score, because a scoring term is exactly where it would leak.
Both clients flip the card in place with an Undo rather than yanking it out from under your finger, and keep a "Not right now" list below the deck with a return estimate — that list is the only route back once a card has left the deck, so it isn't optional garnish. On Android the write goes through the offline queue, modelled so a snooze queued while offline can't replay after you've undone it.
It can now tell you why
Cards say "Matches your taste in shoegaze and dream pop." when the artist's own tags overlap your taste profile — describing the music rather than the graph ("adjacent to something you played").
This needed a tag cache for artists you don't own (
track_tagscan't serve it — it's keyed to tracks you have). A background worker fills it strongest-candidates-first, because the candidate pool is far larger than MusicBrainz's ~1 req/s can ever fully cover. Expect this to be sparse at first and to fill in over days. Coverage will always be partial — obscure artists stay thin — so an untagged candidate is scored exactly as before rather than pushed down.New: Admin → Tuning → "Discover requests"
Two DB-backed knobs, no restart:
- Taste-tag weight — how strongly a tag match boosts a candidate.
score × (1 + w × overlap). Setting it to 0 turns the tag term off entirely and returns the deck to similarity-only ranking. - Snooze length — how long "not right now" parks something.
Its own scope rather than part of the taste card, deliberately: snooze length filed under "taste" would sit one careless join away from the leak described above.
Also fixed
- Generated database code was never verified against its SQL sources (#2380). 307 queries, ~12k lines of committed codegen, and nothing checked it still matched. CI now verifies it before anything else runs — and it has already caught two real drifts that the integration tests passed straight through. Passing tests against a real database proves the SQL is valid, not that the committed Go matches it.
- That check was blind to newly added files (#2382) —
git diffignores untracked paths, so a commit that added a query and forgot its generated file would have passed. This release contains the first commit that would have tripped it. - "in about a month" was unreachable in both clients (#2392) — a threshold sitting above its own divisor made the branch dead code. Found by the unit test written for it.
Install
- Redeploy the server image — this release carries server changes and migrations 0049, 0050, 0051, which apply automatically on restart. 0051 also widens the tuning-audit scope constraint (same migration, per policy).
- Install the attached signed APK — the Android client carries the snooze affordance and the tag attribution.
Web needs nothing beyond the server redeploy.
Verify
CI covered all three lanes on every commit. These are the parts it cannot sign off:
- Come back tomorrow. Does the deck actually change without you requesting anything? That's symptom one.
- Over the next weeks, does it stop narrowing as your listening concentrates? That's symptom two, and it's the one that used to get worse with use.
- Snooze a suggestion. The card should flip in place with an Undo, not vanish under your finger — then appear in "Not right now" below with a return estimate. Undo from either place.
- Tag attribution. Watch for "Matches your taste in …" appearing on more cards over the coming days as the enricher works through the backlog. Sparse at first is expected, not a fault.
- Admin → Tuning → Discover requests. Set taste-tag weight to 0, refresh Discover: the ordering should visibly revert to similarity-only. Reset to defaults to restore.
Known and deliberate
Tag coverage for out-of-library artists is permanently partial — it depends on the artist having an MBID and on MusicBrainz/Last.fm actually having tags for them. The ranking is built so that costs an untagged candidate nothing rather than penalising it, and the admin card's copy says so. Coverage counts are queryable server-side if you want to see the shape of it.
🤖 Generated with Claude Code
Downloads
-
released this
2026-07-31 23:38:11 -04:00 | 364 commits to main since this releasePer-day CalVer. Android-only — no server or web changes, no schema migrations.
mainCI green on0cea8298(lint + detekt + unit tests + signed-debug assemble, plus the image build). Merged via PR #114; closes issue #2327.The "Updating your mixes…" veil from v2026.07.14 was reported as "swipes on and off before the update completes in most cases", with "a number of other times where the contents will visibly update without the veil triggering." This is that, fixed at the root rather than re-tuned.
Most of the churn no longer happens at all
The veil was papering over a data-layer bug.
replaceSectionwas delete-then-insert, per section and un-transacted, so the observing Flow genuinely emittedemptyList()— a real, visible collapse — before the new ids landed, and did it seven times in sequence. Even unchanged tiles flickered.- One
@Transactionswap across all seven sections. Room notifies observers only on commit, so the empty gap is never observed and Home swaps in a single emission. (CachedQuarantineDaoalready carried this exact fix for this exact reason; Home never got it.) distinctUntilChangedon the id list ahead offlatMapLatest, so a section whose contents didn't move no longer tears down and rebuilds every tile's hydration flow.
The veil is now driven by the screen, not a stopwatch
Three separate reasons it used to lower early: the old
refresh().join() + 500msmeasured the network pull, which finishes long before the visible work (ids land → tiles hydrate → artwork loads); a plainisUpdatingBoolean cleared in afinallygot cleared by whichever of two overlapping refreshes finished first (reconnect and the 03:00 rebuild routinely arrive together); and failures were swallowed, so a failed pull reported success with no retry.It now raises, works (retrying behind the veil), then holds until the content has been still for a quiet window and nothing is still loading — with a floor so it can't flash and a hard ceiling so it can never strand you. The ceiling caps the veil, never the refresh.
It only appears when something actually changed
A refresh that returns what's already cached raises nothing, so a launch over a warm cache is immediate — no veil hiding an update that isn't happening.
Artwork no longer pops in after the veil lifts
Art was the most visible offender and the refresh coroutine can't see it, so the composition reports upward:
ServerImage— the single choke point behind every album, artist and playlist cover — counts its in-flight loads into a tracker the veil waits on. Art also crossfades app-wide now, with the placeholder fading out over the same window, which softens the pop everywhere the veil isn't involved.And it tells you when nothing changed
Because an unchanged refresh raises no veil, a pull that found nothing new would otherwise give no response at all. A pull now always produces exactly one piece of feedback: the veil if something changed, "Already up to date" if not, "Couldn't check for updates" if it couldn't reach the server. Only for refreshes you asked for — background checks stay silent rather than toasting on every launch.
Also veiled now: library-scan completion (Home never reacted to it before), playlist create/update/delete/tracks-changed, manual pull, and the initial load over a warm cache.
Install
Install the attached signed APK. Redeploying the server image is optional — it carries no changes in this release; it only matters for offering the new APK to other devices via in-app update.
Verify on device
This release is almost entirely about feel, which is the part CI cannot sign off:
- Open the app on a warm cache → content appears immediately. No veil, no sections collapsing and refilling.
- Pull to refresh with nothing new → "Already up to date".
- Pull to refresh after the 03:00 rebuild → the veil wipes on and holds until tiles and their artwork have landed, then wipes off. Nothing should pop in after it lifts.
- Pull with the server unreachable → "Couldn't check for updates" after it retries; your cached content stays put.
- First run / cleared cache → skeleton as before, no veil over an empty screen.
Known and deliberate
The background freshness sweeper still re-fetches stale metadata, so a single tile can change unannounced. Left unveiled on purpose — raising a full-screen panel for one tile would be worse than the pop, and the new crossfade softens it. Worth revisiting only if it's noticeable in use.
🤖 Generated with Claude Code
Downloads
- One
-
released this
2026-07-23 08:38:10 -04:00 | 372 commits to main since this releasePer-day CalVer. A web playback-blocking fix plus a batch of cross-client queue ("up next") improvements.
mainCI green ona26ef4e9(all three lanes, incl. the signed debug APK assemble).Fix — web: clicking play did nothing (#1928)
QueueDrawerwas rendered outsideQueryClientProvider. Its always-mounted<aside>renders a row per queued track, and the queue like-heart (added in #1596) callsuseQueryClient()at init — so the instant the queue populated on first play, it threw "No QueryClient was found in Svelte context" and aborted the reactive flush that starts playback. The play button appeared dead and the console logged the error. Moved the drawer inside the provider.CI didn't catch it because
QueueDrawer.test.tsmocks the likes API, which hid the real provider dependency.Queue enhancements (web + Android)
- Opens on the now-playing track — the queue no longer starts at the top when the current song is further down (#1929 Android, #1931 web).
- Auto-follow + "Jump to current" — the queue keeps the playing row in view as the track auto-advances, but only while you're watching it; scroll away to browse and a Jump to current pill brings you back.
- Album-art thumbnails in every queue row.
- Clear-queue action.
- Android drag-to-reorder + per-row remove — closes the functional gap with the web queue (which already had both), plus a header showing track count + total time.
Verify
- Web: open a system playlist (e.g. Discover) and press play → it plays, no console error. Open the queue → it's scrolled to the current track; let it advance → it follows; scroll up → the "Jump to current" pill appears.
- Android: open the Queue from the player → it's positioned on the now-playing track, rows show artwork; drag the grip handle to reorder; tap ✕ to remove; the clear-queue action empties it. (The reorder/pill are new touch interactions — worth a quick real-device sanity check.)
🤖 Generated with Claude Code
Downloads
-
released this
2026-07-15 19:17:49 -04:00 | 382 commits to main since this releasePer-day CalVer, re-cut forward to
47de7be4(PR #110 + notification-skip follow-up PR #111). Android UPnP/Sonos playback-state rework + a cross-client queue like button.Install this APK. The cursor sync + no-flicker behaviour is device-confirmed; this re-cut adds the notification next/prev fix — please re-verify the transport controls.
UPnP/Sonos — one authoritative "what's playing" (Android, #171)
While casting, the local ExoPlayer cursor and the Sonos renderer were two competing sources of truth. The local cursor lagged (forward-only, index-based sync switched off during every queue-load / re-cast window) and two writers of the current-track index fought — so the in-app player flickered to the pre-cast track on reopen and the notification metadata went stale.
The fix completes
MinstrelForwardingPlayerinto a single source of truth:- Identity-locked cursor — the paused local cursor now tracks the song the renderer is actually playing, matched by track identity (the id in the Sonos
TrackURIvs the localMediaItem.mediaId), not the raw index. Survives queue-reload index wobble; moves both directions. ✅ device-confirmed: stays in sync, no flicker. - One writer — the position tick only moves the scrubber; the current track/index is owned solely by the player-event path. The stale-tick stomp is gone.
- Handoff for free — ending the cast resumes local playback on the correct track + position.
- Notification transport now controls Sonos — the system next/prev buttons issue
COMMAND_SEEK_TO_NEXT/COMMAND_SEEK_TO_PREVIOUS→Player.seekToNext()/seekToPrevious(), which are distinct from theseekTo*MediaItem()methods the in-app buttons use. Those two were un-overridden, so notification next/prev nudged the paused local player instead of Sonos (they read as dead — play/pause worked because it was routed). Now overridden to route to Sonos while casting. (new in this re-cut)
Resolves the index-drift half of #1211; builds on the June UPnP foundation (#606 / #608 / #612).
Verify on device
- Sync / no flicker — already confirmed. ✅
- Notification/lock-screen transport — while casting, the next and previous buttons (and play/pause) should now drive Sonos.
- Handoff — end the cast mid-queue → local resumes on the same track + position.
Queue like button (web + Android, #1596)
The queue ("up next") rows in the full-screen player were the only track-list surface without a like heart. Added the shared like button to the web queue rows (
QueueTrackRow— covers the/now-playingside panel and the mobile queue drawer) and the Android "Queue" screen rows. Liked state stays sourced from the shared store by track id — no data-model change.🤖 Generated with Claude Code
Downloads
- Identity-locked cursor — the paused local cursor now tracks the song the renderer is actually playing, matched by track identity (the id in the Sonos
-
v2026.07.14 — Taste-profile fidelity (M160)
Stabletest-go / test (push) Successful in 44stest-web / test (push) Successful in 49stest-go / integration (push) Successful in 5m1sandroid / Build + lint + test (push) Successful in 5m5srelease / Build signed APK (tag releases only) (push) Successful in 4m9srelease / Build + push container image (push) Successful in 19sreleased this
2026-07-14 13:37:44 -04:00 | 389 commits to main since this releaseMilestone #160 — Taste-profile fidelity via metadata enrichment (no sidecar) — plus adjacent home/discover polish. Merged via PR #109;
mainCI green on4f69c230.Taste profile: 2 facets → multi-dimensional. Now scores across artists, enriched genre tags, era, and mood, with a collaborative co-play candidate arm and time/weekday/device context conditioning — all in the single Go+Postgres binary.
- Folksonomy tag enrichment — MusicBrainz (keyless) + Last.fm (keyed) tag enricher →
track_tagscache → tag facet; admin tag-sources card (web + Android);EnrichedTagScaleknob. - Era facet — decade weights from
albums.release_date;era_scaleknob. - Mood facet — folksonomy tags → canonical mood buckets;
mood_scaleknob. - Time-of-day / weekday context — per-artist affinity for the current daypart × weekday cell;
context_time_weightknob. - Device-class context — clients send
device_classon play-start; radio narrows the affinity cell by current device. - Household co-play — SQL co-occurrence over
play_events→user_cooccurrencesimilarity edges → candidate arm (empty on single-user servers). - (AcousticBrainz was evaluated and deliberately dropped — frozen 2022 dataset.)
Also: Songs-like → dedicated home row + wider spread; CoverTile play-button un-clip; discover novelty rebalance; "Updating your mixes…" veil.
Android: this release builds the first signed APK carrying the admin tag-sources screen (#1521) and the device-class play-event change (#1551). Schema migrations 0042→0048 apply automatically on server restart.
🤖 Generated with Claude Code
Downloads
- Folksonomy tag enrichment — MusicBrainz (keyless) + Last.fm (keyed) tag enricher →
-
released this
2026-07-02 16:45:36 -04:00 | 424 commits to main since this releaseFixes the "fails once, stays broken forever" behavior (issue #1245, PR #106): a screen that failed to load in a dead zone or during a server restart now heals itself the moment connectivity returns — no more navigating away and back or pull-refreshing to unstick the app.
Install this APK to get screen-level auto-recovery everywhere.
Auto-recovery on reconnect
- New
NetworkStatusController.recoveries()signal — the screen-level half of the recovery idiom the sync/mutation/diagnostics singletons already used. - Every screen re-runs its load when server health returns: Home, Playlists, Liked, History, Hidden, Requests, Search, Discover, Album/Artist/Playlist detail, and all four admin screens. Direct-load screens only re-fetch from an error state, so good content never flashes a spinner.
Failure no longer looks like an empty account
- Cold-start against an unreachable server used to show "Welcome to Minstrel" / "No playlists yet". Home, Playlists, Liked, and Library now surface a real "Couldn't load…" error with a Retry button when the cache is empty and the refresh failed. A failed refresh over cached content stays silent — stale beats a full-screen error.
Stuck corners unstuck
- Requests: the 12s progress poll can now escape a failed initial load (it previously required a loaded list to ever fire), which also covers server errors that transport-level health monitoring never sees.
- Search: re-running the same query after a transient failure now works, and the error state has a Retry button.
- Artist detail: "Similar artists" / "Top tracks" re-fetch on recovery instead of staying silently absent after a blip.
Consistent Retry everywhere
- The shared error widget gained a title and now participates in pull-to-refresh, and it's used on every error branch — every load failure in the app is now recoverable by button, pull, or automatically on reconnect.
Use it
Install the APK, then try it: airplane-mode the phone, open a tab so it errors, drop out of airplane mode — the screen should populate itself within seconds of the server probe going healthy.
🤖 Generated with Claude Code
Downloads
- New
-
v2026.06.30 — Device diagnostics & debug reporting (M9) + follow-ups
Stabletest-go / test (push) Successful in 38stest-web / test (push) Successful in 47sandroid / Build + lint + test (push) Successful in 4m18stest-go / integration (push) Successful in 4m39srelease / Build signed APK (tag releases only) (push) Successful in 3m46srelease / Build + push container image (push) Successful in 16sreleased this
2026-06-30 19:24:58 -04:00 | 427 commits to main since this releasePer-day mutable CalVer, re-cut forward to
61171515(M9 + same-day diagnostics follow-ups, PRs #104 #105).Toggleable per-account device diagnostics: remotely enable debug mode on an account → its phone(s) stream a timeseries of device state → an admin timestamp view with copy/export for analysis. Targets roaming/poor-data recovery and locked-phone UPnP/Sonos desync.
Install this APK to get the diagnostics reporter + the follow-up fixes below; the prior build had only the index-level capture.
Diagnostics core (M9)
- Migration 0036:
diagnostic_events+ per-accountusers.debug_mode_enabled.POST /api/diagnostics(gated batch ingest),GET /api/admin/diagnostics(account/device/kind/time-window filters + export),/diagnostics/devices,PUT /api/admin/users/{id}/debug-mode. 30-day retention. - Android
DiagnosticsReporter(connectivity, server-health, UPnP, power/Doze/screen, lifecycle, heartbeat) → Room ring buffer →DiagnosticsUploader(offline-buffered, batched). Per-device OFF switch in Settings. - Web
/admin/diagnostics: timeline (auto-polls), remote toggle, Copy-JSON / Download-NDJSON.
Same-day follow-ups
- Web: diagnostics defaults to recent-500; time window under "Advanced"; newest-first sort.
playbackkind: route + player-state events relabeled out ofupnp_sync(they fire for any output); migration 0037.- Output-picker fix: connected Bluetooth now reads "Connected" (was "Available") —
isConnectedno longer derived from the wrongconnectionStatesignal. - Per-skip
track_changeevent + track-identity enrichment (local_track_id+sonos_uri) so a re-cast desync is unambiguous;sonos_*zeroed when not casting.
Use it
Install the APK, enable debug mode for the account at
/admin/diagnostics, open the app on that account, reproduce the issue, then export the window.🤖 Generated with Claude Code
Downloads
- Migration 0036:
-
v2026.06.20 — Image rendering resilience + player error recovery (skip, self-heal, live refresh)
Stableandroid / Build + lint + test (push) Successful in 4m11stest-go / test (push) Successful in 46stest-web / test (push) Successful in 55stest-go / integration (push) Successful in 4m44srelease / Build signed APK (tag releases only) (push) Successful in 3m53srelease / Build + push container image (push) Successful in 1m41sreleased this
2026-06-20 21:04:04 -04:00 | 438 commits to main since this releasePer-day CalVer. Cover-art rendering and playback error-recovery hardening across server + web + Android, landed together. Issues #968 (images + player resilience) and #980 (stale-view refresh).
Covers never sit blank (#968)
One shared cover renderer per client, so a tile shows a placeholder while loading and a fallback on a 404 instead of an empty box — worst on the You might like row, whose unplayed items often have un-backfilled art.
- Web: new
Covercomponent owns the loading placeholder + error fallback; AlbumCard / ArtistCard / CompactTrackCard reuse it.ArtistCardpreviously had no error handler at all. - Android:
ServerImagenow tracks Coil's load state so the per-caller fallback doubles as a placeholder (loading) and an error painter (404), not just a null-URL guard.
Playback recovers instead of dead-ending (#968)
- Auto-skip a failed track (web + Android): a track that won't load advances to the next instead of stranding the player on "Try again" (web) / a stalled queue (Android). The error state only appears once the whole queue is unplayable. On Android the "Skipped N unplayable tracks" snackbar now tells the truth — it was firing while nothing actually skipped.
- Self-heal a stale queue: when the whole queue is unplayable — e.g. a tab/app left open across the daily 03:00 rebuild — the player re-pulls its source (a system playlist via the rotation-aware shuffle, or a radio re-seed) and resumes. Bounded to one re-pull per exhaustion so a still-broken refresh can't loop.
Browse views refresh themselves (#968)
The server now emits
playlist.system_rebuilton the daily rebuild and on manual refresh. The web client gained its first inbound SSE listener and Android consumes it viaEventsStream— both refresh the Home + playlist views, so a tab open since yesterday stops showing yesterday's mix.Stale-view refresh on an open mix (#980)
A system-playlist detail screen open across the rebuild shows a "This mix was refreshed · Refresh" banner (web) / snackbar (Android). Refresh re-resolves the rotated playlist id to the fresh mix — no forced redirect, no auto-reload, you refresh on your terms.
Verify
- Open Home (web or Android) with un-backfilled art → cover tiles show a placeholder then a fallback, never a blank box.
- Play a queue containing a dead/removed track → it skips to the next instead of dead-ending; only an all-unplayable queue surfaces "Try again".
- Leave a For You tab open across the 03:00 rebuild → the Home tiles refresh on their own; if you then play the stale queue it re-pulls the fresh mix.
- Open a system-playlist detail screen and trigger a refresh → a "This mix was refreshed · Refresh" prompt appears; Refresh lands on the new mix.
🤖 Generated with Claude Code
Downloads
- Web: new
-
released this
2026-06-12 21:26:09 -04:00 | 462 commits to main since this releasePer-day mutable CalVer, re-cut forward to
3e258507. A full day of Android UPnP/Sonos cast hardening on top of the morning's recommendation batch.Cast survives a locked screen (PR #97)
While casting, the wrapped ExoPlayer is paused, releasing its
WAKE_MODE_NETWORKWiFi/wake locks — so on a locked, on-battery phone the WiFi power-saved within seconds and starved the liveness poll, dropping a perfectly-healthy speaker. NewCastNetworkLock(high-perf/low-latencyWifiLock+ partialWakeLock) held for the lifetime of a cast.Don't drop the speaker (or blast local audio) on a phone-side network gap (PR #98)
A transient
ServerDowngap (WiFi still associated) tripped the drop threshold and fell back to the local player — which started playing the song out of the phone while the Sonos was still streaming it. The drop is now gated onNetworkStatusController: only drop when the phone's network isHealthy. During a phone-side outage the route is held and the poll reconciles on recovery (a local fallback couldn't reach the server either).Adopt a running session instead of clear+reload (PR #98)
Selecting a speaker that's already playing your queue (a session the phone got disconnected from but the speaker kept streaming) now attaches in place — syncs to its live track/position, no queue wipe, no jarring restart — and skip/seek drive its live queue. Falls through to the full reload only when it isn't your queue.
Anti-stickiness + faster recovery (PR #98)
- Powered-off speakers now leave the picker (lastSeen + post-scan prune) and the selection reverts to the phone, so a later play never targets a ghost route.
- On network recovery, a held cast reconciles immediately rather than waiting for the next poll tick.
Verify on device — on battery (USB masks the network-gap class)
- Cast → unplug → lock → keeps playing.
- Cast → brief network blip → Sonos keeps playing, phone does not blare local audio, reconciles on recovery.
- Phone dropped but Sonos still playing → re-select the group → attaches to the live session, skip works (no restart).
- Power a speaker off → reopen the picker → it disappears; selection falls back to phone.
Also in this release (recommendation tuning)
- You-might-like fallback (#790), taste 2b (
taste_overlaparm), recommendation metrics (Settings → Recommendation metrics). Deferred: taste phase 3 (context).
🤖 Generated with Claude Code
Downloads
-
released this
2026-06-11 23:34:02 -04:00 | 471 commits to main since this releasePer-day mutable CalVer, moved forward to
7b7bd0c3(PR #95). Adds a
liked-entity fallback so the "You might like" rows fill out for heavy
listeners.You-might-like fallback (this re-cut — PR #95)
The taste roll-up surfaces top-similar albums/artists, which for a heavy
listener are mostly ones they already play — so the dedup (vs Most Played +
Rediscover + Last Played) could strip the section to a single tile. When a row
comes up short, it now tops up from your liked artists/albums (a much larger
pool), reusing the same exclusions so nothing is shown twice or suggests an
actively-played entity. Read-side — fills immediately after deploy, no rebuild."You might like" — on web + Android (#790)
A You might like block (albums + artists) directly under the system
playlists row on web and Android, from the server's daily-built, taste-aware
recommendations + the liked fallback above.Taste profile — Minstrel learns taste (#796)
Persistent, decaying per-user taste model that re-ranks every recommendation
surface. Graded engagement (completion/skip, time-decayed); no dislike button
(passive, track-scoped); aTasteMatchscoring term toward learned taste.Earlier in this release
History repair + offline robustness (PR #90), server rows + taste profile
(PR #91), Android row (PR #92), repositioned (PR #93), web row (PR #94).Verify
- Redeploy this
:latest. On Home (web or Android), the You might like
section sits under the playlists row. With a decent number of likes it now
fills out (no longer a single tile) immediately — the liked fallback is
read-side, so no rebuild/gate wait is needed for it.
🤖 Generated with Claude Code
Downloads
- Redeploy this