# Task 2-a — v6.7 part A (server): resolve anti-spam semaphore + music-service framework Task ID: 2-a Agent: full-stack-developer Status: COMPLETE — lint clean, tsc clean on all four files, verified live against the dev server (curl) and at the process level (ps snapshots). ## Scope (ONLY these files — nothing else touched) 1. `src/lib/music-services.ts` (modified) 2. `src/lib/server/music-providers.ts` (NEW) 3. `src/app/api/resolve/route.ts` (modified) 4. `src/components/ava/settings-tab.tsx` (modified — Music services chips only) ## What was built ### 1. `src/lib/music-services.ts` - `export type StreamStatus = 'live' | 'coming-soon'` - `ServiceDef` gained required `streamStatus: StreamStatus` + optional `comingSoonNote?: string` (required when coming-soon). - youtube / soundcloud → `'live'`; spotify / apple → `'coming-soon'` with notes like: "Spotify playback is coming in a later update — your saved Spotify login will be used automatically the moment it ships. Meanwhile, YouTube, SoundCloud and direct-file links work today." - All pre-existing fields/exports intact (`SERVICES`, `serviceDef`, `serviceForUrl`, `isServiceId`, `ServiceId`). ### 2. `src/lib/server/music-providers.ts` — exact exports ```ts export type ProviderStatus = 'live' | 'coming-soon' export interface ProviderAdapter { id: ServiceId | 'generic' label: string status: ProviderStatus comingSoonNote: string handlesUrl(url: string): boolean // commented v6.8+ extension points: // resolveTrack?(url, auth) / listPlaylist?(url, auth) } export const YTDLP_PROVIDER: ProviderAdapter // id 'generic', live, handlesUrl → true export const SPOTIFY_PROVIDER: ProviderAdapter // id 'spotify', coming-soon export const APPLE_PROVIDER: ProviderAdapter // id 'apple', coming-soon export function providerForUrl(url: string): ProviderAdapter ``` - Domain matching mirrors `serviceForUrl` (host/subdomain, "www." ignored) scoped to the one service. - `providerForUrl` is status-agnostic: flip an adapter to 'live' and it still owns its URLs at the route's dispatch point. - Big "HOW TO SHIP A NEW SERVICE (v6.8+)" header comment: 3-step recipe (implement resolveTrack/listPlaylist with the exact payload shapes → flip streamStatus in music-services.ts + status here → extend the marked dispatch spot in the route). ### 3. `src/app/api/resolve/route.ts` - **Coming-soon dispatch** right after `assertPublicHttpUrl`, BEFORE cache/direct-audio/yt-dlp: `{ ok: false, code: 'coming-soon', service: provider.id, error: provider.comingSoonNote }` (422). Spotify/Apple links never spawn yt-dlp. Generic "That site is not supported" 422 kept but without the DRM parenthetical. - **Semaphore**: `YTDLP_MAX_CONCURRENCY = 2`, `YTDLP_QUEUE_TIMEOUT_MS = 40_000`, class `YtdlpSemaphore` on globalThis (`__avaYtdlpSemaphore`). FIFO waiters, direct slot handoff in `release()` (active count unchanged). Acquire wrapped around `runYtDlp` with a `held` flag → release exactly once in the finally; busy → 503 "The resolver is busy right now — try again in a moment."; timed-out waiter that was granted a slot in the same tick does NOT double-release (queue membership is the single source of truth). Cache hits + direct-file links bypass the gate. - Fixed a pre-existing strict-type bug in `buildCookieParts` (`pairs: string[]` receiving `[string,string]` tuples → now `[string, string][]`; no runtime change). ### 4. `src/components/ava/settings-tab.tsx` - Coming-soon service chips get an amber "soon" badge: `soon` (placed between name and the check/— marker). Nothing else changed; login buttons fully functional for coming-soon services. ## Verification - `bun run lint` clean; `bunx tsc --noEmit` clean for all four files. - Live: spotify track/playlist + apple album → 422 coming-soon in ~6ms (zero yt-dlp); SSRF gate still first (localhost → 400); direct MP3 → 200; spotify+cookies → coming-soon before cookie staging; real YouTube resolve still passes through (bot-wall 422 as before). - End-to-end anti-spam: 6 concurrent resolves of the same YouTube link — ps snapshots show ≤2 top-level `bin/yt-dlp` processes at any moment (extra PIDs = the onefile binary's own self-exec children, not extra extractions); FIFO waves of 2; zero 503s; dev.log error-free. - Scratch semaphore tests (cap/FIFO/timeout/handoff/no-leak) all pass; scratch dir deleted. ## Notes for the lead agent - **apple.com is broad**: the apple ServiceDef domains include `apple.com`, so ALL apple.com links (e.g. podcasts.apple.com, which yt-dlp's ApplePodcasts extractor could handle) now get the Apple Music coming-soon answer. Consistent with client-side matching; say the word to scope the adapter to music.apple.com only. - Worklog has no v6.6 section (the v6.6 music-login files were committed inside the v6.5 commit 8fa0eb9); my entry follows v6.5 directly. - Not-mine tsc errors (untouched): examples/websocket/* (missing deps), api/rust-scaffold (Uint8Array vs BodyInit), ava/audio-source.ts ('never' narrowing — the concurrent client agent's WIP). - Did NOT commit; did NOT touch version.ts / page.tsx / audio.ts / queue files / link-dialog / audio-source / queue-bar.