feat: preload alternate mode source on episode load
This commit is contained in:
@@ -3,7 +3,7 @@ import type { SkipSegment } from "../types";
|
||||
import { resolveActiveSegments, renderSegments } from "../skip/segments";
|
||||
import { updateSubtitleOptions } from "../subtitles";
|
||||
import { updateQualityOptions } from "../quality";
|
||||
import { updateModeButtons } from "../mode";
|
||||
import { hydrateAlternateMode, updateModeButtons } from "../mode";
|
||||
import { updateOverlay, isAutoplayEnabled, switchEpisodeRange } from "./ui";
|
||||
import { markEpisodeTransition } from "../progress";
|
||||
import { safeLocalStorage } from "../storage";
|
||||
@@ -105,6 +105,7 @@ export const goToNextEpisode = async (): Promise<void> => {
|
||||
updateQualityOptions();
|
||||
updateModeButtons();
|
||||
updateOverlay(state.currentEpisode, data.episode_title ?? "");
|
||||
void hydrateAlternateMode();
|
||||
|
||||
// update skip segments
|
||||
if (data.segments?.length) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { setupKeyboard } from "./keyboard";
|
||||
import { setupSubtitles, updateSubtitleOptions, updateSubtitleRender } from "./subtitles";
|
||||
import { setupSkip, updateSkipButton, updateAutoSkipButton } from "./skip";
|
||||
import { setupQuality, updateQualityOptions } from "./quality";
|
||||
import { setupMode, updateModeButtons } from "./mode";
|
||||
import { hydrateAlternateMode, setupMode, updateModeButtons } from "./mode";
|
||||
import { setupAutoplayButton, updateEpisodeHighlight, switchEpisodeRange } from "./episodes/ui";
|
||||
import { goToNextEpisode } from "./episodes/nav";
|
||||
import { resolveActiveSegments, renderSegments } from "./skip/segments";
|
||||
@@ -385,6 +385,7 @@ const initPlayer = (): void => {
|
||||
}
|
||||
|
||||
setupThumbnails();
|
||||
void hydrateAlternateMode(signal);
|
||||
};
|
||||
|
||||
onReady(initPlayer);
|
||||
|
||||
@@ -5,6 +5,85 @@ 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<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,
|
||||
subtitles,
|
||||
qualities: isStringArray(qualities) ? qualities : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
const alternateModeFor = (mode: string): "sub" | "dub" | null => {
|
||||
if (mode === "sub") return "dub";
|
||||
if (mode === "dub") return "sub";
|
||||
return null;
|
||||
};
|
||||
|
||||
const mergeAvailableMode = (mode: string): void => {
|
||||
if (state.availableModes.includes(mode)) return;
|
||||
state.availableModes = [...state.availableModes, mode].sort();
|
||||
};
|
||||
|
||||
export const hydrateAlternateMode = async (signal?: AbortSignal): Promise<void> => {
|
||||
const alternateMode = alternateModeFor(state.currentMode);
|
||||
if (!alternateMode) return;
|
||||
if (state.modeSources[alternateMode]?.token) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/watch/episode/${state.malID}/${encodeURIComponent(state.currentEpisode)}?mode=${encodeURIComponent(alternateMode)}`,
|
||||
{ signal },
|
||||
);
|
||||
if (!res.ok) return;
|
||||
|
||||
const data: unknown = await res.json();
|
||||
if (!isRecord(data)) return;
|
||||
|
||||
const sources = parseModeSources(data.mode_sources);
|
||||
const alternateSource = sources[alternateMode];
|
||||
if (!alternateSource?.token) return;
|
||||
|
||||
state.modeSources = {
|
||||
...state.modeSources,
|
||||
[alternateMode]: alternateSource,
|
||||
};
|
||||
mergeAvailableMode(alternateMode);
|
||||
|
||||
updateSubtitleOptions();
|
||||
updateQualityOptions();
|
||||
updateModeButtons();
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof DOMException && error.name === "AbortError") return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Switches between sub/dub mode.
|
||||
|
||||
Reference in New Issue
Block a user