diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 2eb6a4c..96d92bf 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -319,6 +319,20 @@ app.get('/api/itunes/:endpoint', (req, res) => { 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'); + res.status(upstream.statusCode || 200); + upstream.pipe(res); + }).on('error', e => res.status(502).json({ error: e.message })); +}); + +// Apple "most-played" charts RSS — the browser can't fetch rss.applemarketingtools.com directly +// (no CORS header), so proxy it. kind = albums | songs; country defaults to de. +app.get('/api/itunes-charts/:kind', (req, res) => { + const kind = req.params.kind === 'songs' ? 'songs' : 'albums'; + const country = String(req.query.country || 'de').replace(/[^a-z]/gi, '').slice(0, 2).toLowerCase() || 'de'; + const target = `https://rss.applemarketingtools.com/api/v2/${country}/music/most-played/50/${kind}.json`; + https.get(target, { headers: { 'User-Agent': 'NovaSonic/1.0' } }, upstream => { + res.setHeader('Content-Type', 'application/json'); + res.status(upstream.statusCode || 200); upstream.pipe(res); }).on('error', e => res.status(502).json({ error: e.message })); }); diff --git a/public/sw.js b/public/sw.js index e1771bf..32ddc13 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'ns-v47'; +const CACHE = 'ns-v48'; const PRECACHE = ['/', 'index.html', 'manifest.json', 'icon.svg']; const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js']; // Auth token params stripped from cache keys so artwork survives across sessions/token refreshes. diff --git a/src/api.js b/src/api.js index 06eeae1..4edcbff 100644 --- a/src/api.js +++ b/src/api.js @@ -272,14 +272,29 @@ export async function romajiConvert(texts) { function _itunesBase() { return S.cfg.backendUrl ? `${S.cfg.backendUrl}/api/itunes` : 'https://itunes.apple.com'; } +// Apple charts RSS — proxied through the backend (browsers can't fetch rss.applemarketingtools.com; +// no CORS header). kind = albums | songs. +function _chartsUrl(country, kind) { + return S.cfg.backendUrl + ? `${S.cfg.backendUrl}/api/itunes-charts/${kind}?country=${country}` + : `https://rss.applemarketingtools.com/api/v2/${country}/music/most-played/50/${kind}.json`; +} + +// Last iTunes /search outcome: null = ok, else the HTTP status (e.g. 403 when Apple blocks the +// Search API) or 'network'. Read by the search view to tell "no results" apart from "API down". +let _itunesLastError = null; +export function itunesLastError() { return _itunesLastError; } export async function itunesSearch(query, entity='musicTrack', limit=25) { try { // US store only — one consistent naming source across search + download (matches the Android // client, which is already US). A single fetch also halves the iTunes request rate. const r = await fetch(`${_itunesBase()}/search?term=${encodeURIComponent(query)}&media=music&entity=${entity}&limit=${limit}&country=us&explicit=Yes`); - return JSON.parse(await r.text()).results || []; - } catch { return []; } + if (!r.ok) { _itunesLastError = r.status; return []; } + _itunesLastError = null; + const t = await r.text(); + return t ? (JSON.parse(t).results || []) : []; + } catch { _itunesLastError = 'network'; return []; } } export async function itunesLookupAlbum(collectionId) { @@ -302,10 +317,7 @@ export async function itunesLookupArtistAlbums(artistId, limit=200) { } 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', - ]; + const urls = [_chartsUrl('de', 'albums'), _chartsUrl('us', 'albums')]; for(const url of urls) { try { const r = await fetch(url, { signal: AbortSignal.timeout(6000) }); @@ -325,10 +337,7 @@ export async function itunesTopCharts() { } 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', - ]; + const urls = [_chartsUrl('de', 'songs'), _chartsUrl('us', 'songs')]; for(const url of urls) { try { const r = await fetch(url, { signal: AbortSignal.timeout(6000) }); diff --git a/src/views/search.js b/src/views/search.js index 8ca75d9..919c59b 100644 --- a/src/views/search.js +++ b/src/views/search.js @@ -1,6 +1,6 @@ import { S } from '../state.js'; import { esc, toast } from '../utils.js'; -import { api, itunesSearch, itunesLookupAlbum, fetchRomajiIndex, dispArtistById } from '../api.js'; +import { api, itunesSearch, itunesLastError, itunesLookupAlbum, fetchRomajiIndex, dispArtistById } from '../api.js'; import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem, mkSection } from '../ui/components.js'; import { playPreview } from '../player/player.js'; import { openDlModal } from '../ui/ui.js'; @@ -175,7 +175,19 @@ export async function doGlobalSearch(q) { s.appendChild(list); el.appendChild(s); } if(!artists.length && !albums.length && !trackResults.length) { - el.innerHTML = '
Keine Ergebnisse gefunden
Apple-Suche gerade nicht verfügbar
${detail}
Keine Ergebnisse gefunden
Suche fehlgeschlagen: ${e.message}
`; } }