/* The discography as the desktop files it (CatalogArtist.svelte's tabs): one category per release, the categories' order and labels from the shared TAB_DEFS, editions folded into families by editions.js. The desktop also weighs MusicBrainz type hints and each record's own description before the title heuristics; the phone files on what the catalog page carries — the edition link, the announced date, the mixtape flag, then the same title rules the library uses. */ import { categoryForLocal, TAB_DEFS } from "../lib/localReleaseType.js"; import { releaseFamilies } from "../lib/editions.js"; const today = () => new Date().toISOString().slice(0, 10); /** An announced full-length: dated, and the date is still ahead. */ export const isUpcoming = (a) => (a.record_type || "").toLowerCase() === "album" && typeof a.release_date === "string" && a.release_date.length >= 10 && a.release_date.slice(0, 10) > today(); function ownCategory(album) { if (isUpcoming(album)) return "upcoming"; if (album.is_mixtape) return "mixtape"; return categoryForLocal(album); } /** A linked edition files WITH its base release (one hop). */ export function categoryFor(album, albums) { if (album.edition_group != null && album.id !== album.edition_group) { const base = (albums || []).find((o) => o.id === album.edition_group); if (base) return ownCategory(base); } return ownCategory(album); } /** [{ key, label, items: families }] — only the tabs with something in them. */ export function discoTabs(albums) { const list = albums || []; const by = {}; for (const def of TAB_DEFS) by[def.key] = []; for (const a of list) (by[categoryFor(a, list)] ?? by.other).push(a); return TAB_DEFS .map((def) => ({ key: def.key, label: def.label, items: releaseFamilies(def.cats.flatMap((c) => by[c] || []), def.key) })) .filter((t) => t.items.length > 0); } /** "1997–present" / "1997–2013": the released records' span. */ export function activeYears(albums) { const years = (albums || []) .filter((a) => !isUpcoming(a)) .map((a) => Number(String(a.release_date || "").slice(0, 4))) .filter((y) => Number.isFinite(y) && y > 1900); if (!years.length) return null; const first = Math.min(...years); const last = Math.max(...years); const nowYear = new Date().getFullYear(); return last >= nowYear - 1 ? `${first}–present` : `${first}–${last}`; } /** The newest dated record. */ export function latestOf(albums) { let best = null; for (const a of albums || []) { if (!a.release_date) continue; if (!best || a.release_date > best.release_date) best = a; } return best; } /** One column per year across the whole span, gaps included — null under * three years, when it would say nothing. */ export function timelineOf(albums) { const byYear = new Map(); for (const a of albums || []) { const y = String(a.release_date || "").slice(0, 4); if (y) byYear.set(y, (byYear.get(y) ?? 0) + 1); } const years = [...byYear.keys()].sort(); if (years.length < 3) return null; const first = Number(years[0]); const last = Number(years[years.length - 1]); const peak = Math.max(...byYear.values()); const cols = []; for (let y = first; y <= last; y++) { const n = byYear.get(String(y)) ?? 0; cols.push({ year: y, n, h: n ? Math.max(8, Math.round((n / peak) * 44)) : 2 }); } return { cols, first, last, total: (albums || []).length }; } /** The desktop's social chips: a label per link kind. */ export const SOCIAL_LABEL = { website: "Website", wikipedia: "Wikipedia", bandcamp: "Bandcamp", soundcloud: "SoundCloud", youtube: "YouTube", instagram: "Instagram", twitter: "X", x: "X", facebook: "Facebook", tiktok: "TikTok", spotify: "Spotify", apple: "Apple Music", discogs: "Discogs", lastfm: "Last.fm", musicbrainz: "MusicBrainz", }; /* ── the whole discography, on a page of its own ───────────────────────── The artist page's tabs are one lane per category; the discography page is the same filing seen whole. Both read ONE classifier, and it is this one. The desktop cannot do that: CatalogDiscography.svelte takes the artist page's already-computed tab list as the same array object, because the desktop's filing depends on a background description harvest and on deferred MusicBrainz type hints that a re-run would not read (CatalogDiscography.svelte:28-46). `discoTabs` here is pure and reads only what the catalog page carries, so the phone simply calls it twice over the same cached albums and the two pages can never disagree. */ /** "All" in front of the categories — built by CONCATENATING the per-category * families, never by re-folding the whole set. A single that shares an * album's base title (Pond released "Terrestrials" in 2026 as an album AND * as a single) would fold into it as a phantom edition, and one of the two * would vanish from its own tab. */ export function allTabs(tabs) { const ts = tabs || []; if (!ts.length) return []; return [{ key: "all", label: "All", items: ts.flatMap((t) => t.items) }, ...ts]; } /** The four orders the page offers, and what each is called. The sort sheet * in HalfSheet.svelte writes the same four out inline — it is edited by * every other round and a new import there is a collision waiting to * happen — so if a label changes it changes in both places. */ export const ORDER_LABEL = { new: "Newest first", old: "Oldest first", az: "A–Z", za: "Z–A" }; /** The desktop's sort (CatalogDiscography.svelte:71-78): dates on * `release_date`, names on the title, either direction. */ export function orderFamilies(items, order) { const list = [...(items || [])]; const byName = order === "az" || order === "za"; list.sort((a, b) => (byName ? (a.title ?? "").localeCompare(b.title ?? "") : (a.release_date ?? "").localeCompare(b.release_date ?? ""))); return order === "new" || order === "za" ? list.reverse() : list; } /** [{ key: "2001", items }] — the year headers, cut out of the list in the * order it is already in rather than re-sorted, which is why a name order * must not be grouped: the years would repeat out of sequence. `on` false * gives the whole list back under one null key, so the caller renders one * loop either way. */ export function byYear(items, on) { const list = items || []; if (!on) return list.length ? [{ key: null, items: list }] : []; const out = []; for (const r of list) { const y = String(r.release_date || "").slice(0, 4) || "—"; const last = out[out.length - 1]; if (last && last.key === y) last.items.push(r); else out.push({ key: y, items: [r] }); } return out; }