From 9d8a497c4801ce69e4a30cd4707c3541fb0d870d Mon Sep 17 00:00:00 2001 From: mkelvers Date: Tue, 16 Jun 2026 09:30:09 +0200 Subject: [PATCH] refactor: deduplicate runtime validation into shared module --- static/player/mode.ts | 37 +---------------------------- static/player/state.ts | 49 +-------------------------------------- static/player/validate.ts | 47 +++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 84 deletions(-) create mode 100644 static/player/validate.ts diff --git a/static/player/mode.ts b/static/player/mode.ts index 1d41937..97560be 100644 --- a/static/player/mode.ts +++ b/static/player/mode.ts @@ -5,42 +5,7 @@ import { updateQualityOptions } from "./quality"; import { safeLocalStorage } from "./storage"; import { streamUrlForMode } from "./source"; import { loadVideoSource } from "./video"; -import type { ModeSource } from "./types"; - -const isRecord = (v: unknown): v is Record => - typeof v === "object" && v !== null && !Array.isArray(v); - -const isStringArray = (v: unknown): v is string[] => - Array.isArray(v) && v.every((item) => typeof item === "string"); - -const isSubtitleItemArray = (v: unknown): v is { lang: string; token: string }[] => - Array.isArray(v) && - v.every( - (item) => isRecord(item) && typeof item.lang === "string" && typeof item.token === "string", - ); - -const parseModeSources = (v: unknown): Record => { - if (!isRecord(v)) return {}; - - const out: Record = {}; - for (const [key, value] of Object.entries(v)) { - if (!isRecord(value)) continue; - if (typeof value.token !== "string" || value.token === "") continue; - - const subtitles = value.subtitles == null ? [] : value.subtitles; - if (!isSubtitleItemArray(subtitles)) continue; - - const qualities = value.qualities; - out[key] = { - token: value.token, - type: typeof value.type === "string" ? value.type : undefined, - subtitles, - qualities: isStringArray(qualities) ? qualities : undefined, - }; - } - - return out; -}; +import { isRecord, parseModeSources } from "./validate"; const alternateModeFor = (mode: string): "sub" | "dub" | null => { if (mode === "sub") return "dub"; diff --git a/static/player/state.ts b/static/player/state.ts index d2c304f..afeb7a1 100644 --- a/static/player/state.ts +++ b/static/player/state.ts @@ -1,4 +1,5 @@ import type { ModeSource, SkipSegment, SubtitleCue, SubtitleTrack, ActiveSegment } from "./types"; +import { parseModeSources, parseSegments } from "./validate"; import { q, qs, dataset } from "../q"; import { safeLocalStorage } from "./storage"; @@ -180,54 +181,6 @@ export const initState = (c: HTMLElement): boolean => { } }; - const isRecord = (v: unknown): v is Record => - typeof v === "object" && v !== null && !Array.isArray(v); - - const isStringArray = (v: unknown): v is string[] => - Array.isArray(v) && v.every((item) => typeof item === "string"); - - const isSubtitleItemArray = (v: unknown): v is { lang: string; token: string }[] => - Array.isArray(v) && - v.every( - (item) => isRecord(item) && typeof item.lang === "string" && typeof item.token === "string", - ); - - const parseModeSources = (v: unknown): Record => { - if (!isRecord(v)) return {}; - const out: Record = {}; - for (const [key, value] of Object.entries(v)) { - if (!isRecord(value)) continue; - if (typeof value.token !== "string" || value.token === "") continue; - // `subtitles` can be `null` when the backend has no subtitles for the stream. - // Treat that as an empty list instead of dropping the whole mode source. - const subtitles = value.subtitles == null ? [] : value.subtitles; - if (!isSubtitleItemArray(subtitles)) continue; - const qualities = value.qualities; - out[key] = { - token: value.token, - type: typeof value.type === "string" ? value.type : undefined, - subtitles, - qualities: isStringArray(qualities) ? qualities : undefined, - }; - } - return out; - }; - - const parseSegments = (v: unknown): SkipSegment[] => { - if (!Array.isArray(v)) return []; - const out: SkipSegment[] = []; - for (const item of v) { - if (!isRecord(item)) continue; - const type = typeof item.type === "string" ? item.type : ""; - const start = typeof item.start === "number" ? item.start : Number(item.start); - const end = typeof item.end === "number" ? item.end : Number(item.end); - const source = typeof item.source === "string" ? item.source : undefined; - if (!type || !Number.isFinite(start) || !Number.isFinite(end)) continue; - out.push({ type, start, end, source }); - } - return out; - }; - // mode sources = { sub: { token, subtitles, qualities }, dub: { ... } } state.modeSources = parseModeSources(safeJsonUnknown(dataset(c, "modeSources"))); diff --git a/static/player/validate.ts b/static/player/validate.ts new file mode 100644 index 0000000..7d036e4 --- /dev/null +++ b/static/player/validate.ts @@ -0,0 +1,47 @@ +import type { ModeSource, SkipSegment } from "./types"; + +export const isRecord = (v: unknown): v is Record => + typeof v === "object" && v !== null && !Array.isArray(v); + +export const isStringArray = (v: unknown): v is string[] => + Array.isArray(v) && v.every((item) => typeof item === "string"); + +export const isSubtitleItemArray = (v: unknown): v is { lang: string; token: string }[] => + Array.isArray(v) && + v.every( + (item) => isRecord(item) && typeof item.lang === "string" && typeof item.token === "string", + ); + +export const parseModeSources = (v: unknown): Record => { + if (!isRecord(v)) return {}; + const out: Record = {}; + for (const [key, value] of Object.entries(v)) { + if (!isRecord(value)) continue; + if (typeof value.token !== "string" || value.token === "") continue; + const subtitles = value.subtitles == null ? [] : value.subtitles; + if (!isSubtitleItemArray(subtitles)) continue; + const qualities = value.qualities; + out[key] = { + token: value.token, + type: typeof value.type === "string" ? value.type : undefined, + subtitles, + qualities: isStringArray(qualities) ? qualities : undefined, + }; + } + return out; +}; + +export const parseSegments = (v: unknown): SkipSegment[] => { + if (!Array.isArray(v)) return []; + const out: SkipSegment[] = []; + for (const item of v) { + if (!isRecord(item)) continue; + const type = typeof item.type === "string" ? item.type : ""; + const start = typeof item.start === "number" ? item.start : Number(item.start); + const end = typeof item.end === "number" ? item.end : Number(item.end); + const source = typeof item.source === "string" ? item.source : undefined; + if (!type || !Number.isFinite(start) || !Number.isFinite(end)) continue; + out.push({ type, start, end, source }); + } + return out; +};