refactor: deduplicate runtime validation into shared module
This commit is contained in:
@@ -5,42 +5,7 @@ import { updateQualityOptions } from "./quality";
|
|||||||
import { safeLocalStorage } from "./storage";
|
import { safeLocalStorage } from "./storage";
|
||||||
import { streamUrlForMode } from "./source";
|
import { streamUrlForMode } from "./source";
|
||||||
import { loadVideoSource } from "./video";
|
import { loadVideoSource } from "./video";
|
||||||
import type { ModeSource } from "./types";
|
import { isRecord, parseModeSources } from "./validate";
|
||||||
|
|
||||||
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
||||||
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<string, ModeSource> => {
|
|
||||||
if (!isRecord(v)) return {};
|
|
||||||
|
|
||||||
const out: Record<string, ModeSource> = {};
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
const alternateModeFor = (mode: string): "sub" | "dub" | null => {
|
const alternateModeFor = (mode: string): "sub" | "dub" | null => {
|
||||||
if (mode === "sub") return "dub";
|
if (mode === "sub") return "dub";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ModeSource, SkipSegment, SubtitleCue, SubtitleTrack, ActiveSegment } from "./types";
|
import type { ModeSource, SkipSegment, SubtitleCue, SubtitleTrack, ActiveSegment } from "./types";
|
||||||
|
import { parseModeSources, parseSegments } from "./validate";
|
||||||
import { q, qs, dataset } from "../q";
|
import { q, qs, dataset } from "../q";
|
||||||
import { safeLocalStorage } from "./storage";
|
import { safeLocalStorage } from "./storage";
|
||||||
|
|
||||||
@@ -180,54 +181,6 @@ export const initState = (c: HTMLElement): boolean => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
||||||
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<string, ModeSource> => {
|
|
||||||
if (!isRecord(v)) return {};
|
|
||||||
const out: Record<string, ModeSource> = {};
|
|
||||||
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: { ... } }
|
// mode sources = { sub: { token, subtitles, qualities }, dub: { ... } }
|
||||||
state.modeSources = parseModeSources(safeJsonUnknown(dataset(c, "modeSources")));
|
state.modeSources = parseModeSources(safeJsonUnknown(dataset(c, "modeSources")));
|
||||||
|
|
||||||
|
|||||||
47
static/player/validate.ts
Normal file
47
static/player/validate.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import type { ModeSource, SkipSegment } from "./types";
|
||||||
|
|
||||||
|
export const isRecord = (v: unknown): v is Record<string, unknown> =>
|
||||||
|
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<string, ModeSource> => {
|
||||||
|
if (!isRecord(v)) return {};
|
||||||
|
const out: Record<string, ModeSource> = {};
|
||||||
|
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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user