import: the whole feature, and releases put back together
Everything the import grew over the last week, landing in one go — the window, the picker, the audition pen, apply, and the review that stands between them. The fold is the new part. A release filed as a container of parts arrived as N albums, because the scan groups audio by its immediate parent and the container holds none of its own: Analord's eleven volumes were eleven records, and six more releases split the same way. Records now fold into the release above them, and apply is handed an explicit part list — never a recursive walk, which is what let one stray file swallow 288 descendants and duplicate 9 GB. Where releases live is read off the collection rather than assumed, so a genre-nested library moves the answer instead of breaking; below a dominant majority at one depth the pass refuses entirely, because no folding beats wrong folding. Three vetoes independent of depth catch a misread level. Every fold is listed in the review with a way out. research/import/fold-audit.mjs imports the shipping module, so the audited code is the running code: 9 folds, 0 swallows across four library shapes, and none of the 148 artist folders touched. It was that harness that killed the tempting version of this rule — matching a part against its container folded 40 artist folders, Mac Demarco's 8 albums into one, because album folders carry the artist's name. The regression test found a live bug on the way: parts number from 01 each, so flattening two discs collided, and "same name, same size" read the second file as already-imported and dropped it while still counting it. That reuse now also requires that this run has not already written the path. Also here: the picker's accent crossfades between records instead of cutting through the app's blue (ownership moved to the window, which survives the per-record re-key), the fullscreen viewer wears the album's accent instead of the one it left behind in the portal, Skip defers a record without deciding it, and Keep mine is gone — with the cards picked field by field it was Next under another name.
This commit is contained in:
1 parent
4b30c5e573
commit
e7d0971de2
23 files changed
+6498
-258
No files matched your search
@@ -0,0 +1,63 @@
|
||||
// The import review's match evidence — ONE definition, read by both the
|
||||
// picker's verdict panel and the window's outcome buckets, so the filter
|
||||
// and the panel can never disagree about the same record (the mock's rule,
|
||||
// research/import/render.js evidence()).
|
||||
|
||||
export const normx = (s) => (s ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
||||
|
||||
const EDITION_WORD =
|
||||
/\b(deluxe|expanded|remaster(?:ed)?|anniversary|edition|version|reissue|bonus|explicit|clean|mono|stereo|original|special|limited)\b/gi;
|
||||
|
||||
/** What a release is CALLED beyond the record's own name: bracketed years a
|
||||
* ripper added, edition words a service adds. Strip both and two namings of
|
||||
* one record meet. */
|
||||
export const coreTitle = (s) =>
|
||||
normx(
|
||||
String(s ?? "")
|
||||
.replace(/[\[(<][^\])>]*[\])>]/g, " ")
|
||||
.replace(EDITION_WORD, " ")
|
||||
.replace(/\b(19|20)\d{2}\b/g, " "),
|
||||
) || normx(s);
|
||||
|
||||
/** What is actually known about whether candidate `c` is the same record as
|
||||
* the scanned album (title + artist + your track count). Each row is a
|
||||
* fact the user can check, not a score they have to trust. */
|
||||
export function evidence(c, artist, title, trackCount) {
|
||||
if (!c) return { items: [], verdict: "none", edition: false };
|
||||
const artistOk = normx(c.artist) === normx(artist);
|
||||
const titleOk = normx(c.title) === normx(title);
|
||||
const titleClose =
|
||||
!titleOk &&
|
||||
(normx(c.title).includes(normx(title)) || normx(title).includes(normx(c.title)));
|
||||
// Same record, named for a different edition — the ONE case where a
|
||||
// different track count is expected rather than evidence of the wrong
|
||||
// album.
|
||||
const edition = !titleOk && coreTitle(title) === coreTitle(c.title);
|
||||
const countOk = c.trackCount === trackCount;
|
||||
const items = [
|
||||
{ text: artistOk ? "Artist matches" : `Artist: ${c.artist}`, cls: artistOk ? "ok" : "no" },
|
||||
{
|
||||
text: titleOk
|
||||
? "Title matches"
|
||||
: edition
|
||||
? `Same record, different edition: ${c.title}`
|
||||
: titleClose
|
||||
? `Title: ${c.title}`
|
||||
: `Different title: ${c.title}`,
|
||||
cls: titleOk ? "ok" : edition || titleClose ? "warn" : "no",
|
||||
},
|
||||
{
|
||||
text: countOk
|
||||
? `${c.trackCount} tracks, same`
|
||||
: edition
|
||||
? `${trackCount} tracks vs ${c.trackCount} — expected on another edition`
|
||||
: `${trackCount} tracks vs ${c.trackCount}`,
|
||||
cls: countOk ? "ok" : edition ? "warn" : "no",
|
||||
},
|
||||
];
|
||||
const bad = items.filter((i) => i.cls === "no").length;
|
||||
let verdict = bad >= 2 ? "bad" : bad === 1 ? "warn" : "ok";
|
||||
// An edition difference is never "everything agrees".
|
||||
if (edition && verdict === "ok") verdict = "warn";
|
||||
return { items, verdict, edition };
|
||||
}
|
||||
Reference in new issue
Block a user