// How the LIBRARY files a record as an album, an EP, a single, a live set, a // compilation, a soundtrack, a remix record or bonus material. // // The app already decides this, in CatalogArtist.svelte's `categoryFor` — with // MusicBrainz secondary types, editorial description hints, edition-group // folding, Deezer's record_type and a set of title heuristics tuned against // real catalogues. This module is NOT a second opinion on any of that: it is // the subset of that classifier whose evidence a LOCAL record can actually // carry. A file on disk has no MusicBrainz release group, no editorial lead // sentence, no edition_group and usually no record_type — the only evidence it // brings is its title, its track count and its running time. So: // // · every title heuristic below is COPIED VERBATIM from CatalogArtist.svelte // (the regexes are the tuned artifact — the comments there record what each // clause caught). If one is improved there, re-copy it here; they are // one-screen functions and the provenance comments say exactly where from. // · the MB / description-hint / edition branches are omitted because their // inputs cannot exist here — which is exactly how the approved Library // prototype ran this classifier (with those maps empty). // · under it sits the floor Kyle approved for the prototype: when the // classifier lands on "other" AND the row carries no record_type, the two // facts a file always has decide — ≤3 tracks or under 10 minutes is a // single, ≤7 or under 25 an EP. `isSessionOrDemo` is re-checked inside the // floor so a five-track practice tape stays bonus material. // // The right end state is one shared module that CatalogArtist imports too; // that refactor is deliberate follow-up work, not something to smuggle into // the Library landing (the artist page's copy is entangled with its harvest // and identity machinery, and it is tuned and shipping). // ---- title heuristics, verbatim from CatalogArtist.svelte ---------------- // CatalogArtist.svelte `COMPILATION_TITLE`. const COMPILATION_TITLE = /\b(?:greatest\s+hits|best\s+of|very\s+best|anthology|singles\s+collection|the\s+singles|the\s+complete\s+recordings|complete\s+collection|collected\s+works|selected\s+works|retrospective|b[-\s]?sides\s+collection|box\s?set|\d+\s+album\s+set|the\s+essential|collection|the\s+hits|\d+["″]?\s+singles|complete\s+(?:\w+\s+)?recordings|bootleg\s+series)\b/i; // CatalogArtist.svelte `isBoxSetTitle`. A slash-joined title of short // single-word parts carrying a full album's worth of tracks is a box set of // separate records, not a split single. const isBoxSetTitle = (a) => { if ((a.record_type ?? "").toLowerCase() !== "album") return false; if ((a.track_count ?? 0) < 15) return false; const parts = (a.title ?? "").split("/").map((part) => part.trim()); return parts.length >= 2 && parts.every((part) => /^[\p{L}\p{N}]{1,14}$/u.test(part)); }; // CatalogArtist.svelte `isCompilation`. export const isCompilation = (a) => { const type = (a.record_type ?? "").toLowerCase(); return type === "compile" || type === "compilation" || COMPILATION_TITLE.test(a.title ?? "") || isBoxSetTitle(a); }; // CatalogArtist.svelte `isSoundtrack`. export const isSoundtrack = (a) => a.is_soundtrack || /\b(?:soundtracks?|original\s+(?:motion\s+picture\s+)?score|film\s+score|motion\s+picture|music\s+from\s+(?:and\s+inspired\s+by\s+)?(?:the\s+)?(?:original\s+)?(?:films?|movies|television(?:\s+series)?|tv\s+(?:series|show)|video\s+game|\S+\s+(?:trilogy|documentary|saga)))\b/i.test(a.title ?? ""); // CatalogArtist.svelte `isSessionOrDemo`. Practice takes, rehearsals, demos, // outtakes and radio-session odds — bonus material, not studio albums. Checked // AFTER isLive so a genuine "BBC Sessions" live album stays in Live. export const isSessionOrDemo = (a) => /\b(?:practice|rehearsals?|demos?|outtakes?|bbc\s+(?:recordings?|tracks?)|radio\s+sessions?|karaoke)\b/i.test(a.title ?? ""); // CatalogArtist.svelte `isRemix`. export const isRemix = (a) => /\b(?:remix(?:es|ed)?|rework(?:s|ed)?|reconfigur(?:e|ed|ation)|rmx|mixes|dj\s+mix)\b/i.test(a.title ?? ""); // CatalogArtist.svelte `isLive`. export const isLive = (a) => a.is_live || /\b(?:live\s+(?:at|from|in|on|around)|in\s+concert|unplugged|concert\s+recording|live\s+(?:album|recording)|bbc\s+sessions?|peel\s+sessions?|alive\s+(?:19|20)\d{2}|live\s+(?:19|20)\d{2}|concerts?\b|en\s+vivo|ao\s+vivo|en\s+directo|dal\s+vivo|live\s+aus)\b|(?:^|[\s(\[–—-])live(?:\s*[()\[\],:–—-]|$)/i.test(a.title ?? ""); // ---- the classifier ------------------------------------------------------- /** CatalogArtist.svelte `categoryFor`, reduced to the branches whose evidence * a local record can carry, in the same order. (The MB-corroboration branch — * where `page.albums` fed `titleContainsAnotherAlbum` — is gone with the MB * hints that gated it, so no sibling list is needed here.) */ export function categoryForLocal(album) { const type = (album.record_type ?? "").toLowerCase(); if (isSoundtrack(album)) return "soundtrack"; if (isLive(album)) return "live"; if (isCompilation(album)) return "compilation"; if (isRemix(album)) return "remix"; if (isSessionOrDemo(album)) return "other"; if (["album", "ep", "single"].includes(type)) return type; return "other"; } /** The floor under the classifier, exactly as approved on the prototype: only * when it says "other" AND the row carries no provider record_type is there * nothing left to reason from, and the two facts a file always has decide. */ export function localFallback(album) { if (isSessionOrDemo(album)) return "other"; const n = album.track_count || (album.tracks ? album.tracks.length : 0); const mins = (album.secs || 0) / 60; if (!n) return "other"; if (n <= 3 || (mins && mins < 10)) return "single"; if (n <= 7 || (mins && mins < 25)) return "ep"; return "album"; } /** File one album: the classifier, floored. */ export function fileRelease(album) { const cat = categoryForLocal(album); if (cat === "other" && !album.record_type) { const guess = localFallback(album); if (guess !== "other") return guess; } return cat; } // CatalogArtist.svelte `TAB_DEFS` — the categories, their labels and their // ORDER. The Library's shelves-by-type and its Type grouping follow this list // so the two pages file and order identically. export const TAB_DEFS = [ { key: "upcoming", label: "Upcoming", cats: ["upcoming"] }, { key: "album", label: "Albums", cats: ["album"] }, { key: "mixtape", label: "Mixtapes", cats: ["mixtape"] }, { key: "ep", label: "EPs", cats: ["ep"] }, { key: "single", label: "Singles", cats: ["single"] }, { key: "live", label: "Live", cats: ["live"] }, { key: "compilation", label: "Compilations", cats: ["compilation"] }, { key: "soundtrack", label: "Soundtracks", cats: ["soundtrack"] }, { key: "remix", label: "Remixes", cats: ["remix"] }, { key: "other", label: "Other", cats: ["other"] }, ]; export const CAT_ORDER = TAB_DEFS.map((t) => t.cats[0]); export const CAT_LABEL = Object.fromEntries(TAB_DEFS.map((t) => [t.cats[0], t.label])); /** Singular, for a card's sub line and the list view's Type column. */ export const CAT_ONE = { upcoming: "Upcoming", album: "Album", mixtape: "Mixtape", ep: "EP", single: "Single", live: "Live", compilation: "Compilation", soundtrack: "Soundtrack", remix: "Remix", other: "Other", };