-
v2026.06.03
Stabletest-go / test (push) Successful in 30sandroid / Build + lint + test (push) Successful in 4m49stest-go / integration (push) Successful in 10m56srelease / Build signed APK (tag releases only) (push) Successful in 4m15srelease / Build + push container image (push) Successful in 17sreleased this
2026-06-03 15:45:09 -04:00 | 339 commits to main since this releaseSame-day re-cut on the per-day mutable CalVer scheme. This re-cut is a hotfix for v2026.06.03's UPnP path — operator on-device testing with a real Sonos speaker found three issues that ship together in this re-cut.
Same-day hotfix re-cut (PR #78)
Root cause — UPnP error 714 from Sonos
On-device tap of a Sonos device dismissed the picker sheet but no audio routed. Logcat:
UPnP select: mint token for track=da85199c-..., route=Living Room POST /api/cast/stream-token → 200 (token minted) SetAVTransportURI to http://minstrel.fabledsword.com/api/tracks/.../stream?token=...&exp=... <-- 500 from Sonos SoapFaultException: SOAP fault 714 (IllegalMimeType)UPnP error 714 = "Illegal MIME-Type" — Sonos does a HEAD/probe to detect the audio MIME at the URL. Our
cast_token.goURL builder trustedr.TLSfor the scheme, which is nil behind a TLS-terminating reverse proxy, so the URL came outhttp://. Sonos hithttp://→ 301-redirected tohttps://by the proxy → MIME probe found no audio body → 714.Fix: honor
X-Forwarded-Proto+X-Forwarded-Hostbefore falling back tor.TLS+r.Host. URL the speaker fetches now matches the scheme/host the client used.Sonos friendlyName cleaned in the picker
Devices appeared as
Living Room - Sonos Play:1 Media Renderer - RINCON_5CAAFD79...— the room name buried at the start of a long ID-laden string.UpnpDiscoveryController.displayName(...)now strips on the first-for Sonos-manufactured devices so the chip shows justLiving Room/Kitchen/ etc. Generic UPnP devices that append a(192.168.x.x)IP suffix also get that stripped. Manufacturer + model still render as the row subtitle, so the device-type info isn't lost.Operator-diagnostic plumbing
- Loud
Timber.w/Timber.ilogs on every code path ofselectUpnp— the silent early-return cases (currentTrack == null,transportFor() == null) now log loudly, and the success path adds breadcrumbs (mint, SetAVTransportURI, Play, done) so future SOAP failures show exactly how far we got. ReleaseTreeTimber tree on release builds. Debug builds hadDebugTree; release builds had nothing planted, so the diagnostic Timber output was silently dropped in production.ReleaseTreenow plants at WARN+ viaandroid.util.Log.printlnwith the canonicalMinstreltag — keeps DEBUG/INFO chatter out of production logcat while letting operator-drivenadb logcatsessions see real failures.
29 commits across four feature slices + a small bug-fix batch. PR #77.
Slice 1 — Media3 like / heart button on the media session
Heart appears in the phone notification card, lock-screen card, Pixel Watch system media controls, and Android Auto. Toggle routes through the existing
LikesRepository.toggleLikeso it inherits the offline-resilient MutationQueue.LikeMediaCallbackgrantsCMD_TOGGLE_LIKEinonConnect(Media3 issue #2679 guard) and routesonCustomCommandthrough the repository.MinstrelPlayerServiceinjectsLikesRepository, attaches the callback, sets initial preferences, and launches acurrentMediaItem × observeIsLikedlike-state job so the icon mirrors cross-device likes (web tap flips the phone notification heart automatically).PlayerController.setQueuenow thread-dispatches to the MediaController'sapplicationLooper— cold-boot resume was crashing withIllegalStateException: method is called from a wrong thread.
Notification-surface end-to-end verified.
Slice 2 — Bluetooth output picker
Symfonium-style device chip above the scrubber + bottom sheet showing all available routes (built-in / wired / Bluetooth).
OutputPickerController(Hilt singleton, MediaRouter facade) exposesroutesState: StateFlow<RouteSnapshot>with passive-by-default discovery, upgrades to active when the sheet opens.OutputPickerViewModelprojects routes + owns sheet lifecycle, fans discovery toggle to the controller.DeviceChip+OutputPickerSheet(Material 3 ModalBottomSheet) Compose UI with Lucide icons +BLUETOOTH_CONNECTpermission flow.OutputRoute.Protocolenum (SYSTEM/UPNP/CAST/SONOS) baked in as forward-compat — UPnP slot in without data-model changes.
Slice 3 — UPnP / DLNA / Sonos basic playback
Discovery + playback to any UPnP-compliant MediaRenderer (Sonos basic, Yamaha MusicCast, Bose SoundTouch, generic DLNA renderers). Audio flows server → speaker directly; the phone is just the controller.
Server:
- HMAC-SHA256 stream token:
SignStreamToken(secret, trackID, exp)+VerifyStreamTokenwithhmac.Equalconstant-time compare. The stream handler accepts either session cookie OR?token=&exp=query params. - New
POST /api/cast/stream-tokenendpoint — clampedexpSeconds, returns{token, exp, url}for the client to hand off to AVTransport. MINSTREL_STREAM_SECRETenv var with file-fallback at<DataDir>/stream_secret(auto-generated on first boot, mode 0600, logged once).auth.OptionalUsermiddleware — attaches user on cookie but doesn't 401 on absence.- Stream handler auth-checks BEFORE the DB lookup (also closes a small probe-via-404 info leak).
Client:
SsdpDiscovery— UDP multicast listener on239.255.255.250:1900, passiveNOTIFYlisten + explicitM-SEARCHwhen sheet opens,MulticastLocklifecycle.DeviceDescription— pull-parser over the UPnP device XML, filters non-renderers, resolves relative control URLs.SoapClient— minimal SOAP 1.1 envelope builder + POST via the shared OkHttp; throwsSoapFaultExceptionwith the UPnPerrorCode.AVTransportClient— v1 surface:SetAVTransportURI+Play+Stop.UpnpDiscoveryController— Hilt singleton composing SSDP + DeviceDescription + AVTransport.OutputPickerControllernowcombines system + UPnP routes;select()branches byprotocol: SYSTEM → MediaRouter, UPNP → mint token + SOAPSetAVTransportURI + Play.OutputPickerSheetgains a multicast-blocked footer hint.
Slice 4 — System playlist daily rotation + diversity
SQL audit of the 5 discovery-mix queries showed Rediscover, New for you, and First listens have no date parameter in their
ORDER BY— same content day-over-day until library state shifts.- Unified the 5 near-identical
produceXxxfunctions into oneproduceDiscoveryMix(spec)factory + a per-mixdiscoveryMixSpecslice. Adding a new mix is now one struct literal + a SQL query. dailyRotate: trueon all 3 deterministic mixes — applies a daily-deterministic offset rotate of the pool BEFORE diversify+truncate so each day's slice rotates while contiguous-block ordering is preserved (matters for album-coherent mixes). Deep Cuts + On this day stayfalsebecause their SQL already day-keys viamd5(id||$2::text).diversify: trueon all 5 — per-album ≤2 / per-artist ≤3 caps applied everywhere, withtopUpFromRawfallback: when caps strip the pool below 100 (album-heavy libraries), append non-capped tracks from the raw pool until the target is hit. Diversity where possible, full playlist always.
Bug-fix batch (Android)
LikesRepository.observeLikedTrackIds(): Flow<Set<String>>— direct DAO query, notrackDaojoin.PlaylistDetailViewModel+AlbumDetailViewModelswitched to it; cross-device likes whose track row isn't cached locally now correctly mark playlist/album rows as liked.PlaylistsRepositorywrapscoverPath/coverUrlthroughresolveServerUrlso the playlist header art actually fetches viaBaseUrlInterceptor(was a relative URL Coil couldn't resolve).
Verify on device
- Notification heart tap on a playing track flips state; cross-device (web tap) flips the phone notification heart automatically.
- Open NowPlaying sheet → Bluetooth devices listed, selection routes audio.
- On a Sonos / UPnP speaker on the LAN: open NowPlaying sheet → device appears with manufacturer name; select → audio plays through it.
- Multicast-blocked network: sheet shows "Your router may be blocking multicast discovery."
- Open Rediscover / New for you / First listens today and tomorrow → content visibly rotates day-over-day.
- Playlist detail header art renders (was blank); liked-state badge on tracks within a playlist reflects actual liked state.
Downloads
- Loud
-
v2026.06.02
Stablereleased this
2026-06-02 22:30:20 -04:00 | 374 commits to main since this releaseSame-day re-cut on the per-day mutable CalVer scheme. This re-cut is a hotfix for the v2026.06.02 (run #289) connectivity regression — fresh installs of the prior cut couldn't sign in and were stuck at the Welcome screen.
Hotfix: Android interceptor order (PR #76)
NetworkModule.ktchainedBaseUrlInterceptorbeforeAuthCookieInterceptor. Both interceptors scope work onhost == PLACEHOLDER_HOST(drift #568/#569, to keep the session cookie off Coil image fetches to MusicBrainz / coverartarchive / Lidarr). With BaseUrl first, the URL was rewritten to the real Minstrel host before auth ran — so auth's placeholder check failed on every request, the session cookie was never attached on outgoing requests, andSet-Cookiefrom login was never captured. TheAuthCookieInterceptorTestchains the interceptors in the correct order, which is why the regression went undetected. Fix: swap to(auth, baseUrl, logging). Client-side only — server is unchanged.Anyone who installed the prior v2026.06.02 APK needs to re-install from this release.
Highlight: drift audit shipped (26 findings)
Multi-system audit run on 2026-06-02: 14 auditors (12 subsystem + 2 cross-cutting) fanned out across server / Android / web / Flutter looking for the 10-pattern drift taxonomy. 26 candidate findings surfaced; 24 confirmed by adversarial verify and shipped, 1 cancelled as duplicate, 1 doc-only correction. Scribe parent task: project 12 / task 552.
Highest-impact fixes
- Server
/api/meprofile shape (#578). The endpoint emitted UserView (id/username/is_admin) but Android's MyProfileWire expected display_name + email. Every Settings → Profile open silently saw nulls; saving from that blank state DESTROYED stored profile fields via the "empty string clears to NULL" path. Server now returns the full profile shape. Existing test extended; new regression-guard test added. - AuthCookieInterceptor scoping (#568 #569). The Minstrel session cookie was being attached to every external request that the shared OkHttpClient services (Coil image fetches to artwork.musicbrainz.org / coverartarchive.org / Lidarr /MediaCover URLs), and a 401 from any external host wiped the user's session. Both attach + clear now scoped to the placeholder.invalid sentinel host — mirrors the BaseUrlInterceptor fix from earlier today.
- System-playlist rotation alignment (#563 #564). Server's rotation whitelist had
for_you+discoveronly but migrations 0021 + 0028 added 6 more variants; plays from Rediscover / Deep Cuts / Songs Like X / etc. didn't advance the per-user rotation. Android also emittedsource="playlist:<variant>"while the server matched the bare variant, so system-mix plays from Android Home never registered at all. Whitelist extended; Android source format aligned with web's bare-variant contract. - Cold-boot resume coordination (#560 #562). ResumeController could race the MediaController IPC handshake — setQueue early-returned on null controller and the persisted queue was silently dropped. ResumeController now awaits a new
awaitReady()suspend before calling setQueue, and setQueue gains anautoplayopt-out so cold-boot resume restores paused (not auto-playing). - Cross-device unlike on Android (#570). LikesRepository.refreshIds only INSERTed — rows the server no longer surfaced (because the user unliked elsewhere) stayed in the local cache forever. Atomic
replaceAllForUserDAO transaction now wipes-then-inserts so the local set is exactly what the server reports. - CONFLATED playback-error channel (#561). The reporter's burst-coalescing path was dead code —
Channel.CONFLATEDsilently dropped every emission except the latest. A network blip that failed 5 tracks surfaced only the last; the admin inbox only got one POST. NowChannel.BUFFERED. - Periodic GC worker (#565 #566 #567 #574 #575). New
internal/gcpackage — one Worker, 5 idempotent sweeps on a 1h tick: close stale play_events (24h), close idle play_sessions (6h) / empty-starved (1h), expire scrobble_queue failed rows (14 days), reset stuck system_playlist_runs (10 min), delete expired password_resets. Closes the lifecycle-gap cluster from the audit in one shot. - Profile + lifecycle UX (#553 #554 #555 #557 #558 #559 #571 #572 #576 #577). Tailwind class fix on the playback-errors Delete confirm button; play_started
sourcefield added to the EventRequest TS type; coverage rollup test extended to deezer/lastfm; /forgot-password + /reset-password/[token] reachable without auth; SSR-only redirects converted to client-side; scanner indexes .opus/.aac/.wav; delete.go docstring honest about missing reconcile; LikesRepository uses real user UUID + clears on user-switch; RequestsViewModel doesn't refresh() on offline-queued cancel.
Verify on device
- Fresh APK install: sign in succeeds and lands on Home (was stuck at Welcome).
- Android Settings → Profile shows stored display_name + email (was wiping them).
- Drag-dismiss + cold-boot of NowPlaying: pop animation latches, resume opens paused (not playing).
- /admin/playback-errors → Delete file modal: confirm button is the proper Oxblood destructive color.
- Tap Cancel on a request while offline — row stays gone, no Error screen.
- Discover view shows artist artwork (regression for prior fix).
- New scrubber thumb reads as a small flat circle on a thin 4dp track.
Carried from prior cuts
Earlier same-day cuts already included: playback-errors slice (server + Android + admin UI), drag-dismiss latch, portrait lock, Material LibraryMusic icon (later reverted to Lucide.LibraryBig), notification tap → NowPlaying, NowPlaying polish (dominant swatch, slim scrubber, swipe tabs, refresh flicker fix, Start Radio preserves playback), web alphabet rail page-chasing, Songs Like fix, two-pane desktop NowPlaying, scrubber smoothing, CalVer-with-iteration APK versionName.
Downloads
- Server
-
v2026.06.01
Stablereleased this
2026-06-01 21:55:49 -04:00 | 453 commits to main since this releaseFirst release on the new per-day mutable tag scheme. Batch release covering web UX tiers A/B/C, M8 native-Android port, M7 server polish, and the CI dedup pass.
Web UX (this session)
- Tier A — Full-screen NowPlaying view, global keyboard shortcuts (Space/J/L/M/Arrows), multi-select + bulk actions on track lists, Home secondary system-playlists backflow.
- Tier B — Debounced quick-filter on every Library tab; smart empty-state onramps everywhere.
- Tier C — Keyboard reorder on playlist tracks, position-derived crossfade (0-12s, default off), copy-link button on owned public playlists.
Platform
- M8 Android native — full Kotlin/Compose client with Media3, Room cache-first storage, hydration prewarmer; ships as
minstrel-v2026.06.01.apk. - M7 web feature sweep — full Subsonic-API parity client + admin UI, system playlists v2 (For You / Discover / Deep Cuts / Rediscover / Most Played multi-row), event bus, cache-first Library, offline v2.
- Server — skip classification moved server-side, Lidarr bootstrap, admin dashboard, admin requests queue with override modal.
Infrastructure
- Migrated to Gitea Actions with keystore signing, registry auth via
REGISTRY_TOKEN. - CI dedup pass: dropped
pull_requesttriggers (push covers it on a single-author repo), addedconcurrencywithcancel-in-progressto every workflow. - Tag scheme changed to per-day mutable CalVer (
vYYYY.MM.DD). Same-day re-releases force-move the tag. - Main pushes now tag both
:mainand:latest; tag pushes tag both:vYYYY.MM.DDand:latest.
Downloads
-
released this
2026-05-21 15:42:28 -04:00 | 752 commits to main since this releaseHighlights
Last meaningful-feature release on Flutter. Future releases on this codebase are bugfix-only; the v1 Android native rewrite begins in parallel.
Player / Wear OS
- Notification + Pixel Watch transport controls stay live across idle periods. Previously the buttons could go dead after ~5 min idle (Wear companion's MediaController held a stale binding to a torn-down MediaSession). Idle teardown now soft-tears-down only — display state clears, but the FGS + MediaSession persist so the Watch binding stays valid.
Playlists
- Tapping a system-playlist tile before its mix has loaded now surfaces clear SnackBar feedback ("Mix isn't ready yet — try again in a moment" / timeout / API error) instead of silently stalling.
Behind the scenes
- Drift test cohort re-enabled in CI after the
ci-flutterimage picked up libsqlite3-dev. - Tier-A dep sweep (audio_session 0.2, flutter_lucide, permission_handler now direct), Go server bumps, CI runner-image policy migration.
Known limitations
- Quarantine throwing-path test skipped pending Fable #476 (Riverpod StreamProvider lifecycle in async catch path).
- Offline-view flash on app resume (Fable #473) deferred — bugfix-future on Flutter; resolved in native rewrite.
- audio_service plugin is upstream-stale; this release works around the lifecycle bug client-side rather than waiting for a maintenance update that isn't coming.
What's next
Native Android rewrite begins under
android/sibling toflutter_client/. Targets: Media3 directly, Wear OS Compose first-class, Android Auto support, native MediaSessionService lifecycle. iOS will not ship.Downloads
-
v2026.05.19.3 Stable
released this
2026-05-19 15:48:31 -04:00 | 770 commits to main since this releaseMobile client
2026.5.19+13. Flutter client + web-test changes only — no server code or server migration changes. Includes one Flutter (on-device) drift schema migration: 10 → 11.Headline — #66: playback stall resilience
Fixes the real-world failure you hit on poor coverage: a track finishes and the next one never starts (the stream attempt fails and nothing retries — the queue silently hangs). The player now:
- runs a 15s stall watchdog keyed off buffered-position progress (not wall-clock alone),
- makes one bounded recover attempt on the stalled track,
- then skips to the next cached track, or pauses cleanly, so the queue keeps moving instead of dead-ending.
Also in this release
- #406 — drop legacy home snapshot path. Removes
CachedHomeSnapshot/ legacyhomeProvider/ home-data JSON encoders; the metadata prefetcher now warms artists off the per-itemhomeIndexProvider. DriftschemaVersion10 → 11 drops the deadcached_home_snapshottable; the migration chain remains valid for upgrades from any prior schema. - #374 — web route-test triage. Restored the
discover/requestsSvelte route test suites (they crashed at module-load on the un-mocked$app/state); test-only, no runtime impact.
Caveat — verify in the field
#66 has not been device-verified on a throttled / poor-coverage connection (tracked). After installing, validate on a weak or airplane-toggled connection: when a next-track stream fails, the queue should retry once and then skip to a cached track (or pause cleanly) rather than hanging silently.
Regression check
Media notification + watch transport controls should still appear during playback (the v2026.05.19.2 fix stands — nothing in this release touches that path).
Downloads
-
v2026.05.19.2 Stable
released this
2026-05-19 07:48:25 -04:00 | 775 commits to main since this releaseHotfix for v2026.05.19.1. Mobile client
2026.5.19+12. Flutter client only — no server/migration changes.Restores the media notification + watch controls, which were completely absent on physical devices (worked on the emulator).
Root cause (isolated on a physical Pixel 6 Pro via device logcat, after disproving permission / PathParser / FGS / aapt2 / git-tag / resource-shrinker): the v2026.05.19.0 custom favorite
MediaControl.customreferences a drawable that, while valid and present in the tagged source, is absent from the shipped CI release APK. audio_service resolves the icon to id 0 andPlaybackStateCompat.CustomAction.Builderthrows on every state broadcast, which aborts the entire media notification.Fix: remove the custom favorite control. The notification rebuilds with the standard transport controls (audio_service's own always-present icons). Like/favorite remains available in-app and on the lock-screen media UI. The auto-minimize-on-teardown fix from v2026.05.19.1 stands.
The underlying "valid tagged drawable not in the CI APK" is a separate CI build-packaging investigation (tracked) and does not block playback or the notification.
After installing: confirm the media notification appears during playback and the watch shows transport controls.
Downloads
-
v2026.05.19.1 Stable
released this
2026-05-18 23:09:15 -04:00 | 778 commits to main since this releaseHotfix for v2026.05.19.0. Mobile client
2026.5.19+11. CI green.Two issues surfaced on a physical Android 13+ phone (both worked on the emulator — exactly the gap the device-unverified .0 release flagged):
- Media notification was absent on physical phones. The app never requested
POST_NOTIFICATIONSat runtime — the manifest declares it and the foreground media service is correct, but Android 13+ denies the permission by default until the app explicitly asks. Addedpermission_handler; the app now requestsPermission.notificationonce at startup (post-first-frame, Android-only, no-op on <13 or once already decided). On first launch of this build you'll get the system notification prompt — allow it (if it was previously blocked in system settings, clear it there and the prompt returns). - Full player could strand on "Nothing playing." When the v2026.05.19.0 idle/dismiss MediaSession teardown cleared the current track while the full Now-Playing screen was open, it showed an empty screen instead of closing. It now auto-minimizes back.
No server/migration changes — Flutter client only. After installing, confirm: the notification permission prompt appears and the media notification shows during playback; and that triggering the idle/dismiss teardown from the full player auto-minimizes it.
Downloads
- Media notification was absent on physical phones. The app never requested
-
v2026.05.19.0 Stable
released this
2026-05-18 21:43:05 -04:00 | 783 commits to main since this release26 commits since v2026.05.18.0. Mobile client
2026.5.19+10. CI green.Headline: the Flutter media/playback experience now behaves like a real media app, and the whole app moved to the Lucide icon set.
MediaSession / playback (Flutter):
- Fixed the mini-player / watch ~30s stale-track race (logical-index base — same audio_service stream the Pixel Watch mirrors, so track-change staleness on the watch improves too).
- Audio focus, interruptions & becoming-noisy via
audio_session— pause for phone calls, duck for navigation prompts, pause on earbud unplug / Bluetooth drop instead of blasting the speaker. - MediaSession terminal-state teardown — the watch tile / notification now close when playback sits idle 5 min or the app is dismissed while paused (previously lingered forever on a stale track).
- Resume-last-session on relaunch, plus resume on a media-button press when the session was fully torn down.
- Periodic PlaybackState refresh so the lock-screen / Wear / Android Auto scrubber advances smoothly; notification progress bar.
- Notification favorite (heart) via
MediaControl.custom+customAction— not the upstream-brokensetRatingpath. - AudioServiceConfig art downscale + preload for external surfaces.
- Playback errors surfaced via a debounced/coalesced SnackBar instead of a silent skip.
Lidarr (backend):
- Durable approve — a Lidarr-down approve no longer 503s / stays pending; the reconciler idempotently retries the add until it lands (migration
0031, additive). No failed-state by design: Lidarr keeps trying, operator monitors. - Idempotent
Create()— a re-request for an in-flight MBID returns the existing row instead of duplicating.
Design system:
- Full Lucide migration: 30 files swept Material
Icons.*→LucideIcons.*;LikeButtonand the notification heart re-derived from the verbatim Lucide heart path (outline vs filled).
Migration
0031applies automatically on startup.⚠️ This batch shipped CI-green but, per an explicit decision, without on-device verification. After installing this release APK, smoke-check on the phone + Pixel Watch: icons render correctly app-wide; mini-player/playlist marker update instantly; call/earbud interruptions; watch tile teardown + media-button resume; the notification favorite. Hotfix-release anything off.
Deferred (tracked, not in this release): #399 — add libsqlite3 to the flutter-ci runner image to re-enable the skipped drift tests.
Downloads
-
v2026.05.18.0 Stable
released this
2026-05-17 22:41:32 -04:00 | 799 commits to main since this release11 commits since v2026.05.15.1. Mobile client
2026.5.18+9. CI fully green — including, for the first time, the integration suite.Headline: integration tests now run in CI. Previously
test-go.ymlran onlygo test -short(no database), so the entire integration suite silently skipped — "CI green" never covered the API/DB/scanner surface. This release adds anintegrationjob (Postgres service, bridge-IP discovery via the docker socket,minstrel migrate,go test -p 1 -race ./...) that surfaced and cleared ~200 latent failures. It is now a durable safety net for everything that ships.Also in this release:
- #321 —
minstrel admin reset-password+minstrel migratesubcommands (operator can recover admin without DB surgery). - #339 — local test-DB isolation (
make test-integration) sogo testnever truncates the dev DB, plus the CI integration job. - #388 — removed the global 500-album cover-art backfill cap; per-provider rate-limited (local sources at disk speed).
- Discover suggestions — on-demand Lidarr artist art; the Flutter Discover screen now shows the ListenBrainz suggestion feed (web parity).
- Migration 0030 — relaxed the cover/artist art-source CHECK from a fixed provider allowlist to non-empty (matches the pluggable provider registry; same brittleness class as the v2026.05.15.1 discovery-mix fix).
- Cover-art / test-harness correctness fixes uncovered by the new CI integration suite.
Migrations
0028–0030apply automatically on startup. After deploying, exercise the offline two-bucket cache (incl. eviction) and the Flutter Discover suggestions feed on the deployed build.Downloads
- #321 —
-
v2026.05.16.0 Stable
released this
2026-05-16 18:59:06 -04:00 | 811 commits to main since this releaseFirst release since v2026.05.15.1. Mobile client
2026.5.16+8. Device-verified; CI green.Headline: ListenBrainz-driven recommendations work end-to-end for the first time. Two latent bugs were starving the whole recommendation system:
- The scanner never extracted the MusicBrainz recording MBID, so
tracks.mbidwas 100% NULL and the similarity worker never ran. Fixed + a one-shot library backfill (migration0029). - The similarity client was calling a ListenBrainz website route (404 every time) with an invalid algorithm. Now hits the Labs API correctly.
track_similarity/artist_similarity/artist_similarity_unmatched(Discover/suggestions) all populate.
Also in this release:
- For-You no longer silently vanishes after a week idle; tiered seed + deeper fill; Rediscover & On-this-day young-library fallbacks.
- Similarity worker throughput 5→25/tick; track-MBID backfill uncapped.
- #388: global 500-album cover-art backfill cap removed — local art at disk speed, remote providers self-throttle per provider.
- System playlists v2 (registry, 5 discovery mixes, shuffle-on-play, "Refreshed…" subtitle) and the offline two-bucket cache rework (reachability marker, settings UI, offline gating + Shuffle-all).
Migrations
0028+0029apply on startup. After deploy: confirm For-You/Discover refresh works and the similarity tables continue populating as the worker ticks.Downloads
- The scanner never extracted the MusicBrainz recording MBID, so