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:
1 parent
a65613b5a1
commit
1e69831dd8
19 files changed
+628
-350
No files matched your search
@@ -0,0 +1,109 @@
|
||||
'use strict';
|
||||
// Unit tests for the Deezer metadata mapping/matching (backend/lib/deezer.js).
|
||||
// Pure logic only — no network. Run: `node --test` from backend/ (or `npm test`).
|
||||
// A live smoke test against api.deezer.com is included but only runs with RUN_LIVE=1.
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const dz = require('../lib/deezer');
|
||||
|
||||
// ── Fixtures: the exact Deezer shapes the mappers consume ─────────────────────
|
||||
const ALBUM = {
|
||||
id: 302127,
|
||||
title: 'Discovery',
|
||||
cover_xl: 'https://img/cover_xl.jpg',
|
||||
cover_big: 'https://img/cover_big.jpg',
|
||||
release_date: '2001-03-07',
|
||||
nb_tracks: 14,
|
||||
record_type: 'album',
|
||||
genres: { data: [{ id: 113, name: 'Dance' }] },
|
||||
artist: { id: 27, name: 'Daft Punk' },
|
||||
};
|
||||
const TRACK = {
|
||||
id: 3135556,
|
||||
title: 'Harder, Better, Faster, Stronger',
|
||||
duration: 224,
|
||||
explicit_lyrics: false,
|
||||
preview: 'https://img/preview.mp3',
|
||||
artist: { id: 27, name: 'Daft Punk' },
|
||||
};
|
||||
|
||||
test('mapAlbumTrack: numbers by album order and inherits album-level fields', () => {
|
||||
const item = dz.mapAlbumTrack(TRACK, ALBUM, 3); // 4th track
|
||||
assert.equal(item.wrapperType, 'track');
|
||||
assert.equal(item.kind, undefined); // shape has no kind here; downloadAlbumTracks doesn't read it
|
||||
assert.equal(item.trackNumber, 4); // index + 1 (Deezer album tracks carry no track_position)
|
||||
assert.equal(item.trackName, 'Harder, Better, Faster, Stronger');
|
||||
assert.equal(item.collectionName, 'Discovery');
|
||||
assert.equal(item.collectionArtistName, 'Daft Punk');
|
||||
assert.equal(item.primaryGenreName, 'Dance');
|
||||
assert.equal(item.releaseDate, '2001-03-07');
|
||||
assert.equal(item.trackCount, 14);
|
||||
assert.equal(item.trackTimeMillis, 224000); // seconds → ms
|
||||
assert.equal(item.trackExplicitness, 'notExplicit');
|
||||
assert.equal(item.artworkUrl100, 'https://img/cover_xl.jpg');
|
||||
});
|
||||
|
||||
test('mapAlbumTrack: per-track artist wins over the album artist (mixed-artist albums)', () => {
|
||||
const feat = { ...TRACK, artist: { id: 99, name: 'MeloMance' } };
|
||||
const item = dz.mapAlbumTrack(feat, ALBUM, 0);
|
||||
assert.equal(item.artistName, 'MeloMance'); // drives the per-track YTM search
|
||||
assert.equal(item.collectionArtistName, 'Daft Punk'); // album_artist stays stable → no album split
|
||||
});
|
||||
|
||||
test('mapAlbumTrack: explicit flag maps through', () => {
|
||||
const item = dz.mapAlbumTrack({ ...TRACK, explicit_lyrics: true }, ALBUM, 0);
|
||||
assert.equal(item.trackExplicitness, 'explicit');
|
||||
});
|
||||
|
||||
test('pickTrackMatch: strict match on artist + title', () => {
|
||||
const results = [
|
||||
{ title: 'Some Other Song', artist: { name: 'Daft Punk' } },
|
||||
{ title: 'Harder, Better, Faster, Stronger', artist: { name: 'Daft Punk' } },
|
||||
];
|
||||
const m = dz.pickTrackMatch(results, 'Daft Punk', 'Harder, Better, Faster, Stronger');
|
||||
assert.equal(m, results[1]);
|
||||
});
|
||||
|
||||
test('pickTrackMatch: title-only fallback for non-Japanese when artist differs', () => {
|
||||
const results = [{ title: 'Levitating', artist: { name: 'Some Cover Act' } }];
|
||||
const m = dz.pickTrackMatch(results, 'Dua Lipa', 'Levitating');
|
||||
assert.equal(m, results[0]); // ASCII title → title-only fallback allowed
|
||||
});
|
||||
|
||||
test('pickTrackMatch: NO title-only fallback for Japanese titles', () => {
|
||||
const results = [{ title: '残酷な天使のテーゼ', artist: { name: 'Someone Else' } }];
|
||||
const m = dz.pickTrackMatch(results, '高橋洋子', '残酷な天使のテーゼ');
|
||||
assert.equal(m, null); // JP title + wrong artist must not match a same-titled cover
|
||||
});
|
||||
|
||||
test('pickTrackMatch: Japanese title DOES match when the artist matches', () => {
|
||||
const results = [{ title: '残酷な天使のテーゼ', artist: { name: '高橋洋子' } }];
|
||||
const m = dz.pickTrackMatch(results, '高橋洋子', '残酷な天使のテーゼ');
|
||||
assert.equal(m, results[0]);
|
||||
});
|
||||
|
||||
test('pickTrackMatch: leading aesthetic punctuation is ignored on the artist', () => {
|
||||
const results = [{ title: 'nostalgia', artist: { name: '.diedlonely' } }];
|
||||
const m = dz.pickTrackMatch(results, '.diedlonely', 'nostalgia');
|
||||
assert.equal(m, results[0]);
|
||||
});
|
||||
|
||||
test('pickTrackMatch: empty / no match returns null', () => {
|
||||
assert.equal(dz.pickTrackMatch([], 'x', 'y'), null);
|
||||
assert.equal(dz.pickTrackMatch([{ title: 'A', artist: { name: 'B' } }], 'C', 'D'), null);
|
||||
});
|
||||
|
||||
test('normalizeTitle: unifies variant suffixes and punctuation', () => {
|
||||
assert.equal(dz.normalizeTitle("don't rush - slowed"), dz.normalizeTitle("don't rush (slowed)"));
|
||||
assert.equal(dz.normalizeTitle('Mr. Lawrence'), dz.normalizeTitle('Mr Lawrence'));
|
||||
});
|
||||
|
||||
// ── Live smoke test (opt-in): confirms the real Deezer shape still maps ────────
|
||||
test('LIVE: lookupTrackMeta returns enriched fields', { skip: process.env.RUN_LIVE !== '1' }, async () => {
|
||||
const meta = await dz.lookupTrackMeta('Daft Punk', 'Harder, Better, Faster, Stronger');
|
||||
assert.ok(meta.album, 'album present');
|
||||
assert.ok(meta.genre, 'genre present');
|
||||
assert.ok(meta.year, 'year present');
|
||||
assert.ok(meta.durationMs > 0, 'duration present');
|
||||
});
|
||||
Reference in new issue
Block a user