- Python 94.1%
- JavaScript 4.1%
- Dockerfile 0.6%
- CSS 0.5%
- HTML 0.5%
- Other 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Replaces the old resolver/subsonic-gateway endpoint-by-endpoint audit with a system overview, a core-service and gateway component table, and a walkthrough of how each gateway composes core services per request type — matching the stack that actually runs under compose.yml post-rearchitecture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
| .claude | ||
| clients/ma-provider | ||
| discovery | ||
| Docs | ||
| docs | ||
| ma | ||
| packages/iq-common | ||
| resolver | ||
| scripts | ||
| scrobble | ||
| services | ||
| slskd | ||
| subsonic-gateway | ||
| tools | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| compose.legacy.yml | ||
| compose.yml | ||
| gateway.log | ||
| README.md | ||
Infinite Queue
A self-hosted alternative to Spotify that does not pre-own a full catalog.
Search is always global — any song, in-library or not — and playback triggers
a lazy fetch pipeline rather than requiring a pre-downloaded library. Tracks
are matched against MusicBrainz, streamed immediately via a fast source
(YouTube), and quietly upgraded to lossless/verified audio in the background
via Soulseek (slskd). Everything is exposed to real players through two
gateways: a Music Assistant provider and an OpenSubsonic server, so
IQ works both as an MA-managed library and as a server standalone clients
like Symfonium can talk to directly.
Architecture
IQ is a set of small, single-purpose FastAPI services, each owning exactly one SQLite database (mounted into exactly one container — "no service reads another's database" is a filesystem fact, not a convention). A core tier holds catalogue facts, audio bytes, lifecycle state, and listen history. Three gateways sit in front of the core, translating it into whatever protocol a given client speaks:
ui subsonic-gateway ma-gateway <- gateways (protocol translation)
\_______________|________________/
| IQ Core API (HTTP/JSON)
┌───────────┬─────┴──────┬────────────┬───────────┐
v v v v v
metadata audio library scrobble discovery <- core (each owns its own data)
^ | |
└────────────┴────────────┘
(audio, library, discovery call metadata; library also calls audio)
Core services never call a gateway, and — with one documented exception
(metadata calls scrobble to blend local play counts into top-tracks
ranking) — never call each other's siblings outside this graph. Gateways may
call any core service but never reach into another gateway. This keeps each
piece independently testable and replaceable: ui, for instance, owns no
data at all and is the cheapest thing in the stack to throw away and rebuild.
This is the result of a 2026-08 rearchitecture that replaced an earlier
monolithic resolver service with this service-per-concern split. The old
stack (resolver/, and early standalone discovery/, scrobble/,
subsonic-gateway/ directories) still exists on disk and still runs via
compose.legacy.yml, kept only for continuity until it's formally retired.
compose.yml — the stack described below — is the one actually in use.
The two stacks bind conflicting ports and can't run at the same time.
Design docs live in docs/superpowers/specs/; the
system-level design is
2026-08-03-iq-microservices-design.md
and the gateway layer is
2026-08-04-gateways-design.md.
Each service directory under services/ has its own AGENTS.md describing
what it owns, what it depends on, its routes, and its known gaps — read that
before changing a service.
Core services
| Service | Port | Owns | Depends on | What it does |
|---|---|---|---|---|
| metadata | 8100 | Normalised MusicBrainz artists/albums/tracks, art, freshness state | scrobble (top-tracks blending only) |
Global search and catalogue lookups against MusicBrainz; the only source of "what exists." |
| audio | 8300 | Cached audio files, the Soulseek share tree, fetch/upgrade jobs | metadata |
Resolves a playable stream fast (YouTube via yt-dlp) and upgrades tracks to verified/lossless audio in the background via slskd. |
| library | 8200 | Per-track lifecycle state: added, favourite, want-hq, status | metadata, audio |
The single owner of "is this in my library / has it been upgraded" — polls audio rather than being polled. |
| scrobble | 8400 | Listen history: users, scrobbles, now-playing | (none) | Purpose-built per-user play tracking; sits at the bottom of the graph. |
| discovery | 8600 | Derived similarity graph, radio, deep-cuts | scrobble, metadata |
Session-co-occurrence similarity computed from the household's own listen history — a disposable cache that rebuilds from scrobble + metadata. Stood up after ListenBrainz's popularity API went down in production. |
Audio delivery never goes through a shared filesystem to a client — every
gateway fetches bytes over HTTP from audio, range-capable, so seeking and
partial downloads work the same way everywhere.
Gateways
Gateways hold no catalogue data of their own (subsonic-gateway keeps a small user/credential table; ma-gateway and ui own nothing at all) and answer every request their protocol defines — where the core can't yet answer something, a gateway degrades to the emptiest valid response rather than erroring, so clients don't break on features IQ hasn't built yet.
| Gateway | Port | Talks to | For |
|---|---|---|---|
| ma-gateway | 8700 | metadata, library, discovery, scrobble | The Music Assistant provider (clients/ma-provider/), baked into a custom MA image (ma/). Flattens core responses into MA's MusicProvider DTOs and exposes a /capabilities endpoint so MA can widen what it advertises (e.g. Radio Mode) as core gaps close, without an image rebuild. |
| subsonic-gateway | 8500 | metadata, library, audio, scrobble, discovery | Direct Subsonic/OpenSubsonic clients (e.g. Symfonium) that need to browse, stream, and offline-cache without going through MA — a sibling to MA, not downstream of it. Also forwards scrobbles, keyed to the same per-user history MA plays contribute to. |
| ui | 8000 | metadata, library, audio, scrobble | A small operator console (vanilla JS, no build step) for browsing the library, inspecting metadata, and watching scrobble/operations state — composes a couple of BFF routes across services but is not a player-facing surface. |
How a gateway talks to the core
Each gateway is thin: it does protocol translation and response shaping, not business logic. A request generally fans out to one or more core services concurrently and gets recomposed into the client's expected shape:
- Search (any gateway) →
metadata /search, optionally joined withlibrarystate (is this track already added/favourited) so a result can say "in your library" without the client making a second call. - Play / stream → the gateway resolves an id, asks
audiofor a stream (or the cached file), and proxies bytes back withRangesupport intact.library /play/{id}is the one route allowed to hitaudioat interactive priority, ahead of background upgrade jobs. - Library add / favourite / want-hq → forwarded straight to
library, which owns that state exclusively and pollsaudioafterward rather than being told when a job finishes. - Radio / similar / recommendations →
discovery, seeded from a track, artist, or album id; a gateway expands non-track seeds (e.g. an artist) into a few representative tracks before calling discovery, since discovery only understands recording-level neighbours. - Scrobbling → every gateway forwards completed listens to
scrobble, keyed to its own per-user identity (an MA user id, or a Subsonic username mapped to one at gateway-user setup).scrobblenever validates or blocks a listen — ingestion is fire-and-forget by design.
Supporting infrastructure
slskd— the Soulseek daemonaudiodrives for HQ upgrades. Shares both the legacy resolver's hardlinked tree (music-cache/shared, ~2.8 GB of earned peer reputation) and the newaudio-media/sharetree (small, still growing) simultaneously, read-only, so upgrade quality doesn't regress while the new share tree builds its own standing.packages/iq-common— wire-level code shared across services: theitem_idnamespace (mb:/mba:/mbrg:MusicBrainz ids,dz:reserved for Deezer), the uniform error envelope, request-id propagation, and cooperative request cancellation. No business logic or DB access lives here by design.tools/— one-off/maintenance scripts, not part of the running stack:spotify-import(seed listen history from a Spotify GDPR export),seed-metadata,migrate-scrobbles,scrobble-rematch.docs/contracts/— committed OpenAPI schema per service, checked against the live schema byscripts/check_contracts.pyso a contract change can't drift silently from its implementation.
Running it
docker compose up
brings up the full stack in compose.yml: metadata, audio, library,
scrobble, discovery, subsonic-gateway, ma-gateway, ma, slskd,
ui, and a log viewer (dozzle, port 9090). MA requires host networking for
player discovery, so it isn't published on a fixed port — reach it via its
own UI once running. The console lives at http://localhost:8000, the
Subsonic gateway at http://localhost:8500.
Copy .env.example to .env first; see it for the full list of required and
optional variables (MusicBrainz contact string, Soulseek credentials,
Subsonic gateway users, etc).