diff --git a/app/(auth)/(tabs)/(home)/downloads/index.tsx b/app/(auth)/(tabs)/(home)/downloads/index.tsx
index 06669d56..8e63c332 100644
--- a/app/(auth)/(tabs)/(home)/downloads/index.tsx
+++ b/app/(auth)/(tabs)/(home)/downloads/index.tsx
@@ -2,10 +2,15 @@ import { Text } from "@/components/common/Text";
import ProgressCircle from "@/components/ProgressCircle";
import { DownloadInfo } from "@/modules/hls-downloader/src/HlsDownloader.types";
import { useNativeDownloads } from "@/providers/NativeDownloadProvider";
+import { storage } from "@/utils/mmkv";
+import { formatTimeString, ticksToSeconds } from "@/utils/time";
+import { useActionSheet } from "@expo/react-native-action-sheet";
import { Ionicons } from "@expo/vector-icons";
import { useQueryClient } from "@tanstack/react-query";
+import * as FileSystem from "expo-file-system";
+import { Image } from "expo-image";
import { useRouter } from "expo-router";
-import { useMemo } from "react";
+import { useCallback, useMemo } from "react";
import {
ActivityIndicator,
RefreshControl,
@@ -14,20 +19,6 @@ import {
View,
} from "react-native";
-const formatETA = (seconds: number): string => {
- const hrs = Math.floor(seconds / 3600);
- const mins = Math.floor((seconds % 3600) / 60);
- const secs = Math.floor(seconds % 60);
-
- const parts: string[] = [];
-
- if (hrs > 0) parts.push(`${hrs}h`);
- if (mins > 0) parts.push(`${mins}m`);
- if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
-
- return parts.join(" ");
-};
-
const getETA = (download: DownloadInfo): string | null => {
if (
!download.startTime ||
@@ -37,34 +28,57 @@ const getETA = (download: DownloadInfo): string | null => {
console.log(download);
return null;
}
-
- const elapsed = Date.now() / 1000 - download.startTime; // seconds
-
+ const elapsed = Date.now() / 1000 - download.startTime;
if (elapsed <= 0 || download.secondsDownloaded <= 0) return null;
-
- const speed = download.secondsDownloaded / elapsed; // downloaded seconds per second
+ const speed = download.secondsDownloaded / elapsed;
const remainingBytes = download.secondsTotal - download.secondsDownloaded;
-
if (speed <= 0) return null;
-
const secondsLeft = remainingBytes / speed;
-
- return formatETA(secondsLeft);
-};
-
-const formatBytes = (i: number) => {
- const units = ["B", "KB", "MB", "GB", "TB"];
- let l = 0;
- let n = parseInt(i.toString(), 10) || 0;
- while (n >= 1024 && ++l) {
- n = n / 1024;
- }
- return n.toFixed(n < 10 && l > 0 ? 1 : 0) + " " + units[l];
+ return formatTimeString(secondsLeft, "s");
};
export default function Index() {
- const { downloadedFiles, activeDownloads } = useNativeDownloads();
+ const { showActionSheetWithOptions } = useActionSheet();
+ const {
+ downloadedFiles,
+ activeDownloads,
+ cancelDownload,
+ refetchDownloadedFiles,
+ } = useNativeDownloads();
const router = useRouter();
+ const queryClient = useQueryClient();
+
+ const handleItemPress = (item: any) => {
+ showActionSheetWithOptions(
+ {
+ options: ["Play", "Delete", "Cancel"],
+ destructiveButtonIndex: 1,
+ cancelButtonIndex: 2,
+ },
+ async (selectedIndex) => {
+ if (selectedIndex === 0) {
+ goToVideo(item);
+ } else if (selectedIndex === 1) {
+ await deleteFile(item.id);
+ }
+ }
+ );
+ };
+
+ const handleActiveItemPress = (id: string) => {
+ showActionSheetWithOptions(
+ {
+ options: ["Cancel Download", "Cancel"],
+ destructiveButtonIndex: 0,
+ cancelButtonIndex: 1,
+ },
+ async (selectedIndex) => {
+ if (selectedIndex === 0) {
+ await cancelDownload(id);
+ }
+ }
+ );
+ };
const goToVideo = (item: any) => {
// @ts-expect-error
@@ -80,7 +94,15 @@ export default function Index() {
[downloadedFiles]
);
- const queryClient = useQueryClient();
+ const base64Image = useCallback((id: string) => {
+ return storage.getString(id);
+ }, []);
+
+ const deleteFile = async (id: string) => {
+ const downloadsDir = FileSystem.documentDirectory + "downloads/";
+ await FileSystem.deleteAsync(downloadsDir + id + ".json");
+ await queryClient.invalidateQueries({ queryKey: ["downloadedFiles"] });
+ };
return (
0
}
- onRefresh={async () => {
- await queryClient.invalidateQueries({
- queryKey: ["downloadedFiles"],
- });
+ onRefresh={() => {
+ refetchDownloadedFiles();
}}
/>
}
- className="p-4 space-y-2"
+ className="flex-1 "
>
- {activeDownloads.length ? (
-
-
- ACTIVE DOWNLOADS
-
- {activeDownloads.map((i) => {
- const progress =
- i.secondsTotal && i.secondsDownloaded
- ? i.secondsDownloaded / i.secondsTotal
- : 0;
- const eta = getETA(i);
- const item = i.metadata?.item;
- return (
-
-
- {item.Type === "Episode" ? (
- {item?.SeriesName}
- ) : null}
- {item?.Name}
-
- {`S${item.ParentIndexNumber?.toString()}:E${item.IndexNumber?.toString()}`}
-
-
-
- {eta ? `${eta} remaining` : "Calculating time..."}
-
-
-
+
+ {!movies.length && !episodes.length && !activeDownloads.length ? (
+
+
+ No downloaded items
+
+
+ ) : null}
- {i.state === "PENDING" ? (
-
- ) : (
-
- )}
+ {activeDownloads.length ? (
+
+
+ ACTIVE DOWNLOADS
+
+
+ {activeDownloads.map((i) => {
+ const progress =
+ i.secondsTotal && i.secondsDownloaded
+ ? i.secondsDownloaded / i.secondsTotal
+ : 0;
+ const eta = getETA(i);
+ const item = i.metadata?.item;
+ return (
+ {
+ if (!i.metadata.item.Id) throw new Error("No item id");
+ handleActiveItemPress(i.metadata.item.Id);
+ }}
+ key={i.id}
+ className="flex flex-row items-center p-2 pr-4 rounded-xl bg-neutral-900 space-x-4"
+ >
+ {i.metadata.item.Id && (
+
+
+
+ )}
+
+ {item.Type === "Episode" ? (
+ <>
+ {item?.SeriesName}
+ >
+ ) : (
+ <>
+
+ {item?.ProductionYear}
+
+ >
+ )}
+ {item?.Name}
+
+ {i.metadata.item.Type === "Episode" && (
+
+ {`S${item.ParentIndexNumber?.toString()}:E${item.IndexNumber?.toString()}`}{" "}
+ -{" "}
+
+ )}
+ {item?.RunTimeTicks ? (
+
+ {formatTimeString(
+ ticksToSeconds(item?.RunTimeTicks),
+ "s"
+ )}
+
+ ) : null}
+
+
+
+ {eta ? `~${eta} remaining` : "Calculating time..."}
+
+
+
+
+
+ {i.state === "PENDING" ? (
+
+ ) : (
+
+
+
+ {(progress * 100).toFixed(0)}%
+
+
+ )}
+
+
+ );
+ })}
+
+
+ ) : null}
+
+
+ {movies && movies.length ? (
+
+ MOVIES
+
+ {movies.map((i) => (
+ handleItemPress(i)}
+ className="flex flex-row items-center p-2 pr-4 rounded-xl bg-neutral-900 space-x-4"
+ >
+ {i.metadata.item.Id && (
+
+
+
+ )}
+
+
+ {i.metadata.item?.ProductionYear}
+
+
+ {i.metadata.item?.Name}
+
+ {i.metadata.item?.RunTimeTicks ? (
+
+ {formatTimeString(
+ ticksToSeconds(i.metadata.item?.RunTimeTicks),
+ "s"
+ )}
+
+ ) : null}
+
+
+
+ ))}
- );
- })}
+
+ ) : null}
+ {episodes && episodes.length ? (
+
+
+ EPISODES
+
+
+ {episodes.map((i) => (
+ handleItemPress(i)}
+ className="bg-neutral-900 p-2 pr-4 rounded-xl flex flex-row items-center space-x-4"
+ >
+ {i.metadata.item.Id && (
+
+
+
+ )}
+
+ {i.metadata.item.Type === "Episode" ? (
+
+ {i.metadata.item?.SeriesName}
+
+ ) : null}
+
+ {i.metadata.item?.Name}
+
+
+ {`S${i.metadata.item.ParentIndexNumber?.toString()}:E${i.metadata.item.IndexNumber?.toString()}`}
+
+
+
+
+ ))}
+
+
+ ) : null}
- ) : null}
-
-
- {movies && movies.length ? (
-
- MOVIES
-
- {movies.map((i) => (
- goToVideo(i)}
- className="bg-neutral-900 p-4 rounded-xl flex flex-row items-center justify-between"
- >
-
- {i.metadata.item.Type === "Episode" ? (
-
- {i.metadata.item?.SeriesName}
-
- ) : null}
-
- {i.metadata.item?.Name}
-
-
- {`S${i.metadata.item.ParentIndexNumber?.toString()}:E${i.metadata.item.IndexNumber?.toString()}`}
-
-
-
-
- ))}
-
-
- ) : null}
- {episodes && episodes.length ? (
-
- EPISODES
-
- {episodes.map((i) => (
- goToVideo(i)}
- className="bg-neutral-900 p-4 rounded-xl flex flex-row items-center justify-between"
- >
-
- {i.metadata.item.Type === "Episode" ? (
-
- {i.metadata.item?.SeriesName}
-
- ) : null}
-
- {i.metadata.item?.Name}
-
-
- {`S${i.metadata.item.ParentIndexNumber?.toString()}:E${i.metadata.item.IndexNumber?.toString()}`}
-
-
-
-
- ))}
-
-
- ) : null}
);
diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx
index 04b82a28..a4bbfc92 100644
--- a/app/(auth)/player/direct-player.tsx
+++ b/app/(auth)/player/direct-player.tsx
@@ -130,6 +130,7 @@ export default function page() {
let m3u8Url = "";
const path = `${FileSystem.documentDirectory}/downloads/${item?.Id}/Data`;
const files = await FileSystem.readDirectoryAsync(path);
+
for (const file of files) {
if (file.endsWith(".m3u8")) {
console.log(file);
@@ -138,10 +139,11 @@ export default function page() {
}
}
- console.log({
- mediaSource: data.mediaSource,
+ if (!m3u8Url) throw new Error("No m3u8 file found");
+
+ console.log("stream ~", {
+ mediaSource: data.mediaSource.Id,
url: m3u8Url,
- sessionId: undefined,
});
if (item)
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 7f12fb8d..047f158c 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -29,9 +29,6 @@ import { SystemBars } from "react-native-edge-to-edge";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import "react-native-reanimated";
import { Toaster } from "sonner-native";
-const BackGroundDownloader = !Platform.isTV
- ? require("@kesha-antonov/react-native-background-downloader")
- : null;
const Notifications = !Platform.isTV ? require("expo-notifications") : null;
if (!Platform.isTV) {
@@ -171,26 +168,6 @@ function Layout() {
);
}
}, [settings]);
-
- useEffect(() => {
- const subscription = AppState.addEventListener(
- "change",
- (nextAppState) => {
- if (
- appState.current.match(/inactive|background/) &&
- nextAppState === "active"
- ) {
- BackGroundDownloader.checkForExistingDownloads();
- }
- }
- );
-
- BackGroundDownloader.checkForExistingDownloads();
-
- return () => {
- subscription.remove();
- };
- }, []);
}
const [loaded] = useFonts({
diff --git a/components/downloads/NativeDownloadButton.tsx b/components/downloads/NativeDownloadButton.tsx
index 94a184a6..b86134d7 100644
--- a/components/downloads/NativeDownloadButton.tsx
+++ b/components/downloads/NativeDownloadButton.tsx
@@ -116,7 +116,18 @@ export const NativeDownloadButton: React.FC = ({
selectedSubtitleStream,
selectedMediaSource,
});
- toast.success("Download started");
+ toast.success(
+ t("home.downloads.toasts.download_started_for", { item: item.Name }),
+ {
+ action: {
+ label: "Go to download",
+ onClick: () => {
+ router.push("/downloads");
+ toast.dismiss();
+ },
+ },
+ }
+ );
} catch (error) {
console.error("Download error:", error);
toast.error("Failed to start download");
diff --git a/modules/hls-downloader/index.ts b/modules/hls-downloader/index.ts
index 25740d74..3237935c 100644
--- a/modules/hls-downloader/index.ts
+++ b/modules/hls-downloader/index.ts
@@ -33,6 +33,11 @@ async function checkForExistingDownloads(): Promise {
return HlsDownloaderModule.checkForExistingDownloads();
}
+/**
+ * Cancels an ongoing download.
+ * @param id - The unique identifier for the download.
+ * @returns void
+ */
async function cancelDownload(id: string): Promise {
return HlsDownloaderModule.cancelDownload(id);
}
diff --git a/modules/hls-downloader/ios/HlsDownloaderModule.swift b/modules/hls-downloader/ios/HlsDownloaderModule.swift
index 8480357e..c49af455 100644
--- a/modules/hls-downloader/ios/HlsDownloaderModule.swift
+++ b/modules/hls-downloader/ios/HlsDownloaderModule.swift
@@ -8,6 +8,13 @@ public class HlsDownloaderModule: Module {
startTime: Double
)] = [:]
+ struct DownloadRequest {
+ let providedId: String
+ let url: String
+ let metadata: [String: Any]?
+ }
+ var pendingDownloads: [DownloadRequest] = []
+
public func definition() -> ModuleDefinition {
Name("HlsDownloader")
@@ -16,9 +23,51 @@ public class HlsDownloaderModule: Module {
Function("downloadHLSAsset") {
(providedId: String, url: String, metadata: [String: Any]?) -> Void in
let startTime = Date().timeIntervalSince1970
- print(
- "Starting download - ID: \(providedId), URL: \(url), Metadata: \(String(describing: metadata)), StartTime: \(startTime)"
- )
+
+ // Enforce max 3 concurrent downloads.
+ if self.activeDownloads.count >= 3 {
+ self.pendingDownloads.append(
+ DownloadRequest(providedId: providedId, url: url, metadata: metadata))
+ self.sendEvent(
+ "onProgress",
+ [
+ "id": providedId,
+ "progress": 0.0,
+ "state": "QUEUED",
+ "metadata": metadata ?? [:],
+ "startTime": startTime,
+ ])
+ return
+ }
+
+ // First check if the asset already exists
+ let fm = FileManager.default
+ let docs = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
+ let downloadsDir = docs.appendingPathComponent("downloads", isDirectory: true)
+ let potentialExistingLocation = downloadsDir.appendingPathComponent(
+ providedId, isDirectory: true)
+
+ if fm.fileExists(atPath: potentialExistingLocation.path) {
+ // Check if the download is complete by looking for the master playlist
+ if let files = try? fm.contentsOfDirectory(atPath: potentialExistingLocation.path),
+ files.contains(where: { $0.hasSuffix(".m3u8") })
+ {
+ // Asset exists and appears complete, send completion event
+ self.sendEvent(
+ "onComplete",
+ [
+ "id": providedId,
+ "location": potentialExistingLocation.absoluteString,
+ "state": "DONE",
+ "metadata": metadata ?? [:],
+ "startTime": startTime,
+ ])
+ return
+ } else {
+ // Asset exists but appears incomplete, clean it up
+ try? fm.removeItem(at: potentialExistingLocation)
+ }
+ }
guard let assetURL = URL(string: url) else {
self.sendEvent(
@@ -33,7 +82,7 @@ public class HlsDownloaderModule: Module {
return
}
- // Add asset options to allow cellular downloads and specify allowed media types
+ // Rest of the download logic remains the same
let asset = AVURLAsset(
url: assetURL,
options: [
@@ -42,7 +91,6 @@ public class HlsDownloaderModule: Module {
"AVURLAssetAllowsCellularAccessKey": true,
])
- // Validate the asset before proceeding
asset.loadValuesAsynchronously(forKeys: ["playable", "duration"]) {
var error: NSError?
let status = asset.statusOfValue(forKey: "playable", error: &error)
@@ -63,7 +111,7 @@ public class HlsDownloaderModule: Module {
}
let configuration = URLSessionConfiguration.background(
- withIdentifier: "com.streamyfin.hlsdownload")
+ withIdentifier: "com.streamyfin.hlsdownload.\(providedId)") // Add unique identifier
configuration.allowsCellularAccess = true
configuration.sessionSendsLaunchEvents = true
configuration.isDiscretionary = false
@@ -103,7 +151,9 @@ public class HlsDownloaderModule: Module {
}
delegate.taskIdentifier = task.taskIdentifier
+
self.activeDownloads[task.taskIdentifier] = (task, delegate, metadata ?? [:], startTime)
+
self.sendEvent(
"onProgress",
[
@@ -115,7 +165,6 @@ public class HlsDownloaderModule: Module {
])
task.resume()
- print("Download task started with identifier: \(task.taskIdentifier)")
}
}
}
@@ -153,8 +202,6 @@ public class HlsDownloaderModule: Module {
return
}
let (task, delegate, metadata, startTime) = entry.value
- task.cancel()
- self.activeDownloads.removeValue(forKey: task.taskIdentifier)
self.sendEvent(
"onError",
[
@@ -164,6 +211,9 @@ public class HlsDownloaderModule: Module {
"metadata": metadata,
"startTime": startTime,
])
+ task.cancel()
+ self.activeDownloads.removeValue(forKey: task.taskIdentifier)
+ self.startNextDownloadIfNeeded()
print("Download cancelled for identifier: \(providedId)")
}
@@ -183,16 +233,26 @@ public class HlsDownloaderModule: Module {
try fm.createDirectory(at: downloadsDir, withIntermediateDirectories: true)
}
let newLocation = downloadsDir.appendingPathComponent(folderName, isDirectory: true)
+
+ // New atomic move implementation
+ let tempLocation = downloadsDir.appendingPathComponent("\(folderName)_temp", isDirectory: true)
+
+ // Clean up any existing temp folder
+ if fm.fileExists(atPath: tempLocation.path) {
+ try fm.removeItem(at: tempLocation)
+ }
+
+ // Move to temp location first
+ try fm.moveItem(at: originalLocation, to: tempLocation)
+
+ // If target exists, remove it
if fm.fileExists(atPath: newLocation.path) {
try fm.removeItem(at: newLocation)
}
- // If the original file exists, move it. Otherwise, if newLocation already exists, assume it was moved.
- if fm.fileExists(atPath: originalLocation.path) {
- try fm.moveItem(at: originalLocation, to: newLocation)
- } else if !fm.fileExists(atPath: newLocation.path) {
- throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: nil)
- }
+ // Final move from temp to target
+ try fm.moveItem(at: tempLocation, to: newLocation)
+
return newLocation
}
@@ -215,6 +275,7 @@ class HLSDownloadDelegate: NSObject, AVAssetDownloadDelegate {
var downloadedSeconds: Double = 0
var totalSeconds: Double = 0
var startTime: Double = 0
+ private var wasCancelled = false
init(module: HlsDownloaderModule) {
self.module = module
@@ -255,6 +316,10 @@ class HLSDownloadDelegate: NSObject, AVAssetDownloadDelegate {
_ session: URLSession, assetDownloadTask: AVAssetDownloadTask,
didFinishDownloadingTo location: URL
) {
+ if wasCancelled {
+ return
+ }
+
let metadata = module?.activeDownloads[assetDownloadTask.taskIdentifier]?.metadata ?? [:]
let startTime = module?.activeDownloads[assetDownloadTask.taskIdentifier]?.startTime ?? 0
let folderName = providedId
@@ -330,6 +395,12 @@ class HLSDownloadDelegate: NSObject, AVAssetDownloadDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
+ if (error as NSError).code == NSURLErrorCancelled {
+ wasCancelled = true
+ module?.removeDownload(with: taskIdentifier)
+ return
+ }
+
let metadata = module?.activeDownloads[task.taskIdentifier]?.metadata ?? [:]
let startTime = module?.activeDownloads[task.taskIdentifier]?.startTime ?? 0
module?.sendEvent(
diff --git a/modules/hls-downloader/src/HlsDownloader.types.ts b/modules/hls-downloader/src/HlsDownloader.types.ts
index 229c10d7..98f268f1 100644
--- a/modules/hls-downloader/src/HlsDownloader.types.ts
+++ b/modules/hls-downloader/src/HlsDownloader.types.ts
@@ -4,11 +4,13 @@ import {
} from "@jellyfin/sdk/lib/generated-client";
export type DownloadState =
+ | "QUEUED"
| "PENDING"
| "DOWNLOADING"
| "PAUSED"
| "DONE"
| "FAILED"
+ | "CANCELLED"
| "STOPPED";
export interface DownloadMetadata {
diff --git a/providers/NativeDownloadProvider.tsx b/providers/NativeDownloadProvider.tsx
index b566f1ac..626fe6fb 100644
--- a/providers/NativeDownloadProvider.tsx
+++ b/providers/NativeDownloadProvider.tsx
@@ -5,6 +5,7 @@ import {
addProgressListener,
checkForExistingDownloads,
downloadHLSAsset,
+ cancelDownload,
} from "@/modules/hls-downloader";
import {
DownloadInfo,
@@ -45,8 +46,10 @@ type DownloadContextType = {
}: DownloadOptionsData
) => Promise;
getDownloadedItem: (id: string) => Promise;
+ cancelDownload: (id: string) => Promise;
activeDownloads: DownloadInfo[];
downloadedFiles: DownloadedFileInfo[];
+ refetchDownloadedFiles: () => void;
};
const DownloadContext = createContext(
@@ -82,22 +85,26 @@ export type DownloadedFileInfo = {
};
const getDownloadedFiles = async (): Promise => {
- const downloadsDir = FileSystem.documentDirectory + "downloads/";
- const dirInfo = await FileSystem.getInfoAsync(downloadsDir);
- if (!dirInfo.exists) return [];
- const files = await FileSystem.readDirectoryAsync(downloadsDir);
const downloaded: DownloadedFileInfo[] = [];
- for (const file of files) {
+ const downloadsDir = FileSystem.documentDirectory + "downloads/";
+ const dirInfo = await FileSystem.getInfoAsync(downloadsDir);
+
+ if (!dirInfo.exists) return [];
+
+ const files = await FileSystem.readDirectoryAsync(downloadsDir);
+
+ for (let file of files) {
const fileInfo = await FileSystem.getInfoAsync(downloadsDir + file);
if (fileInfo.isDirectory) continue;
+ if (!file.endsWith(".json")) continue;
- const doneFile = await isFileMarkedAsDone(file.replace(".json", ""));
- if (!doneFile) continue;
+ const fileContent = await FileSystem.readAsStringAsync(downloadsDir + file);
- const fileContent = await FileSystem.readAsStringAsync(
- downloadsDir + file.replace("-done", "")
- );
+ // Check that fileContent is actually DownloadMetadata
+ if (!fileContent) continue;
+ if (!fileContent.includes("mediaSource")) continue;
+ if (!fileContent.includes("item")) continue;
downloaded.push({
id: file.replace(".json", ""),
@@ -112,8 +119,6 @@ const getDownloadedFile = async (id: string) => {
const downloadsDir = FileSystem.documentDirectory + "downloads/";
const fileInfo = await FileSystem.getInfoAsync(downloadsDir + id + ".json");
if (!fileInfo.exists) return null;
- const doneFile = await isFileMarkedAsDone(id);
- if (!doneFile) return null;
const fileContent = await FileSystem.readAsStringAsync(
downloadsDir + id + ".json"
);
@@ -130,7 +135,7 @@ export const NativeDownloadProvider: React.FC<{
const user = useAtomValue(userAtom);
const api = useAtomValue(apiAtom);
- const { data: downloadedFiles } = useQuery({
+ const { data: downloadedFiles, refetch: refetchDownloadedFiles } = useQuery({
queryKey: ["downloadedFiles"],
queryFn: getDownloadedFiles,
refetchOnWindowFocus: true,
@@ -140,9 +145,7 @@ export const NativeDownloadProvider: React.FC<{
});
useEffect(() => {
- // Initialize downloads from both HLS and regular downloads
const initializeDownloads = async () => {
- // Check HLS downloads
const hlsDownloads = await checkForExistingDownloads();
const hlsDownloadStates = hlsDownloads.reduce(
(acc, download) => ({
@@ -204,7 +207,7 @@ export const NativeDownloadProvider: React.FC<{
await queryClient.invalidateQueries({ queryKey: ["downloadedFiles"] });
- toast.success("Download complete ✅");
+ if (payload.state === "DONE") toast.success("Download complete ✅");
} catch (error) {
console.error("Failed to download file:", error);
toast.error("Failed to download ❌");
@@ -217,7 +220,12 @@ export const NativeDownloadProvider: React.FC<{
delete newDownloads[error.id];
return newDownloads;
});
- toast.error("Failed to download ❌");
+
+ if (error.state === "CANCELLED") toast.info("Download cancelled 🟡");
+ else {
+ toast.error("Download failed ❌");
+ console.error("Download error:", error);
+ }
});
return () => {
@@ -227,43 +235,6 @@ export const NativeDownloadProvider: React.FC<{
};
}, []);
- useEffect(() => {
- // Go through all the files in the folder downloads, check for the file id.json and id-done.json, if the id.json exists but id-done.json does not exist, then the download is still in done but not parsed. Parse it.
- const checkForUnparsedDownloads = async () => {
- const downloadsFolder = await FileSystem.getInfoAsync(
- FileSystem.documentDirectory + "downloads"
- );
- if (!downloadsFolder.exists) return;
- const files = await FileSystem.readDirectoryAsync(
- FileSystem.documentDirectory + "downloads"
- );
- for (const file of files) {
- if (file.endsWith(".json")) {
- const id = file.replace(".json", "");
- const doneFile = await FileSystem.getInfoAsync(
- FileSystem.documentDirectory + "downloads/" + id + "-done"
- );
- if (!doneFile.exists) {
- console.log("Found unparsed download:", id);
-
- const p = async () => {
- await rewriteM3U8Files(
- FileSystem.documentDirectory + "downloads/" + id
- );
- await markFileAsDone(id);
- };
- toast.promise(p(), {
- error: () => "Failed to download ❌",
- loading: "Finishing up download...",
- success: () => "Download complete ✅",
- });
- }
- }
- }
- };
- // checkForUnparsedDownloads();
- }, []);
-
const startDownload = async (
item: BaseItemDto,
url: string,
@@ -315,6 +286,8 @@ export const NativeDownloadProvider: React.FC<{
downloadedFiles: downloadedFiles ?? [],
getDownloadedItem: getDownloadedFile,
activeDownloads: Object.values(downloads),
+ cancelDownload: cancelDownload,
+ refetchDownloadedFiles,
}}
>
{children}