Root cause: deezer pickTrackMatch fell back to a blind title-only match, grabbing a
same-titled song by a completely different artist (e.g. 'Memories' by Rab Noakes for
'☆NoAki☆') and applying its album/cover/year. The download's clean fallback existed
but was never reached because the lookup almost always returned *something*.
Stufe A — deezer.js: the title-only fallback now REQUIRES an artist correspondence
(equal, or one name contained in the other after normalization). No match → {} →
the existing clean fallback finally runs (album_artist = song artist, album =
'Singles', cover = source thumbnail, year/genre untouched). Test that encoded the old
blind behaviour replaced; NoAki + loose-match regression tests added.
Stufe B — musicbrainz.js: a rate-limited (~1/s, User-Agent) MusicBrainz recording
lookup with the SAME artist discipline + Cover Art Archive cover. trackmeta.js is the
single resolver (Deezer → MusicBrainz → clean fallback); the three single-track call
sites use it (album batch stays Deezer-only to avoid MB rate-limit slowdowns).
Unit-tested (17/17) + live-verified: NoAki→clean fallback, real track→Deezer, MB finds
real albums when Deezer misses. Backend-only; no frontend/Android change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
121 lines
5.6 KiB
JavaScript
121 lines
5.6 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: NO match when the artist is completely different (no wrong album)', () => {
|
||
// Regression: "☆NoAki☆ – Memories" used to grab a same-titled song by an unrelated artist and
|
||
// apply its album/cover/year. A wholly different artist must now return null → clean fallback.
|
||
const results = [
|
||
{ title: 'Memories', artist: { name: 'Rab Noakes' } },
|
||
{ title: 'Memories', artist: { name: 'Maroon 5' } },
|
||
];
|
||
assert.equal(dz.pickTrackMatch(results, '☆NoAki☆', 'Memories'), null);
|
||
assert.equal(dz.pickTrackMatch(results, 'Some Cover Act', 'Levitating'), null);
|
||
});
|
||
|
||
test('pickTrackMatch: loose artist match — one name contained in the other', () => {
|
||
const results = [{ title: 'Flower Dance', artist: { name: 'DJ Okawari' } }];
|
||
const m = dz.pickTrackMatch(results, 'Okawari', 'Flower Dance');
|
||
assert.equal(m, results[0]); // "okawari" ⊂ "djokawari" → accepted
|
||
});
|
||
|
||
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');
|
||
});
|