diff --git a/app/(auth)/(tabs)/(home)/downloads.tsx b/app/(auth)/(tabs)/(home)/downloads.tsx
index 5430c27d..718a22b6 100644
--- a/app/(auth)/(tabs)/(home)/downloads.tsx
+++ b/app/(auth)/(tabs)/(home)/downloads.tsx
@@ -41,17 +41,6 @@ const downloads: React.FC = () => {
return Object.values(series);
}, [downloadedFiles]);
- const eta = useMemo(() => {
- const length = process?.item?.RunTimeTicks || 0;
-
- if (!process?.speed || !process?.progress) return "";
-
- const timeLeft =
- (length - length * (process.progress / 100)) / process.speed;
-
- return formatNumber(timeLeft / 10000);
- }, [process]);
-
useEffect(() => {
(async () => {
const dir = FileSystem.documentDirectory;
@@ -171,14 +160,8 @@ const downloads: React.FC = () => {
- {process.progress.toFixed(0)}%
+ {(process.progress * 100).toFixed(0)}%
-
- {process.speed?.toFixed(2)}x
-
-
- ETA {eta}
-
= ({ item }) => {
const { deleteFile } = useFiles();
- const router = useRouter();
+ const { openFile } = useFileOpener();
- const { startDownloadedFilePlayback } = usePlayback();
-
- const handleOpenFile = useCallback(async () => {
- const url = `${FileSystem.documentDirectory}${item.Id}/0.ts`;
- console.log(url);
-
- const fileInfo = await FileSystem.getInfoAsync(url);
-
- if (!fileInfo.exists) {
- console.warn("m3u8 file does not exist:", url);
- }
-
- startDownloadedFilePlayback({
- item,
- url,
- });
- router.push("/play");
- }, [item, startDownloadedFilePlayback]);
+ const handleOpenFile = useCallback(() => {
+ openFile(item);
+ }, [item, openFile]);
/**
* Handles deleting the file with haptic feedback.
diff --git a/components/downloads/MovieCard.tsx b/components/downloads/MovieCard.tsx
index 13833e66..16808bbf 100644
--- a/components/downloads/MovieCard.tsx
+++ b/components/downloads/MovieCard.tsx
@@ -1,5 +1,4 @@
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
-import * as FileSystem from "expo-file-system";
import * as Haptics from "expo-haptics";
import React, { useCallback } from "react";
import { TouchableOpacity, View } from "react-native";
@@ -9,9 +8,7 @@ import { useFiles } from "@/hooks/useFiles";
import { runtimeTicksToMinutes } from "@/utils/time";
import { Text } from "../common/Text";
-import { usePlayback } from "@/providers/PlaybackProvider";
-import { useRouter } from "expo-router";
-import { deleteDownloadedItem } from "@/hooks/useDownloadM3U8Files";
+import { useFileOpener } from "@/hooks/useDownloadedFileOpener";
interface MovieCardProps {
item: BaseItemDto;
@@ -24,35 +21,11 @@ interface MovieCardProps {
*/
export const MovieCard: React.FC = ({ item }) => {
const { deleteFile } = useFiles();
- const router = useRouter();
- const { startDownloadedFilePlayback } = usePlayback();
+ const { openFile } = useFileOpener();
- const handleOpenFile = useCallback(async () => {
- try {
- const directoryPath = `${FileSystem.documentDirectory}${item.Id}`;
- const m3u8FilePath = `${directoryPath}/local.m3u8`;
-
- console.log("Path: ", m3u8FilePath);
-
- // Check if the m3u8 file exists
- const fileInfo = await FileSystem.getInfoAsync(m3u8FilePath);
-
- if (!fileInfo.exists) {
- console.warn("m3u8 file does not exist:", m3u8FilePath);
- }
-
- // Start playback
- startDownloadedFilePlayback({
- item,
- url: `${m3u8FilePath}`,
- });
-
- // Navigate to the play screen
- router.push("/play");
- } catch (error) {
- console.error("Error opening file:", error);
- }
- }, [item, startDownloadedFilePlayback, router, deleteDownloadedItem]);
+ const handleOpenFile = useCallback(() => {
+ openFile(item);
+ }, [item, openFile]);
/**
* Handles deleting the file with haptic feedback.
diff --git a/hooks/useDownloadM3U8Files.ts b/hooks/useDownloadM3U8Files.ts
index f3e1b909..708942f1 100644
--- a/hooks/useDownloadM3U8Files.ts
+++ b/hooks/useDownloadM3U8Files.ts
@@ -6,6 +6,7 @@ import { download } from "@kesha-antonov/react-native-background-downloader";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useQueryClient } from "@tanstack/react-query";
import * as FileSystem from "expo-file-system";
+import { FFmpegKit, ReturnCode } from "ffmpeg-kit-react-native";
import { useAtom } from "jotai";
import { useCallback } from "react";
import { toast } from "sonner-native";
@@ -27,6 +28,11 @@ export const useDownloadM3U8Files = (item: BaseItemDto) => {
toast.success("Download started", { invert: true });
writeToLog("INFO", `Starting download for item ${item.Name}`);
+ setProgress({
+ startTime: new Date(),
+ item,
+ progress: 0,
+ });
try {
const directoryPath = `${FileSystem.documentDirectory}${item.Id}`;
@@ -55,12 +61,22 @@ export const useDownloadM3U8Files = (item: BaseItemDto) => {
const segmentUrl = `${api.basePath}/videos/${item.Id}/${segment.path}`;
const destination = `${directoryPath}/${i}.ts`;
- await download({
+ download({
id: `${item.Id}_segment_${i}`,
url: segmentUrl,
destination: destination,
}).done((e) => {
console.log("Download completed for segment", i);
+ setProgress((prev) => {
+ const newProgress = ((prev?.progress || 0) + 1) / segments.length;
+ if (prev === null) {
+ return null;
+ }
+ return {
+ ...prev,
+ progress: newProgress,
+ };
+ });
});
}
@@ -194,3 +210,5 @@ export async function getAllDownloadedItems(): Promise {
return [];
}
}
+
+
diff --git a/hooks/useDownloadedFileOpener.ts b/hooks/useDownloadedFileOpener.ts
new file mode 100644
index 00000000..6c0ece93
--- /dev/null
+++ b/hooks/useDownloadedFileOpener.ts
@@ -0,0 +1,89 @@
+// hooks/useFileOpener.ts
+
+import { useCallback } from "react";
+import { useRouter } from "expo-router";
+import * as FileSystem from "expo-file-system";
+import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
+import { usePlayback } from "@/providers/PlaybackProvider";
+import { FFmpegKit, ReturnCode } from "ffmpeg-kit-react-native";
+
+export const useFileOpener = () => {
+ const router = useRouter();
+ const { startDownloadedFilePlayback } = usePlayback();
+
+ const openFile = useCallback(
+ async (item: BaseItemDto) => {
+ const m3u8File = `${FileSystem.documentDirectory}${item.Id}/playlist.m3u8`;
+ const outputFile = `${FileSystem.documentDirectory}${item.Id}/output.mp4`;
+
+ console.log("Checking for output file:", outputFile);
+
+ const outputFileInfo = await FileSystem.getInfoAsync(outputFile);
+
+ if (outputFileInfo.exists) {
+ console.log("Output MP4 file already exists. Playing directly.");
+ startDownloadedFilePlayback({
+ item,
+ url: outputFile,
+ });
+ router.push("/play");
+ return;
+ }
+
+ console.log("Output MP4 file does not exist. Converting from M3U8.");
+
+ const m3u8FileInfo = await FileSystem.getInfoAsync(m3u8File);
+
+ if (!m3u8FileInfo.exists) {
+ console.warn("m3u8 file does not exist:", m3u8File);
+ return;
+ }
+
+ const conversionSuccess = await convertM3U8ToMP4(m3u8File, outputFile);
+
+ if (conversionSuccess) {
+ startDownloadedFilePlayback({
+ item,
+ url: outputFile,
+ });
+ router.push("/play");
+ } else {
+ console.error("Failed to convert M3U8 to MP4");
+ // Handle conversion failure (e.g., show an error message to the user)
+ }
+ },
+ [startDownloadedFilePlayback]
+ );
+
+ return { openFile };
+};
+
+export async function convertM3U8ToMP4(
+ inputM3U8: string,
+ outputMP4: string
+): Promise {
+ console.log("Converting M3U8 to MP4");
+ console.log("Input M3U8:", inputM3U8);
+ console.log("Output MP4:", outputMP4);
+
+ try {
+ const command = `-i ${inputM3U8} -c copy ${outputMP4}`;
+ console.log("Executing FFmpeg command:", command);
+
+ const session = await FFmpegKit.execute(command);
+ const returnCode = await session.getReturnCode();
+
+ if (ReturnCode.isSuccess(returnCode)) {
+ console.log("Conversion completed successfully");
+ return true;
+ } else {
+ console.error("Conversion failed. Return code:", returnCode);
+ const output = await session.getOutput();
+ console.error("FFmpeg output:", output);
+ return false;
+ }
+ } catch (error) {
+ console.error("Error during conversion:", error);
+ return false;
+ }
+}
diff --git a/utils/atoms/downloads.ts b/utils/atoms/downloads.ts
index 143345f0..1fc47d18 100644
--- a/utils/atoms/downloads.ts
+++ b/utils/atoms/downloads.ts
@@ -4,7 +4,6 @@ import { atom } from "jotai";
export type ProcessItem = {
item: BaseItemDto;
progress: number;
- speed?: number;
startTime?: Date;
};