// Public-playlist-by-URL lookup — a guest pastes any public Spotify // playlist URL and gets a card, with no login of their own required. // // This originally used a Client Credentials (app-only) token so it // wouldn't need anyone's login at all — but Spotify's playlist-items // endpoint returns 401 "Valid user authentication required" even for // fully public playlists under a pure app token (confirmed directly, // not assumed). Routes through the host's already-authenticated // connection instead: still genuinely user-authenticated (satisfies // Spotify's requirement), and guests still never log into anything — // the host's session is what's doing the asking on their behalf. import { Router } from 'express'; import { getHostAccessToken } from './hostAuth.js'; const router = Router(); function extractPlaylistId(input) { const trimmed = input.trim(); const urlMatch = trimmed.match(/playlist[/:]([a-zA-Z0-9]+)/); if (urlMatch) return urlMatch[1]; if (/^[a-zA-Z0-9]{10,30}$/.test(trimmed)) return trimmed; // bare ID return null; } function summarizeTrack(entry) { const t = entry.item; if (!t) return null; return { id: t.id, title: t.name, artist: t.artists?.[0]?.name ?? 'Unknown artist', album: t.album?.name ?? '', image: t.album?.images?.[0]?.url ?? null, uri: t.uri, }; } // GET /api/public-playlist?url= router.get('/', async (req, res) => { const id = extractPlaylistId(String(req.query.url || '')); if (!id) return res.status(400).json({ error: 'could not find a playlist id in that URL' }); try { const token = await getHostAccessToken(); const authed = (path) => fetch(`https://api.spotify.com/v1${path}`, { headers: { Authorization: `Bearer ${token}` }, }); const metaRes = await authed(`/playlists/${id}?fields=name,images,public`); if (!metaRes.ok) { const status = metaRes.status; return res.status(status === 404 ? 404 : 502).json({ error: status === 404 ? 'playlist not found' : `Spotify → ${status}`, }); } const meta = await metaRes.json(); if (meta.public === false) { return res.status(403).json({ error: 'this playlist is private — only the playlist owner can use it as a song pool' }); } const tracks = []; let url = `/playlists/${id}/items?limit=100&fields=next,items(item(id,name,uri,artists(name),album(name,images)))`; while (url) { const pageRes = await authed(url); if (!pageRes.ok) { return res.status(502).json({ error: `Spotify → ${pageRes.status} while reading tracks (page may be an algorithmic/editorial playlist, which Spotify restricts third-party access to)` }); } const page = await pageRes.json(); tracks.push(...(page.items ?? [])); url = page.next ? page.next.replace('https://api.spotify.com/v1', '') : null; } res.json({ name: meta.name, image: meta.images?.[0]?.url ?? null, tracks: tracks.map(summarizeTrack).filter(Boolean), }); } catch (e) { res.status(502).json({ error: e.message }); } }); export default router;