6d6802bac10be9074a6773eb10b621a03f5d36dd
4
Commits
-
Import a shared playlist link and build it in Navidrome
Paste a Spotify, Apple Music or Deezer playlist URL; muse resolves it, matches every track through the same Deezer path a pasted line takes, and writes an .m3u the library scanner picks up. Written per import rather than at the end, so the playlist appears in Navidrome with the first track that lands and grows as the rest arrive -- twenty minutes of nothing, then everything, is the worse shape. A track rejected in review drops out on the next rewrite, and one that never resolves is reported instead of silently vanishing. Resolve is split from queue on purpose: a shared link is opaque, you cannot tell twelve tracks from four hundred by looking at it, and four hundred is a quota decision. Two of the three sources are scrapes and the code says so plainly. Deezer has a real API. Spotify and Apple Music have no free way to read a public playlist -- Apple's MusicKit needs a paid developer account -- so those read the public page and parse the JSON it already embeds for its own player. No key, no OAuth, no account, and no guarantee: they break whenever either company reshuffles their frontend. So the resolvers fail loudly and name themselves, because an empty playlist and a changed page must not produce the same message. The .m3u uses MUSE_PLAYLIST_PREFIX (/music), not muse's own library path. Both containers mount the same directory at different points and a playlist written with muse's view resolves to nothing on Navidrome's side. Also reorders the hooks in search-bar so the URL-seeding effect is declared after the state it sets, rather than closing over a binding declared below it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-
Recover blocked searches, fall through to the next peer, rebuild the UI
Measured against a live instance: 47 failed jobs against 33 imported. Three independent causes, all of which ended the request. The Soulseek server filters some artist names. "Michael Jackson Billie Jean" and bare "Michael Jackson" both return zero responses; "Billie Jean" returns 1083 files. The Weeknd, Kanye West and Frank Ocean behave the same way. Since pick_and_enqueue sent exactly one query of `artist title`, every request for a filtered artist failed reporting "searched 0 files" -- 28 of 29 occurrences. search_queries() now walks a ladder, loosest last, ending at the bare title, and escalates only when a search underdelivers so the common case still costs one search. Dropping the artist from the query does not mean ignoring it: match_ratio() looks for it in the folder path, where an organised share puts it. A rejected transfer ended the request. pick_and_enqueue kept a running maximum and discarded every runner-up, so "Transfer rejected: File not shared." -- a per-file condition where candidate two almost always works -- was terminal, as were timeouts and slskd's 500 on an unreachable peer. The ranked list is now persisted on the job row, ordered to put a different peer next, and try_next_candidate() advances through it. A job fails only once the list is exhausted, and then reports every peer it tried and what each one said. norm() destroyed every non-ASCII character, so "Beyonce" never matched Beyonce, nor Bjork Bjork, nor Dont Don't. It now folds accents and drops apostrophes while leaving & as a separator. Also: availability outranks format in score(), because a FLAC behind 5000 queued users used to beat an immediately available MP3; transfers that stall or that slskd forgets are abandoned instead of sitting in `downloading` forever; searches are deleted only once finished, since deleting one mid-flight makes slskd throw an optimistic-concurrency error and lose the record; a failed search POST is retried rather than failing the job on one blip. The UI moves to a sidebar shell -- rail at tablet, bottom bar on phones -- with neutral chrome and the sampled artwork colour confined to hero areas rather than repainting the whole interface. Adds URL routing, a primitive set under components/ui, and a request sheet showing the attempt ladder. Fixes hover:bg-glass-hi, which was never a valid class, so track rows and the search field had no hover or focus state at all; a reduced-motion path that could strand elements at opacity 0; the login page's hardcoded palette; and the missing 404 and error boundaries. tests/test_acquisition.py covers this path against a faked slskd. It had none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Aryan SinghandClaude Opus 5
2026-09-02 11:33:52 -05:00 -
Add review, discover, email; rebuild the design system
Three features and a typographic pass over everything. REVIEW. Picking a file and trusting it are different questions, so they now use different numbers. The existing rank decides which candidate to fetch and is right to weigh peer upload slots and queue length -- but none of that says anything about whether the file is the correct recording, and a fast peer must not be able to raise the confidence of a wrong match. So confidence() scores the filename alone: title 0.75, artist 0.25, album +0.05, and a variant tag you did not ask for multiplies by 0.8 AND caps below the auto threshold. Confidence gates the IMPORT, not the download. The transfer completes either way and the file waits in downloads, so approving is instant and rejecting is a delete. Holding the transfer instead would mean the peer is usually gone by the time anyone looks. The two cases it exists for: "02 Creep.flac" in an unsorted folder scores 0.75 -- title exactly right, artist simply absent, the commonest honest case there is -- and must be reviewed rather than binned. And "Creep (Live at Glastonbury)" scores 0.80 with BOTH artist and title perfect; the cap is what stops a live recording auto-filing over the studio version. Ask for "Creep (Acoustic)" and it auto-imports at 0.97, because then the variant is the request. DISCOVER. Related artists seeded from what you have actually imported, plus a recap of top artists, genres and decades. Built on muse's own request history, never on listening data: Navidrome owns play counts and history, and a second source of truth for those would disagree with the one you listen through. EMAIL. Optional, off unless MUSE_SMTP_HOST is set. Sends on import and on review-needed. smtplib is blocking so it runs off the event loop, and every failure is logged and swallowed -- a notification is never worth failing an import over. Google accounts need no configuration, since the identity is an address. Each user owns their own setting under Settings, deliberately not an admin route: an address you cannot change yourself is someone else's chore. DESIGN. A real type scale (display/title/headline/body/callout/caption) whose tracking tightens as size grows, which is the single biggest tell of typography nobody set. Specular top edges on glass surfaces -- blur alone reads as a grey rectangle. A two-part elevation scale, contact shadow plus soft light. Semantic colours are now per-theme: the amber tuned for the dark background failed contrast outright on white, which was exactly the text a reviewer most needs to read. The tab underline became a sliding pill so the eye tracks one object, and Review only appears when something is actually waiting. Add tests/test_matching.py: 13 assertions on the confidence bands rather than the exact scores. 90 tests pass across the three suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Aryan SinghandClaude Opus 5
2026-08-05 02:10:33 -05:00 -
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