• v2026.05.19.1 Stable

    bvandeusen released this 2026-05-18 23:09:15 -04:00 | 1055 commits to main since this release

    Hotfix 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_NOTIFICATIONS at 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. Added permission_handler; the app now requests Permission.notification once 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
  • v2026.05.19.0 Stable

    bvandeusen released this 2026-05-18 21:43:05 -04:00 | 1060 commits to main since this release

    26 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-broken setRating path.
    • 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.*; LikeButton and the notification heart re-derived from the verbatim Lucide heart path (outline vs filled).

    Migration 0031 applies 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

    bvandeusen released this 2026-05-17 22:41:32 -04:00 | 1076 commits to main since this release

    11 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.yml ran only go test -short (no database), so the entire integration suite silently skipped — "CI green" never covered the API/DB/scanner surface. This release adds an integration job (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 migrate subcommands (operator can recover admin without DB surgery).
    • #339 — local test-DB isolation (make test-integration) so go test never 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–0030 apply automatically on startup. After deploying, exercise the offline two-bucket cache (incl. eviction) and the Flutter Discover suggestions feed on the deployed build.

    Downloads
  • v2026.05.16.0 Stable

    bvandeusen released this 2026-05-16 18:59:06 -04:00 | 1088 commits to main since this release

    First 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.mbid was 100% NULL and the similarity worker never ran. Fixed + a one-shot library backfill (migration 0029).
    • 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+0029 apply on startup. After deploy: confirm For-You/Discover refresh works and the similarity tables continue populating as the worker ticks.

    Downloads
  • v2026.05.15.1 Stable

    bvandeusen released this 2026-05-16 00:32:34 -04:00 | 1097 commits to main since this release

    Hotfix 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_variant values were missing from the playlists CHECK 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 both playlists_kind_variant_consistent and playlists_seed_consistent to 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

    bvandeusen released this 2026-05-15 23:16:08 -04:00 | 1099 commits to main since this release

    System 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 (/healthz probe).
    • 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

    bvandeusen released this 2026-05-14 21:17:57 -04:00 | 1127 commits to main since this release

    Player 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 (setQueueFromTracks now sets source before broadcasting; _scheduleSwap has 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_mutations table + 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 (cacheFirst cold-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
  • bvandeusen released this 2026-05-14 14:19:53 -04:00 | 1140 commits to main since this release

    Patch 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
  • bvandeusen released this 2026-05-14 12:31:56 -04:00 | 1145 commits to main since this release

    Patch 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>/cover from 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
  • bvandeusen released this 2026-05-14 11:18:05 -04:00 | 1149 commits to main since this release

    Patch 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 empty during 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