mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-18 18:24:18 +01:00
wip
This commit is contained in:
255
providers/NativeDownloadProvider.tsx
Normal file
255
providers/NativeDownloadProvider.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import RNBackgroundDownloader, {
|
||||
DownloadTaskState,
|
||||
} from "@kesha-antonov/react-native-background-downloader";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import {
|
||||
addCompleteListener,
|
||||
addErrorListener,
|
||||
addProgressListener,
|
||||
checkForExistingDownloads,
|
||||
downloadHLSAsset,
|
||||
} from "@/modules/hls-downloader";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import { DownloadInfo } from "@/modules/hls-downloader/src/HlsDownloader.types";
|
||||
|
||||
type DownloadContextType = {
|
||||
downloads: Record<string, DownloadInfo>;
|
||||
startDownload: (item: BaseItemDto, url: string) => Promise<void>;
|
||||
cancelDownload: (id: string) => void;
|
||||
};
|
||||
|
||||
const DownloadContext = createContext<DownloadContextType | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const persistDownloadedFile = async (
|
||||
originalLocation: string,
|
||||
fileName: string
|
||||
) => {
|
||||
const destinationDir = `${FileSystem.documentDirectory}downloads/`;
|
||||
const newLocation = `${destinationDir}${fileName}`;
|
||||
|
||||
try {
|
||||
// Ensure the downloads directory exists
|
||||
await FileSystem.makeDirectoryAsync(destinationDir, {
|
||||
intermediates: true,
|
||||
});
|
||||
|
||||
// Move the file to its final destination
|
||||
await FileSystem.moveAsync({
|
||||
from: originalLocation,
|
||||
to: newLocation,
|
||||
});
|
||||
|
||||
return newLocation;
|
||||
} catch (error) {
|
||||
console.error("Error persisting file:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const NativeDownloadProvider: React.FC<{
|
||||
children: React.ReactNode;
|
||||
}> = ({ children }) => {
|
||||
const [downloads, setDownloads] = useState<Record<string, DownloadInfo>>({});
|
||||
|
||||
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) => ({
|
||||
...acc,
|
||||
[download.id]: {
|
||||
id: download.id,
|
||||
progress: download.progress,
|
||||
state: download.state,
|
||||
},
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
// Check regular downloads
|
||||
const regularDownloads =
|
||||
await RNBackgroundDownloader.checkForExistingDownloads();
|
||||
const regularDownloadStates = regularDownloads.reduce(
|
||||
(acc, download) => ({
|
||||
...acc,
|
||||
[download.id]: {
|
||||
id: download.id,
|
||||
progress: download.bytesDownloaded / download.bytesTotal,
|
||||
state: download.state,
|
||||
},
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
setDownloads({ ...hlsDownloadStates, ...regularDownloadStates });
|
||||
};
|
||||
|
||||
initializeDownloads();
|
||||
|
||||
// Set up HLS download listeners
|
||||
const progressListener = addProgressListener((download) => {
|
||||
console.log("[HLS] Download progress:", download);
|
||||
setDownloads((prev) => ({
|
||||
...prev,
|
||||
[download.id]: {
|
||||
id: download.id,
|
||||
progress: download.progress,
|
||||
state: download.state,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
const completeListener = addCompleteListener(async (payload) => {
|
||||
if (typeof payload === "string") {
|
||||
// Handle string ID (old HLS downloads)
|
||||
setDownloads((prev) => {
|
||||
const newDownloads = { ...prev };
|
||||
delete newDownloads[payload];
|
||||
return newDownloads;
|
||||
});
|
||||
} else {
|
||||
// Handle OnCompleteEventPayload (with location)
|
||||
console.log("Download complete event received:", payload);
|
||||
console.log("Original download location:", payload.location);
|
||||
|
||||
try {
|
||||
// Get the download info from our state
|
||||
const downloadInfo = downloads[payload.id];
|
||||
if (downloadInfo?.metadata?.Name) {
|
||||
const newLocation = await persistDownloadedFile(
|
||||
payload.location,
|
||||
downloadInfo.metadata.Name
|
||||
);
|
||||
console.log("File successfully persisted to:", newLocation);
|
||||
} else {
|
||||
console.log("No filename in metadata, using original location");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to persist file:", error);
|
||||
}
|
||||
|
||||
setDownloads((prev) => {
|
||||
const newDownloads = { ...prev };
|
||||
delete newDownloads[payload.id];
|
||||
return newDownloads;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const errorListener = addErrorListener((error) => {
|
||||
console.error("Download error:", error);
|
||||
if (error.id) {
|
||||
setDownloads((prev) => {
|
||||
const newDownloads = { ...prev };
|
||||
delete newDownloads[error.id];
|
||||
return newDownloads;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
progressListener.remove();
|
||||
completeListener.remove();
|
||||
errorListener.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const startDownload = async (item: BaseItemDto, url: string) => {
|
||||
if (!item.Id || !item.Name) throw new Error("Item ID or Name is missing");
|
||||
const jobId = item.Id;
|
||||
|
||||
if (url.includes("master.m3u8")) {
|
||||
// HLS download
|
||||
downloadHLSAsset(jobId, url, item.Name, {
|
||||
Name: item.Name,
|
||||
});
|
||||
} else {
|
||||
// Regular download
|
||||
try {
|
||||
const task = RNBackgroundDownloader.download({
|
||||
id: jobId,
|
||||
url: url,
|
||||
destination: `${FileSystem.documentDirectory}${jobId}`,
|
||||
});
|
||||
|
||||
task.begin(({ expectedBytes }) => {
|
||||
setDownloads((prev) => ({
|
||||
...prev,
|
||||
[jobId]: {
|
||||
id: jobId,
|
||||
progress: 0,
|
||||
state: "DOWNLOADING",
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
task.progress(({ bytesDownloaded, bytesTotal }) => {
|
||||
console.log(
|
||||
"[Normal] Download progress:",
|
||||
bytesDownloaded,
|
||||
bytesTotal
|
||||
);
|
||||
setDownloads((prev) => ({
|
||||
...prev,
|
||||
[jobId]: {
|
||||
id: jobId,
|
||||
progress: bytesDownloaded / bytesTotal,
|
||||
state: "DOWNLOADING",
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
task.done(() => {
|
||||
setDownloads((prev) => {
|
||||
const newDownloads = { ...prev };
|
||||
delete newDownloads[jobId];
|
||||
return newDownloads;
|
||||
});
|
||||
});
|
||||
|
||||
task.error(({ error }) => {
|
||||
console.error("Download error:", error);
|
||||
setDownloads((prev) => {
|
||||
const newDownloads = { ...prev };
|
||||
delete newDownloads[jobId];
|
||||
return newDownloads;
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error starting download:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cancelDownload = (id: string) => {
|
||||
// Implement cancel logic here
|
||||
setDownloads((prev) => {
|
||||
const newDownloads = { ...prev };
|
||||
delete newDownloads[id];
|
||||
return newDownloads;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DownloadContext.Provider
|
||||
value={{ downloads, startDownload, cancelDownload }}
|
||||
>
|
||||
{children}
|
||||
</DownloadContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useNativeDownloads = () => {
|
||||
const context = useContext(DownloadContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useDownloads must be used within a NativeDownloadProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
Reference in New Issue
Block a user