feat: move the whole metadata/search stack from iTunes to Deezer (1.3)

Apple's iTunes /search returns 403 from server/datacenter IPs, which had broken global
search, charts, album/artist/single downloads and the metadata-editor autofill in
production. Everything now sources metadata from Deezer (free, no key, more requests,
returns explicit versions + real artist images + durations). Web + Android share one
source, so an artist resolves to one consistent name everywhere. Deezer results are
mapped to the legacy "ItunesItem" shape, so the UI + download flow are unchanged.

Backend:
- New backend/lib/deezer.js: lookupTrackMeta, fetchAlbumTracks, searchAlbumTracks,
  fetchArtistAlbums (+ pure pickTrackMatch/mapAlbumTrack for tests).
- metadata.js drops the iTunes lookups; novasonic-server download paths (track, album,
  artist, playlist enrich, spotify artwork) + autofill now use Deezer.
- Artist download uses Deezer discography (real albums only), not the iTunes album search.
- Editor autofill endpoint /api/itunes/lookup -> /api/meta/lookup; removed the dead
  iTunes /search proxy and the Apple charts RSS proxy.
- backend/test/deezer.test.js: unit tests for numbering, mixed-artist album-artist
  stability, explicit flags, the Japanese-title match guard (npm test; RUN_LIVE=1 for live).

Web:
- api.js search/lookups already on Deezer; charts now use Deezer /chart. Artist cards
  show the real Deezer photo. Visible "iTunes"/"Apple Music" labels -> "Deezer"
  (search badge, charts badge, preview bar, editor button, artist heading). SW ns-v51.

Android:
- New DeezerModels.kt with Dz* models + mappers to ItunesItem/ItunesRssEntry.
- ItunesClient is Deezer-backed (search = tracks+albums+artists in parallel; album/artist
  lookups; /chart charts), same method signatures. Visible strings -> Deezer.

Docs: README, CLAUDE.md and CHANGELOG (1.3) updated; Platform Parity Matrix reflects the
shared Deezer source + Android non-local artist page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-07-31 12:48:33 +02:00
1 parent a65613b5a1
commit 1e69831dd8
19 files changed
+628 -350

No files matched your search

+17 -41
View File
@@ -267,23 +267,11 @@ export async function romajiConvert(texts) {
} catch { return texts; }
}
// ─── ITUNES ───
// 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';
}
// 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`;
}
// ─── DEEZER (search source) ───
// Apple's iTunes /search returns 403 from server IPs, so Deezer is the search backend. Free, no key,
// returns explicit versions + artist images + track durations. Proxied through the backend to avoid
// any CORS surprises. All results are mapped to the iTunes "ItunesItem" shape so the UI + download
// ─── DEEZER (search + charts source) ───
// Apple's iTunes /search returns 403 from server IPs, so Deezer is the metadata backend for
// everything: search, album/artist lookups AND the "Neu & Beliebt" charts. Free, no key, returns
// explicit versions + artist images + track durations. Proxied through the backend to avoid any
// CORS surprises. All results are mapped to the legacy "ItunesItem" shape so the UI + download
// flow stay unchanged.
function _deezerBase() {
return S.cfg.backendUrl ? `${S.cfg.backendUrl}/api/deezer` : 'https://api.deezer.com';
@@ -380,39 +368,27 @@ export async function deezerArtistImage(artistId) {
} catch { return null; }
}
// "Neu & Beliebt" — Deezer's global chart (Apple's charts RSS needed a proxy and the search
// fallback is 403 from server IPs). Mapped to { name, artistName, artworkUrl, artworkUrl100 }.
export async function itunesTopCharts() {
const urls = [_chartsUrl('de', 'albums'), _chartsUrl('us', 'albums')];
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 {}
}
try {
const r = await fetch(`${_itunesBase()}/search?term=new+music&media=music&entity=album&limit=50&country=de`);
const r = await fetch(`${_deezerBase()}/chart/0/albums?limit=50`);
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,
return (d.data || []).map(a => ({
name: a.title, artistName: a.artist?.name || '',
artworkUrl: a.cover_big || a.cover_medium || '', artworkUrl100: a.cover_medium || '',
}));
} catch { return []; }
}
export async function itunesTopSongs() {
const urls = [_chartsUrl('de', 'songs'), _chartsUrl('us', 'songs')];
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 {}
}
try {
const r = await fetch(`${_itunesBase()}/search?term=top+hits&media=music&entity=musicTrack&limit=50&country=de`);
const r = await fetch(`${_deezerBase()}/chart/0/tracks?limit=50`);
const d = await r.json();
return (d.results||[]).map(t => ({ name: t.trackName, artistName: t.artistName, artworkUrl: t.artworkUrl100 }));
return (d.data || []).map(t => ({
name: t.title, artistName: t.artist?.name || '',
artworkUrl: t.album?.cover_medium || t.album?.cover_big || '',
previewUrl: t.preview || '',
}));
} catch { return []; }
}