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

+1 -1
View File
@@ -210,7 +210,7 @@ export async function loadArtistDetail(artist) {
}
}
if(iTracks.length) {
const heading = topSongs.length ? 'Weltweit populär (iTunes)' : 'Populäre Titel';
const heading = topSongs.length ? 'Weltweit populär (Deezer)' : 'Populäre Titel';
const sec = document.createElement('div');
sec.innerHTML = `<div class="section-title" style="margin-top:${topSongs.length?'8px':'0'};margin-bottom:10px">${heading}</div>`;
const list = document.createElement('div'); list.className = 'song-list'; list.style.marginBottom = '20px';
+8 -8
View File
@@ -247,7 +247,7 @@ function _setEnrichStatus(done, current, total) {
if (!el) return;
if (done) {
el.className = 'done';
el.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg> iTunes-Daten geladen';
el.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg> Deezer-Daten geladen';
el.style.display = 'flex';
el.style.opacity = '1';
setTimeout(() => {
@@ -256,7 +256,7 @@ function _setEnrichStatus(done, current, total) {
}, 2000);
} else {
el.className = '';
el.innerHTML = `<div class="enrich-spinner"></div> Lade iTunes-Daten… (${current}/${total})`;
el.innerHTML = `<div class="enrich-spinner"></div> Lade Deezer-Daten… (${current}/${total})`;
el.style.display = 'flex';
el.style.opacity = '1';
}
@@ -274,7 +274,7 @@ async function _loadSpotifyTrackArtwork(tracks, listEl) {
_setArt(el, url);
});
// Enrich each track with iTunes data — same call as global search
// Enrich each track with Deezer data — same call as global search
const CONC = 4;
let enriched = 0;
_setEnrichStatus(false, 0, tracks.length);
@@ -287,15 +287,15 @@ async function _loadSpotifyTrackArtwork(tracks, listEl) {
const results = await itunesSearch(`${t.trackName} ${firstArtist}`, 'musicTrack', 1);
const match = results[0];
if (!match) return;
// Always safe to enrich: artwork, preview, explicit, iTunes artist name
// Always safe to enrich: artwork, preview, explicit, canonical artist name
if (match.artworkUrl100) t.artworkUrl100 = match.artworkUrl100;
if (match.trackExplicitness) t.trackExplicitness = match.trackExplicitness;
if (match.previewUrl) t.previewUrl = match.previewUrl;
// Store iTunes canonical name separately — keeps original Spotify/Japanese in artistName
// Store the Deezer canonical name separately — keeps original Spotify/Japanese in artistName
// so the UI shows the original and YTM search uses the original channel name.
if (match.artistName) t.itunesArtistName = match.artistName;
// Only enrich album/duration if we're confident the iTunes match is the correct song.
// For Japanese titles we can't word-match, so we skip — the backend's lookupItunesTrackMeta
if (match.artistName) t.metaArtistName = match.artistName;
// Only enrich album/duration if we're confident the Deezer match is the correct song.
// For Japanese titles we can't word-match, so we skip — the backend's lookupTrackMeta
// handles it with better matching. For ASCII titles, require at least one shared word.
const JP_RE = /[ぁ-んァ-ン一-龯]/;
const isJapanese = JP_RE.test(t.trackName);