This commit is contained in:
Fredrik Burmester
2024-09-30 16:34:54 +02:00
parent 0263ad6109
commit 7ce2c90376
9 changed files with 331 additions and 285 deletions

View File

@@ -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();

View File

@@ -112,6 +112,7 @@ export const DownloadItem: React.FC<DownloadProps> = ({ 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<DownloadProps> = ({ 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<DownloadProps> = ({ 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 (

View File

@@ -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> = ({ ...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> = ({ ...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> = ({ ...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> = ({ ...props }) => {
[process]
);
if (processes.length === 0)
if (processes?.length === 0)
return (
<View {...props} className="bg-neutral-900 p-4 rounded-2xl">
<Text className="text-lg font-bold">Active download</Text>
@@ -79,7 +77,7 @@ export const ActiveDownloads: React.FC<Props> = ({ ...props }) => {
<View {...props} className="bg-neutral-900 p-4 rounded-2xl">
<Text className="text-lg font-bold mb-2">Active downloads</Text>
<View className="space-y-2">
{processes.map((p) => (
{processes?.map((p) => (
<TouchableOpacity
key={p.id}
onPress={() => router.push(`/(auth)/items/page?id=${p.item.Id}`)}
@@ -109,7 +107,7 @@ export const ActiveDownloads: React.FC<Props> = ({ ...props }) => {
)}
</View>
<View className="flex flex-row items-center space-x-2 mt-1 text-purple-600">
<Text className="text-xs capitalize">{p.state}</Text>
<Text className="text-xs capitalize">{p.status}</Text>
</View>
</View>
<TouchableOpacity onPress={() => cancelJobMutation.mutate(p.id)}>

View File

@@ -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> = ({ ...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> = ({ ...props }) => {
key="1"
onSelect={() => {
updateSettings({ downloadMethod: "remux" });
queryClient.invalidateQueries({ queryKey: ["search"] });
setProcesses([]);
}}
>
<DropdownMenu.ItemTitle>Default</DropdownMenu.ItemTitle>
@@ -504,6 +506,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
key="2"
onSelect={() => {
updateSettings({ downloadMethod: "optimized" });
setProcesses([]);
queryClient.invalidateQueries({ queryKey: ["search"] });
}}
>

View File

@@ -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]
);

View File

@@ -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 };
};

View File

@@ -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<BaseItemDto>;
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<ReturnType<
function useDownloadProvider() {
const queryClient = useQueryClient();
const [processes, setProcesses] = useState<ProcessItem[]>([]);
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<JobStatus[]>([]);
// 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<BackgroundFetch.BackgroundFetchStatus | null>(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<BackgroundFetch.BackgroundFetchStatus | null>(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<ProcessItem>) => {
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<ProcessItem[]> => {
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;
};

View File

@@ -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;
};

100
utils/optimize-server.ts Normal file
View File

@@ -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<BaseItemDto>;
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<JobStatus[]>} 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<JobStatus[]> {
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<boolean> {
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;
}