feat(tv): persist downloaded opensubtitles across app restarts

This commit is contained in:
Fredrik Burmester
2026-02-01 19:19:32 +01:00
parent bc575c26c1
commit c35e97f388
8 changed files with 489 additions and 57 deletions

View File

@@ -10,6 +10,7 @@ import {
getPlaystateApi,
getUserLibraryApi,
} from "@jellyfin/sdk/lib/utils/api";
import { File } from "expo-file-system";
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { useAtomValue } from "jotai";
@@ -49,6 +50,7 @@ import { useInactivity } from "@/providers/InactivityProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
import { getSubtitlesForItem } from "@/utils/atoms/downloadedSubtitles";
import { useSettings } from "@/utils/atoms/settings";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
@@ -1075,6 +1077,28 @@ export default function page() {
if (settings.mpvSubtitleAlignY !== undefined) {
await videoRef.current?.setSubtitleAlignY?.(settings.mpvSubtitleAlignY);
}
// Apply subtitle background (iOS only - doesn't work on tvOS due to composite OSD limitation)
// mpv uses #RRGGBBAA format (alpha last, same as CSS)
if (settings.mpvSubtitleBackgroundEnabled) {
const opacity = settings.mpvSubtitleBackgroundOpacity ?? 75;
const alphaHex = Math.round((opacity / 100) * 255)
.toString(16)
.padStart(2, "0")
.toUpperCase();
// Enable background-box mode (required for sub-back-color to work)
await videoRef.current?.setSubtitleBorderStyle?.("background-box");
await videoRef.current?.setSubtitleBackgroundColor?.(
`#000000${alphaHex}`,
);
// Force override ASS subtitle styles so background shows on styled subtitles
await videoRef.current?.setSubtitleAssOverride?.("force");
} else {
// Restore default outline-and-shadow style
await videoRef.current?.setSubtitleBorderStyle?.("outline-and-shadow");
await videoRef.current?.setSubtitleBackgroundColor?.("#00000000");
// Restore default ASS behavior (keep original styles)
await videoRef.current?.setSubtitleAssOverride?.("no");
}
};
applySubtitleSettings();
@@ -1094,6 +1118,28 @@ export default function page() {
applyInitialPlaybackSpeed();
}, [isVideoLoaded, initialPlaybackSpeed]);
// TV only: Pre-load locally downloaded subtitles when video loads
// This adds them to MPV's track list without auto-selecting them
useEffect(() => {
if (!Platform.isTV || !isVideoLoaded || !videoRef.current || !itemId)
return;
const preloadLocalSubtitles = async () => {
const localSubs = getSubtitlesForItem(itemId);
for (const sub of localSubs) {
// Verify file still exists (cache may have been cleared)
const subtitleFile = new File(sub.filePath);
if (!subtitleFile.exists) {
continue;
}
// Add subtitle file to MPV without selecting it (select: false)
await videoRef.current?.addSubtitleFile?.(sub.filePath, false);
}
};
preloadLocalSubtitles();
}, [isVideoLoaded, itemId]);
// Show error UI first, before checking loading/missingdata
if (itemStatus.isError || streamStatus.isError) {
return (

View File

@@ -659,8 +659,30 @@ export default function TVSubtitleModal() {
// Do NOT close modal - user can see and select the new track
} else if (downloadResult.type === "local" && downloadResult.path) {
// Notify parent that a local subtitle was downloaded
modalState?.onLocalSubtitleDownloaded?.(downloadResult.path);
handleClose(); // Only close for local downloads
// Check if component is still mounted after callback
if (!isMountedRef.current) return;
// Refresh tracks to include the newly downloaded subtitle
if (modalState?.refreshSubtitleTracks) {
const newTracks = await modalState.refreshSubtitleTracks();
// Check if component is still mounted after fetching tracks
if (!isMountedRef.current) return;
// Update atom with new tracks
store.set(tvSubtitleModalAtom, {
...modalState,
subtitleTracks: newTracks,
});
// Switch to tracks tab to show the new subtitle
setActiveTab("tracks");
} else {
// No refreshSubtitleTracks available (e.g., from player), just close
handleClose();
}
}
} catch (error) {
console.error("Failed to download subtitle:", error);
@@ -685,13 +707,17 @@ export default function TVSubtitleModal() {
value: -1,
selected: currentSubtitleIndex === -1,
setTrack: () => modalState?.onDisableSubtitles?.(),
isLocal: false,
};
const options = subtitleTracks.map((track: Track) => ({
label: track.name,
sublabel: undefined as string | undefined,
sublabel: track.isLocal
? t("player.downloaded") || "Downloaded"
: (undefined as string | undefined),
value: track.index,
selected: track.index === currentSubtitleIndex,
setTrack: track.setTrack,
isLocal: track.isLocal ?? false,
}));
return [noneOption, ...options];
}, [subtitleTracks, currentSubtitleIndex, t, modalState]);