Rewires EnrichAlbum to iterate EnabledAlbumProviders from the SettingsService instead of a single hardcoded MBCAA fetcher. Sidecar layer is unchanged (always tried first). Per-row eligibility check honors cover_art_sources_version: terminal 'found' values skip; 'none' flips to NULL via ClearAlbumCoverNone and proceeds (the DB- level ListAlbumsMissingCover query guards the version check for batch callers; RetryAlbum clears via ClearAlbumCover first); NULL is eligible. The provider chain uses an inline allWere404 accumulator to settle the row to 'none' only when every provider returned ErrNotFound. Any provider returning ErrTransient leaves the row NULL for next- pass retry — even if other providers returned ErrNotFound — since the transient call's MBID might succeed on a future attempt. Boot wiring (cmd/minstrel/main.go) replaces the NewFetcher+NewEnricher(fetcher, mbcaaOn) shape with NewMBCAAProviderFromConfig (swaps in production User-Agent on the registered provider) + NewSettingsService + NewEnricher(settings). The old cfg.Library.CoverArtFromMBCAA flag is no-op'd; DB-backed settings replace it. Field stays in the config struct for backward compat with deployed config files. Consolidates fetcher.go's HTTP logic into provider_mbcaa.go (no more wrapper indirection): mbcaaProvider now holds cfg/mu/lastCall/client directly. Deletes fetcher.go and fetcher_test.go. ErrNotFound and ErrTransient move into provider.go alongside the other sentinels. Updates admin_covers_test.go and provider_mbcaa_test.go to the new constructor shapes. Adds multi-provider-chain, transient-leaves-null, and stale-none-flips-and-retries test cases in enricher_test.go. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 toadmin; 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/:
- Server spec — current implementation focus.
- Client spec — Flutter companion app.
Development
Two concurrent dev processes:
- Backend:
docker compose up— Postgres + Minstrel on:4533. - Frontend:
cd web && npm install && npm run dev— Vite dev server on:5173with HMR. The Vite server proxies/api/*and/rest/*to:4533so 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 intodev). mainis protected — changes land via PR fromdev.- Releases are cut by tagging
v*offmain; 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.