feat(web): switch the Apple/Erkunden search to Deezer

Apple's iTunes /search returns 403 from server IPs, killing the Erkunden search. Deezer
is free, needs no key, and is actually better: it returns explicit versions, artist
images, track durations and 30s previews.

- Backend: /api/deezer/* proxy (CORS-safe); album download accepts a `tracks` array in
  the request body (Deezer albums send their tracks directly, pre-mapped).
- api.js: itunesSearch / itunesLookupAlbum / itunesLookupArtistSongs|Albums now hit
  Deezer and map results to the existing ItunesItem shape, so the UI + download flow are
  unchanged. Function names kept.
- downloads.js: album downloads fetch the Deezer album's tracks and send them in the body.

iTunes /lookup (by ID) and the charts RSS proxy stay. Known follow-ups: artist-type
download still calls iTunes /search server-side; the metadata-editor iTunes autofill;
and the Android client — all still on iTunes for now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-07-30 20:54:10 +02:00
1 parent ccf704754a
commit 68a9cdd7d1
4 files changed
+103 -22

No files matched your search

+19 -2
View File
@@ -324,6 +324,20 @@ app.get('/api/itunes/:endpoint', (req, res) => {
}).on('error', e => res.status(502).json({ error: e.message }));
});
// Deezer API proxy — the search source (Apple's /search returns 403 from server IPs). Deezer is
// free, needs no key, and returns explicit versions + artist images + track durations. Covers
// /search, /search/album, /search/artist, /album/{id}, /album/{id}/tracks, /artist/{id}/albums, …
app.get('/api/deezer/*', (req, res) => {
const sub = String(req.params[0] || '').replace(/[^a-zA-Z0-9/_-]/g, '');
const qs = new URLSearchParams(req.query).toString();
const target = `https://api.deezer.com/${sub}${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) => {
@@ -626,8 +640,11 @@ app.post('/api/download', async (req, res) => {
}
} else if (type === 'album') {
let tracks = [];
if (collectionId) {
// Prefer a track list supplied in the request body (Deezer albums send their tracks directly,
// pre-mapped to the iTunes track shape). Fall back to the iTunes lookup for legacy callers.
let tracks = Array.isArray(req.body.tracks) ? req.body.tracks : [];
if (tracks.length) log(jobId, `📋 ${tracks.length} Tracks aus Anfrage (Deezer)`);
if (!tracks.length && collectionId) {
log(jobId, '📋 Lade Track-Liste von iTunes…');
tracks = await fetchItunesTracks(collectionId);
}