bvandeusen 369ed800b9 fix(flutter): non-blocking version check + 1h staleness throttle
Cold-start spinner was up to ~38s on slow / remote connections
because VersionGate blocked the entire ShellRoute on /healthz, with
the default dio's 8s connect + 30s receive timeouts. The /healthz
server handler itself is fine (microsecond JSON encode); the blocker
was client-side. Three issues fixed in one pass:

1. Optimistic render. VersionGate becomes a ConsumerStatefulWidget
   that always renders its child and just activates the version
   check controller on mount. The "you're too old" experience moves
   from a full-screen hard-block (_TooOldScreen, deleted) to a soft
   banner above the AppBar that lets the user keep playing cached
   content while they update.

2. 1h-throttled background check. New VersionCheckController
   (AsyncNotifier) hydrates from a secure-storage cache on boot,
   returning the cached result instantly. If the cache is missing
   or >1h old, fires a background recheck. AppLifecycleState.resumed
   triggers recheckIfStale so foregrounding after >1h re-checks
   without per-frame hammering. "Check now" button on the banner
   bypasses the staleness gate so dev iteration (push new APK, want
   to see banner clear) doesn't wait an hour.

3. Bounded health-check dio. The /healthz request uses a dedicated
   dio with connectTimeout: 3s + receiveTimeout: 2s rather than the
   default 8s / 30s. Health probes should fail fast — if the server
   can't ack in 5s, the user has bigger problems than a stale
   min_client_version and the cached value remains in effect.

Cache keys live alongside the existing tz cadence cache in
flutter_secure_storage (kResult + kAtMs). On any network error or
parse failure, _runCheck soft-fails without bumping the timestamp,
so the next staleness check will retry.

VersionTooOldBanner renders in _ShellWithPlayerBar's Column above
the existing UpdateBanner — the two coexist when both apply
(server rejects you AND an APK is queued).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:14:42 -04:00
2026-04-18 17:33:35 +00:00

Minstrel

A self-hosted music server that thinks for you. Smart shuffle, contextual likes, ListenBrainz-aware radio, and Lidarr automation — server-side, so every client (web, mobile, Subsonic third-party) gets the same intelligence.

State and intelligence belong on the server, not the client.

Highlights

  • OpenSubsonic-compatible. Existing Subsonic clients (DSub, Symfonium, play:Sub, etc.) connect with no special configuration.
  • Server-side smart shuffle. Track-similarity vectors, dual-like model (general + contextual), and session memory keep mixes coherent across devices.
  • ListenBrainz radio. Session-aware "more like this" pulls from ListenBrainz similarity data, not a static genre tag.
  • Lidarr integration. Triggered scans, request-driven album imports, and a quarantine flow when something doesn't fit.
  • Built-in web SPA. Full-feature library, search, queue, playlists, and admin — no separate frontend container to deploy.
  • Flutter mobile client in flight. Tracking issue #356.

Quickstart

# compose.yaml
services:
  minstrel:
    image: git.fabledsword.com/bvandeusen/minstrel:latest
    ports: ['4533:4533']
    volumes:
      - ./music:/music:ro
      - minstrel-data:/data
    environment:
      MINSTREL_DATABASE_URL: postgres://minstrel:minstrel@db:5432/minstrel?sslmode=disable
      MINSTREL_LIBRARY_SCAN_PATHS: /music
    depends_on: [db]

  db:
    image: postgres:17
    environment:
      POSTGRES_USER: minstrel
      POSTGRES_PASSWORD: minstrel
      POSTGRES_DB: minstrel
    volumes: [pgdata:/var/lib/postgresql/data]

volumes:
  minstrel-data:
  pgdata:
docker compose up -d

After the stack is up, visit http://localhost:4533/register and create your admin account. The first user to register on a fresh instance is automatically marked as the administrator; subsequent users can register through the same form (or via invite tokens generated from the admin Users panel, depending on how you configure registration).

For the full configuration surface, see config.example.yaml.

Configuration

Most operators only need the env vars in the quickstart above. A few extras worth knowing:

  • MINSTREL_BRANDING_APP_NAME — rename the instance ("Family Jukebox", "Office Music"). Surfaces in the header, browser tab, and OG share previews.
  • MINSTREL_STORAGE_DATA_DIR — defaults to ./data. Holds playlist cover collages and other generated artefacts.
  • MINSTREL_LIBRARY_SCAN_PATHS — colon-separated list of music library roots to scan. Supports multiple roots (/music:/podcasts).

ListenBrainz integration (per-user scrobble + similarity tokens) and Lidarr integration (URL + API key) are configured through the admin Settings UI rather than env vars or yaml — per Minstrel's "config in UI" rule, integration settings live where operators can edit them without restarting.

Most operational keys have a MINSTREL_<SECTION>_<FIELD> env override. Recommendation and events tuning are yaml-only. See config.example.yaml for the authoritative surface.

Updating

  • :main — rolling, follows the dev branch's tested tip. Recommended only for the operator who's running an upstream-watching deployment.
  • :v1.0.x — pinned releases. Recommended default. Database migrations run automatically at startup; rollbacks require restoring a Postgres dump.

Specs

Authoritative scope lives under docs/:

Development

Two concurrent dev processes:

  1. Backend: docker compose up — Postgres + Minstrel on :4533.
  2. Frontend: cd web && npm install && npm run dev — Vite dev server on :5173 with HMR. The Vite server proxies /api/* and /rest/* to :4533 so session cookies work.

Production build

docker build -t minstrel . runs the SvelteKit build inside a node stage, copies the output into the golang stage, and //go:embeds it into the final binary. The container serves the SPA from / alongside the API surfaces; no separate static-file server is required.

Branches

  • Day-to-day work happens on dev (or feature branches merged into dev).
  • main is protected — changes land via PR from dev.
  • Releases are cut by tagging v* off main; the release workflow builds and pushes the container image to the Forgejo registry.

Task and milestone tracking: Fable (Minstrel project, id 12).

License

See LICENSE.

S
Description
Self-hosted music server with OpenSubsonic compatibility, server-side smart shuffle, dual-like model, session-aware radio, and Lidarr integration. Go + Postgres.
Readme MIT 63 MiB
Languages
Go 39.2%
Kotlin 25%
Dart 14.2%
TypeScript 12%
Svelte 9.3%
Other 0.1%