• v2026.05.15.0 Stable

    bvandeusen released this 2026-05-15 23:16:08 -04:00 | 687 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 | 715 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 | 728 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 | 733 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 | 737 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
  • bvandeusen released this 2026-05-13 22:37:08 -04:00 | 747 commits to main since this release

    Substantial batch since v2026.05.12.1 — 45 commits. See PR #43 for the full breakdown.

    Highlights

    • Live updates (#392, #402) — SSE event bus at /api/events/stream; Flutter consumer with screen-scoped invalidation for likes, quarantine, playlist mutations, request status, scan lifecycle
    • Per-user playlist scheduling (#403) — system playlists refresh at 03:00 in each user's local timezone via CRON_TZ-prefixed gocron jobs; PUT /api/me/timezone + flutter_timezone capture
    • Discover playlist (#400) — fixed (SELECT DISTINCT + ORDER BY plan error); daily seed rotation + jitter + 12/13 head/tail split so the same library produces noticeably different daily mixes
    • Non-blocking version check — VersionGate no longer blocks app render on /healthz; 1-minute active-use polling with bounded 3s/2s timeouts; banner surfaces when client is too old without stalling the app
    • Offline cache completion (#357) — drift-first homeProvider, library_changes retention compactor (30-day window), drift-first Liked / History / MyQuarantine tabs
    • Cover caching — cached_network_image disk cache for every cover URL (survives scroll-off + app restart); SyncController prewarms covers for new/updated entities at concurrency=3
    • Per-item rendering — new /api/home/index endpoint returns IDs only; HydrationQueue (concurrency=4) drives per-tile fetches against /api/albums/:id, /api/artists/:id, /api/tracks/:id; skeleton tiles match real-card dimensions; 220ms cross-fade reveal
    • Flutter UI polish#393 always-visible play buttons on home carousels, #396 full-screen now-playing polish (gradient backdrop + Hero animation + crossfade), #401 now-playing row highlight

    Cold-visit behavior: shaped page paints instantly with skeletons, content cascades in as the hydration queue drains. Warm visits paint fully from drift in the first frame.

    Deferred

    • Full per-track playlist detail (Fable #405) — skeleton-row variant captures most of the perceptual win; full version awaits if cold visits to very large playlists still feel sluggish
    • CachedHomeSnapshot cleanup (Fable #406) — legacy table + homeProvider left in place as revert path

    Operator notes

    • Drift schema bumps 2 → 6 (4 new tables). Migrations are in-place.
    • New users.timezone column on the server (migration 0026)
    • In-app updater should upgrade cleanly from v2026.05.12.0
    • Pull the new server container image and roll prod
    • Sideload minstrel-v2026.05.13.0.apk from this release page when CI finishes attaching it
    Downloads
  • bvandeusen released this 2026-05-11 23:58:58 -04:00 | 793 commits to main since this release

    Polish + fix batch since v2026.05.12.0. See PR #42.

    Highlights:

    • Discover system playlist now appears as a tile on both home pages (Flutter + web)
    • Full-player kebab "Go to album/artist" no longer crashes
    • Artist detail album grids re-fetch instead of rendering partial caches forever
    • Diagnostic-print cleanup pass — log noise dropped, errors-only retained

    In-app updater should upgrade you cleanly from v2026.05.12.0.

    Downloads
  • bvandeusen released this 2026-05-11 20:36:13 -04:00 | 802 commits to main since this release

    Substantial batch since v2026.05.11.2. See PR #41 for the full breakdown.

    Server perf wins on three hot endpoints:

    • /api/albums/{id} — 3 round trips to 2
    • /api/artists/{id} — 1+N round trips to 2 (32 collapsed to 2 for a 30-album artist)
    • /api/home — aggregate-first rewrites of the two scan-the-world recommendation queries

    Flutter caching reset: stale playlist reconcile, 404 recovery, full track row writes from playlist fetches, stream-cached audio files now register in the index (eviction works), conservative metadata prefetcher (artists only, one-shot per id), dropped redundant SWR refresh on album/artist providers.

    Flutter player: /queue duplicate-page-key crash fixed, full-player layout consolidated (shuffle/repeat/queue + like + kebab in one row), error/state diagnostics in place to catch silent stops.

    Flutter UX: playlist cover art now persists across the drift round-trip; 1px album-card overflow silenced.

    Operator notes: this APK is the second release signed with the persistent keystore, so the in-app updater should upgrade cleanly from v2026.05.11.2 — no sideload needed unless something goes wrong.

    Downloads
  • bvandeusen released this 2026-05-11 15:51:52 -04:00 | 819 commits to main since this release

    Two changes since v2026.05.11.1. See PR #40.

    • Persistent release signing key via CI secret. This is the first release signed with the operator's keystore, so existing installs (debug-keyed from prior releases) must be uninstalled before sideloading this APK. All future tagged builds will upgrade cleanly via the in-app updater.
    • Library Artists + Albums tabs now paginate via infinite scroll.

    Operator notes:

    • Uninstall current build on the test phone before sideloading minstrel-v2026.05.11.2.apk
    • Verify the APK installs cleanly past the scanner this time
    • Confirm signing-key secrets (ANDROID_KEYSTORE_B64, ANDROID_KEY_ALIAS, ANDROID_STORE_PASSWORD, ANDROID_KEY_PASSWORD) are in place — CI will hard-fail without them
    Downloads
  • bvandeusen released this 2026-05-11 13:47:55 -04:00 | 822 commits to main since this release

    Flutter-only follow-up to v2026.05.11.0. See PR #39 for the full breakdown.

    Highlights:

    • SWR everywhere + navigation hydration + cold-start home skeleton (no more 30s blank)
    • Library Artists tab unblocked (was 404), Albums tab in 3-up grid
    • System playlist tap now loads tracks
    • Player title/artist/cover update on skip
    • Player kebab "Go to artist" works
    • New "Start radio" menu item

    Operator notes:

    • Pull the new server container image (no server changes this release, but tag triggers a rebuild)
    • Sideload minstrel-v2026.05.11.1.apk from this release page when CI finishes attaching it
    Downloads