Files
NovaSonic/backend/test/deezer.test.js
T
HoneyandClaude Opus 4.8 1e69831dd8 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>
2026-07-31 12:48:33 +02:00

110 lines
5.0 KiB
JavaScript

'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');
});