Files
SoulStream/src/lib/import-title.js
T

93 lines
5.0 KiB
JavaScript

// What a scanned record calls ITSELF — the name the catalog should be asked
// about.
//
// The folder is the ripper's naming. It carries catalog numbers, rip
// descriptors, years and initials: "[WARP CD 156] Battles - Mirrored - 2007",
// "AJJ_ChristmasIsland", "ACPP". The tags are the record's own claim:
// "Mirrored", "Christmas Island", "Prospect Park, Brooklyn NY (7.12.2011)".
// Across the reference library 501 of 515 records carry a tag album and 315 of
// those disagree with their folder — so asking a provider about the folder is
// asking the wrong question, and no amount of cleaning turns "ACPP" into the
// right one.
//
// This is deliberately NOT used for the Soulseek search. Peers are searched by
// FILE NAME, where the ripper's descriptors are exactly what finds a match —
// the folder that fails at Deezer is the one that found "[WARP 156CD] Battles"
// on the network. Catalog lookups want the claim; file search wants the naming.
/** Which generation of the identify logic produced a cached answer.
*
* A cached MATCH is good forever. A cached "nothing found" is only as good as
* the thing that did the looking, so when identification learns a new way to
* find a record, every stored negative becomes a stale opinion — and nothing
* re-asks, because the cache is never revisited once written.
*
* Bump this whenever identify gains ground. Negatives stamped with an older
* number are dropped on the next session load and asked again.
*
* 1. Deezer try-list, MusicBrainz fallback.
* 2. + the artist's own catalogue, ranked by title similarity — finds records
* whose folder name no release carries ("1989 Pauls Boutique").
* 3. + the artist the TAGS name, asked when the folder's artist found nothing
* or the wrong record, and + a try-list that refuses to strip a title down
* to its catalogue number ("Band Of Gypsys (SICP 2643)" → "(SICP 2643)").
* 4. + the title without a trailing release-type word ("Willoughby's Beach EP"
* → "Willoughby's Beach"). Both providers require every word of the
* phrase, so the one appended word hid a record each of them holds.
* 5. + the FOLDER as a second witness, asked after the record's own claim.
* Tags carry typos the folder does not ("Nonagon Infiniry" on all nine
* files, in a correctly-spelled folder), and until now one wrong tag was
* the only thing any provider was ever asked about.
* 6. + the record's TRACK COUNT breaks the tie when one title names several
* MusicBrainz release groups ("Murder of the Universe" is a 21-track Album
* and a 1-track Single, both scored 100, in an unstable order), and a
* provider request that FAILED no longer caches as "nothing found".
* 7. The last resort stopped being a title search dressed as a comparison. It
* ranks the artist's WHOLE MusicBrainz discography — complete, where
* Deezer's listing omitted EPs entirely — and decides on every fact the
* scan has: wording, track count, year, the TRACKLIST, and the record's own
* ARTWORK hashed against the discography's covers. A name is one fact about
* a record and the one most often wrong; `Rock Bottom 7"` scores 0.667
* against the release actually called "Rock Bottom / Octopus", and 2 of 2
* on its songs.
*
* BUMP THIS AFTER THE RUST BUILD LANDS, never before. Vite hot-reloads this
* constant instantly while the capability it describes lives in
* `metadata::identify_titles`, so bumping it against a running old binary
* makes that binary sweep the negatives and re-stamp the record on screen as
* freshly looked at by code that learned nothing. */
export const IDENTIFY_ENGINE = 7;
/** The value most of the list agrees on, ignoring blanks. Ties go to the first
* value seen, which keeps the result stable for a re-render. */
export function majority(values) {
const tally = new Map();
for (const v of values) if (v != null && v !== "") tally.set(v, (tally.get(v) ?? 0) + 1);
let best = null;
let n = 0;
for (const [v, c] of tally) if (c > n) { best = v; n = c; }
return best;
}
/** The album name to ask a provider about: the tags' majority, the folder as
* fallback.
*
* A FOLDED release is the exception and takes its folder's name. Its parts'
* tags describe the parts ("Analord 01"…"Analord 11"), so a tag majority
* would title the whole set after whichever volume carried the most tracks.
*
* `record` is a scanned album: { parts?, title, tracks: [{ album }] }. */
export function albumNameOf(record) {
if (!record) return null;
if (record.parts) return record.title;
return majority((record.tracks ?? []).map((t) => t.album)) ?? record.title;
}
/** The artist to ask about. A record nested under an artist folder is named by
* that folder; a loose one has only its tags to go on. */
export function artistNameOf(record) {
if (!record) return null;
if (record.depth >= 2) return record.artistFolder;
return majority((record.tracks ?? []).map((t) => t.artist)) ?? record.artistFolder;
}