89 lines
2.8 KiB
JavaScript
89 lines
2.8 KiB
JavaScript
|
|
import { Router } from 'express';
|
||
|
|
import { spotifyAsHost } from './hostAuth.js';
|
||
|
|
|
||
|
|
const router = Router();
|
||
|
|
|
||
|
|
// Simple in-memory TTL cache — playlists/tracks don't need to be fresher
|
||
|
|
// than this, and it keeps a session-start from hammering Spotify's API.
|
||
|
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
||
|
|
const cache = new Map();
|
||
|
|
function cached(key, fn) {
|
||
|
|
const hit = cache.get(key);
|
||
|
|
if (hit && Date.now() - hit.ts < CACHE_TTL_MS) return Promise.resolve(hit.data);
|
||
|
|
return fn().then(data => { cache.set(key, { data, ts: Date.now() }); return data; });
|
||
|
|
}
|
||
|
|
|
||
|
|
function summarizePlaylist(p) {
|
||
|
|
return {
|
||
|
|
id: p.id,
|
||
|
|
name: p.name,
|
||
|
|
trackCount: p.items?.total ?? p.tracks?.total ?? 0, // Spotify's field name for this varies by endpoint/version; checked the real response directly
|
||
|
|
image: p.images?.[0]?.url ?? null,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Both facets of the card, plus cover art — track title, primary artist,
|
||
|
|
// and album art, everything a square could render as.
|
||
|
|
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/host/playlists — the host's own playlists as candidate song pools.
|
||
|
|
router.get('/playlists', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const data = await cached('host-playlists', async () => {
|
||
|
|
const out = [];
|
||
|
|
let url = '/me/playlists?limit=50';
|
||
|
|
while (url) {
|
||
|
|
const page = await spotifyAsHost(url);
|
||
|
|
out.push(...(page.items ?? []));
|
||
|
|
url = page.next ? page.next.replace('https://api.spotify.com/v1', '') : null;
|
||
|
|
}
|
||
|
|
return out.map(summarizePlaylist);
|
||
|
|
});
|
||
|
|
res.json(data);
|
||
|
|
} catch (e) {
|
||
|
|
res.status(502).json({ error: e.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// GET /api/host/playlist/:id/tracks — full track pool for one playlist.
|
||
|
|
router.get('/playlist/:id/tracks', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const data = await cached(`host-tracks:${req.params.id}`, async () => {
|
||
|
|
const out = [];
|
||
|
|
let url = `/playlists/${req.params.id}/items?limit=100&fields=next,items(item(id,name,uri,artists(name),album(name,images)))`;
|
||
|
|
while (url) {
|
||
|
|
const page = await spotifyAsHost(url);
|
||
|
|
out.push(...(page.items ?? []));
|
||
|
|
url = page.next ? page.next.replace('https://api.spotify.com/v1', '') : null;
|
||
|
|
}
|
||
|
|
return out.map(summarizeTrack).filter(Boolean);
|
||
|
|
});
|
||
|
|
res.json(data);
|
||
|
|
} catch (e) {
|
||
|
|
res.status(502).json({ error: e.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// GET /api/host/now-playing — thin wrapper for the auto-call poller.
|
||
|
|
router.get('/now-playing', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const data = await spotifyAsHost('/me/player/currently-playing');
|
||
|
|
res.json(data ?? { playing: false });
|
||
|
|
} catch (e) {
|
||
|
|
res.status(502).json({ error: e.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
export default router;
|