No description
  • Python 94.1%
  • JavaScript 4.1%
  • Dockerfile 0.6%
  • CSS 0.5%
  • HTML 0.5%
  • Other 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
leigh f39605a921 docs: rewrite README for the IQ microservices architecture
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>
2026-08-12 11:18:47 +08:00
.claude plan: HQ candidate fallback + banned-peer handling 2026-07-22 17:46:07 +08:00
clients/ma-provider feat(ma-provider): set disc_number/track_number on MA Track 2026-08-12 09:12:36 +08:00
discovery feat(discovery): /artist-top-tracks route over popularity 2026-07-27 21:04:40 +08:00
Docs fix(subsonic-gateway): stop first-search 500 and getOpenSubsonicExtensions 404 2026-07-28 21:49:41 +08:00
docs docs: add implementation plans for art viewer, scrobble rematch, auto-upgrade 2026-08-12 10:37:42 +08:00
ma refactor(ma-provider): move provider/infinitequeue to clients/ma-provider 2026-08-11 16:42:31 +08:00
packages/iq-common fix(search): debounce typeahead and stop abandoned search legs downstream 2026-08-09 11:36:53 +08:00
resolver feat(cover-art): release-group art, local cache, weekly re-check 2026-08-01 12:57:13 +08:00
scripts docs(ma-gateway): AGENTS guide, OpenAPI contract, compose wiring 2026-08-11 16:41:36 +08:00
scrobble Merge branch 'feat/scrobble-explorer' 2026-07-30 17:05:05 +08:00
services fix(audio): stop trusting YouTube-claimed bitrates in upgrade ranking 2026-08-12 10:37:38 +08:00
slskd fix(slskd): share the audio tree alongside the resolver's, not over it 2026-08-04 08:24:37 +08:00
subsonic-gateway fix(subsonic-gateway): scrub null display strings at the resolver boundary 2026-08-01 12:57:38 +08:00
tools feat(scrobble-rematch): log artist/track on every correction, add --verbose 2026-08-09 09:39:04 +08:00
.env.example fix(ma-gateway): absolutize library's art_url against metadata 2026-08-11 16:53:38 +08:00
.gitignore refactor(scrobble-rematch): match via metadata's ranked /search, not raw MB 2026-08-09 09:25:16 +08:00
AGENTS.md feat(ui): compose wiring, committed contract and service guide 2026-08-04 16:05:37 +08:00
compose.legacy.yml chore: make the IQ stack the default compose file 2026-08-08 13:52:16 +08:00
compose.yml feat(compose): add dozzle for one-page log monitoring across the stack 2026-08-12 06:55:24 +08:00
gateway.log docs(readme): document observed Tempus client traffic against the gateway 2026-07-30 12:13:35 +08:00
README.md docs: rewrite README for the IQ microservices architecture 2026-08-12 11:18:47 +08:00

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 with library state (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 audio for a stream (or the cached file), and proxies bytes back with Range support intact. library /play/{id} is the one route allowed to hit audio at interactive priority, ahead of background upgrade jobs.
  • Library add / favourite / want-hq → forwarded straight to library, which owns that state exclusively and polls audio afterward 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). scrobble never validates or blocks a listen — ingestion is fire-and-forget by design.

Supporting infrastructure

  • slskd — the Soulseek daemon audio drives for HQ upgrades. Shares both the legacy resolver's hardlinked tree (music-cache/shared, ~2.8 GB of earned peer reputation) and the new audio-media/share tree (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: the item_id namespace (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 by scripts/check_contracts.py so 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).