diff --git a/app/(auth)/(tabs)/(home)/downloads.tsx b/app/(auth)/(tabs)/(home)/downloads.tsx
index 30ba6352..ab61ad8d 100644
--- a/app/(auth)/(tabs)/(home)/downloads.tsx
+++ b/app/(auth)/(tabs)/(home)/downloads.tsx
@@ -2,7 +2,7 @@ import { Text } from "@/components/common/Text";
import { ActiveDownloads } from "@/components/downloads/ActiveDownloads";
import { MovieCard } from "@/components/downloads/MovieCard";
import { SeriesCard } from "@/components/downloads/SeriesCard";
-import { useDownload } from "@/providers/DownloadProvider";
+import { DownloadedItem, useDownload } from "@/providers/DownloadProvider";
import { queueAtom } from "@/utils/atoms/queue";
import { useSettings } from "@/utils/atoms/settings";
import { Ionicons } from "@expo/vector-icons";
@@ -20,16 +20,16 @@ const downloads: React.FC = () => {
const [settings] = useSettings();
const movies = useMemo(
- () => downloadedFiles?.filter((f) => f.Type === "Movie") || [],
+ () => downloadedFiles?.filter((f) => f.item.Type === "Movie") || [],
[downloadedFiles]
);
const groupedBySeries = useMemo(() => {
- const episodes = downloadedFiles?.filter((f) => f.Type === "Episode");
- const series: { [key: string]: BaseItemDto[] } = {};
+ const episodes = downloadedFiles?.filter((f) => f.item.Type === "Episode");
+ const series: { [key: string]: DownloadedItem[] } = {};
episodes?.forEach((e) => {
- if (!series[e.SeriesName!]) series[e.SeriesName!] = [];
- series[e.SeriesName!].push(e);
+ if (!series[e.item.SeriesName!]) series[e.item.SeriesName!] = [];
+ series[e.item.SeriesName!].push(e);
});
return Object.values(series);
}, [downloadedFiles]);
@@ -98,17 +98,20 @@ const downloads: React.FC = () => {
- {movies?.map((item: BaseItemDto) => (
-
-
+ {movies?.map((item) => (
+
+
))}
)}
- {groupedBySeries?.map((items: BaseItemDto[], index: number) => (
-
+ {groupedBySeries?.map((items, index) => (
+ i.item)}
+ key={items[0].item.SeriesId}
+ />
))}
{downloadedFiles?.length === 0 && (
diff --git a/app/(auth)/player/offline-player.tsx b/app/(auth)/player/offline-player.tsx
deleted file mode 100644
index 8ef74b37..00000000
--- a/app/(auth)/player/offline-player.tsx
+++ /dev/null
@@ -1,2 +0,0 @@
-// @ts-ignore
-export { default } from "@/components/video-player/offline-player";
diff --git a/app/(auth)/player/player.tsx b/app/(auth)/player/player.tsx
index c86f2281..d332cb8d 100644
--- a/app/(auth)/player/player.tsx
+++ b/app/(auth)/player/player.tsx
@@ -27,7 +27,7 @@ import {
} from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import * as Haptics from "expo-haptics";
-import { useLocalSearchParams } from "expo-router";
+import { useGlobalSearchParams, useLocalSearchParams } from "expo-router";
import { useAtomValue } from "jotai";
import React, { useCallback, useMemo, useRef, useState } from "react";
import { Alert, Pressable, View } from "react-native";
@@ -58,7 +58,7 @@ export default function page() {
mediaSourceId,
bitrateValue: bitrateValueStr,
offline: offlineStr,
- } = useLocalSearchParams<{
+ } = useGlobalSearchParams<{
itemId: string;
audioIndex: string;
subtitleIndex: string;
@@ -82,21 +82,20 @@ export default function page() {
} = useQuery({
queryKey: ["item", itemId],
queryFn: async () => {
- if (!api) return;
-
+ console.log("Offline:", offline);
if (offline) {
const item = await getDownloadedItem(itemId);
if (item) return item.item;
}
- const res = await getUserLibraryApi(api).getItem({
+ const res = await getUserLibraryApi(api!).getItem({
itemId,
userId: user?.Id,
});
return res.data;
},
- enabled: !!itemId && !!api,
+ enabled: !!itemId,
staleTime: 0,
});
@@ -114,8 +113,7 @@ export default function page() {
bitrateValue,
],
queryFn: async () => {
- if (!api) return;
-
+ console.log("Offline:", offline);
if (offline) {
const item = await getDownloadedItem(itemId);
if (!item?.mediaSource) return null;
@@ -146,7 +144,10 @@ export default function page() {
const { mediaSource, sessionId, url } = res;
- if (!sessionId || !mediaSource || !url) return null;
+ if (!sessionId || !mediaSource || !url) {
+ Alert.alert("Error", "Failed to get stream url");
+ return null;
+ }
return {
mediaSource,
@@ -154,7 +155,7 @@ export default function page() {
url,
};
},
- enabled: !!itemId && !!api && !!item && !offline,
+ enabled: !!itemId && !!item,
staleTime: 0,
});
@@ -292,7 +293,7 @@ export default function page() {
pauseVideo: pause,
playVideo: play,
stopPlayback: stop,
- offline: offline,
+ offline,
});
const onPlaybackStateChanged = useCallback((e: PlaybackStatePayload) => {
@@ -338,7 +339,13 @@ export default function page() {
);
- if (!stream || !item) return null;
+ if (!stream || !item)
+ return (
+
+ No stream or item
+ Offline: {offline}
+
+ );
return (
= ({ item, ...props }) => {
deviceProfile: native,
});
- if (!res) return null;
+ if (!res) {
+ Alert.alert(
+ "Something went wrong",
+ "Could not get stream url from Jellyfin"
+ );
+ return;
+ }
const { mediaSource, url } = res;
if (!url || !mediaSource) throw new Error("No url");
- if (!mediaSource.TranscodingContainer) throw new Error("No file extension");
+
+ saveDownloadItemInfoToDiskTmp(item, mediaSource, url);
if (settings?.downloadMethod === "optimized") {
- return await startBackgroundDownload(
- url,
- item,
- mediaSource.TranscodingContainer
- );
+ return await startBackgroundDownload(url, item, mediaSource);
} else {
return await startRemuxing(item, url, mediaSource);
}
@@ -147,7 +150,7 @@ export const DownloadItem: React.FC = ({ item, ...props }) => {
const isDownloaded = useMemo(() => {
if (!downloadedFiles) return false;
- return downloadedFiles.some((file) => file.Id === item.Id);
+ return downloadedFiles.some((file) => file.item.Id === item.Id);
}, [downloadedFiles, item.Id]);
const renderBackdrop = useCallback(
@@ -164,7 +167,7 @@ export const DownloadItem: React.FC = ({ item, ...props }) => {
const process = useMemo(() => {
if (!processes) return null;
- return processes.find((process) => process?.item?.item.Id === item.Id);
+ return processes.find((process) => process?.item?.Id === item.Id);
}, [processes, item.Id]);
return (
@@ -172,7 +175,7 @@ export const DownloadItem: React.FC = ({ item, ...props }) => {
className="bg-neutral-800/80 rounded-full h-10 w-10 flex items-center justify-center"
{...props}
>
- {process && process?.item.item.Id === item.Id ? (
+ {process && process?.item.Id === item.Id ? (
{
router.push("/downloads");
diff --git a/components/downloads/ActiveDownloads.tsx b/components/downloads/ActiveDownloads.tsx
index 05bc0efe..9a418357 100644
--- a/components/downloads/ActiveDownloads.tsx
+++ b/components/downloads/ActiveDownloads.tsx
@@ -26,7 +26,7 @@ import { storage } from "@/utils/mmkv";
interface Props extends ViewProps {}
export const ActiveDownloads: React.FC = ({ ...props }) => {
- const { processes, startDownload } = useDownload();
+ const { processes } = useDownload();
if (processes?.length === 0)
return (
@@ -93,20 +93,18 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
const eta = (p: JobStatus) => {
if (!p.speed || !p.progress) return null;
- const length = p?.item?.item.RunTimeTicks || 0;
+ const length = p?.item?.RunTimeTicks || 0;
const timeLeft = (length - length * (p.progress / 100)) / p.speed;
return formatTimeString(timeLeft, "tick");
};
const base64Image = useMemo(() => {
- return storage.getString(process.item.item.Id!);
+ return storage.getString(process.item.Id!);
}, []);
return (
- router.push(`/(auth)/items/page?id=${process.item.item.Id}`)
- }
+ onPress={() => router.push(`/(auth)/items/page?id=${process.item.Id}`)}
className="relative bg-neutral-900 border border-neutral-800 rounded-2xl overflow-hidden"
{...props}
>
@@ -140,12 +138,10 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
)}
- {process.item.item.Type}
-
- {process.item.item.Name}
-
+ {process.item.Type}
+ {process.item.Name}
- {process.item.item.ProductionYear}
+ {process.item.ProductionYear}
{process.progress === 0 ? (
diff --git a/components/video-player/offline-player.android.tsx b/components/video-player/offline-player.android.tsx
deleted file mode 100644
index a77912be..00000000
--- a/components/video-player/offline-player.android.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-import { Controls } from "@/components/video-player/Controls";
-import { useOrientation } from "@/hooks/useOrientation";
-import { useOrientationSettings } from "@/hooks/useOrientationSettings";
-import { apiAtom } from "@/providers/JellyfinProvider";
-import {
- PlaybackType,
- usePlaySettings,
-} from "@/providers/PlaySettingsProvider";
-import { secondsToTicks } from "@/utils/secondsToTicks";
-import { Api } from "@jellyfin/sdk";
-import * as Haptics from "expo-haptics";
-import { useFocusEffect } from "expo-router";
-import { useAtomValue } from "jotai";
-import React, {
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from "react";
-import { Pressable, useWindowDimensions, View } from "react-native";
-import { SystemBars } from "react-native-edge-to-edge";
-import { useSharedValue } from "react-native-reanimated";
-import Video, { OnProgressData, VideoRef } from "react-native-video";
-
-const OfflinePlayer = () => {
- const { playSettings, playUrl } = usePlaySettings();
-
- const api = useAtomValue(apiAtom);
- const videoRef = useRef(null);
- const videoSource = useVideoSource(playSettings, api, playUrl);
- const firstTime = useRef(true);
-
- const dimensions = useWindowDimensions();
- useOrientation();
- useOrientationSettings();
-
- const [showControls, setShowControls] = useState(true);
- const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false);
- const [isPlaying, setIsPlaying] = useState(false);
- const [isBuffering, setIsBuffering] = useState(true);
- const [isReady, setIsReady] = useState(false);
-
- useEffect(() => {
- const timer = setTimeout(() => {
- setIsReady(true);
- }, 2000);
-
- return () => clearTimeout(timer);
- }, []);
-
- const progress = useSharedValue(0);
- const isSeeking = useSharedValue(false);
- const cacheProgress = useSharedValue(0);
-
- const [embededTextTracks, setEmbededTextTracks] = useState<
- {
- index: number;
- language?: string | undefined;
- selected?: boolean | undefined;
- title?: string | undefined;
- type: any;
- }[]
- >([]);
-
- const togglePlay = useCallback(async () => {
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- if (isPlaying) {
- videoRef.current?.pause();
- } else {
- videoRef.current?.resume();
- }
- }, [isPlaying]);
-
- const play = useCallback(() => {
- setIsPlaying(true);
- videoRef.current?.resume();
- }, [videoRef]);
-
- const stop = useCallback(() => {
- setIsPlaying(false);
- videoRef.current?.pause();
- }, [videoRef]);
-
- const pause = useCallback(() => {
- videoRef.current?.pause();
- }, [videoRef]);
-
- const seek = useCallback(
- (seconds: number) => {
- videoRef.current?.seek(seconds);
- },
- [videoRef]
- );
-
- useFocusEffect(
- useCallback(() => {
- play();
-
- return () => {
- stop();
- };
- }, [play, stop])
- );
-
- const onProgress = useCallback(async (data: OnProgressData) => {
- if (isSeeking.value === true) return;
- progress.value = secondsToTicks(data.currentTime);
- cacheProgress.value = secondsToTicks(data.playableDuration);
- setIsBuffering(data.playableDuration === 0);
- }, []);
-
- if (!isReady) return null;
-
- if (!playSettings || !playUrl || !api || !videoSource || !playSettings.item)
- return null;
-
- return (
-
- {
- setShowControls(!showControls);
- }}
- className="absolute z-0 h-full w-full"
- >
-
-
-
-
- );
-};
-
-export function useVideoSource(
- playSettings: PlaybackType | null,
- api: Api | null,
- playUrl?: string | null
-) {
- const videoSource = useMemo(() => {
- if (!playSettings || !api || !playUrl) {
- return null;
- }
-
- const startPosition = 0;
-
- return {
- uri: playUrl,
- isNetwork: false,
- startPosition,
- metadata: {
- artist: playSettings.item?.AlbumArtist ?? undefined,
- title: playSettings.item?.Name || "Unknown",
- description: playSettings.item?.Overview ?? undefined,
- subtitle: playSettings.item?.Album ?? undefined,
- },
- };
- }, [playSettings, api]);
-
- return videoSource;
-}
-
-export default OfflinePlayer;
diff --git a/components/video-player/offline-player.ios.tsx b/components/video-player/offline-player.ios.tsx
deleted file mode 100644
index 451e446b..00000000
--- a/components/video-player/offline-player.ios.tsx
+++ /dev/null
@@ -1,232 +0,0 @@
-import { Controls } from "@/components/video-player/Controls";
-import { useOrientation } from "@/hooks/useOrientation";
-import { useOrientationSettings } from "@/hooks/useOrientationSettings";
-import { VlcPlayerView } from "@/modules/vlc-player";
-import {
- PlaybackStatePayload,
- ProgressUpdatePayload,
- VlcPlayerViewRef,
-} from "@/modules/vlc-player/src/VlcPlayer.types";
-import { apiAtom } from "@/providers/JellyfinProvider";
-import { usePlaySettings } from "@/providers/PlaySettingsProvider";
-import { useSettings } from "@/utils/atoms/settings";
-import * as Haptics from "expo-haptics";
-import { useFocusEffect } from "expo-router";
-import { useAtomValue } from "jotai";
-import React, {
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from "react";
-import { Pressable, useWindowDimensions, View } from "react-native";
-import { SystemBars } from "react-native-edge-to-edge";
-import { useSharedValue } from "react-native-reanimated";
-import { SelectedTrackType } from "react-native-video";
-
-const OfflinePlayer = () => {
- const { playSettings, playUrl } = usePlaySettings();
- const api = useAtomValue(apiAtom);
- const [settings] = useSettings();
- const videoRef = useRef(null);
-
- const dimensions = useWindowDimensions();
- useOrientation();
- useOrientationSettings();
-
- const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
- const [showControls, setShowControls] = useState(true);
- const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false);
- const [isPlaying, setIsPlaying] = useState(false);
- const [isBuffering, setIsBuffering] = useState(true);
-
- const progress = useSharedValue(0);
- const isSeeking = useSharedValue(false);
- const cacheProgress = useSharedValue(0);
-
- const [playbackState, setPlaybackState] = useState<
- PlaybackStatePayload["nativeEvent"] | null
- >(null);
-
- if (!playSettings || !playUrl || !api || !playSettings.item) return null;
-
- const togglePlay = useCallback(
- async (ticks: number) => {
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- if (isPlaying) {
- videoRef.current?.pause();
- } else {
- videoRef.current?.play();
- }
- },
- [isPlaying, api, playSettings?.item?.Id, videoRef, settings]
- );
-
- const play = useCallback(() => {
- videoRef.current?.play();
- }, [videoRef]);
-
- const pause = useCallback(() => {
- videoRef.current?.pause();
- }, [videoRef]);
-
- const stop = useCallback(() => {
- setIsPlaybackStopped(true);
- videoRef.current?.stop();
- }, [videoRef]);
-
- const onProgress = useCallback(
- async (data: ProgressUpdatePayload) => {
- if (isSeeking.value === true) return;
- if (isPlaybackStopped === true) return;
-
- const { currentTime, duration, isBuffering, isPlaying } =
- data.nativeEvent;
-
- progress.value = currentTime;
-
- // cacheProgress.value = secondsToTicks(data.playableDuration);
- // setIsBuffering(data.playableDuration === 0);
- },
- [playSettings?.item.Id, isPlaying, api, isPlaybackStopped]
- );
-
- useFocusEffect(
- useCallback(() => {
- play();
-
- return () => {
- stop();
- };
- }, [play, stop])
- );
-
- useOrientation();
- useOrientationSettings();
-
- const selectedSubtitleTrack = useMemo(() => {
- const a = playSettings?.mediaSource?.MediaStreams?.find(
- (s) => s.Index === playSettings.subtitleIndex
- );
- console.log(a);
- return a;
- }, [playSettings]);
-
- const [hlsSubTracks, setHlsSubTracks] = useState<
- {
- index: number;
- language?: string | undefined;
- selected?: boolean | undefined;
- title?: string | undefined;
- type: any;
- }[]
- >([]);
-
- const selectedTextTrack = useMemo(() => {
- for (let st of hlsSubTracks) {
- if (st.title === selectedSubtitleTrack?.DisplayTitle) {
- return {
- type: SelectedTrackType.TITLE,
- value: selectedSubtitleTrack?.DisplayTitle ?? "",
- };
- }
- }
- return undefined;
- }, [hlsSubTracks]);
-
- const onPlaybackStateChanged = (e: PlaybackStatePayload) => {
- const { target, state, isBuffering, isPlaying } = e.nativeEvent;
-
- if (state === "Playing") {
- setIsPlaying(true);
- return;
- }
-
- if (state === "Paused") {
- setIsPlaying(false);
- return;
- }
-
- if (isPlaying) {
- setIsPlaying(true);
- setIsBuffering(false);
- } else if (isBuffering) {
- setIsBuffering(true);
- }
-
- setPlaybackState(e.nativeEvent);
- };
-
- useEffect(() => {
- return () => {
- stop();
- };
- }, []);
-
- useEffect(() => {
- console.log(playUrl);
- }, [playUrl]);
-
- return (
-
- {
- setShowControls(!showControls);
- }}
- className="absolute z-0 h-full w-full"
- >
-
-
- {videoRef.current && (
-
- )}
-
- );
-};
-
-export default OfflinePlayer;
diff --git a/components/video-player/player.android.tsx b/components/video-player/player.android.tsx
deleted file mode 100644
index 6f4bd52f..00000000
--- a/components/video-player/player.android.tsx
+++ /dev/null
@@ -1,400 +0,0 @@
-import { BITRATES } from "@/components/BitrateSelector";
-import { Text } from "@/components/common/Text";
-import { Loader } from "@/components/Loader";
-import { Controls } from "@/components/video-player/Controls";
-import { useOrientation } from "@/hooks/useOrientation";
-import { useOrientationSettings } from "@/hooks/useOrientationSettings";
-import { useWebSocket } from "@/hooks/useWebsockets";
-import { VlcPlayerView } from "@/modules/vlc-player";
-import {
- PlaybackStatePayload,
- ProgressUpdatePayload,
- VlcPlayerViewRef,
-} from "@/modules/vlc-player/src/VlcPlayer.types";
-import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
-import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
-import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
-import { writeToLog } from "@/utils/log";
-import native from "@/utils/profiles/native";
-import { msToTicks, ticksToMs } from "@/utils/time";
-import { Api } from "@jellyfin/sdk";
-import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
-import {
- getPlaystateApi,
- getUserLibraryApi,
-} from "@jellyfin/sdk/lib/utils/api";
-import { useQuery } from "@tanstack/react-query";
-import * as Haptics from "expo-haptics";
-import { useLocalSearchParams } from "expo-router";
-import { useAtomValue } from "jotai";
-import React, { useCallback, useMemo, useRef, useState } from "react";
-import { Alert, Pressable, useWindowDimensions, View } from "react-native";
-import { SystemBars } from "react-native-edge-to-edge";
-import { useSharedValue } from "react-native-reanimated";
-
-const Player = () => {
- const videoRef = useRef(null);
- const user = useAtomValue(userAtom);
- const api = useAtomValue(apiAtom);
-
- const windowDimensions = useWindowDimensions();
-
- const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
- const [showControls, setShowControls] = useState(true);
- const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false);
- const [isPlaying, setIsPlaying] = useState(false);
- const [isBuffering, setIsBuffering] = useState(true);
- const [isVideoLoaded, setIsVideoLoaded] = useState(false);
-
- const progress = useSharedValue(0);
- const isSeeking = useSharedValue(false);
- const cacheProgress = useSharedValue(0);
-
- const {
- itemId,
- audioIndex: audioIndexStr,
- subtitleIndex: subtitleIndexStr,
- mediaSourceId,
- bitrateValue: bitrateValueStr,
- } = useLocalSearchParams<{
- itemId: string;
- audioIndex: string;
- subtitleIndex: string;
- mediaSourceId: string;
- bitrateValue: string;
- }>();
-
- const audioIndex = audioIndexStr ? parseInt(audioIndexStr, 10) : undefined;
- const subtitleIndex = subtitleIndexStr ? parseInt(subtitleIndexStr, 10) : -1;
- const bitrateValue = bitrateValueStr
- ? parseInt(bitrateValueStr, 10)
- : BITRATES[0].value;
-
- const {
- data: item,
- isLoading: isLoadingItem,
- isError: isErrorItem,
- } = useQuery({
- queryKey: ["item", itemId],
- queryFn: async () => {
- if (!api) return;
- const res = await getUserLibraryApi(api).getItem({
- itemId,
- userId: user?.Id,
- });
-
- return res.data;
- },
- enabled: !!itemId && !!api,
- staleTime: 0,
- });
-
- const {
- data: stream,
- isLoading: isLoadingStreamUrl,
- isError: isErrorStreamUrl,
- } = useQuery({
- queryKey: [
- "stream-url",
- itemId,
- audioIndex,
- subtitleIndex,
- mediaSourceId,
- bitrateValue,
- ],
- queryFn: async () => {
- if (!api) return;
- const res = await getStreamUrl({
- api,
- item,
- startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
- userId: user?.Id,
- audioStreamIndex: audioIndex,
- maxStreamingBitrate: bitrateValue,
- mediaSourceId: mediaSourceId,
- subtitleStreamIndex: subtitleIndex,
- deviceProfile: native,
- });
-
- if (!res) return null;
-
- const { mediaSource, sessionId, url } = res;
-
- if (!sessionId || !mediaSource || !url) return null;
-
- console.log(url);
-
- return {
- mediaSource,
- sessionId,
- url,
- };
- },
- enabled: !!itemId && !!api && !!item,
- staleTime: 0,
- });
-
- const togglePlay = useCallback(
- async (ms: number) => {
- if (!api || !stream) return;
-
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- if (isPlaying) {
- await videoRef.current?.pause();
-
- await getPlaystateApi(api).onPlaybackProgress({
- itemId: item?.Id!,
- audioStreamIndex: audioIndex ? audioIndex : undefined,
- subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
- mediaSourceId: mediaSourceId,
- positionTicks: msToTicks(ms),
- isPaused: true,
- playMethod: stream.url?.includes("m3u8")
- ? "Transcode"
- : "DirectStream",
- playSessionId: stream.sessionId,
- });
- console.log("ACtually marked as paused");
- } else {
- videoRef.current?.play();
- await getPlaystateApi(api).onPlaybackProgress({
- itemId: item?.Id!,
- audioStreamIndex: audioIndex ? audioIndex : undefined,
- subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
- mediaSourceId: mediaSourceId,
- positionTicks: msToTicks(ms),
- isPaused: false,
- playMethod: stream?.url.includes("m3u8")
- ? "Transcode"
- : "DirectStream",
- playSessionId: stream.sessionId,
- });
- }
- },
- [
- isPlaying,
- api,
- item,
- stream,
- videoRef,
- audioIndex,
- subtitleIndex,
- mediaSourceId,
- ]
- );
-
- const play = useCallback(() => {
- videoRef.current?.play();
- reportPlaybackStart();
- }, [videoRef]);
-
- const pause = useCallback(() => {
- videoRef.current?.pause();
- }, [videoRef]);
-
- const stop = useCallback(() => {
- setIsPlaybackStopped(true);
- videoRef.current?.stop();
- reportPlaybackStopped();
- }, [videoRef]);
-
- const reportPlaybackStopped = async () => {
- const currentTimeInTicks = msToTicks(progress.value);
-
- await getPlaystateApi(api!).onPlaybackStopped({
- itemId: item?.Id!,
- mediaSourceId: mediaSourceId,
- positionTicks: currentTimeInTicks,
- playSessionId: stream?.sessionId!,
- });
- };
-
- const reportPlaybackStart = async () => {
- if (!api || !stream) return;
- await getPlaystateApi(api).onPlaybackStart({
- itemId: item?.Id!,
- audioStreamIndex: audioIndex ? audioIndex : undefined,
- subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
- mediaSourceId: mediaSourceId,
- playMethod: stream.url?.includes("m3u8") ? "Transcode" : "DirectStream",
- playSessionId: stream?.sessionId ? stream?.sessionId : undefined,
- });
- };
-
- const onProgress = useCallback(
- async (data: ProgressUpdatePayload) => {
- if (isSeeking.value === true) return;
- if (isPlaybackStopped === true) return;
- if (!item?.Id || !api || !stream) return;
-
- const { currentTime } = data.nativeEvent;
-
- if (isBuffering) {
- setIsBuffering(false);
- }
-
- progress.value = currentTime;
- const currentTimeInTicks = msToTicks(currentTime);
-
- await getPlaystateApi(api).onPlaybackProgress({
- itemId: item.Id,
- audioStreamIndex: audioIndex ? audioIndex : undefined,
- subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
- mediaSourceId: mediaSourceId,
- positionTicks: Math.floor(currentTimeInTicks),
- isPaused: !isPlaying,
- playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
- playSessionId: stream.sessionId,
- });
- },
- [item?.Id, isPlaying, api, isPlaybackStopped]
- );
-
- useOrientation();
- useOrientationSettings();
-
- useWebSocket({
- isPlaying: isPlaying,
- pauseVideo: pause,
- playVideo: play,
- stopPlayback: stop,
- });
-
- const onPlaybackStateChanged = (e: PlaybackStatePayload) => {
- const { state, isBuffering, isPlaying } = e.nativeEvent;
-
- if (state === "Playing") {
- setIsPlaying(true);
- return;
- }
-
- if (state === "Paused") {
- setIsPlaying(false);
- return;
- }
-
- if (isPlaying) {
- setIsPlaying(true);
- setIsBuffering(false);
- } else if (isBuffering) {
- setIsBuffering(true);
- }
- };
-
- if (isLoadingItem || isLoadingStreamUrl)
- return (
-
-
-
- );
-
- if (isErrorItem || isErrorStreamUrl)
- return (
-
- Error
-
- );
-
- if (!stream || !item) return null;
-
- const startPosition = item?.UserData?.PlaybackPositionTicks
- ? ticksToMs(item.UserData.PlaybackPositionTicks)
- : 0;
-
- return (
-
- {
- setShowControls(!showControls);
- }}
- className="absolute z-0 h-full w-full"
- >
- {}}
- onVideoLoadEnd={() => {
- setIsVideoLoaded(true);
- }}
- onVideoError={(e) => {
- console.error("Video Error:", e.nativeEvent);
- Alert.alert(
- "Error",
- "An error occurred while playing the video. Check logs in settings."
- );
- writeToLog("ERROR", "Video Error", e.nativeEvent);
- }}
- />
-
-
- {videoRef.current && (
-
- )}
-
- );
-};
-
-export function usePoster(
- item: BaseItemDto,
- api: Api | null
-): string | undefined {
- const poster = useMemo(() => {
- if (!item || !api) return undefined;
- return item.Type === "Audio"
- ? `${api.basePath}/Items/${item.AlbumId}/Images/Primary?tag=${item.AlbumPrimaryImageTag}&quality=90&maxHeight=200&maxWidth=200`
- : getBackdropUrl({
- api,
- item: item,
- quality: 70,
- width: 200,
- });
- }, [item, api]);
-
- return poster ?? undefined;
-}
-
-export default Player;
diff --git a/components/video-player/player.ios.tsx b/components/video-player/player.ios.tsx
deleted file mode 100644
index 6f4bd52f..00000000
--- a/components/video-player/player.ios.tsx
+++ /dev/null
@@ -1,400 +0,0 @@
-import { BITRATES } from "@/components/BitrateSelector";
-import { Text } from "@/components/common/Text";
-import { Loader } from "@/components/Loader";
-import { Controls } from "@/components/video-player/Controls";
-import { useOrientation } from "@/hooks/useOrientation";
-import { useOrientationSettings } from "@/hooks/useOrientationSettings";
-import { useWebSocket } from "@/hooks/useWebsockets";
-import { VlcPlayerView } from "@/modules/vlc-player";
-import {
- PlaybackStatePayload,
- ProgressUpdatePayload,
- VlcPlayerViewRef,
-} from "@/modules/vlc-player/src/VlcPlayer.types";
-import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
-import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
-import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
-import { writeToLog } from "@/utils/log";
-import native from "@/utils/profiles/native";
-import { msToTicks, ticksToMs } from "@/utils/time";
-import { Api } from "@jellyfin/sdk";
-import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
-import {
- getPlaystateApi,
- getUserLibraryApi,
-} from "@jellyfin/sdk/lib/utils/api";
-import { useQuery } from "@tanstack/react-query";
-import * as Haptics from "expo-haptics";
-import { useLocalSearchParams } from "expo-router";
-import { useAtomValue } from "jotai";
-import React, { useCallback, useMemo, useRef, useState } from "react";
-import { Alert, Pressable, useWindowDimensions, View } from "react-native";
-import { SystemBars } from "react-native-edge-to-edge";
-import { useSharedValue } from "react-native-reanimated";
-
-const Player = () => {
- const videoRef = useRef(null);
- const user = useAtomValue(userAtom);
- const api = useAtomValue(apiAtom);
-
- const windowDimensions = useWindowDimensions();
-
- const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
- const [showControls, setShowControls] = useState(true);
- const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false);
- const [isPlaying, setIsPlaying] = useState(false);
- const [isBuffering, setIsBuffering] = useState(true);
- const [isVideoLoaded, setIsVideoLoaded] = useState(false);
-
- const progress = useSharedValue(0);
- const isSeeking = useSharedValue(false);
- const cacheProgress = useSharedValue(0);
-
- const {
- itemId,
- audioIndex: audioIndexStr,
- subtitleIndex: subtitleIndexStr,
- mediaSourceId,
- bitrateValue: bitrateValueStr,
- } = useLocalSearchParams<{
- itemId: string;
- audioIndex: string;
- subtitleIndex: string;
- mediaSourceId: string;
- bitrateValue: string;
- }>();
-
- const audioIndex = audioIndexStr ? parseInt(audioIndexStr, 10) : undefined;
- const subtitleIndex = subtitleIndexStr ? parseInt(subtitleIndexStr, 10) : -1;
- const bitrateValue = bitrateValueStr
- ? parseInt(bitrateValueStr, 10)
- : BITRATES[0].value;
-
- const {
- data: item,
- isLoading: isLoadingItem,
- isError: isErrorItem,
- } = useQuery({
- queryKey: ["item", itemId],
- queryFn: async () => {
- if (!api) return;
- const res = await getUserLibraryApi(api).getItem({
- itemId,
- userId: user?.Id,
- });
-
- return res.data;
- },
- enabled: !!itemId && !!api,
- staleTime: 0,
- });
-
- const {
- data: stream,
- isLoading: isLoadingStreamUrl,
- isError: isErrorStreamUrl,
- } = useQuery({
- queryKey: [
- "stream-url",
- itemId,
- audioIndex,
- subtitleIndex,
- mediaSourceId,
- bitrateValue,
- ],
- queryFn: async () => {
- if (!api) return;
- const res = await getStreamUrl({
- api,
- item,
- startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
- userId: user?.Id,
- audioStreamIndex: audioIndex,
- maxStreamingBitrate: bitrateValue,
- mediaSourceId: mediaSourceId,
- subtitleStreamIndex: subtitleIndex,
- deviceProfile: native,
- });
-
- if (!res) return null;
-
- const { mediaSource, sessionId, url } = res;
-
- if (!sessionId || !mediaSource || !url) return null;
-
- console.log(url);
-
- return {
- mediaSource,
- sessionId,
- url,
- };
- },
- enabled: !!itemId && !!api && !!item,
- staleTime: 0,
- });
-
- const togglePlay = useCallback(
- async (ms: number) => {
- if (!api || !stream) return;
-
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- if (isPlaying) {
- await videoRef.current?.pause();
-
- await getPlaystateApi(api).onPlaybackProgress({
- itemId: item?.Id!,
- audioStreamIndex: audioIndex ? audioIndex : undefined,
- subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
- mediaSourceId: mediaSourceId,
- positionTicks: msToTicks(ms),
- isPaused: true,
- playMethod: stream.url?.includes("m3u8")
- ? "Transcode"
- : "DirectStream",
- playSessionId: stream.sessionId,
- });
- console.log("ACtually marked as paused");
- } else {
- videoRef.current?.play();
- await getPlaystateApi(api).onPlaybackProgress({
- itemId: item?.Id!,
- audioStreamIndex: audioIndex ? audioIndex : undefined,
- subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
- mediaSourceId: mediaSourceId,
- positionTicks: msToTicks(ms),
- isPaused: false,
- playMethod: stream?.url.includes("m3u8")
- ? "Transcode"
- : "DirectStream",
- playSessionId: stream.sessionId,
- });
- }
- },
- [
- isPlaying,
- api,
- item,
- stream,
- videoRef,
- audioIndex,
- subtitleIndex,
- mediaSourceId,
- ]
- );
-
- const play = useCallback(() => {
- videoRef.current?.play();
- reportPlaybackStart();
- }, [videoRef]);
-
- const pause = useCallback(() => {
- videoRef.current?.pause();
- }, [videoRef]);
-
- const stop = useCallback(() => {
- setIsPlaybackStopped(true);
- videoRef.current?.stop();
- reportPlaybackStopped();
- }, [videoRef]);
-
- const reportPlaybackStopped = async () => {
- const currentTimeInTicks = msToTicks(progress.value);
-
- await getPlaystateApi(api!).onPlaybackStopped({
- itemId: item?.Id!,
- mediaSourceId: mediaSourceId,
- positionTicks: currentTimeInTicks,
- playSessionId: stream?.sessionId!,
- });
- };
-
- const reportPlaybackStart = async () => {
- if (!api || !stream) return;
- await getPlaystateApi(api).onPlaybackStart({
- itemId: item?.Id!,
- audioStreamIndex: audioIndex ? audioIndex : undefined,
- subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
- mediaSourceId: mediaSourceId,
- playMethod: stream.url?.includes("m3u8") ? "Transcode" : "DirectStream",
- playSessionId: stream?.sessionId ? stream?.sessionId : undefined,
- });
- };
-
- const onProgress = useCallback(
- async (data: ProgressUpdatePayload) => {
- if (isSeeking.value === true) return;
- if (isPlaybackStopped === true) return;
- if (!item?.Id || !api || !stream) return;
-
- const { currentTime } = data.nativeEvent;
-
- if (isBuffering) {
- setIsBuffering(false);
- }
-
- progress.value = currentTime;
- const currentTimeInTicks = msToTicks(currentTime);
-
- await getPlaystateApi(api).onPlaybackProgress({
- itemId: item.Id,
- audioStreamIndex: audioIndex ? audioIndex : undefined,
- subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
- mediaSourceId: mediaSourceId,
- positionTicks: Math.floor(currentTimeInTicks),
- isPaused: !isPlaying,
- playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
- playSessionId: stream.sessionId,
- });
- },
- [item?.Id, isPlaying, api, isPlaybackStopped]
- );
-
- useOrientation();
- useOrientationSettings();
-
- useWebSocket({
- isPlaying: isPlaying,
- pauseVideo: pause,
- playVideo: play,
- stopPlayback: stop,
- });
-
- const onPlaybackStateChanged = (e: PlaybackStatePayload) => {
- const { state, isBuffering, isPlaying } = e.nativeEvent;
-
- if (state === "Playing") {
- setIsPlaying(true);
- return;
- }
-
- if (state === "Paused") {
- setIsPlaying(false);
- return;
- }
-
- if (isPlaying) {
- setIsPlaying(true);
- setIsBuffering(false);
- } else if (isBuffering) {
- setIsBuffering(true);
- }
- };
-
- if (isLoadingItem || isLoadingStreamUrl)
- return (
-
-
-
- );
-
- if (isErrorItem || isErrorStreamUrl)
- return (
-
- Error
-
- );
-
- if (!stream || !item) return null;
-
- const startPosition = item?.UserData?.PlaybackPositionTicks
- ? ticksToMs(item.UserData.PlaybackPositionTicks)
- : 0;
-
- return (
-
- {
- setShowControls(!showControls);
- }}
- className="absolute z-0 h-full w-full"
- >
- {}}
- onVideoLoadEnd={() => {
- setIsVideoLoaded(true);
- }}
- onVideoError={(e) => {
- console.error("Video Error:", e.nativeEvent);
- Alert.alert(
- "Error",
- "An error occurred while playing the video. Check logs in settings."
- );
- writeToLog("ERROR", "Video Error", e.nativeEvent);
- }}
- />
-
-
- {videoRef.current && (
-
- )}
-
- );
-};
-
-export function usePoster(
- item: BaseItemDto,
- api: Api | null
-): string | undefined {
- const poster = useMemo(() => {
- if (!item || !api) return undefined;
- return item.Type === "Audio"
- ? `${api.basePath}/Items/${item.AlbumId}/Images/Primary?tag=${item.AlbumPrimaryImageTag}&quality=90&maxHeight=200&maxWidth=200`
- : getBackdropUrl({
- api,
- item: item,
- quality: 70,
- width: 200,
- });
- }, [item, api]);
-
- return poster ?? undefined;
-}
-
-export default Player;
diff --git a/hooks/useDownloadedFileOpener.ts b/hooks/useDownloadedFileOpener.ts
index 791dd753..b8e28295 100644
--- a/hooks/useDownloadedFileOpener.ts
+++ b/hooks/useDownloadedFileOpener.ts
@@ -34,13 +34,10 @@ export const useDownloadedFileOpener = () => {
const openFile = useCallback(
async (item: BaseItemDto) => {
try {
- const url = await getDownloadedFileUrl(item.Id!);
-
- setOfflineSettings({
- item,
- });
- setPlayUrl(url);
-
+ console.log(
+ "Go to offline movie",
+ "/player?offline=true&itemId=" + item.Id
+ );
// @ts-expect-error
router.push("/player?offline=true&itemId=" + item.Id);
} catch (error) {
diff --git a/hooks/useRemuxHlsToMp4.ts b/hooks/useRemuxHlsToMp4.ts
index d38ae713..ffba7c7c 100644
--- a/hooks/useRemuxHlsToMp4.ts
+++ b/hooks/useRemuxHlsToMp4.ts
@@ -71,10 +71,7 @@ export const useRemuxHlsToMp4 = () => {
id: "",
deviceId: "",
inputUrl: "",
- item: {
- item,
- mediaSource,
- },
+ item: item,
itemId: item.Id!,
outputPath: "",
progress: 0,
@@ -119,7 +116,7 @@ export const useRemuxHlsToMp4 = () => {
if (returnCode.isValueSuccess()) {
if (!item) throw new Error("Item is undefined");
- await saveDownloadedItemInfo(item, mediaSource);
+ await saveDownloadedItemInfo(item);
toast.success("Download completed");
writeToLog(
"INFO",
diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx
index 49b6c3fe..a8dbf663 100644
--- a/providers/DownloadProvider.tsx
+++ b/providers/DownloadProvider.tsx
@@ -4,7 +4,9 @@ import { writeToLog } from "@/utils/log";
import {
cancelAllJobs,
cancelJobById,
+ deleteDownloadItemInfoFromDiskTmp,
getAllJobsByDeviceId,
+ getDownloadItemInfoFromDiskTmp,
JobStatus,
} from "@/utils/optimize-server";
import {
@@ -130,7 +132,7 @@ function useDownloadProvider() {
if (settings.autoDownload) {
startDownload(job);
} else {
- toast.info(`${job.item.item.Name} is ready to be downloaded`, {
+ toast.info(`${job.item.Name} is ready to be downloaded`, {
action: {
label: "Go to downloads",
onClick: () => {
@@ -141,8 +143,8 @@ function useDownloadProvider() {
});
Notifications.scheduleNotificationAsync({
content: {
- title: job.item.item.Name,
- body: `${job.item.item.Name} is ready to be downloaded`,
+ title: job.item.Name,
+ body: `${job.item.Name} is ready to be downloaded`,
data: {
url: `/downloads`,
},
@@ -190,7 +192,7 @@ function useDownloadProvider() {
const startDownload = useCallback(
async (process: JobStatus) => {
- if (!process?.item.item.Id || !authHeader) throw new Error("No item id");
+ if (!process?.item.Id || !authHeader) throw new Error("No item id");
setProcesses((prev) =>
prev.map((p) =>
@@ -213,7 +215,7 @@ function useDownloadProvider() {
},
});
- toast.info(`Download started for ${process.item.item.Name}`, {
+ toast.info(`Download started for ${process.item.Name}`, {
action: {
label: "Go to downloads",
onClick: () => {
@@ -228,7 +230,7 @@ function useDownloadProvider() {
download({
id: process.id,
url: settings?.optimizedVersionsServerUrl + "download/" + process.id,
- destination: `${baseDirectory}/${process.item.item.Id}.mp4`,
+ destination: `${baseDirectory}/${process.item.Id}.mp4`,
})
.begin(() => {
setProcesses((prev) =>
@@ -260,11 +262,8 @@ function useDownloadProvider() {
);
})
.done(async () => {
- await saveDownloadedItemInfo(
- process.item.item,
- process.item.mediaSource
- );
- toast.success(`Download completed for ${process.item.item.Name}`, {
+ await saveDownloadedItemInfo(process.item);
+ toast.success(`Download completed for ${process.item.Name}`, {
duration: 3000,
action: {
label: "Go to downloads",
@@ -289,15 +288,13 @@ function useDownloadProvider() {
if (error.errorCode === 404) {
errorMsg = "File not found on server";
}
- toast.error(
- `Download failed for ${process.item.item.Name} - ${errorMsg}`
- );
- writeToLog("ERROR", `Download failed for ${process.item.item.Name}`, {
+ toast.error(`Download failed for ${process.item.Name} - ${errorMsg}`);
+ writeToLog("ERROR", `Download failed for ${process.item.Name}`, {
error,
processDetails: {
id: process.id,
- itemName: process.item.item.Name,
- itemId: process.item.item.Id,
+ itemName: process.item.Name,
+ itemId: process.item.Id,
},
});
console.error("Error details:", {
@@ -309,12 +306,15 @@ function useDownloadProvider() {
);
const startBackgroundDownload = useCallback(
- async (url: string, item: BaseItemDto, fileExtension: string) => {
+ async (url: string, item: BaseItemDto, mediaSource: MediaSourceInfo) => {
if (!api || !item.Id || !authHeader)
throw new Error("startBackgroundDownload ~ Missing required params");
try {
+ const fileExtension = mediaSource.TranscodingContainer;
const deviceId = await getOrSetDeviceId();
+
+ // Save poster to disk
const itemImage = getItemImage({
item,
api,
@@ -322,9 +322,9 @@ function useDownloadProvider() {
quality: 90,
width: 500,
});
-
await saveImage(item.Id, itemImage?.uri);
+ // POST to start optimization job on the server
const response = await axios.post(
settings?.optimizedVersionsServerUrl + "optimize-version",
{
@@ -529,17 +529,23 @@ function useDownloadProvider() {
}
}
- async function saveDownloadedItemInfo(
- item: BaseItemDto,
- mediaSource: MediaSourceInfo
- ) {
+ async function saveDownloadedItemInfo(item: BaseItemDto) {
try {
const downloadedItems = await AsyncStorage.getItem("downloadedItems");
- let items: { item: BaseItemDto; mediaSource: MediaSourceInfo }[] =
- downloadedItems ? JSON.parse(downloadedItems) : [];
+ let items: DownloadedItem[] = downloadedItems
+ ? JSON.parse(downloadedItems)
+ : [];
const existingItemIndex = items.findIndex((i) => i.item.Id === item.Id);
- const newItem = { item, mediaSource };
+
+ const data = getDownloadItemInfoFromDiskTmp(item.Id!);
+
+ if (!data?.mediaSource)
+ throw new Error(
+ "Media source not found in tmp storage. Did you forget to save it before starting download?"
+ );
+
+ const newItem = { item, mediaSource: data.mediaSource };
if (existingItemIndex !== -1) {
items[existingItemIndex] = newItem;
@@ -547,6 +553,8 @@ function useDownloadProvider() {
items.push(newItem);
}
+ deleteDownloadItemInfoFromDiskTmp(item.Id!);
+
await AsyncStorage.setItem("downloadedItems", JSON.stringify(items));
await queryClient.invalidateQueries({ queryKey: ["downloadedItems"] });
refetch();
diff --git a/utils/optimize-server.ts b/utils/optimize-server.ts
index 54c0439c..61d17a9a 100644
--- a/utils/optimize-server.ts
+++ b/utils/optimize-server.ts
@@ -6,6 +6,7 @@ import {
import axios from "axios";
import { writeToLog } from "./log";
import { DownloadedItem } from "@/providers/DownloadProvider";
+import { MMKV } from "react-native-mmkv";
interface IJobInput {
deviceId?: string | null;
@@ -27,7 +28,7 @@ export interface JobStatus {
inputUrl: string;
deviceId: string;
itemId: string;
- item: DownloadedItem;
+ item: BaseItemDto;
speed?: number;
timestamp: Date;
base64Image?: string;
@@ -158,3 +159,81 @@ export async function getStatistics({
return null;
}
}
+
+/**
+ * Saves the download item info to disk - this data is used temporarily to fetch additional download information
+ * in combination with the optimize server. This is used to not have to send all item info to the optimize server.
+ *
+ * @param {BaseItemDto} item - The item to save.
+ * @param {MediaSourceInfo} mediaSource - The media source of the item.
+ * @param {string} url - The URL of the item.
+ * @return {boolean} A promise that resolves when the item info is saved.
+ */
+export function saveDownloadItemInfoToDiskTmp(
+ item: BaseItemDto,
+ mediaSource: MediaSourceInfo,
+ url: string
+): boolean {
+ try {
+ const storage = new MMKV();
+
+ const downloadInfo = JSON.stringify({
+ item,
+ mediaSource,
+ url,
+ });
+
+ storage.set(`tmp_download_info_${item.Id}`, downloadInfo);
+
+ return true;
+ } catch (error) {
+ console.error("Failed to save download item info to disk:", error);
+ throw error;
+ }
+}
+
+/**
+ * Retrieves the download item info from disk.
+ *
+ * @param {string} itemId - The ID of the item to retrieve.
+ * @return {{
+ * item: BaseItemDto;
+ * mediaSource: MediaSourceInfo;
+ * url: string;
+ * } | null} The retrieved download item info or null if not found.
+ */
+export function getDownloadItemInfoFromDiskTmp(itemId: string): {
+ item: BaseItemDto;
+ mediaSource: MediaSourceInfo;
+ url: string;
+} | null {
+ try {
+ const storage = new MMKV();
+ const rawInfo = storage.getString(`tmp_download_info_${itemId}`);
+
+ if (rawInfo) {
+ return JSON.parse(rawInfo);
+ }
+ return null;
+ } catch (error) {
+ console.error("Failed to retrieve download item info from disk:", error);
+ return null;
+ }
+}
+
+/**
+ * Deletes the download item info from disk.
+ *
+ * @param {string} itemId - The ID of the item to delete.
+ * @return {boolean} True if the item info was successfully deleted, false otherwise.
+ */
+export function deleteDownloadItemInfoFromDiskTmp(itemId: string): boolean {
+ try {
+ const storage = new MMKV();
+ storage.delete(`tmp_download_info_${itemId}`);
+ return true;
+ } catch (error) {
+ console.error("Failed to delete download item info from disk:", error);
+ return false;
+ }
+}