From a3c8b1afd127254583f902ed13d1114085dcd55d Mon Sep 17 00:00:00 2001 From: Honey Date: Sun, 26 Jul 2026 22:39:21 +0200 Subject: [PATCH] fix(backend): cache the iTunes proxy to avoid Apple rate-limiting the search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The web Apple-Music search proxies through /api/itunes (browsers can't call itunes.apple.com — no CORS). The proxy forwarded every request uncached, so heavy download activity (each download does several album/track lookups) plus client searches blew past Apple's ~20 req/min/IP limit and the search started returning nothing. Add a 30-min in-memory cache keyed by the target URL; only genuine 200 responses (with the resultCount signature) are cached, never throttle/error pages. Co-Authored-By: Claude Opus 4.8 --- backend/novasonic-server.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index c3152a5..ee90b48 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -312,14 +312,38 @@ app.get('/api/itunes/lookup', async (req, res, next) => { // ─── ITUNES PROXY ───────────────────────────────────────────────────────────── // Browsers can't call itunes.apple.com directly (no CORS headers). Proxy it here. // Note: /api/itunes/lookup is handled above — proxy only covers /search and id-based /lookup. +// +// Cached (30 min): iTunes' Search API rate-limits ~20 req/min per IP. Without a cache, heavy +// download activity (each download does several album/track lookups) plus client searches blow +// past that and Apple starts returning empty/throttle responses — so the Apple-Music search "finds +// nothing". Serving repeated identical queries from cache keeps us well under the limit. +const _itunesCache = new Map(); // target url → { ts, body } +const ITUNES_TTL = 30 * 60 * 1000; 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 => { + + const cached = _itunesCache.get(target); + if (cached && Date.now() - cached.ts < ITUNES_TTL) { res.setHeader('Content-Type', 'application/json'); - upstream.pipe(res); + return res.send(cached.body); + } + + https.get(target, { headers: { 'User-Agent': 'NovaSonic/1.0' } }, upstream => { + let body = ''; + upstream.on('data', d => body += d); + upstream.on('end', () => { + // Only cache a genuine iTunes response (200 + the resultCount signature) — never a throttle + // or error page, or an empty result would get pinned for 30 min. + if (upstream.statusCode === 200 && body.includes('"resultCount"')) { + if (_itunesCache.size > 500) _itunesCache.clear(); // simple bound + _itunesCache.set(target, { ts: Date.now(), body }); + } + res.setHeader('Content-Type', 'application/json'); + res.status(upstream.statusCode || 200).send(body); + }); }).on('error', e => res.status(502).json({ error: e.message })); });