mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-31 19:18:26 +01:00
feat: move to custom download handler with background download support (#675)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
import useImageStorage from "@/hooks/useImageStorage";
|
||||
import { useInterval } from "@/hooks/useInterval";
|
||||
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
|
||||
import { getOrSetDeviceId } from "@/utils/device";
|
||||
import useDownloadHelper from "@/utils/download";
|
||||
@@ -18,6 +19,7 @@ import type {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
|
||||
import BackGroundDownloader from "@kesha-antonov/react-native-background-downloader";
|
||||
import { focusManager, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
@@ -38,6 +40,7 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AppState, type AppStateStatus, Platform } from "react-native";
|
||||
import { toast } from "sonner-native";
|
||||
import { Bitrate } from "../components/BitrateSelector";
|
||||
import { apiAtom } from "./JellyfinProvider";
|
||||
|
||||
export type DownloadedItem = {
|
||||
@@ -74,6 +77,17 @@ function useDownloadProvider() {
|
||||
return api?.accessToken;
|
||||
}, [api]);
|
||||
|
||||
const usingOptimizedServer = useMemo(
|
||||
() => settings?.downloadMethod === DownloadMethod.Optimized,
|
||||
[settings],
|
||||
);
|
||||
|
||||
const getDownloadUrl = (process: JobStatus) => {
|
||||
return usingOptimizedServer
|
||||
? `${settings.optimizedVersionsServerUrl}download/${process.id}`
|
||||
: process.inputUrl;
|
||||
};
|
||||
|
||||
const { data: downloadedFiles, refetch } = useQuery({
|
||||
queryKey: ["downloadedItems"],
|
||||
queryFn: getAllDownloadedItems,
|
||||
@@ -164,6 +178,59 @@ function useDownloadProvider() {
|
||||
enabled: settings?.downloadMethod === DownloadMethod.Optimized,
|
||||
});
|
||||
|
||||
/// Cant use the background downloader callback. As its not triggered if size is unknown.
|
||||
const updateProgress = async () => {
|
||||
if (settings?.downloadMethod === DownloadMethod.Optimized) {
|
||||
return;
|
||||
}
|
||||
// const response = await getSessionApi(api).getSessions({
|
||||
// activeWithinSeconds: 300,
|
||||
// });
|
||||
|
||||
const tasks = await BackGroundDownloader.checkForExistingDownloads();
|
||||
|
||||
const updatedProcesses = processes.map((p) => {
|
||||
// const result = response.data.find((s) => s.Id == p.sessionId);
|
||||
// if (result) {
|
||||
// return {
|
||||
// ...p,
|
||||
// progress: result.TranscodingInfo?.CompletionPercentage,
|
||||
// };
|
||||
// }
|
||||
|
||||
// fallback. Doesn't really work for transcodes as they may be a lot smaller. We make an wild guess
|
||||
const task = tasks.find((s) => s.id === p.id);
|
||||
if (task) {
|
||||
let progress = p.progress;
|
||||
let size = p.mediaSource.Size;
|
||||
const maxBitrate = p.maxBitrate.value;
|
||||
if (maxBitrate && maxBitrate < p.mediaSource.Bitrate) {
|
||||
size = (size / p.mediaSource.Bitrate) * maxBitrate;
|
||||
}
|
||||
// console.log(
|
||||
// p.mediaSource.Size,
|
||||
// size,
|
||||
// maxBitrate,
|
||||
// p.mediaSource.Bitrate,
|
||||
// );
|
||||
progress = (100 / size) * task.bytesDownloaded;
|
||||
if (progress >= 100) {
|
||||
progress = 99;
|
||||
}
|
||||
|
||||
return {
|
||||
...p,
|
||||
progress,
|
||||
};
|
||||
}
|
||||
return p;
|
||||
});
|
||||
|
||||
setProcesses(updatedProcesses);
|
||||
};
|
||||
|
||||
useInterval(updateProgress, 3000);
|
||||
|
||||
useEffect(() => {
|
||||
const checkIfShouldStartDownload = async () => {
|
||||
if (processes.length === 0) return;
|
||||
@@ -176,18 +243,25 @@ function useDownloadProvider() {
|
||||
const removeProcess = useCallback(
|
||||
async (id: string) => {
|
||||
const deviceId = await getOrSetDeviceId();
|
||||
if (!deviceId || !authHeader || !settings?.optimizedVersionsServerUrl)
|
||||
return;
|
||||
if (!deviceId || !authHeader) return;
|
||||
|
||||
try {
|
||||
await cancelJobById({
|
||||
authHeader,
|
||||
id,
|
||||
url: settings?.optimizedVersionsServerUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (usingOptimizedServer) {
|
||||
try {
|
||||
await cancelJobById({
|
||||
authHeader,
|
||||
id,
|
||||
url: settings?.optimizedVersionsServerUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
setProcesses((prev: any[]) => {
|
||||
return prev.filter(
|
||||
(process: { itemId: string | undefined }) => process.id !== id,
|
||||
);
|
||||
});
|
||||
},
|
||||
[settings?.optimizedVersionsServerUrl, authHeader],
|
||||
);
|
||||
@@ -238,7 +312,7 @@ function useDownloadProvider() {
|
||||
|
||||
BackGroundDownloader?.download({
|
||||
id: process.id,
|
||||
url: `${settings?.optimizedVersionsServerUrl}download/${process.id}`,
|
||||
url: getDownloadUrl(process),
|
||||
destination: `${baseDirectory}/${process.item.Id}.mp4`,
|
||||
})
|
||||
.begin(() => {
|
||||
@@ -256,6 +330,9 @@ function useDownloadProvider() {
|
||||
);
|
||||
})
|
||||
.progress((data) => {
|
||||
if (!usingOptimizedServer) {
|
||||
return;
|
||||
}
|
||||
const percent = (data.bytesDownloaded / data.bytesTotal) * 100;
|
||||
setProcesses((prev) =>
|
||||
prev.map((p) =>
|
||||
@@ -328,7 +405,12 @@ function useDownloadProvider() {
|
||||
);
|
||||
|
||||
const startBackgroundDownload = useCallback(
|
||||
async (url: string, item: BaseItemDto, mediaSource: MediaSourceInfo) => {
|
||||
async (
|
||||
url: string,
|
||||
item: BaseItemDto,
|
||||
mediaSource: MediaSourceInfo,
|
||||
maxBitrate?: Bitrate,
|
||||
) => {
|
||||
if (!api || !item.Id || !authHeader)
|
||||
throw new Error("startBackgroundDownload ~ Missing required params");
|
||||
|
||||
@@ -345,26 +427,42 @@ function useDownloadProvider() {
|
||||
width: 500,
|
||||
});
|
||||
await saveImage(item.Id, itemImage?.uri);
|
||||
|
||||
const response = await axios.post(
|
||||
`${settings?.optimizedVersionsServerUrl}optimize-version`,
|
||||
{
|
||||
url,
|
||||
fileExtension,
|
||||
deviceId,
|
||||
itemId: item.Id,
|
||||
item,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: authHeader,
|
||||
if (usingOptimizedServer) {
|
||||
const response = await axios.post(
|
||||
`${settings?.optimizedVersionsServerUrl}optimize-version`,
|
||||
{
|
||||
url,
|
||||
fileExtension,
|
||||
deviceId,
|
||||
itemId: item.Id,
|
||||
item,
|
||||
},
|
||||
},
|
||||
);
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: authHeader,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status !== 201) {
|
||||
throw new Error("Failed to start optimization job");
|
||||
if (response.status !== 201) {
|
||||
throw new Error("Failed to start optimization job");
|
||||
}
|
||||
} else {
|
||||
const job: JobStatus = {
|
||||
id: item.Id!,
|
||||
deviceId: deviceId,
|
||||
inputUrl: url,
|
||||
item: item,
|
||||
itemId: item.Id!,
|
||||
mediaSource,
|
||||
progress: 0,
|
||||
maxBitrate,
|
||||
status: "downloading",
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setProcesses([...processes, job]);
|
||||
startDownload(job);
|
||||
}
|
||||
|
||||
toast.success(
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import { storage } from "@/utils/mmkv";
|
||||
import type { JobStatus } from "@/utils/optimize-server";
|
||||
import type {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import * as Application from "expo-application";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import type React from "react";
|
||||
import { createContext, useCallback, useContext, useMemo } from "react";
|
||||
|
||||
export type DownloadedItem = {
|
||||
item: Partial<BaseItemDto>;
|
||||
mediaSource: MediaSourceInfo;
|
||||
};
|
||||
|
||||
export const processesAtom = atom<JobStatus[]>([]);
|
||||
|
||||
const DownloadContext = createContext<ReturnType<
|
||||
typeof useDownloadProvider
|
||||
> | null>(null);
|
||||
|
||||
/**
|
||||
* Dummy download provider for tvOS
|
||||
*/
|
||||
function useDownloadProvider() {
|
||||
const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
|
||||
|
||||
const downloadedFiles: DownloadedItem[] = [];
|
||||
|
||||
const removeProcess = useCallback(async (id: string) => {}, []);
|
||||
|
||||
const startDownload = useCallback(async (process: JobStatus) => {
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
const startBackgroundDownload = useCallback(
|
||||
async (url: string, item: BaseItemDto, mediaSource: MediaSourceInfo) => {
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const deleteAllFiles = async (): Promise<void> => {};
|
||||
|
||||
const deleteFile = async (id: string): Promise<void> => {};
|
||||
|
||||
const deleteItems = async (items: BaseItemDto[]) => {};
|
||||
|
||||
const cleanCacheDirectory = async () => {};
|
||||
|
||||
const deleteFileByType = async (type: BaseItemDto["Type"]) => {};
|
||||
|
||||
const appSizeUsage = useMemo(async () => {
|
||||
return 0;
|
||||
}, []);
|
||||
|
||||
function getDownloadedItem(itemId: string): DownloadedItem | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function saveDownloadedItemInfo(item: BaseItemDto, size = 0) {}
|
||||
|
||||
function getDownloadedItemSize(itemId: string): number {
|
||||
const size = storage.getString(`downloadedItemSize-${itemId}`);
|
||||
return size ? Number.parseInt(size) : 0;
|
||||
}
|
||||
|
||||
const APP_CACHE_DOWNLOAD_DIRECTORY = `${FileSystem.cacheDirectory}${Application.applicationId}/Downloads/`;
|
||||
|
||||
return {
|
||||
processes,
|
||||
startBackgroundDownload,
|
||||
downloadedFiles,
|
||||
deleteAllFiles,
|
||||
deleteFile,
|
||||
deleteItems,
|
||||
saveDownloadedItemInfo,
|
||||
removeProcess,
|
||||
setProcesses,
|
||||
startDownload,
|
||||
getDownloadedItem,
|
||||
deleteFileByType,
|
||||
appSizeUsage,
|
||||
getDownloadedItemSize,
|
||||
APP_CACHE_DOWNLOAD_DIRECTORY,
|
||||
cleanCacheDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
export function DownloadProvider({ children }: { children: React.ReactNode }) {
|
||||
const downloadProviderValue = useDownloadProvider();
|
||||
|
||||
return (
|
||||
<DownloadContext.Provider value={downloadProviderValue}>
|
||||
{children}
|
||||
</DownloadContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useDownload() {
|
||||
const context = useContext(DownloadContext);
|
||||
if (context === null) {
|
||||
throw new Error("useDownload must be used within a DownloadProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user