From 5ef6a6d0ba250628a40a62e6a2ccf0a8c5d7f679 Mon Sep 17 00:00:00 2001 From: Honey Date: Tue, 28 Apr 2026 00:57:29 +0200 Subject: [PATCH] fix: proxy all iTunes/Apple RSS calls through backend to bypass 403 browser block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apple now blocks direct browser requests from custom origins. Added backend proxy routes: GET /api/itunes/search — iTunes search GET /api/itunes/lookup — iTunes lookup by ID GET /api/itunes/charts — Apple RSS most-played charts (albums/songs) All itunesSearch/Lookup/TopCharts/TopSongs functions in api.js now route through backend. Co-Authored-By: Claude Sonnet 4.6 --- backend/novasonic-server.js | 42 +++++++++++++++++++++ src/api.js | 73 ++++++++++++------------------------- 2 files changed, 65 insertions(+), 50 deletions(-) diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index e19cf24..5b42721 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -483,6 +483,48 @@ function _spTrackItem(t, defaultAlbum, defaultArt, defaultAlbumArtist) { }; } +// ─── ITUNES PROXY ──────────────────────────────────────────────────────────── +// Browser origin is blocked by Apple (403). All iTunes/Apple RSS calls go through here. + +async function itunesProxy(url) { + return new Promise((resolve, reject) => { + https.get(url, { headers: { 'User-Agent': 'iTunes/12.0' } }, res => { + let d = ''; + res.on('data', c => d += c); + res.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({}); } }); + }).on('error', reject); + }); +} + +app.get('/api/itunes/search', async (req, res) => { + try { + const { term, entity = 'musicTrack', limit = 25, country = 'us' } = req.query; + if (!term) return res.json({ results: [] }); + const url = `https://itunes.apple.com/search?term=${encodeURIComponent(term)}&media=music&entity=${entity}&limit=${limit}&country=${country}&explicit=Yes`; + const d = await itunesProxy(url); + res.json({ results: d.results || [] }); + } catch { res.json({ results: [] }); } +}); + +app.get('/api/itunes/lookup', async (req, res) => { + try { + const { id, entity = 'song', limit = 200, country = 'us' } = req.query; + if (!id) return res.json({ results: [] }); + const url = `https://itunes.apple.com/lookup?id=${id}&entity=${entity}&limit=${limit}&country=${country}&explicit=Yes`; + const d = await itunesProxy(url); + res.json({ results: d.results || [] }); + } catch { res.json({ results: [] }); } +}); + +app.get('/api/itunes/charts', async (req, res) => { + try { + const { type = 'albums', country = 'de' } = req.query; + const url = `https://rss.applemarketingtools.com/api/v2/${country}/music/most-played/50/${type}.json`; + const d = await itunesProxy(url); + res.json(d.feed ? { results: d.feed.results || [] } : { results: [] }); + } catch { res.json({ results: [] }); } +}); + // ─── ROUTES ────────────────────────────────────────────────────────────────── app.post('/api/spotify/track-artwork', async (req, res) => { diff --git a/src/api.js b/src/api.js index 33c9024..58c1f37 100644 --- a/src/api.js +++ b/src/api.js @@ -39,76 +39,49 @@ export async function ndAuth() { return S._ndToken; } -// ─── ITUNES ─── -const ITUNES = 'https://itunes.apple.com'; +// ─── ITUNES (proxied through backend to avoid Apple's browser origin block) ─── +function _iBase() { return S.cfg.backendUrl || ''; } + +async function _iGet(path) { + try { + const r = await fetch(_iBase() + path); + if (!r.ok) return {}; + return await r.json(); + } catch { return {}; } +} 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 text = await r.text(); - if (!text) return []; - return JSON.parse(text).results || []; - } catch { return []; } + const d = await _iGet(`/api/itunes/search?term=${encodeURIComponent(query)}&entity=${entity}&limit=${limit}`); + return d.results || []; } export async function itunesLookupAlbum(collectionId) { - const r = await fetch(`${ITUNES}/lookup?id=${collectionId}&entity=song&explicit=Yes`); - const d = await r.json(); + const d = await _iGet(`/api/itunes/lookup?id=${collectionId}&entity=song`); 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 d = await r.json(); + const d = await _iGet(`/api/itunes/lookup?id=${artistId}&entity=song&limit=${limit}`); 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 d = await r.json(); + const d = await _iGet(`/api/itunes/lookup?id=${artistId}&entity=album&limit=${limit}`); return (d.results || []).filter(x => x.wrapperType === 'collection' && x.collectionType === 'Album'); } export async function itunesTopCharts() { - const urls = [ - 'https://rss.applemarketingtools.com/api/v2/de/music/most-played/50/albums.json', - 'https://rss.applemarketingtools.com/api/v2/us/music/most-played/50/albums.json', - ]; - for(const url of urls) { - try { - const r = await fetch(url, { signal: AbortSignal.timeout(6000) }); - if(!r.ok) continue; - const d = await r.json(); - if(d.feed?.results?.length) return d.feed.results; - } catch {} + for (const country of ['de', 'us']) { + const d = await _iGet(`/api/itunes/charts?type=albums&country=${country}`); + if (d.results?.length) return d.results; } - try { - const r = await fetch('https://itunes.apple.com/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, - artworkUrl: a.artworkUrl100?.replace('100x100','300x300'), artworkUrl100: a.artworkUrl100, - })); - } catch { return []; } + return []; } export async function itunesTopSongs() { - const urls = [ - 'https://rss.applemarketingtools.com/api/v2/de/music/most-played/50/songs.json', - 'https://rss.applemarketingtools.com/api/v2/us/music/most-played/50/songs.json', - ]; - for(const url of urls) { - try { - const r = await fetch(url, { signal: AbortSignal.timeout(6000) }); - if(!r.ok) continue; - const d = await r.json(); - if(d.feed?.results?.length) return d.feed.results; - } catch {} + for (const country of ['de', 'us']) { + const d = await _iGet(`/api/itunes/charts?type=songs&country=${country}`); + if (d.results?.length) return d.results; } - try { - const r = await fetch('https://itunes.apple.com/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 []; } + return []; }