319 lines
12 KiB
JavaScript
319 lines
12 KiB
JavaScript
// The catalog matcher, against the real import that exposed it: a 258-row
|
|
// playlist where 41 rows read "not in catalog" while plainly being in it.
|
|
// Every case below is a pairing taken from that run.
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
splitVersion,
|
|
qualifierClass,
|
|
primaryArtist,
|
|
bandCore,
|
|
pickCatalogMatch,
|
|
catalogQueries,
|
|
findCatalogTrack,
|
|
pickCrossScript,
|
|
} from "../../src/lib/catalogMatch.js";
|
|
|
|
const track = (artist, title, duration_secs, rank = 500_000) => ({
|
|
id: 1,
|
|
artist,
|
|
title,
|
|
duration_secs,
|
|
rank,
|
|
});
|
|
|
|
test("version wording is split off, not thrown away", () => {
|
|
assert.deepEqual(splitVersion("Bohemian Rhapsody - Remastered 2011"), {
|
|
base: "Bohemian Rhapsody",
|
|
qualifier: "Remastered 2011",
|
|
});
|
|
assert.deepEqual(splitVersion("Lola (2020 Stereo Remaster)"), {
|
|
base: "Lola",
|
|
qualifier: "2020 Stereo Remaster",
|
|
});
|
|
assert.deepEqual(splitVersion("グッドバイ -album version-"), {
|
|
base: "グッドバイ",
|
|
qualifier: "album version",
|
|
});
|
|
assert.deepEqual(splitVersion("Here's to you (feat. Joan Baez)"), {
|
|
base: "Here's to you",
|
|
qualifier: "feat. Joan Baez",
|
|
});
|
|
// A hyphen inside a word is part of the name, never a qualifier.
|
|
assert.deepEqual(splitVersion("A-Punk"), { base: "A-Punk", qualifier: "" });
|
|
// A dash-separated title that names no version keeps its shape.
|
|
assert.deepEqual(splitVersion("Sunday Bloody Sunday - Live"), {
|
|
base: "Sunday Bloody Sunday",
|
|
qualifier: "Live",
|
|
});
|
|
assert.deepEqual(splitVersion("Detroit Rock City"), {
|
|
base: "Detroit Rock City",
|
|
qualifier: "",
|
|
});
|
|
});
|
|
|
|
test("qualifiers are classed by whether they change the recording", () => {
|
|
assert.equal(qualifierClass(""), "none");
|
|
assert.equal(qualifierClass("2011 Remaster"), "edition");
|
|
assert.equal(qualifierClass("Remastered 2011"), "edition");
|
|
assert.equal(qualifierClass("2020 Stereo Remaster"), "edition");
|
|
assert.equal(qualifierClass("feat. Joan Baez"), "edition");
|
|
assert.equal(qualifierClass("Samurai Breaks Remix"), "identity");
|
|
assert.equal(qualifierClass("Acoustic"), "identity");
|
|
assert.equal(qualifierClass("Live"), "identity");
|
|
assert.equal(qualifierClass("Karaoke Version"), "identity");
|
|
});
|
|
|
|
test("the same master worded differently now matches", () => {
|
|
// The headline failure: Spotify's wording vs Deezer's.
|
|
const hit = pickCatalogMatch(
|
|
[track("Queen", "Bohemian Rhapsody (2011 Remaster)", 358)],
|
|
{ artist: "Queen", title: "Bohemian Rhapsody - Remastered 2011", duration_secs: 354 },
|
|
);
|
|
assert.equal(hit?.title, "Bohemian Rhapsody (2011 Remaster)");
|
|
|
|
assert.ok(
|
|
pickCatalogMatch([track("The Kinks", "Lola (Stereo Remastered 2020)", 241)], {
|
|
artist: "The Kinks",
|
|
title: "Lola (2020 Stereo Remaster)",
|
|
duration_secs: 241,
|
|
}),
|
|
);
|
|
// A plain catalog title answering a remastered request, and vice versa.
|
|
assert.ok(
|
|
pickCatalogMatch([track("David Bowie", "Suffragette City", 206)], {
|
|
artist: "David Bowie",
|
|
title: "Suffragette City - 2012 Remastered Version",
|
|
duration_secs: 206,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test("artist wording: ampersands and comma-credited guests", () => {
|
|
assert.ok(
|
|
pickCatalogMatch([track("Joan Jett and the Blackhearts", "Bad Reputation", 170)], {
|
|
artist: "Joan Jett & the Blackhearts",
|
|
title: "Bad Reputation",
|
|
duration_secs: 170,
|
|
}),
|
|
);
|
|
assert.ok(
|
|
pickCatalogMatch([track("Ennio Morricone", "Here's to You", 190)], {
|
|
artist: "Ennio Morricone, Joan Baez",
|
|
title: "Here's to you (feat. Joan Baez)",
|
|
duration_secs: 188,
|
|
}),
|
|
);
|
|
assert.ok(
|
|
pickCatalogMatch([track("King Krule", "Seagirl", 202)], {
|
|
artist: "King Krule, Raveena",
|
|
title: "Seagirl",
|
|
duration_secs: 202,
|
|
}),
|
|
);
|
|
assert.equal(primaryArtist("Ennio Morricone, Joan Baez"), "Ennio Morricone");
|
|
// "and" is part of the act's name, never a credit separator.
|
|
assert.equal(primaryArtist("Joan Jett and the Blackhearts"), "Joan Jett and the Blackhearts");
|
|
|
|
// Deezer files the STUDIO cut under the shorter credit and gives the
|
|
// live takes the full band name; both must reach the same record.
|
|
assert.ok(
|
|
pickCatalogMatch([track("Joan Jett", "Bad Reputation", 169)], {
|
|
artist: "Joan Jett & the Blackhearts",
|
|
title: "Bad Reputation",
|
|
duration_secs: 170,
|
|
}),
|
|
);
|
|
assert.equal(bandCore("Joan Jett and the Blackhearts"), "joan jett");
|
|
// A bare word is never a backing band.
|
|
assert.equal(bandCore("Queen Latifah"), "queen latifah");
|
|
assert.equal(
|
|
pickCatalogMatch([track("Queen Latifah", "U.N.I.T.Y.", 200)], {
|
|
artist: "Queen",
|
|
title: "U.N.I.T.Y.",
|
|
duration_secs: 200,
|
|
}),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test("a different PERFORMANCE still never answers for the original", () => {
|
|
// The DJ-set lesson: a remix must not match the track it samples.
|
|
assert.equal(
|
|
pickCatalogMatch([track("Rebound X", "Rhythm 'N' Gash", 240)], {
|
|
artist: "Rebound X",
|
|
title: "Rhythm 'N' Gash (Samurai Breaks Remix)",
|
|
duration_secs: 240,
|
|
}),
|
|
null,
|
|
);
|
|
// Nor the reverse: asking for the original must not get the remix.
|
|
assert.equal(
|
|
pickCatalogMatch([track("Rebound X", "Rhythm 'N' Gash (Samurai Breaks Remix)", 240)], {
|
|
artist: "Rebound X",
|
|
title: "Rhythm 'N' Gash",
|
|
duration_secs: 240,
|
|
}),
|
|
null,
|
|
);
|
|
// The acoustic/live re-recordings Deezer offered for Bad Reputation.
|
|
assert.equal(
|
|
pickCatalogMatch(
|
|
[
|
|
track("Joan Jett and the Blackhearts", "Bad Reputation (Acoustic)", 160),
|
|
track("Joan Jett and the Blackhearts", "Bad Reputation (Live)", 131),
|
|
],
|
|
{ artist: "Joan Jett & the Blackhearts", title: "Bad Reputation", duration_secs: 170 },
|
|
),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test("covers, karaoke and namesakes are still refused", () => {
|
|
// What Deezer's plain search actually returned for A-Punk.
|
|
assert.equal(
|
|
pickCatalogMatch(
|
|
[
|
|
track("Midium", "A-Punk (Vampire Weekend cover)", 139),
|
|
track("Zoom Karaoke", "A-Punk (Originally By Vampire Weekend) [Karaoke Version]", 133),
|
|
],
|
|
{ artist: "Vampire Weekend", title: "A-Punk", duration_secs: 137 },
|
|
),
|
|
null,
|
|
);
|
|
// A short name inside a longer one is a coincidence, not a match.
|
|
assert.equal(
|
|
pickCatalogMatch([track("Fleetwood Mac", "Go Your Own Way", 223)], {
|
|
artist: "Fleetwood Mac",
|
|
title: "Go",
|
|
duration_secs: 223,
|
|
}),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test("a badly disagreeing duration disqualifies a different cut", () => {
|
|
// The single edit Deezer offered against the album track.
|
|
assert.equal(
|
|
pickCatalogMatch([track("Eagles", "Lyin' Eyes (Single Version; 2018 Remaster)", 255)], {
|
|
artist: "Eagles",
|
|
title: "Lyin' Eyes - 2013 Remaster",
|
|
duration_secs: 381,
|
|
}),
|
|
null,
|
|
);
|
|
// With no duration known, the same pairing is allowed — nothing
|
|
// contradicts it, and a name match is all there is.
|
|
assert.ok(
|
|
pickCatalogMatch([track("Eagles", "Lyin' Eyes (Single Version; 2018 Remaster)", 255)], {
|
|
artist: "Eagles",
|
|
title: "Lyin' Eyes - 2013 Remaster",
|
|
duration_secs: null,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test("the closest cut wins when several versions qualify", () => {
|
|
const hit = pickCatalogMatch(
|
|
[
|
|
track("Queen", "Bohemian Rhapsody (2011 Remaster)", 358, 900_000),
|
|
track("Queen", "Bohemian Rhapsody", 367, 950_000),
|
|
],
|
|
{ artist: "Queen", title: "Bohemian Rhapsody - Remastered 2011", duration_secs: 354 },
|
|
);
|
|
assert.equal(hit?.title, "Bohemian Rhapsody (2011 Remaster)");
|
|
});
|
|
|
|
test("the query ladder asks precisely before asking bluntly", () => {
|
|
assert.deepEqual(
|
|
catalogQueries({ artist: "Eagles", title: "Take It Easy - 2013 Remaster" }),
|
|
[
|
|
'artist:"Eagles" track:"Take It Easy"',
|
|
'artist:"Eagles" track:"Take It Easy - 2013 Remaster"',
|
|
"Eagles Take It Easy",
|
|
],
|
|
);
|
|
// Comma-credited guests are dropped from the query's artist.
|
|
assert.equal(
|
|
catalogQueries({ artist: "King Krule, Raveena", title: "Seagirl" })[0],
|
|
'artist:"King Krule" track:"Seagirl"',
|
|
);
|
|
});
|
|
|
|
test("the ladder stops at the first rung that answers", async () => {
|
|
const asked = [];
|
|
const hit = await findCatalogTrack(
|
|
async (query) => {
|
|
asked.push(query);
|
|
// Mirrors the measured reality: the precise query finds it, the
|
|
// plain one would have returned nothing.
|
|
return query.startsWith("artist:") ? [track("Vashti Bunyan", "Diamond Day", 107)] : [];
|
|
},
|
|
{ artist: "Vashti Bunyan", title: "Diamond Day", duration_secs: 107 },
|
|
);
|
|
assert.equal(hit?.title, "Diamond Day");
|
|
assert.equal(asked.length, 1);
|
|
});
|
|
|
|
test("a romanized catalog entry matches by duration, strictly", () => {
|
|
const want = { artist: "toe", title: "孤独の発明", duration_secs: 203 };
|
|
// The real pairing: Deezer files the track romanized.
|
|
assert.ok(pickCrossScript([track("toe", "Kodokunohatsumei", 203)], want));
|
|
// Two near-length candidates → ambiguous → null.
|
|
assert.equal(
|
|
pickCrossScript(
|
|
[track("toe", "Kodokunohatsumei", 203), track("toe", "Two Moons", 204)],
|
|
want,
|
|
),
|
|
null,
|
|
);
|
|
// No duration on the row → no anchor → null.
|
|
assert.equal(
|
|
pickCrossScript([track("toe", "Kodokunohatsumei", 203)], { ...want, duration_secs: null }),
|
|
null,
|
|
);
|
|
// A Latin-titled row never takes this path.
|
|
assert.equal(
|
|
pickCrossScript([track("toe", "Loneliness Will Shine", 356)], {
|
|
artist: "toe",
|
|
title: "Loneliness Will Shine",
|
|
duration_secs: 356,
|
|
}),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test("the artist shelf is the last rung and answers what search cannot", async () => {
|
|
const asked = { search: 0, shelf: 0 };
|
|
const hit = await findCatalogTrack(
|
|
async () => {
|
|
asked.search += 1;
|
|
return []; // the search index has never heard of it
|
|
},
|
|
{ artist: "Mock Orange", title: "End of the World", duration_secs: null },
|
|
async () => {
|
|
asked.shelf += 1;
|
|
// What the supplemented artist page carries.
|
|
return [track("Mock Orange", "End of the World", 262)];
|
|
},
|
|
);
|
|
assert.equal(hit?.title, "End of the World");
|
|
assert.ok(asked.search >= 2);
|
|
assert.equal(asked.shelf, 1);
|
|
});
|
|
|
|
test("nothing matching still returns null after every rung", async () => {
|
|
const asked = [];
|
|
const hit = await findCatalogTrack(
|
|
async (query) => {
|
|
asked.push(query);
|
|
return [track("Someone Else", "A Different Song", 200)];
|
|
},
|
|
{ artist: "Mock Orange", title: "End of the World", duration_secs: 210 },
|
|
);
|
|
assert.equal(hit, null);
|
|
// Two rungs, not three: this title carries no version wording, so the
|
|
// base-name query and the full-title query are the same string.
|
|
assert.equal(asked.length, 2);
|
|
});
|