3b4b68daaf91e06ae78141014d9cf3934c03f10b
3
Commits
-
Rebuild the UI on Next.js, and give search real metadata
The old page was hand-written HTML/CSS/JS. That was fine for a search box and a list, but the roadmap here is multi-user, an admin dashboard with usage graphs, and an import-review queue — all of which get painful in vanilla DOM code. Moves to Next.js 16 / React 19 / Tailwind 4 / Framer Motion. Built as a STATIC EXPORT served by FastAPI. Running `next start` alongside uvicorn would add a Node runtime, a second process to supervise, and a second thing that can be down while the other is up. There is no use for SSR — every view is behind auth and renders from live JSON — so the deployment stays one container, one process, one healthcheck. Image is 60MB; Node only exists in the build stage. Search now carries what a music UI needs: - year and genre, grafted on from /album/{id} concurrently, deduplicated by album and cached forever since a release date is immutable - 30-second previews, so you can hear a track before committing a Soulseek search to it. Not a library player — that is Navidrome's job. - autocomplete over songs/albums/artists, debounced and aborted per keystroke so a slow response to "rad" cannot land after "radiohead" - album and artist pages, with `Get all` to queue a release in one request - the accent colour is sampled from the artwork, so the palette follows the music. Deezer's CDN sends ACAO:*, which is what makes canvas sampling work with no image proxy. /api/album/{id} deliberately calls Deezer twice: the track list embedded in the album response has no track_position, and using the list index instead misnumbers every multi-disc release — verified against the White Album, where disc 2 track 1 would have been filed as 18. Schema is multi-user ready ahead of the feature: users table with roles and quotas, requested_by on jobs, and the first login becomes admin. Auth is still the single-user GitHub allowlist. CSP survives all of this intact. Next emits inline hydration scripts, which script-src 'self' blocks; rather than per-request nonces, their SHA-256 is computed at startup from the built page — a static export never varies between requests, so hashes need no maintenance and no HTML rewriting. The one concession is style-src-attr 'unsafe-inline', because Framer Motion writes its pre-hydration state into style="" and without it every element flashes in. That permits style attributes only. img-src/media-src now name Deezer's CDN explicitly instead of allowing https: wholesale, closing an exfiltration channel that survived connect-src 'self'. /_next/* sits behind the session too — there is no local login page to style, so the bundle need not be public. Verified: unauthenticated /→302 and every /api/*→401 including the four new endpoints, forged state→400, forged cookie→401, misconfigured auth refuses to start, and the production image hydrates with zero CSP violations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Aryan SinghandClaude Opus 5
2026-08-04 14:26:31 -05:00 -
Add single-user GitHub OAuth, hardened for internet exposure
muse drives a download client and writes to the media library, so exposing it publicly needs more than a login form. It fails closed. Auth is on unless MUSE_AUTH=off is set explicitly, and when it is on with incomplete config the app refuses to start rather than serving open. The mode worth engineering against is not a broken login page, it is a misconfigured deploy quietly publishing an unauthenticated download client. Sessions are server-side: the cookie carries a 256-bit random token and nothing else, and only its SHA-256 is stored. There is no signed payload to get subtly wrong, a stolen database yields no usable cookies, and logout revokes for real. The allowlist is re-checked on every request so removing a user takes effect immediately rather than at session expiry. OAuth state is compared with secrets.compare_digest, so a callback the user did not initiate is rejected. MUSE_BASE_URL comes from config rather than X-Forwarded-* because those headers are attacker-controllable and this one is a redirect target. CSP is default-src 'self' with no unsafe-inline, which is why the stylesheet and script moved out of index.html into static files -- an injected <script> now cannot execute. Also frame-ancestors 'none', base-uri 'none', nosniff, DENY, no-referrer and HSTS. FastAPI's /docs, /redoc and /openapi.json are disabled so the login page does not hand out a map of every endpoint. /healthz is the only unauthenticated route and returns a bare ok, so the container healthcheck works without leaking anything. Verified: misconfigured auth refuses to boot; unauthenticated / redirects and /api/* returns 401; a forged state is rejected 400; a junk cookie is 401; a valid session for a non-allowlisted user is 401; all headers present; and the UI renders with no CSP violations in the console. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

aryansinghandClaude Opus 5
2026-08-04 12:09:51 -05:00 -
muse: request a single song, get it tagged in your library
Lidarr's smallest unit is a MusicBrainz release, so asking for one track means monitoring a whole discography. Soulseek trades individual files but has no concept of a request. muse bridges the two: search, click, and the song lands in the library correctly tagged. The design point is that the user picks the exact track before anything downloads, so muse holds artist, album, year and track number as ground truth and tags deterministically rather than inferring them afterwards from whatever a stranger named the file. Search is Deezer rather than MusicBrainz, decided on measurement: MusicBrainz put eight covers above the real recording for "radiohead creep" and never surfaced Queen for "bohemian rhapsody" across 90 results, because its index matches a word in a cover's title as readily as in the artist field. Re-ranking cannot rescue a result set missing the right row. Imports are os.link(), so downloads keep seeding from the Soulseek share while the library gets a second name for the same bytes.
aryansingh
2026-08-04 03:05:45 -05:00