Files
NovaSonic/backend/test/deezer.test.js
T
HoneyandClaude Opus 4.8 6d084a7b7a fix(download): stop wrong album/cover from same-titled tracks; add MusicBrainz fallback
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>
2026-08-12 01:51:29 +02:00

121 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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');
});