From 7ce2c9037618baac8be26029d44b910300c4d4fd Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 30 Sep 2024 16:34:54 +0200 Subject: [PATCH] wip --- app/(auth)/(tabs)/(home)/downloads.tsx | 7 +- components/DownloadItem.tsx | 7 +- components/downloads/ActiveDownloads.tsx | 46 ++- components/settings/SettingToggles.tsx | 5 +- hooks/useDownloadedFileOpener.ts | 39 ++- hooks/useRemuxHlsToMp4.ts | 56 ++-- providers/DownloadProvider.tsx | 349 ++++++++--------------- utils/jellyfin/media/getStreamUrl.ts | 7 + utils/optimize-server.ts | 100 +++++++ 9 files changed, 331 insertions(+), 285 deletions(-) create mode 100644 utils/optimize-server.ts diff --git a/app/(auth)/(tabs)/(home)/downloads.tsx b/app/(auth)/(tabs)/(home)/downloads.tsx index d6cccb8e..af7cf7d5 100644 --- a/app/(auth)/(tabs)/(home)/downloads.tsx +++ b/app/(auth)/(tabs)/(home)/downloads.tsx @@ -16,12 +16,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; const downloads: React.FC = () => { const [queue, setQueue] = useAtom(queueAtom); - const { - startBackgroundDownload, - updateProcess, - removeProcess, - downloadedFiles, - } = useDownload(); + const { removeProcess, downloadedFiles } = useDownload(); const [settings] = useSettings(); diff --git a/components/DownloadItem.tsx b/components/DownloadItem.tsx index 31936686..37ebcb0e 100644 --- a/components/DownloadItem.tsx +++ b/components/DownloadItem.tsx @@ -112,6 +112,7 @@ export const DownloadItem: React.FC = ({ item, ...props }) => { ); let url: string | undefined = undefined; + let fileExtension: string | undefined | null = "mp4"; const mediaSource: MediaSourceInfo = response.data.MediaSources.find( (source: MediaSourceInfo) => source.Id === selectedMediaSource?.Id @@ -146,12 +147,14 @@ export const DownloadItem: React.FC = ({ item, ...props }) => { } } else if (mediaSource.TranscodingUrl) { url = `${api.basePath}${mediaSource.TranscodingUrl}`; + fileExtension = mediaSource.TranscodingContainer; } if (!url) throw new Error("No url"); + if (!fileExtension) throw new Error("No file extension"); if (settings?.downloadMethod === "optimized") { - return await startBackgroundDownload(url, item); + return await startBackgroundDownload(url, item, fileExtension); } else { return await startRemuxing(url); } @@ -192,7 +195,7 @@ export const DownloadItem: React.FC = ({ item, ...props }) => { const process = useMemo(() => { if (!processes) return null; - return processes.find((process) => process.item.Id === item.Id); + return processes.find((process) => process?.item?.Id === item.Id); }, [processes, item.Id]); return ( diff --git a/components/downloads/ActiveDownloads.tsx b/components/downloads/ActiveDownloads.tsx index 1dbd46f7..d4fc045a 100644 --- a/components/downloads/ActiveDownloads.tsx +++ b/components/downloads/ActiveDownloads.tsx @@ -1,7 +1,8 @@ import { Text } from "@/components/common/Text"; -import { ProcessItem, useDownload } from "@/providers/DownloadProvider"; +import { useDownload } from "@/providers/DownloadProvider"; import { apiAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; +import { cancelJobById, JobStatus } from "@/utils/optimize-server"; import { formatTimeString } from "@/utils/time"; import { Ionicons } from "@expo/vector-icons"; import { checkForExistingDownloads } from "@kesha-antonov/react-native-background-downloader"; @@ -18,7 +19,7 @@ interface Props extends ViewProps {} export const ActiveDownloads: React.FC = ({ ...props }) => { const router = useRouter(); - const { removeProcess, processes } = useDownload(); + const { removeProcess, processes, setProcesses } = useDownload(); const [settings] = useSettings(); const [api] = useAtom(apiAtom); @@ -26,25 +27,22 @@ export const ActiveDownloads: React.FC = ({ ...props }) => { mutationFn: async (id: string) => { if (!process) throw new Error("No active download"); - try { - if (settings?.downloadMethod === "optimized") { - await axios.delete( - settings?.optimizedVersionsServerUrl + "cancel-job/" + id, - { - headers: { - Authorization: api?.accessToken, - }, - } - ); + if (settings?.downloadMethod === "optimized") { + try { const tasks = await checkForExistingDownloads(); - for (const task of tasks) task.stop(); - } else { - FFmpegKit.cancel(); + for (const task of tasks) { + if (task.id === id) { + await task.stop(); + } + } + } catch (e) { + throw e; + } finally { + removeProcess(id); } - } catch (e) { - throw e; - } finally { - removeProcess(id); + } else { + FFmpegKit.cancel(); + setProcesses((prev) => prev.filter((p) => p.id !== id)); } }, onSuccess: () => { @@ -52,12 +50,12 @@ export const ActiveDownloads: React.FC = ({ ...props }) => { }, onError: (e) => { console.log(e); - toast.error("Failed to cancel download on the server"); + toast.error("Could not cancel download"); }, }); const eta = useCallback( - (p: ProcessItem) => { + (p: JobStatus) => { if (!p.speed || !p.progress) return null; const length = p?.item?.RunTimeTicks || 0; @@ -67,7 +65,7 @@ export const ActiveDownloads: React.FC = ({ ...props }) => { [process] ); - if (processes.length === 0) + if (processes?.length === 0) return ( Active download @@ -79,7 +77,7 @@ export const ActiveDownloads: React.FC = ({ ...props }) => { Active downloads - {processes.map((p) => ( + {processes?.map((p) => ( router.push(`/(auth)/items/page?id=${p.item.Id}`)} @@ -109,7 +107,7 @@ export const ActiveDownloads: React.FC = ({ ...props }) => { )} - {p.state} + {p.status} cancelJobMutation.mutate(p.id)}> diff --git a/components/settings/SettingToggles.tsx b/components/settings/SettingToggles.tsx index 7646a7b6..498cfeea 100644 --- a/components/settings/SettingToggles.tsx +++ b/components/settings/SettingToggles.tsx @@ -24,11 +24,13 @@ import { Button } from "../Button"; import { MediaToggles } from "./MediaToggles"; import * as ScreenOrientation from "expo-screen-orientation"; import { opacity } from "react-native-reanimated/lib/typescript/reanimated2/Colors"; +import { useDownload } from "@/providers/DownloadProvider"; interface Props extends ViewProps {} export const SettingToggles: React.FC = ({ ...props }) => { const [settings, updateSettings] = useSettings(); + const { setProcesses } = useDownload(); const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); @@ -495,7 +497,7 @@ export const SettingToggles: React.FC = ({ ...props }) => { key="1" onSelect={() => { updateSettings({ downloadMethod: "remux" }); - queryClient.invalidateQueries({ queryKey: ["search"] }); + setProcesses([]); }} > Default @@ -504,6 +506,7 @@ export const SettingToggles: React.FC = ({ ...props }) => { key="2" onSelect={() => { updateSettings({ downloadMethod: "optimized" }); + setProcesses([]); queryClient.invalidateQueries({ queryKey: ["search"] }); }} > diff --git a/hooks/useDownloadedFileOpener.ts b/hooks/useDownloadedFileOpener.ts index 09aecd26..f41e1f53 100644 --- a/hooks/useDownloadedFileOpener.ts +++ b/hooks/useDownloadedFileOpener.ts @@ -13,13 +13,40 @@ export const useFileOpener = () => { const openFile = useCallback( async (item: BaseItemDto) => { const directory = FileSystem.documentDirectory; - const url = `${directory}/${item.Id}.mp4`; - startDownloadedFilePlayback({ - item, - url, - }); - router.push("/play"); + if (!directory) { + throw new Error("Document directory is not available"); + } + + if (!item.Id) { + throw new Error("Item ID is not available"); + } + + try { + const files = await FileSystem.readDirectoryAsync(directory); + for (let f of files) { + console.log(f); + } + const path = item.Id!; + const matchingFile = files.find((file) => file.startsWith(path)); + + if (!matchingFile) { + throw new Error(`No file found for item ${path}`); + } + + const url = `${directory}${matchingFile}`; + + console.log("Opening " + url); + + startDownloadedFilePlayback({ + item, + url, + }); + router.push("/play"); + } catch (error) { + console.error("Error opening file:", error); + // Handle the error appropriately, e.g., show an error message to the user + } }, [startDownloadedFilePlayback] ); diff --git a/hooks/useRemuxHlsToMp4.ts b/hooks/useRemuxHlsToMp4.ts index 07978337..18c5ee3d 100644 --- a/hooks/useRemuxHlsToMp4.ts +++ b/hooks/useRemuxHlsToMp4.ts @@ -9,6 +9,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner-native"; import { useDownload } from "@/providers/DownloadProvider"; import { useRouter } from "expo-router"; +import { JobStatus } from "@/utils/optimize-server"; /** * Custom hook for remuxing HLS to MP4 using FFmpeg. @@ -19,8 +20,7 @@ import { useRouter } from "expo-router"; */ export const useRemuxHlsToMp4 = (item: BaseItemDto) => { const queryClient = useQueryClient(); - const { clearProcesses, saveDownloadedItemInfo, addProcess, updateProcess } = - useDownload(); + const { saveDownloadedItemInfo, setProcesses } = useDownload(); const router = useRouter(); if (!item.Id || !item.Name) { @@ -52,12 +52,20 @@ export const useRemuxHlsToMp4 = (item: BaseItemDto) => { ); try { - addProcess({ - id: item.Id, - item, - progress: 0, - state: "downloading", - }); + setProcesses((prev) => [ + ...prev, + { + id: "", + deviceId: "", + inputUrl: "", + item, + itemId: item.Id, + outputPath: "", + progress: 0, + status: "running", + timestamp: new Date(), + } as JobStatus, + ]); FFmpegKitConfig.enableStatisticsCallback((statistics) => { const videoLength = @@ -73,9 +81,17 @@ export const useRemuxHlsToMp4 = (item: BaseItemDto) => { : 0; if (!item.Id) throw new Error("Item is undefined"); - updateProcess(item.Id, { - progress: percentage, - speed: Math.max(speed, 0), + setProcesses((prev) => { + return prev.map((process) => { + if (process.itemId === item.Id) { + return { + ...process, + progress: percentage, + speed: Math.max(speed, 0), + }; + } + return process; + }); }); }); @@ -98,14 +114,12 @@ export const useRemuxHlsToMp4 = (item: BaseItemDto) => { }); resolve(); } else if (returnCode.isValueError()) { - toast.success("Download failed"); writeToLog( "ERROR", `useRemuxHlsToMp4 ~ remuxing failed for item: ${item.Name}` ); reject(new Error("Remuxing failed")); // Reject the promise on error } else if (returnCode.isValueCancel()) { - toast.success("Download canceled"); writeToLog( "INFO", `useRemuxHlsToMp4 ~ remuxing was canceled for item: ${item.Name}` @@ -113,7 +127,9 @@ export const useRemuxHlsToMp4 = (item: BaseItemDto) => { resolve(); } - clearProcesses(); + setProcesses((prev) => { + return prev.filter((process) => process.itemId !== item.Id); + }); } catch (error) { reject(error); } @@ -125,17 +141,21 @@ export const useRemuxHlsToMp4 = (item: BaseItemDto) => { "ERROR", `useRemuxHlsToMp4 ~ remuxing failed for item: ${item.Name}` ); - clearProcesses(); + setProcesses((prev) => { + return prev.filter((process) => process.itemId !== item.Id); + }); throw error; // Re-throw the error to propagate it to the caller } }, - [output, item, clearProcesses] + [output, item] ); const cancelRemuxing = useCallback(() => { FFmpegKit.cancel(); - clearProcesses(); - }, [item.Name, clearProcesses]); + setProcesses((prev) => { + return prev.filter((process) => process.itemId !== item.Id); + }); + }, [item.Name]); return { startRemuxing, cancelRemuxing }; }; diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx index 10d94da3..01b4af07 100644 --- a/providers/DownloadProvider.tsx +++ b/providers/DownloadProvider.tsx @@ -1,6 +1,7 @@ import { useSettings } from "@/utils/atoms/settings"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { + checkForExistingDownloads, completeHandler, directories, download, @@ -22,27 +23,21 @@ import React, { useContext, useEffect, useMemo, + useRef, useState, } from "react"; import { toast } from "sonner-native"; import { apiAtom } from "./JellyfinProvider"; import * as BackgroundFetch from "expo-background-fetch"; import * as TaskManager from "expo-task-manager"; - -export type ProcessItem = { - id: string; - item: Partial; - progress: number; - size?: number; - speed?: number; - state: - | "optimizing" - | "downloading" - | "done" - | "error" - | "canceled" - | "queued"; -}; +import { writeToLog } from "@/utils/log"; +import { getOrSetDeviceId } from "@/utils/device"; +import { + cancelAllJobs, + cancelJobById, + getAllJobsByDeviceId, + JobStatus, +} from "@/utils/optimize-server"; export const BACKGROUND_FETCH_TASK = "background-fetch"; @@ -77,7 +72,6 @@ const DownloadContext = createContext([]); const [settings] = useSettings(); const router = useRouter(); const [api] = useAtom(apiAtom); @@ -92,115 +86,133 @@ function useDownloadProvider() { staleTime: 0, }); - useEffect(() => { - // Check background task status - checkStatusAsync(); + const [processes, setProcesses] = useState([]); - // Load initial processes state from AsyncStorage - const loadInitialProcesses = async () => { - const storedProcesses = await readProcesses(); - setProcesses(storedProcesses); + useQuery({ + queryKey: ["jobs"], + queryFn: async () => { + const deviceId = await getOrSetDeviceId(); + const url = settings?.optimizedVersionsServerUrl; + + if ( + settings?.downloadMethod !== "optimized" || + !url || + !deviceId || + !authHeader + ) + return []; + + const jobs = await getAllJobsByDeviceId({ + deviceId, + authHeader, + url, + }); + + setProcesses(jobs); + + return jobs; + }, + staleTime: 0, + refetchInterval: 1000 * 3, // 5 minutes + enabled: settings?.downloadMethod === "optimized", + }); + + useEffect(() => { + const checkIfShouldStartDownload = async () => { + if (!processes) return; + for (let i = 0; i < processes.length; i++) { + const job = processes[i]; + + // Check if the download is already in progress + const tasks = await checkForExistingDownloads(); + if (tasks.find((task) => task.id === job.id)) continue; + + if (job.status === "completed") { + await startDownload(job); + continue; + } + } }; - loadInitialProcesses(); - }, []); + + checkIfShouldStartDownload(); + }, [processes]); /******************** * Background task *******************/ - const [isRegistered, setIsRegistered] = useState(false); - const [status, setStatus] = - useState(null); + // useEffect(() => { + // // Check background task status + // checkStatusAsync(); + // }, []); - const checkStatusAsync = async () => { - const status = await BackgroundFetch.getStatusAsync(); - const isRegistered = await TaskManager.isTaskRegisteredAsync( - BACKGROUND_FETCH_TASK - ); - setStatus(status); - setIsRegistered(isRegistered); + // const [isRegistered, setIsRegistered] = useState(false); + // const [status, setStatus] = + // useState(null); - console.log("Background fetch status:", status); - console.log("Background fetch task registered:", isRegistered); - }; + // const checkStatusAsync = async () => { + // const status = await BackgroundFetch.getStatusAsync(); + // const isRegistered = await TaskManager.isTaskRegisteredAsync( + // BACKGROUND_FETCH_TASK + // ); + // setStatus(status); + // setIsRegistered(isRegistered); - const toggleFetchTask = async () => { - if (isRegistered) { - console.log("Unregistering background fetch task"); - await unregisterBackgroundFetchAsync(); - } else { - console.log("Registering background fetch task"); - await registerBackgroundFetchAsync(); - } + // console.log("Background fetch status:", status); + // console.log("Background fetch task registered:", isRegistered); + // }; - checkStatusAsync(); - }; + // const toggleFetchTask = async () => { + // if (isRegistered) { + // console.log("Unregistering background fetch task"); + // await unregisterBackgroundFetchAsync(); + // } else { + // console.log("Registering background fetch task"); + // await registerBackgroundFetchAsync(); + // } + + // checkStatusAsync(); + // }; /********************** ********************** *********************/ - const clearProcesses = useCallback(async () => { - await AsyncStorage.removeItem(STORAGE_KEY); - setProcesses([]); - }, []); + const removeProcess = useCallback( + async (id: string) => { + const deviceId = await getOrSetDeviceId(); + if (!deviceId || !authHeader || !settings?.optimizedVersionsServerUrl) + return; - const updateProcess = useCallback( - async (id: string, updater: Partial) => { - setProcesses((prevProcesses) => { - const newProcesses = prevProcesses.map((process) => - process.id === id ? { ...process, ...updater } : process - ); - - AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newProcesses)); - - return newProcesses; - }); + try { + await cancelJobById({ + authHeader, + id, + url: settings?.optimizedVersionsServerUrl, + }); + } catch (error) { + console.log(error); + } }, - [] + [settings?.optimizedVersionsServerUrl, authHeader] ); - const addProcess = useCallback(async (item: ProcessItem) => { - setProcesses((prevProcesses) => { - const newProcesses = [...prevProcesses, item]; - AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newProcesses)); - return newProcesses; - }); - }, []); - - const removeProcess = useCallback(async (id: string) => { - setProcesses((prevProcesses) => { - const newProcesses = prevProcesses.filter((process) => process.id !== id); - AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newProcesses)); - return newProcesses; - }); - }, []); - - const readProcesses = useCallback(async (): Promise => { - const items = await AsyncStorage.getItem(STORAGE_KEY); - return items ? JSON.parse(items) : []; - }, []); - const startDownload = useCallback( - (process: ProcessItem) => { + async (process: JobStatus) => { if (!process?.item.Id || !authHeader) throw new Error("No item id"); download({ id: process.id, url: settings?.optimizedVersionsServerUrl + "download/" + process.id, - destination: `${directories.documents}/${process?.item.Id}.mp4`, + destination: `${directories.documents}/${process.item.Id}.mp4`, headers: { Authorization: authHeader, }, }) .begin(() => { toast.info(`Download started for ${process.item.Name}`); - updateProcess(process.id, { state: "downloading", progress: 0 }); }) .progress((data) => { const percent = (data.bytesDownloaded / data.bytesTotal) * 100; - updateProcess(process.id, { - state: "downloading", - progress: percent, - }); + console.log("Progress ~", percent); }) .done(async () => { removeProcess(process.id); @@ -213,112 +225,22 @@ function useDownloadProvider() { toast.success(`Download completed for ${process.item.Name}`); }) .error((error) => { - updateProcess(process.id, { state: "error" }); toast.error(`Download failed for ${process.item.Name}: ${error}`); + writeToLog("ERROR", `Download failed for ${process.item.Name}`, { + error, + }); }); }, [queryClient, settings?.optimizedVersionsServerUrl, authHeader] ); - useEffect(() => { - const checkJobStatusPeriodically = async () => { - if (!settings?.optimizedVersionsServerUrl || !authHeader) return; - - const updatedProcesses = await Promise.all( - processes.map(async (process) => { - if (!settings.optimizedVersionsServerUrl) return process; - if (process.state === "queued" || process.state === "optimizing") { - try { - const job = await checkJobStatus( - process.id, - settings.optimizedVersionsServerUrl, - authHeader - ); - - if (!job) { - return process; - } - - let newState: ProcessItem["state"] = process.state; - if (job.status === "queued") { - newState = "queued"; - } else if (job.status === "running") { - newState = "optimizing"; - } else if (job.status === "completed") { - startDownload(process); - return { - ...process, - progress: 100, - speed: 0, - }; - } else if (job.status === "failed") { - newState = "error"; - } else if (job.status === "cancelled") { - newState = "canceled"; - } - - return { - ...process, - state: newState, - progress: job.progress, - speed: job.speed, - }; - } catch (error) { - if (axios.isAxiosError(error) && !error.response) { - // Network error occurred (server might be down) - console.error("Network error occurred:", error.message); - toast.error( - "Network error: Unable to connect to optimization server" - ); - return { - ...process, - state: "error", - errorMessage: - "Network error: Unable to connect to optimization server", - }; - } else { - // Other types of errors - console.error("Error checking job status:", error); - toast.error( - "An unexpected error occurred while checking job status" - ); - return { - ...process, - state: "error", - errorMessage: "An unexpected error occurred", - }; - } - } - } - return process; - }) - ); - - // Filter out null values (completed jobs) - const filteredProcesses = updatedProcesses.filter( - (process) => process !== null - ) as ProcessItem[]; - - // Update the state with the filtered processes - setProcesses(filteredProcesses); - }; - - const intervalId = setInterval(checkJobStatusPeriodically, 2000); - - return () => clearInterval(intervalId); - }, [ - processes, - settings?.optimizedVersionsServerUrl, - authHeader, - startDownload, - ]); - const startBackgroundDownload = useCallback( - async (url: string, item: BaseItemDto) => { + async (url: string, item: BaseItemDto, fileExtension: string) => { try { + const deviceId = await getOrSetDeviceId(); const response = await axios.post( settings?.optimizedVersionsServerUrl + "optimize-version", - { url }, + { url, fileExtension, deviceId, itemId: item.Id, item }, { headers: { "Content-Type": "application/json", @@ -331,15 +253,6 @@ function useDownloadProvider() { throw new Error("Failed to start optimization job"); } - const { id } = response.data; - - addProcess({ - id, - item: item, - progress: 0, - state: "queued", - }); - toast.success(`Queued ${item.Name} for optimization`, { action: { label: "Go to download", @@ -400,14 +313,19 @@ function useDownloadProvider() { } } await AsyncStorage.removeItem("downloadedItems"); - await AsyncStorage.removeItem("runningProcesses"); - clearProcesses(); + + if (!authHeader) throw new Error("No auth header"); + if (!settings?.optimizedVersionsServerUrl) + throw new Error("No server url"); + cancelAllJobs({ + authHeader, + url: settings?.optimizedVersionsServerUrl, + deviceId: await getOrSetDeviceId(), + }); queryClient.invalidateQueries({ queryKey: ["downloadedItems"] }); - console.log( - "Successfully deleted all files and folders in the directory and cleared AsyncStorage" - ); + toast.success("All files and folders deleted successfully"); } catch (error) { console.error("Failed to delete all files and folders:", error); } @@ -496,16 +414,13 @@ function useDownloadProvider() { return { processes, - updateProcess, startBackgroundDownload, - clearProcesses, - readProcesses, downloadedFiles, deleteAllFiles, deleteFile, saveDownloadedItemInfo, - addProcess, removeProcess, + setProcesses, }; } @@ -527,25 +442,3 @@ export function useDownload() { } return context; } - -const checkJobStatus = async ( - id: string, - baseUrl: string, - authHeader: string -): Promise<{ - progress: number; - status: "queued" | "running" | "completed" | "failed" | "cancelled"; - speed?: string; -}> => { - const statusResponse = await axios.get(`${baseUrl}job-status/${id}`, { - headers: { - Authorization: authHeader, - }, - }); - if (statusResponse.status !== 200) { - throw new Error("Failed to fetch job status"); - } - - const json = statusResponse.data; - return json; -}; diff --git a/utils/jellyfin/media/getStreamUrl.ts b/utils/jellyfin/media/getStreamUrl.ts index 337c355c..df13af1f 100644 --- a/utils/jellyfin/media/getStreamUrl.ts +++ b/utils/jellyfin/media/getStreamUrl.ts @@ -109,5 +109,12 @@ export const getStreamUrl = async ({ if (!url) throw new Error("No url"); + console.log( + mediaSource.VideoType, + mediaSource.Container, + mediaSource.TranscodingContainer, + mediaSource.TranscodingSubProtocol + ); + return url; }; diff --git a/utils/optimize-server.ts b/utils/optimize-server.ts new file mode 100644 index 00000000..6fa08ec3 --- /dev/null +++ b/utils/optimize-server.ts @@ -0,0 +1,100 @@ +import { itemRouter } from "@/components/common/TouchableItemRouter"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import axios from "axios"; + +interface IJobInput { + deviceId: string; + authHeader: string; + url: string; +} + +export interface JobStatus { + id: string; + status: "queued" | "running" | "completed" | "failed" | "cancelled"; + progress: number; + outputPath: string; + inputUrl: string; + deviceId: string; + itemId: string; + item: Partial; + speed?: number; + timestamp: Date; +} + +/** + * Fetches all jobs for a specific device. + * + * @param {IGetAllDeviceJobs} params - The parameters for the API request. + * @param {string} params.deviceId - The ID of the device to fetch jobs for. + * @param {string} params.authHeader - The authorization header for the API request. + * @param {string} params.url - The base URL for the API endpoint. + * + * @returns {Promise} A promise that resolves to an array of job statuses. + * + * @throws {Error} Throws an error if the API request fails or returns a non-200 status code. + */ +export async function getAllJobsByDeviceId({ + deviceId, + authHeader, + url, +}: IJobInput): Promise { + const statusResponse = await axios.get(`${url}all-jobs`, { + headers: { + Authorization: authHeader, + }, + params: { + deviceId, + }, + }); + if (statusResponse.status !== 200) { + throw new Error("Failed to fetch job status"); + } + + return statusResponse.data; +} + +interface ICancelJob { + authHeader: string; + url: string; + id: string; +} + +export async function cancelJobById({ + authHeader, + url, + id, +}: ICancelJob): Promise { + const statusResponse = await axios.delete(`${url}cancel-job/${id}`, { + headers: { + Authorization: authHeader, + }, + }); + if (statusResponse.status !== 200) { + throw new Error("Failed to cancel process"); + } + + return true; +} + +export async function cancelAllJobs({ authHeader, url, deviceId }: IJobInput) { + try { + await getAllJobsByDeviceId({ + deviceId, + authHeader, + url, + }).then((jobs) => { + jobs.forEach((job) => { + cancelJobById({ + authHeader, + url, + id: job.id, + }); + }); + }); + } catch (error) { + console.error(error); + return false; + } + + return true; +}