mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-02-08 21:42:21 +00:00
289 lines
7.8 KiB
TypeScript
289 lines
7.8 KiB
TypeScript
import type {
|
|
BaseItemDto,
|
|
MediaSourceInfo,
|
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
|
import { File, Paths } from "expo-file-system";
|
|
import type { MutableRefObject } from "react";
|
|
import { useCallback } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { toast } from "sonner-native";
|
|
import type { Bitrate } from "@/components/BitrateSelector";
|
|
import useImageStorage from "@/hooks/useImageStorage";
|
|
import { BackgroundDownloader } from "@/modules";
|
|
import { getOrSetDeviceId } from "@/utils/device";
|
|
import useDownloadHelper from "@/utils/download";
|
|
import { getItemImage } from "@/utils/getItemImage";
|
|
import {
|
|
clearAllDownloadedItems,
|
|
getAllDownloadedItems,
|
|
removeDownloadedItem,
|
|
} from "../database";
|
|
import {
|
|
calculateTotalDownloadedSize,
|
|
deleteAllAssociatedFiles,
|
|
} from "../fileOperations";
|
|
import type { JobStatus } from "../types";
|
|
import { generateFilename, uriToFilePath } from "../utils";
|
|
|
|
interface UseDownloadOperationsProps {
|
|
taskMapRef: MutableRefObject<Map<number, string>>;
|
|
processes: JobStatus[];
|
|
setProcesses: (updater: (prev: JobStatus[]) => JobStatus[]) => void;
|
|
removeProcess: (id: string) => void;
|
|
api: any;
|
|
authHeader?: string;
|
|
onDataChange?: () => void;
|
|
}
|
|
|
|
/**
|
|
* Hook providing download operation functions (start, cancel, delete)
|
|
*/
|
|
export function useDownloadOperations({
|
|
taskMapRef,
|
|
processes,
|
|
setProcesses,
|
|
removeProcess,
|
|
api,
|
|
authHeader,
|
|
onDataChange,
|
|
}: UseDownloadOperationsProps) {
|
|
const { t } = useTranslation();
|
|
const { saveSeriesPrimaryImage } = useDownloadHelper();
|
|
const { saveImage } = useImageStorage();
|
|
|
|
const startBackgroundDownload = useCallback(
|
|
async (
|
|
url: string,
|
|
item: BaseItemDto,
|
|
mediaSource: MediaSourceInfo,
|
|
maxBitrate: Bitrate,
|
|
) => {
|
|
if (!api || !item.Id || !authHeader) {
|
|
console.warn("startBackgroundDownload ~ Missing required params");
|
|
throw new Error("startBackgroundDownload ~ Missing required params");
|
|
}
|
|
|
|
try {
|
|
const deviceId = getOrSetDeviceId();
|
|
const processId = item.Id;
|
|
|
|
// Check if already downloading
|
|
const existingProcess = processes.find((p) => p.id === processId);
|
|
if (existingProcess) {
|
|
toast.info(
|
|
t("home.downloads.toasts.item_already_downloading", {
|
|
item: item.Name,
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Pre-download cover images before starting the video download
|
|
console.log(`[DOWNLOAD] Pre-downloading cover images for ${item.Name}`);
|
|
await saveSeriesPrimaryImage(item);
|
|
const itemImage = getItemImage({
|
|
item,
|
|
api,
|
|
variant: "Primary",
|
|
quality: 90,
|
|
width: 500,
|
|
});
|
|
await saveImage(item.Id, itemImage?.uri);
|
|
|
|
// Create job status
|
|
const jobStatus: JobStatus = {
|
|
id: processId,
|
|
inputUrl: url,
|
|
item,
|
|
itemId: item.Id,
|
|
deviceId,
|
|
progress: 0,
|
|
status: "downloading",
|
|
timestamp: new Date(),
|
|
mediaSource,
|
|
maxBitrate,
|
|
bytesDownloaded: 0,
|
|
};
|
|
|
|
// Add to processes
|
|
setProcesses((prev) => [...prev, jobStatus]);
|
|
|
|
// Generate destination path
|
|
const filename = generateFilename(item);
|
|
const videoFile = new File(Paths.document, `${filename}.mp4`);
|
|
const destinationPath = uriToFilePath(videoFile.uri);
|
|
|
|
console.log(`[DOWNLOAD] Starting download for ${filename}`);
|
|
console.log(`[DOWNLOAD] URL: ${url}`);
|
|
console.log(`[DOWNLOAD] Destination: ${destinationPath}`);
|
|
|
|
// Start the download (URL already contains api_key)
|
|
const taskId = await BackgroundDownloader.startDownload(
|
|
url,
|
|
destinationPath,
|
|
);
|
|
|
|
console.log(
|
|
`[DOWNLOAD] Got taskId: ${taskId} for processId: ${processId}`,
|
|
);
|
|
|
|
// Map task ID to process ID
|
|
taskMapRef.current.set(taskId, processId);
|
|
|
|
console.log(`[DOWNLOAD] TaskMap now contains:`, {
|
|
size: taskMapRef.current.size,
|
|
entries: Array.from(taskMapRef.current.entries()),
|
|
});
|
|
|
|
toast.success(
|
|
t("home.downloads.toasts.download_started_for_item", {
|
|
item: item.Name,
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
console.error("Failed to start download:", error);
|
|
toast.error(t("home.downloads.toasts.failed_to_start_download"), {
|
|
description: error instanceof Error ? error.message : "Unknown error",
|
|
});
|
|
throw error;
|
|
}
|
|
},
|
|
[api, authHeader, processes, setProcesses, taskMapRef, t],
|
|
);
|
|
|
|
const cancelDownload = useCallback(
|
|
async (id: string) => {
|
|
// Find the task ID for this process
|
|
let taskId: number | undefined;
|
|
for (const [tId, pId] of taskMapRef.current.entries()) {
|
|
if (pId === id) {
|
|
taskId = tId;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (taskId !== undefined) {
|
|
BackgroundDownloader.cancelDownload(taskId);
|
|
}
|
|
|
|
removeProcess(id);
|
|
toast.info(t("home.downloads.toasts.download_cancelled"));
|
|
},
|
|
[taskMapRef, removeProcess, t],
|
|
);
|
|
|
|
const deleteFile = useCallback(
|
|
async (id: string) => {
|
|
const itemToDelete = removeDownloadedItem(id);
|
|
|
|
if (itemToDelete) {
|
|
try {
|
|
deleteAllAssociatedFiles(itemToDelete);
|
|
toast.success(
|
|
t("home.downloads.toasts.file_deleted", {
|
|
item: itemToDelete.item.Name,
|
|
}),
|
|
);
|
|
onDataChange?.();
|
|
} catch (error) {
|
|
console.error("Failed to delete files:", error);
|
|
}
|
|
}
|
|
},
|
|
[t, onDataChange],
|
|
);
|
|
|
|
const deleteItems = useCallback(
|
|
async (ids: string[]) => {
|
|
for (const id of ids) {
|
|
await deleteFile(id);
|
|
}
|
|
},
|
|
[deleteFile],
|
|
);
|
|
|
|
const deleteAllFiles = useCallback(async () => {
|
|
const allItems = getAllDownloadedItems();
|
|
|
|
for (const item of allItems) {
|
|
try {
|
|
deleteAllAssociatedFiles(item);
|
|
} catch (error) {
|
|
console.error("Failed to delete file:", error);
|
|
}
|
|
}
|
|
|
|
clearAllDownloadedItems();
|
|
toast.success(t("home.downloads.toasts.all_files_deleted"));
|
|
onDataChange?.();
|
|
}, [t, onDataChange]);
|
|
|
|
const deleteFileByType = useCallback(
|
|
async (itemType: string) => {
|
|
const allItems = getAllDownloadedItems();
|
|
const itemsToDelete = allItems.filter(
|
|
(item) => item.item.Type === itemType,
|
|
);
|
|
|
|
if (itemsToDelete.length === 0) {
|
|
console.log(`[DELETE] No items found with type: ${itemType}`);
|
|
return;
|
|
}
|
|
|
|
console.log(
|
|
`[DELETE] Deleting ${itemsToDelete.length} items of type: ${itemType}`,
|
|
);
|
|
|
|
for (const item of itemsToDelete) {
|
|
try {
|
|
deleteAllAssociatedFiles(item);
|
|
removeDownloadedItem(item.item.Id || "");
|
|
} catch (error) {
|
|
console.error(
|
|
`Failed to delete ${itemType} file ${item.item.Name}:`,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
const itemLabel =
|
|
itemType === "Movie"
|
|
? t("common.movies")
|
|
: itemType === "Episode"
|
|
? t("common.episodes")
|
|
: itemType;
|
|
|
|
toast.success(
|
|
t("home.downloads.toasts.files_deleted_by_type", {
|
|
count: itemsToDelete.length,
|
|
type: itemLabel,
|
|
defaultValue: `${itemsToDelete.length} ${itemLabel} deleted`,
|
|
}),
|
|
);
|
|
|
|
onDataChange?.();
|
|
},
|
|
[t, onDataChange],
|
|
);
|
|
|
|
const appSizeUsage = useCallback(async () => {
|
|
const totalSize = calculateTotalDownloadedSize();
|
|
|
|
return {
|
|
total: 0,
|
|
remaining: 0,
|
|
appSize: totalSize,
|
|
};
|
|
}, []);
|
|
|
|
return {
|
|
startBackgroundDownload,
|
|
cancelDownload,
|
|
deleteFile,
|
|
deleteItems,
|
|
deleteAllFiles,
|
|
deleteFileByType,
|
|
appSizeUsage,
|
|
};
|
|
}
|