93 lines
5.0 KiB
JavaScript
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;
|
|
}
|