-
v2026.06.02
Stablereleased this
2026-06-02 22:30:20 -04:00 | 239 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 | 318 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 | 617 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 | 635 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 | 640 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 | 643 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 | 648 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 | 664 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 | 676 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
-
v2026.05.15.1 Stable
released this
2026-05-16 00:32:34 -04:00 | 685 commits to main since this releaseHotfix for v2026.05.15.0.
System playlists were fully broken in v2026.05.15.0: the five new discovery mixes (Deep Cuts, Rediscover, New for you, On This Day, First Listens) shipped with producers/registry/clients but their
system_variantvalues were missing from theplaylistsCHECK constraints. Because the daily build runs in a single all-or-nothing transaction, the first new-mix insert aborted the whole build — so For You / Discover refresh returned 500 and the daily rebuild failed too.Fixed by migration
0028_discovery_mix_variants(extends bothplaylists_kind_variant_consistentandplaylists_seed_consistentto allow the five seedless variants). CHECK-only change. Mobile client 2026.5.15+7.After deploying, confirm For You / Discover refresh works and the five mixes appear.
Downloads