-
v2026.05.19.1 Stable
released this
2026-05-18 23:09:15 -04:00 | 1055 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 | 1060 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 | 1076 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 | 1088 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 | 1097 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
-
v2026.05.15.0 Stable
released this
2026-05-15 23:16:08 -04:00 | 1099 commits to main since this releaseSystem playlists v2, the offline-cache rework, and a major CI speedup.
System playlists v2 (#411)
- For-You no longer rebuilds on play; shuffle-on-play default; For-You expanded to 100 tracks; server-side per-rotation sample-history dedup so re-plays within a day vary.
- Generalized refresh affordance (kebab/Regenerate) across web + Flutter via a kind registry — generic
/api/playlists/system/{kind}/{refresh,shuffle}. - Five new mixes: Deep Cuts, Rediscover, New for you, On This Day, First Listens.
- "Refreshed …" freshness subtitle on system tiles.
- Play-event correctness: naturally-finished in-queue tracks no longer mis-recorded as skips (was degrading recommendations), plus offline play replay so mobile/cached listening actually counts.
Offline cache rework (#427)
- Reachability-based offline mode (
/healthzprobe). - Two independent cache budgets — Liked and Recently-played, 5 GB each, tunable — replacing the single cap; liked tracks are protected from rolling-cache pressure.
- Offline: system playlists gate off; an always-present Shuffle all plays the cached union; Home surfaces "Recently played" / "Liked" pools. Online, Shuffle all draws from the whole library.
CI
- Flutter dev runs drop from ~10 min to ~3 (debug APK now main-only); the runner image bakes the Android SDK so the ~7-min per-run download is gone; release/main builds materially faster.
Mobile client version 2026.5.15+6. Post-release device verification focuses on the two-bucket eviction (Settings) and the airplane-mode flows.
Downloads
-
v2026.05.14.0 Stable
released this
2026-05-14 21:17:57 -04:00 | 1127 commits to main since this releasePlayer feel, background metadata sweeper, and the first offline-resilience layer for outbound user actions.
Player
- Track-change no longer leaks the previous track's art/metadata mid-swap (
setQueueFromTracksnow sets source before broadcasting;_scheduleSwaphas a fast sync path on warm cache). - Prefetcher pre-warms cover bytes + palette for the next N tracks so the swap is instant.
- Playback stream errors auto-skip instead of stranding the UI on a broken track.
- Reverted the Wear MediaSession surface expansion (Pixel Watch 2 regressed when we advertised setRating); LikeBridge stays wired but dormant.
CacheFiller
Background sweeper that walks the drift cache for artists missing albums and albums missing tracks, fills via
/api/artists/:id+/api/albums/:id, and pre-warms covers. 10s initial, 5-min interval, 200ms throttle.Offline mutation queue
New
cached_mutationstable +MutationReplayer. Likes, hide/unhide, add-to-playlist, Lidarr request, request cancel all now persist the user's intent across network loss: optimistic local write, REST attempt, queued for replay on failure. 3s initial drain, 1-minute periodic ticker, 5-attempt drop.Fixes
- Liked tab no longer spins forever on a fresh install (
cacheFirstcold-fetch loop). - Library Artists grid: artist circles stay round at 3-column widths.
- Restored artist coverUrl on drift-cached
ArtistRef(LEFT JOIN cached_albums). - Discover system playlist no longer duplicates tracks across buckets.
Downloads
- Track-change no longer leaks the previous track's art/metadata mid-swap (
-
released this
2026-05-14 14:19:53 -04:00 | 1140 commits to main since this releasePatch batch on top of v2026.05.13.2. See PR #46 for the full breakdown.
Fixes
- Full player showed "Nothing playing." while the mini bar showed a live track. Regression from the previous release's load-then-swap rewrite: ref.listen doesn't fire on initial subscription, so opening the full player while a track was already playing left the displayed state null. initState now seeds it synchronously.
Features
-
External media controllers (Android Wear, Auto, Bluetooth dashes, lock-screen widgets) now see a complete MediaSession surface. Several handler methods were already implemented but never advertised in PlaybackState.systemActions; Android 13+ silently drops any callback for an action not in that set.
- controls list adds MediaControl.stop
- systemActions adds stop, skipToQueueItem, setShuffleMode, setRepeatMode, setRating
- stop() override actually halts the player + dismisses the notification (default was a no-op)
-
Native heart-rating wired through to LikesController. The setRating action is the standard MediaSession protocol for favorite-style buttons on Wear, lock-screen widgets, and Auto. New LikeBridge passes through configure(); _toMediaItem populates MediaItem.rating from likedIdsProvider so the right filled/outlined heart shows on every track change; PlayerActions listens to likedIdsProvider and calls refreshCurrentRating so toggles from TrackRow / kebab / another device (SSE) also reflect on the watch without waiting for the next track.
-
MediaItem.artUri populated on the first broadcast for warm-cache tracks. New AlbumCoverCache.peekCached returns the file path synchronously when the cover is already on disk; external controllers see the cover immediately instead of waiting for the async _loadArtForCurrentItem path.
Operator notes
- No schema changes.
- In-app updater should upgrade cleanly (versionCode 3 → 4).
- Sideload minstrel-v2026.05.13.3.apk from this release page when CI finishes attaching it.
Downloads
-
released this
2026-05-14 12:31:56 -04:00 | 1145 commits to main since this releasePatch batch on top of v2026.05.13.1. See PR #45 for the full breakdown.
Fixes
-
Artist tiles rendered the music-notes placeholder everywhere except the artist detail screen. CachedArtistAdapter.toRef() dropped coverUrl; cached_artists doesn't store the representative album id the server derives at query time. Queries used by artistTileProvider / libraryArtistsProvider / artistProvider now LEFT JOIN cached_albums ordered by sort_title; toRef reconstructs
/api/albums/<id>/coverfrom the first joined album. Fixes home Rediscover Artists, Library Artists tab, and Liked Artists carousel. -
Mini player cover flickered on track change. Drop AnimatedSwitcher; PlayerBar is now stateful and holds the most-recent non-null artUri. Previous cover stays visible across the audio_handler's bare-then-with-artUri rebroadcast pair, new cover snaps in the moment its artUri arrives. No transition, no placeholder flash.
-
Full player track-change still snapped art + backdrop in. Rewrite around "load first, then swap": _displayedMedia + _displayedDominant state, ref.listen schedules a preload that awaits precacheImage + albumColorProvider.future, only then setState atomically commits both. Cover, title, gradient now advance in one frame. Backdrop AnimatedContainer still tweens between successive committed colors so the gradient transition remains smooth.
Operator notes
- No schema changes.
- In-app updater should upgrade cleanly (versionCode 2 → 3).
- Sideload minstrel-v2026.05.13.2.apk from this release page when CI finishes attaching it.
Downloads
-
-
released this
2026-05-14 11:18:05 -04:00 | 1149 commits to main since this releasePatch batch on top of v2026.05.13.0. See PR #44 for the full breakdown.
Highlights
Player fixes
- Queue button on the now-playing screen no longer no-ops (missing skipToQueueItem override; now rebuilds the source list via setQueueFromTracks)
- Tapping a new track in a playlist immediately silences the previous one (pause() before async _buildAudioSource so old audio doesn't bleed through)
- Mini player cover crossfades through track changes instead of flashing slate placeholder
- Full-player track-change animation: precacheImage on the new file:// artUri + held previous dominant color keep the gradient + album art from snapping in mid-transition
Discover playlist
- Adjacent duplicate tracks fixed. interleaveBuckets now dedups across buckets — single-user servers were hit hardest because empty crossUser slots redistributed to dormant + random and shared tracks landed at the same interleaved position. Existing duplicates clear at the next 03:00-local playlist refresh.
Library + UI
- Artist circles in the Library 3-column grid stay perfectly round at any cell width (ArtistCard now mirrors AlbumCard's width-parameter pattern)
- Library Artists + Albums tabs are now drift-first. Reads from cached_artists/cached_albums populated by sync; cacheFirst handles the cold-install fallback. Tab swipes are instant from the second visit forward; loadMore + infinite scroll dropped (drift-all-at-once + GridView.builder lazy rendering handles typical libraries fine).
- systemPlaylistsStatusProvider drift-first via new CachedSystemPlaylistsStatus single-row table (schema 7). Home Playlists row paints with prior status instead of flickering through
emptyduring cold REST. - Library tab pre-warm: ref.listen on all 5 tab providers in LibraryScreen.build subscribes them upfront so the first cold visit pays the network cost in parallel rather than per-tab on swipe.
Cleanup
- 404 log noise from missing playlist covers (collages not yet built) silenced in dev console; auth/connectivity errors still surface.
Operator notes
- Drift schema bump 6 → 7 (CachedSystemPlaylistsStatus added). Migration runs in-place on first launch.
- Pull the new server container image — Discover dedup affects new playlist builds; existing duplicates in drift clear at the next 03:00-local refresh.
- In-app updater should pick this up cleanly (versionCode bumped 1 → 2).
- Sideload minstrel-v2026.05.13.1.apk from this release page when CI finishes attaching it.
Downloads