diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 9953dac..0637976 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -87,6 +87,19 @@ app.post('/api/spotify/track-artwork', async (req, res) => { res.json({ results }); }); +// ─── ITUNES PROXY ───────────────────────────────────────────────────────────── +// Browsers can't call itunes.apple.com directly (no CORS headers). Proxy it here. +app.get('/api/itunes/:endpoint', (req, res) => { + const allowed = new Set(['search', 'lookup']); + if (!allowed.has(req.params.endpoint)) return res.status(404).json({ error: 'not found' }); + const qs = new URLSearchParams(req.query).toString(); + const target = `https://itunes.apple.com/${req.params.endpoint}${qs ? '?' + qs : ''}`; + https.get(target, { headers: { 'User-Agent': 'NovaSonic/1.0' } }, upstream => { + res.setHeader('Content-Type', 'application/json'); + upstream.pipe(res); + }).on('error', e => res.status(502).json({ error: e.message })); +}); + app.get('/api/health', (req, res) => { res.json({ status: 'ok', ytdlpVersion, diff --git a/src/api.js b/src/api.js index 33c9024..e200c4c 100644 --- a/src/api.js +++ b/src/api.js @@ -40,11 +40,14 @@ export async function ndAuth() { } // ─── ITUNES ─── -const ITUNES = 'https://itunes.apple.com'; +// Route through backend proxy to avoid CORS; fall back to direct if no backend configured. +function _itunesBase() { + return S.cfg.backendUrl ? `${S.cfg.backendUrl}/api/itunes` : 'https://itunes.apple.com'; +} export async function itunesSearch(query, entity='musicTrack', limit=25) { try { - const r = await fetch(`${ITUNES}/search?term=${encodeURIComponent(query)}&media=music&entity=${entity}&limit=${limit}&country=us&explicit=Yes`); + const r = await fetch(`${_itunesBase()}/search?term=${encodeURIComponent(query)}&media=music&entity=${entity}&limit=${limit}&country=us&explicit=Yes`); const text = await r.text(); if (!text) return []; return JSON.parse(text).results || []; @@ -52,20 +55,20 @@ export async function itunesSearch(query, entity='musicTrack', limit=25) { } export async function itunesLookupAlbum(collectionId) { - const r = await fetch(`${ITUNES}/lookup?id=${collectionId}&entity=song&explicit=Yes`); + const r = await fetch(`${_itunesBase()}/lookup?id=${collectionId}&entity=song&explicit=Yes`); const d = await r.json(); return (d.results || []).filter(x => x.wrapperType === 'track'); } // Returns actual explicit tracks for an artist via lookup (search endpoint only returns cleaned versions) export async function itunesLookupArtistSongs(artistId, limit=15) { - const r = await fetch(`${ITUNES}/lookup?id=${artistId}&entity=song&limit=${limit}&country=us&explicit=Yes`); + const r = await fetch(`${_itunesBase()}/lookup?id=${artistId}&entity=song&limit=${limit}&country=us&explicit=Yes`); const d = await r.json(); return (d.results || []).filter(x => x.wrapperType === 'track'); } export async function itunesLookupArtistAlbums(artistId, limit=200) { - const r = await fetch(`${ITUNES}/lookup?id=${artistId}&entity=album&limit=${limit}&country=us`); + const r = await fetch(`${_itunesBase()}/lookup?id=${artistId}&entity=album&limit=${limit}&country=us`); const d = await r.json(); return (d.results || []).filter(x => x.wrapperType === 'collection' && x.collectionType === 'Album'); } @@ -84,7 +87,7 @@ export async function itunesTopCharts() { } catch {} } try { - const r = await fetch('https://itunes.apple.com/search?term=new+music&media=music&entity=album&limit=50&country=de'); + const r = await fetch(`${_itunesBase()}/search?term=new+music&media=music&entity=album&limit=50&country=de`); const d = await r.json(); return (d.results||[]).map(a => ({ name: a.collectionName, artistName: a.artistName, @@ -107,7 +110,7 @@ export async function itunesTopSongs() { } catch {} } try { - const r = await fetch('https://itunes.apple.com/search?term=top+hits&media=music&entity=musicTrack&limit=50&country=de'); + const r = await fetch(`${_itunesBase()}/search?term=top+hits&media=music&entity=musicTrack&limit=50&country=de`); const d = await r.json(); return (d.results||[]).map(t => ({ name: t.trackName, artistName: t.artistName, artworkUrl: t.artworkUrl100 })); } catch { return []; }