From 5788216bb6a94a7a4af48608ecb102f779f27fb2 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Tue, 16 Jun 2026 15:35:32 +0200 Subject: [PATCH] feat: restore preferred audio mode on player init --- static/player/main.ts | 40 +++++++++++++++++++---------- static/player/mode.ts | 58 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 74 insertions(+), 24 deletions(-) diff --git a/static/player/main.ts b/static/player/main.ts index 8c3279d..7d5c2f8 100644 --- a/static/player/main.ts +++ b/static/player/main.ts @@ -5,7 +5,12 @@ import { setupKeyboard } from "./keyboard"; import { setupSubtitles, updateSubtitleOptions, updateSubtitleRender } from "./subtitles"; import { setupSkip, updateSkipButton, updateAutoSkipButton } from "./skip"; import { setupQuality, updateQualityOptions } from "./quality"; -import { hydrateAlternateMode, setupMode, updateModeButtons } from "./mode"; +import { + ensurePreferredModeSource, + hydrateAlternateMode, + setupMode, + updateModeButtons, +} from "./mode"; import { setupAutoplayButton, updateEpisodeHighlight, switchEpisodeRange } from "./episodes/ui"; import { goToNextEpisode } from "./episodes/nav"; import { resolveActiveSegments, renderSegments } from "./skip/segments"; @@ -88,7 +93,7 @@ const updatePreviewUI = (ratio: number): void => { state.elements.previewPopover.style.left = `${Math.max(popoverWidth / 2, Math.min(barWidth - popoverWidth / 2, ratio * barWidth))}px`; }; -const initPlayer = (): void => { +const initPlayer = async (): Promise => { const container = document.querySelector("[data-video-player]") as HTMLElement | null; if (!container) return; if (container === currentContainer) return; @@ -119,15 +124,6 @@ const initPlayer = (): void => { } }; - // build video src from mode, token, and saved quality preference - const preferredQuality = safeLocalStorage.getItem("mal:preferred-quality") || "best"; - const streamToken = state.playback.modeSources[state.playback.currentMode]?.token; - if (streamToken) { - const source = state.playback.modeSources[state.playback.currentMode]; - const url = `${state.playback.streamURL}?mode=${encodeURIComponent(state.playback.currentMode)}&token=${encodeURIComponent(streamToken)}${source?.type === "m3u8" ? "&hls=1" : ""}${preferredQuality !== "best" ? `&quality=${encodeURIComponent(preferredQuality)}` : ""}`; - loadVideoSource(url, source?.type); - } - setupProgress(); setupControls(); setupKeyboard(); @@ -143,6 +139,22 @@ const initPlayer = (): void => { setupAutoplayButton(); updateAutoSkipButton(); showControls(); + + await ensurePreferredModeSource(signal); + + // build video src from mode, token, and saved quality preference + const preferredQuality = safeLocalStorage.getItem("mal:preferred-quality") || "best"; + const streamToken = state.playback.modeSources[state.playback.currentMode]?.token; + if (streamToken) { + const source = state.playback.modeSources[state.playback.currentMode]; + const url = `${state.playback.streamURL}?mode=${encodeURIComponent(state.playback.currentMode)}&token=${encodeURIComponent(streamToken)}${source?.type === "m3u8" ? "&hls=1" : ""}${preferredQuality !== "best" ? `&quality=${encodeURIComponent(preferredQuality)}` : ""}`; + loadVideoSource(url, source?.type); + } + + updateSubtitleOptions(); + updateQualityOptions(); + updateModeButtons(); + if (state.playback.modeSwitchedFrom === "dub" && state.playback.currentMode === "sub") { window.showToast?.({ message: `Episode ${state.episode.current} is only available in sub, switched from dub.`, @@ -418,9 +430,11 @@ const initPlayer = (): void => { }; }; -onReady(initPlayer); +onReady(() => { + void initPlayer(); +}); onHtmxLoad((root) => { if (root.matches("[data-video-player]") || root.querySelector("[data-video-player]")) { - initPlayer(); + void initPlayer(); } }); diff --git a/static/player/mode.ts b/static/player/mode.ts index 86f92ca..a34fd1f 100644 --- a/static/player/mode.ts +++ b/static/player/mode.ts @@ -1,3 +1,4 @@ +import type { ModeSource } from "./types"; import { state } from "./state"; import { showControls } from "./controls"; import { updateSubtitleOptions } from "./subtitles"; @@ -13,23 +14,58 @@ const alternateModeFor = (mode: string): "sub" | "dub" | null => { return null; }; +const fetchModeSource = async ( + episode: string, + mode: "sub" | "dub", + signal?: AbortSignal, +): Promise => { + const res = await fetch( + `/api/watch/episode/${state.episode.malID}/${encodeURIComponent(episode)}?mode=${encodeURIComponent(mode)}`, + { signal }, + ); + if (!res.ok) return null; + + const data: unknown = await res.json(); + if (!isRecord(data)) return null; + + const sources = parseModeSources(data.mode_sources); + return sources[mode] ?? null; +}; + +export const ensurePreferredModeSource = async (signal?: AbortSignal): Promise => { + const storedMode = safeLocalStorage.getItem("player-audio-mode"); + const preferredMode = storedMode === "sub" || storedMode === "dub" ? storedMode : null; + if (!preferredMode) return state.playback.currentMode; + if (state.playback.modeSources[preferredMode]?.token) { + state.playback.currentMode = preferredMode; + return preferredMode; + } + + try { + const preferredSource = await fetchModeSource(state.episode.current, preferredMode, signal); + if (!preferredSource?.token) return state.playback.currentMode; + + state.playback.modeSources = { + ...state.playback.modeSources, + [preferredMode]: preferredSource, + }; + state.playback.currentMode = preferredMode; + } catch (error: unknown) { + if (error instanceof DOMException && error.name === "AbortError") { + return state.playback.currentMode; + } + } + + return state.playback.currentMode; +}; + export const hydrateAlternateMode = async (signal?: AbortSignal): Promise => { const alternateMode = alternateModeFor(state.playback.currentMode); if (!alternateMode) return; if (state.playback.modeSources[alternateMode]?.token) return; try { - const res = await fetch( - `/api/watch/episode/${state.episode.malID}/${encodeURIComponent(state.episode.current)}?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]; + const alternateSource = await fetchModeSource(state.episode.current, alternateMode, signal); if (!alternateSource?.token) return; state.playback.modeSources = {