diff --git a/src/lib/anime-filler.ts b/src/lib/anime-filler.ts new file mode 100644 index 0000000..a9f3c54 --- /dev/null +++ b/src/lib/anime-filler.ts @@ -0,0 +1,90 @@ +import { useEffect, useState } from 'react' + +/** + * Bundled per-show anime filler classification. Lives at + * `/data/anime-filler.json`, fetched on demand. Keyed by TMDB id (string). + * + * Shape: + * { + * "": { + * "filler": [12, 13, 28, 29, 30], + * "mostlyFiller": [60, 84] + * } + * } + * + * This is a v1 curated bundled list - covers Naruto, Bleach, One Piece, + * Dragon Ball Z, Hunter x Hunter (1999), etc. Easy to extend by editing + * the JSON. We deliberately don't go online for it because no public + * filler API has reliable CORS / uptime, and the underlying lists rarely + * change. + */ + +export type FillerFlag = 'filler' | 'mostly-filler' | 'canon' | null + +interface ShowFillerData { + filler?: number[] + mostlyFiller?: number[] +} + +type FillerDb = Record + +let cache: FillerDb | null = null +let cachePromise: Promise | null = null + +async function loadFillerDb(): Promise { + if (cache) return cache + if (cachePromise) return cachePromise + cachePromise = (async () => { + try { + const res = await fetch('/data/anime-filler.json') + if (!res.ok) return {} + cache = (await res.json()) as FillerDb + return cache + } catch { + cache = {} + return cache + } + })() + return cachePromise +} + +/** + * Hook: load the filler database once and look up the entry for a given + * TMDB id. Returns null until the data is loaded, or if the show isn't in + * the list (the common case for non-anime). + */ +export function useAnimeFiller(tmdbId?: string | number | null) { + const id = tmdbId != null ? String(tmdbId) : null + const [data, setData] = useState(null) + useEffect(() => { + if (!id) { + setData(null) + return + } + let cancelled = false + loadFillerDb().then(db => { + if (cancelled) return + setData(db[id] || null) + }) + return () => { + cancelled = true + } + }, [id]) + return data +} + +/** + * Classify an absolute episode number against the loaded filler data. + * Anime filler lists almost always count by absolute episode number, not + * by season + episode, so the caller should pass the running episode + * count rather than the per-season number. + */ +export function classifyEpisode( + absoluteEpisode: number, + data: ShowFillerData | null, +): FillerFlag { + if (!data) return null + if (data.filler?.includes(absoluteEpisode)) return 'filler' + if (data.mostlyFiller?.includes(absoluteEpisode)) return 'mostly-filler' + return 'canon' +} diff --git a/src/lib/audio-graph.ts b/src/lib/audio-graph.ts new file mode 100644 index 0000000..1e6671c --- /dev/null +++ b/src/lib/audio-graph.ts @@ -0,0 +1,120 @@ +/** + * Lazy Web Audio graph wrapped around the player's