• v2026.06.02
    android / Build + lint + test (push) Successful in 5m36s
    release / Build signed APK (tag releases only) (push) Successful in 4m28s
    release / Build + push container image (push) Successful in 2m1s
    Stable

    bvandeusen released this 2026-06-02 22:30:20 -04:00 | 239 commits to main since this release

    Same-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.kt chained BaseUrlInterceptor before AuthCookieInterceptor. Both interceptors scope work on host == 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, and Set-Cookie from login was never captured. The AuthCookieInterceptorTest chains 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/me profile 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 + discover only 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 emitted source="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 an autoplay opt-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 replaceAllForUser DAO 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.CONFLATED silently dropped every emission except the latest. A network blip that failed 5 tracks surfaced only the last; the admin inbox only got one POST. Now Channel.BUFFERED.
    • Periodic GC worker (#565 #566 #567 #574 #575). New internal/gc package — 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 source field 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
  • v2026.06.01
    test-web / test (push) Successful in 45s
    release / Build signed APK (tag releases only) (push) Successful in 3m57s
    release / Build + push container image (push) Successful in 19s
    Stable

    bvandeusen released this 2026-06-01 21:55:49 -04:00 | 318 commits to main since this release

    First 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_request triggers (push covers it on a single-author repo), added concurrency with cancel-in-progress to 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 :main and :latest; tag pushes tag both :vYYYY.MM.DD and :latest.
    Downloads
  • bvandeusen released this 2026-05-21 15:42:28 -04:00 | 617 commits to main since this release

    Highlights

    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-flutter image 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 to flutter_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

    bvandeusen released this 2026-05-19 15:48:31 -04:00 | 635 commits to main since this release

    Mobile 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 / legacy homeProvider / home-data JSON encoders; the metadata prefetcher now warms artists off the per-item homeIndexProvider. Drift schemaVersion 10 → 11 drops the dead cached_home_snapshot table; the migration chain remains valid for upgrades from any prior schema.
    • #374 — web route-test triage. Restored the discover / requests Svelte 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

    bvandeusen released this 2026-05-19 07:48:25 -04:00 | 640 commits to main since this release

    Hotfix 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.custom references 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 and PlaybackStateCompat.CustomAction.Builder throws 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

    bvandeusen released this 2026-05-18 23:09:15 -04:00 | 643 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 | 648 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 | 664 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:

    • #321minstrel 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 00280030 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 | 676 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 | 685 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