bvandeusen 081b8e62d5 fix(web): a11y + reactivity warnings (paying down accumulated warnings)
Cleans up the 8 svelte-check warnings I deferred in prior CI runs.
Two distinct categories — handling them differently:

REAL REACTIVITY BUGS — fixed:
- AlbumMenu.svelte / ArtistMenu.svelte: cachedTracks initialized
  from the `tracks` prop only at component creation. When the menu
  is reused across rows (operator opens menu on one album, then
  another), the cache from the first open serves stale tracks.
  Fix: initialize cachedTracks to null; ensureTracks() reads the
  current `tracks` prop on each call before falling through to
  the network fetch.

INTENTIONAL PATTERNS LINT FLAGGED — suppressed with explanatory
comments:
- AlbumMenu / ArtistMenu / PlaylistCard menu containers: the
  onclick={(e) => e.stopPropagation()} is a guard against the
  window-level "any click closes the menu" listener — not
  user-facing interaction. A paired keyboard handler is
  intentionally absent (Escape-to-close lives on svelte:window
  onkeydown). Suppressed a11y_click_events_have_key_events with
  explanatory comments.
- CompactTrackCard overlay div: catches clicks on its inner
  LikeButton + queue button so they don't bubble to the wrapping
  play-card button. Inner buttons carry their own keyboard
  handling. Suppressed both a11y_click_events_have_key_events and
  a11y_no_static_element_interactions.
- theme.svelte.ts module-scope `_resolved = $state(resolve(_theme))`:
  module-init reads _theme to seed _resolved; subsequent updates
  go through setTheme() and the matchMedia listener. $derived
  doesn't apply at module scope. Suppressed state_referenced_locally
  with an explanatory comment.

Promised in the prior fix-forward: "I'll fix bugs when I see them
or open a follow-up task with a specific name, not park them in an
umbrella." Following through.
2026-05-07 18:38:44 -04:00
2026-04-19 15:14:56 +00: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:v1.0.0
    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

Watch docker compose logs minstrel on first start — a one-time admin password is printed to stderr. Sign in at http://localhost:4533 with username admin and that password, then change it under Settings.

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).
  • MINSTREL_AUTH_ADMIN_USERNAME / MINSTREL_AUTH_ADMIN_PASSWORD — bootstrap admin credentials. Username defaults to admin; leave the password unset to have the server generate one and print it to stderr on first start.

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%