mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
wip
This commit is contained in:
@@ -2,10 +2,7 @@ import { Button } from "@/components/Button";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { ListItem } from "@/components/ListItem";
|
||||
import { SettingToggles } from "@/components/settings/SettingToggles";
|
||||
import {
|
||||
registerBackgroundFetchAsync,
|
||||
useDownload,
|
||||
} from "@/providers/DownloadProvider";
|
||||
import { useDownload } from "@/providers/DownloadProvider";
|
||||
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { clearLogs, readFromLog } from "@/utils/log";
|
||||
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
@@ -94,18 +91,6 @@ export default function settings() {
|
||||
|
||||
<SettingToggles />
|
||||
|
||||
<View>
|
||||
<Text className="font-bold text-lg mb-2">Tests</Text>
|
||||
<Button
|
||||
onPress={() => {
|
||||
toast.success("Download started");
|
||||
}}
|
||||
color="black"
|
||||
>
|
||||
Test toast
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text className="font-bold text-lg mb-2">Account and storage</Text>
|
||||
<View className="flex flex-col space-y-2">
|
||||
|
||||
233
app/_layout.tsx
233
app/_layout.tsx
@@ -1,18 +1,27 @@
|
||||
import { DownloadProvider } from "@/providers/DownloadProvider";
|
||||
import { JellyfinProvider } from "@/providers/JellyfinProvider";
|
||||
import {
|
||||
getOrSetDeviceId,
|
||||
getServerUrlFromStorage,
|
||||
getTokenFromStoraage,
|
||||
JellyfinProvider,
|
||||
} from "@/providers/JellyfinProvider";
|
||||
import { JobQueueProvider } from "@/providers/JobQueueProvider";
|
||||
import { PlaybackProvider } from "@/providers/PlaybackProvider";
|
||||
import { orientationAtom } from "@/utils/atoms/orientation";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { Settings, useSettings } from "@/utils/atoms/settings";
|
||||
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
|
||||
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
||||
import { checkForExistingDownloads } from "@kesha-antonov/react-native-background-downloader";
|
||||
import {
|
||||
checkForExistingDownloads,
|
||||
completeHandler,
|
||||
download,
|
||||
} from "@kesha-antonov/react-native-background-downloader";
|
||||
import { DarkTheme, ThemeProvider } from "@react-navigation/native";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useFonts } from "expo-font";
|
||||
import { useKeepAwake } from "expo-keep-awake";
|
||||
import * as Linking from "expo-linking";
|
||||
import { Stack } from "expo-router";
|
||||
import { router, Stack } from "expo-router";
|
||||
import * as ScreenOrientation from "expo-screen-orientation";
|
||||
import * as SplashScreen from "expo-splash-screen";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
@@ -22,9 +31,198 @@ import { AppState } from "react-native";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import "react-native-reanimated";
|
||||
import { Toaster } from "sonner-native";
|
||||
import * as TaskManager from "expo-task-manager";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import * as BackgroundFetch from "expo-background-fetch";
|
||||
import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import * as Notifications from "expo-notifications";
|
||||
import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks";
|
||||
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => ({
|
||||
shouldShowAlert: true,
|
||||
shouldPlaySound: true,
|
||||
shouldSetBadge: false,
|
||||
}),
|
||||
});
|
||||
|
||||
function useNotificationObserver() {
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
function redirect(notification: Notifications.Notification) {
|
||||
const url = notification.request.content.data?.url;
|
||||
if (url) {
|
||||
router.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
Notifications.getLastNotificationResponseAsync().then((response) => {
|
||||
if (!isMounted || !response?.notification) {
|
||||
return;
|
||||
}
|
||||
redirect(response?.notification);
|
||||
});
|
||||
|
||||
const subscription = Notifications.addNotificationResponseReceivedListener(
|
||||
(response) => {
|
||||
redirect(response.notification);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
subscription.remove();
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
|
||||
console.log("TaskManager ~ trigger");
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const settingsData = await AsyncStorage.getItem("settings");
|
||||
|
||||
if (!settingsData) return BackgroundFetch.BackgroundFetchResult.NoData;
|
||||
|
||||
const settings: Partial<Settings> = JSON.parse(settingsData);
|
||||
const url = settings?.optimizedVersionsServerUrl;
|
||||
|
||||
if (!settings?.autoDownload || !url)
|
||||
return BackgroundFetch.BackgroundFetchResult.NoData;
|
||||
|
||||
const token = await getTokenFromStoraage();
|
||||
const deviceId = await getOrSetDeviceId();
|
||||
const baseDirectory = FileSystem.documentDirectory;
|
||||
|
||||
if (!token || !deviceId || !baseDirectory)
|
||||
return BackgroundFetch.BackgroundFetchResult.NoData;
|
||||
|
||||
console.log({
|
||||
token,
|
||||
url,
|
||||
deviceId,
|
||||
});
|
||||
|
||||
const jobs = await getAllJobsByDeviceId({
|
||||
deviceId,
|
||||
authHeader: token,
|
||||
url,
|
||||
});
|
||||
|
||||
console.log("TaskManager ~ Active jobs: ", jobs.length);
|
||||
|
||||
for (let job of jobs) {
|
||||
if (job.status === "completed") {
|
||||
const downloadUrl = url + "download/" + job.id;
|
||||
console.log({
|
||||
token,
|
||||
deviceId,
|
||||
baseDirectory,
|
||||
url,
|
||||
downloadUrl,
|
||||
});
|
||||
|
||||
const tasks = await checkForExistingDownloads();
|
||||
|
||||
if (tasks.find((task) => task.id === job.id)) {
|
||||
console.log("TaskManager ~ Download already in progress: ", job.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
download({
|
||||
id: job.id,
|
||||
url: url + "download/" + job.id,
|
||||
destination: `${baseDirectory}${job.item.Id}.mp4`,
|
||||
headers: {
|
||||
Authorization: token,
|
||||
},
|
||||
})
|
||||
.begin(() => {
|
||||
console.log("TaskManager ~ Download started: ", job.id);
|
||||
Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: job.item.Name,
|
||||
body: "Download started",
|
||||
data: {
|
||||
url: `/downloads`,
|
||||
},
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
})
|
||||
.done(() => {
|
||||
console.log("TaskManager ~ Download completed: ", job.id);
|
||||
saveDownloadedItemInfo(job.item);
|
||||
completeHandler(job.id);
|
||||
cancelJobById({
|
||||
authHeader: token,
|
||||
id: job.id,
|
||||
url: url,
|
||||
});
|
||||
Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: job.item.Name,
|
||||
body: "Download completed",
|
||||
data: {
|
||||
url: `/downloads`,
|
||||
},
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
})
|
||||
.error((error) => {
|
||||
console.log("TaskManager ~ Download error: ", job.id, error);
|
||||
completeHandler(job.id);
|
||||
Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: job.item.Name,
|
||||
body: "Download failed",
|
||||
data: {
|
||||
url: `/downloads`,
|
||||
},
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Auto download started: ${new Date(now).toISOString()}`);
|
||||
|
||||
// Be sure to return the successful result type!
|
||||
return BackgroundFetch.BackgroundFetchResult.NewData;
|
||||
});
|
||||
|
||||
const checkAndRequestPermissions = async () => {
|
||||
try {
|
||||
const hasAskedBefore = await AsyncStorage.getItem(
|
||||
"hasAskedForNotificationPermission"
|
||||
);
|
||||
|
||||
if (hasAskedBefore !== "true") {
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
|
||||
if (status === "granted") {
|
||||
console.log("Notification permissions granted.");
|
||||
} else {
|
||||
console.log("Notification permissions denied.");
|
||||
}
|
||||
|
||||
await AsyncStorage.setItem("hasAskedForNotificationPermission", "true");
|
||||
} else {
|
||||
console.log("Already asked for notification permissions before.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking/requesting notification permissions:", error);
|
||||
}
|
||||
};
|
||||
|
||||
export default function RootLayout() {
|
||||
const [loaded] = useFonts({
|
||||
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
|
||||
@@ -52,6 +250,7 @@ function Layout() {
|
||||
const [orientation, setOrientation] = useAtom(orientationAtom);
|
||||
|
||||
useKeepAwake();
|
||||
useNotificationObserver();
|
||||
|
||||
const queryClientRef = useRef<QueryClient>(
|
||||
new QueryClient({
|
||||
@@ -67,6 +266,10 @@ function Layout() {
|
||||
})
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
checkAndRequestPermissions();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (settings?.autoRotate === true)
|
||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.DEFAULT);
|
||||
@@ -164,7 +367,7 @@ function Layout() {
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
<Toaster
|
||||
duration={2000}
|
||||
duration={4000}
|
||||
toastOptions={{
|
||||
style: {
|
||||
backgroundColor: "#262626",
|
||||
@@ -188,3 +391,23 @@ function Layout() {
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
|
||||
async function saveDownloadedItemInfo(item: BaseItemDto) {
|
||||
try {
|
||||
const downloadedItems = await AsyncStorage.getItem("downloadedItems");
|
||||
let items: BaseItemDto[] = downloadedItems
|
||||
? JSON.parse(downloadedItems)
|
||||
: [];
|
||||
|
||||
const existingItemIndex = items.findIndex((i) => i.Id === item.Id);
|
||||
if (existingItemIndex !== -1) {
|
||||
items[existingItemIndex] = item;
|
||||
} else {
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
await AsyncStorage.setItem("downloadedItems", JSON.stringify(items));
|
||||
} catch (error) {
|
||||
console.error("Failed to save downloaded item information:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,8 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
||||
className="relative bg-neutral-900 border border-neutral-800 rounded-2xl overflow-hidden"
|
||||
{...props}
|
||||
>
|
||||
{process.status === "optimizing" && (
|
||||
{(process.status === "optimizing" ||
|
||||
process.status === "downloading") && (
|
||||
<View
|
||||
className={`
|
||||
bg-purple-600 h-1 absolute bottom-0 left-0
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAtom } from "jotai";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Linking,
|
||||
Switch,
|
||||
TouchableOpacity,
|
||||
@@ -19,12 +20,19 @@ import * as DropdownMenu from "zeego/dropdown-menu";
|
||||
import { Text } from "../common/Text";
|
||||
import { Loader } from "../Loader";
|
||||
import { Input } from "../common/Input";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
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";
|
||||
import * as BackgroundFetch from "expo-background-fetch";
|
||||
import * as TaskManager from "expo-task-manager";
|
||||
import {
|
||||
BACKGROUND_FETCH_TASK,
|
||||
registerBackgroundFetchAsync,
|
||||
unregisterBackgroundFetchAsync,
|
||||
} from "@/utils/background-tasks";
|
||||
|
||||
interface Props extends ViewProps {}
|
||||
|
||||
@@ -37,10 +45,50 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
||||
|
||||
const [marlinUrl, setMarlinUrl] = useState<string>("");
|
||||
const [optimizedVersionsServerUrl, setOptimizedVersionsServerUrl] =
|
||||
useState<string>("");
|
||||
useState<string>(settings?.optimizedVersionsServerUrl || "");
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
/********************
|
||||
* Background task
|
||||
*******************/
|
||||
const [isRegistered, setIsRegistered] = useState<boolean | null>(null);
|
||||
const [status, setStatus] =
|
||||
useState<BackgroundFetch.BackgroundFetchStatus | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
checkStatusAsync();
|
||||
}, []);
|
||||
|
||||
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 task");
|
||||
await unregisterBackgroundFetchAsync();
|
||||
updateSettings({
|
||||
autoDownload: false,
|
||||
});
|
||||
} else {
|
||||
console.log("Registering task");
|
||||
await registerBackgroundFetchAsync();
|
||||
updateSettings({
|
||||
autoDownload: true,
|
||||
});
|
||||
}
|
||||
|
||||
checkStatusAsync();
|
||||
};
|
||||
/**********************
|
||||
*********************/
|
||||
|
||||
const {
|
||||
data: mediaListCollections,
|
||||
isLoading: isLoadingMediaListCollections,
|
||||
@@ -515,6 +563,23 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</View>
|
||||
<View className="flex flex-row space-x-2 items-center justify-between bg-neutral-900 p-4">
|
||||
<View className="flex flex-col shrink">
|
||||
<Text className="font-semibold">Auto download</Text>
|
||||
<Text className="text-xs opacity-50 shrink">
|
||||
This will automatically download the media file when it's
|
||||
finished optimizing on the server.
|
||||
</Text>
|
||||
</View>
|
||||
{isRegistered === null ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : (
|
||||
<Switch
|
||||
value={isRegistered}
|
||||
onValueChange={(value) => toggleFetchTask()}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View
|
||||
pointerEvents={
|
||||
settings.downloadMethod === "optimized" ? "auto" : "none"
|
||||
@@ -536,11 +601,6 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
||||
<View className="flex flex-col">
|
||||
<Input
|
||||
placeholder="Optimized versions server URL..."
|
||||
defaultValue={
|
||||
settings.optimizedVersionsServerUrl
|
||||
? settings.optimizedVersionsServerUrl
|
||||
: ""
|
||||
}
|
||||
value={optimizedVersionsServerUrl}
|
||||
keyboardType="url"
|
||||
returnKeyType="done"
|
||||
@@ -565,12 +625,6 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
||||
Save
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{settings.optimizedVersionsServerUrl && (
|
||||
<View className="p-4 bg-neutral-800 rounded-xl mt-2">
|
||||
<Text selectable>{settings.optimizedVersionsServerUrl}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -62,7 +62,7 @@ export const useRemuxHlsToMp4 = (item: BaseItemDto) => {
|
||||
itemId: item.Id,
|
||||
outputPath: "",
|
||||
progress: 0,
|
||||
status: "running",
|
||||
status: "downloading",
|
||||
timestamp: new Date(),
|
||||
} as JobStatus,
|
||||
]);
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"expo-linking": "~6.3.1",
|
||||
"expo-navigation-bar": "~3.0.7",
|
||||
"expo-network": "~6.0.1",
|
||||
"expo-notifications": "~0.28.17",
|
||||
"expo-router": "~3.5.23",
|
||||
"expo-screen-orientation": "~7.0.5",
|
||||
"expo-sensors": "~13.0.9",
|
||||
|
||||
@@ -11,22 +11,20 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import {
|
||||
checkForExistingDownloads,
|
||||
completeHandler,
|
||||
directories,
|
||||
download,
|
||||
setConfig,
|
||||
} from "@kesha-antonov/react-native-background-downloader";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import {
|
||||
focusManager,
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import * as BackgroundFetch from "expo-background-fetch";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import { useRouter } from "expo-router";
|
||||
import * as TaskManager from "expo-task-manager";
|
||||
import { useAtom } from "jotai";
|
||||
import React, {
|
||||
createContext,
|
||||
@@ -34,37 +32,14 @@ import React, {
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { AppState, AppStateStatus } from "react-native";
|
||||
import { toast } from "sonner-native";
|
||||
import { apiAtom } from "./JellyfinProvider";
|
||||
|
||||
export const BACKGROUND_FETCH_TASK = "background-fetch";
|
||||
|
||||
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
|
||||
const now = Date.now();
|
||||
|
||||
console.log(
|
||||
`Got background fetch call at date: ${new Date(now).toISOString()}`
|
||||
);
|
||||
|
||||
// Be sure to return the successful result type!
|
||||
return BackgroundFetch.BackgroundFetchResult.NewData;
|
||||
});
|
||||
|
||||
const STORAGE_KEY = "runningProcesses";
|
||||
|
||||
export async function registerBackgroundFetchAsync() {
|
||||
return BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, {
|
||||
minimumInterval: 60 * 15, // 1 minutes
|
||||
stopOnTerminate: false, // android only,
|
||||
startOnBoot: true, // android only
|
||||
});
|
||||
}
|
||||
|
||||
export async function unregisterBackgroundFetchAsync() {
|
||||
return BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_TASK);
|
||||
function onAppStateChange(status: AppStateStatus) {
|
||||
focusManager.setFocused(status === "active");
|
||||
}
|
||||
|
||||
const DownloadContext = createContext<ReturnType<
|
||||
@@ -87,8 +62,17 @@ function useDownloadProvider() {
|
||||
queryKey: ["downloadedItems"],
|
||||
queryFn: getAllDownloadedItems,
|
||||
staleTime: 0,
|
||||
refetchOnMount: true,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnWindowFocus: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = AppState.addEventListener("change", onAppStateChange);
|
||||
|
||||
return () => subscription.remove();
|
||||
}, []);
|
||||
|
||||
useQuery({
|
||||
queryKey: ["jobs"],
|
||||
queryFn: async () => {
|
||||
@@ -109,6 +93,29 @@ function useDownloadProvider() {
|
||||
url,
|
||||
});
|
||||
|
||||
jobs.forEach((job) => {
|
||||
const process = processes.find((p) => p.id === job.id);
|
||||
if (
|
||||
process &&
|
||||
process.status === "optimizing" &&
|
||||
job.status === "completed"
|
||||
) {
|
||||
if (settings.autoDownload) {
|
||||
startDownload(job);
|
||||
} else {
|
||||
toast.info(`${job.item.Name} is ready to be downloaded`, {
|
||||
action: {
|
||||
label: "Go to downloads",
|
||||
onClick: () => {
|
||||
router.push("/downloads");
|
||||
toast.dismiss();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Local downloading processes that are still valid
|
||||
const downloadingProcesses = processes
|
||||
.filter((p) => p.status === "downloading")
|
||||
@@ -123,66 +130,30 @@ function useDownloadProvider() {
|
||||
return jobs;
|
||||
},
|
||||
staleTime: 0,
|
||||
refetchInterval: 1000,
|
||||
refetchInterval: 2000,
|
||||
enabled: settings?.downloadMethod === "optimized",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const checkIfShouldStartDownload = async () => {
|
||||
if (processes.length === 0) return;
|
||||
const tasks = await checkForExistingDownloads();
|
||||
// for (let i = 0; i < processes.length; i++) {
|
||||
// const job = processes[i];
|
||||
// if (settings?.autoDownload) {
|
||||
// for (let i = 0; i < processes.length; i++) {
|
||||
// const job = processes[i];
|
||||
|
||||
// if (job.status === "completed") {
|
||||
// // Check if the download is already in progress
|
||||
// if (tasks.find((task) => task.id === job.id)) continue;
|
||||
// await startDownload(job);
|
||||
// continue;
|
||||
// if (job.status === "completed") {
|
||||
// // Check if the download is already in progress
|
||||
// if (tasks.find((task) => task.id === job.id)) continue;
|
||||
// await startDownload(job);
|
||||
// continue;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
};
|
||||
|
||||
checkIfShouldStartDownload();
|
||||
}, []);
|
||||
|
||||
/********************
|
||||
* Background task
|
||||
*******************/
|
||||
// useEffect(() => {
|
||||
// // Check background task status
|
||||
// checkStatusAsync();
|
||||
// }, []);
|
||||
|
||||
// const [isRegistered, setIsRegistered] = useState(false);
|
||||
// const [status, setStatus] =
|
||||
// useState<BackgroundFetch.BackgroundFetchStatus | null>(null);
|
||||
|
||||
// const checkStatusAsync = async () => {
|
||||
// const status = await BackgroundFetch.getStatusAsync();
|
||||
// const isRegistered = await TaskManager.isTaskRegisteredAsync(
|
||||
// BACKGROUND_FETCH_TASK
|
||||
// );
|
||||
// setStatus(status);
|
||||
// setIsRegistered(isRegistered);
|
||||
|
||||
// console.log("Background fetch status:", status);
|
||||
// console.log("Background fetch task registered:", isRegistered);
|
||||
// };
|
||||
|
||||
// const toggleFetchTask = async () => {
|
||||
// if (isRegistered) {
|
||||
// console.log("Unregistering background fetch task");
|
||||
// await unregisterBackgroundFetchAsync();
|
||||
// } else {
|
||||
// console.log("Registering background fetch task");
|
||||
// await registerBackgroundFetchAsync();
|
||||
// }
|
||||
|
||||
// checkStatusAsync();
|
||||
// };
|
||||
/**********************
|
||||
**********************
|
||||
*********************/
|
||||
}, [settings, processes]);
|
||||
|
||||
const removeProcess = useCallback(
|
||||
async (id: string) => {
|
||||
@@ -228,6 +199,16 @@ function useDownloadProvider() {
|
||||
},
|
||||
});
|
||||
|
||||
toast.info(`Download started for ${process.item.Name}`, {
|
||||
action: {
|
||||
label: "Go to downloads",
|
||||
onClick: () => {
|
||||
router.push("/downloads");
|
||||
toast.dismiss();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const baseDirectory = FileSystem.documentDirectory;
|
||||
|
||||
download({
|
||||
@@ -236,7 +217,6 @@ function useDownloadProvider() {
|
||||
destination: `${baseDirectory}/${process.item.Id}.mp4`,
|
||||
})
|
||||
.begin(() => {
|
||||
toast.info(`Download started for ${process.item.Name}`);
|
||||
setProcesses((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === process.id
|
||||
@@ -268,7 +248,16 @@ function useDownloadProvider() {
|
||||
})
|
||||
.done(async () => {
|
||||
await saveDownloadedItemInfo(process.item);
|
||||
toast.success(`Download completed for ${process.item.Name}`);
|
||||
toast.success(`Download completed for ${process.item.Name}`, {
|
||||
duration: 3000,
|
||||
action: {
|
||||
label: "Go to downloads",
|
||||
onClick: () => {
|
||||
router.push("/downloads");
|
||||
toast.dismiss();
|
||||
},
|
||||
},
|
||||
});
|
||||
setTimeout(() => {
|
||||
completeHandler(process.id);
|
||||
removeProcess(process.id);
|
||||
|
||||
@@ -40,17 +40,6 @@ const JellyfinContext = createContext<JellyfinContextValue | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const getOrSetDeviceId = async () => {
|
||||
let deviceId = await AsyncStorage.getItem("deviceId");
|
||||
|
||||
if (!deviceId) {
|
||||
deviceId = uuid.v4() as string;
|
||||
await AsyncStorage.setItem("deviceId", deviceId);
|
||||
}
|
||||
|
||||
return deviceId;
|
||||
};
|
||||
|
||||
export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
@@ -269,10 +258,10 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const token = await AsyncStorage.getItem("token");
|
||||
const serverUrl = await AsyncStorage.getItem("serverUrl");
|
||||
const token = await getTokenFromStoraage();
|
||||
const serverUrl = await getServerUrlFromStorage();
|
||||
const user = JSON.parse(
|
||||
(await AsyncStorage.getItem("user")) as string
|
||||
(await getUserFromStorage()) as string
|
||||
) as UserDto;
|
||||
|
||||
if (serverUrl && token && user.Id && jellyfin) {
|
||||
@@ -331,3 +320,26 @@ function useProtectedRoute(user: UserDto | null, loading = false) {
|
||||
}
|
||||
}, [user, segments, loading]);
|
||||
}
|
||||
|
||||
export async function getTokenFromStoraage() {
|
||||
return await AsyncStorage.getItem("token");
|
||||
}
|
||||
|
||||
export async function getUserFromStorage() {
|
||||
return await AsyncStorage.getItem("user");
|
||||
}
|
||||
|
||||
export async function getServerUrlFromStorage() {
|
||||
return await AsyncStorage.getItem("serverUrl");
|
||||
}
|
||||
|
||||
export async function getOrSetDeviceId() {
|
||||
let deviceId = await AsyncStorage.getItem("deviceId");
|
||||
|
||||
if (!deviceId) {
|
||||
deviceId = uuid.v4() as string;
|
||||
await AsyncStorage.setItem("deviceId", deviceId);
|
||||
}
|
||||
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,8 @@ export type Settings = {
|
||||
forwardSkipTime: number;
|
||||
rewindSkipTime: number;
|
||||
optimizedVersionsServerUrl?: string | null;
|
||||
downloadMethod?: "optimized" | "remux";
|
||||
downloadMethod: "optimized" | "remux";
|
||||
autoDownload: boolean;
|
||||
};
|
||||
/**
|
||||
*
|
||||
@@ -110,6 +111,7 @@ const loadSettings = async (): Promise<Settings> => {
|
||||
rewindSkipTime: 10,
|
||||
optimizedVersionsServerUrl: null,
|
||||
downloadMethod: "remux",
|
||||
autoDownload: false,
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
23
utils/background-tasks.ts
Normal file
23
utils/background-tasks.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as BackgroundFetch from "expo-background-fetch";
|
||||
|
||||
export const BACKGROUND_FETCH_TASK = "background-fetch";
|
||||
|
||||
export async function registerBackgroundFetchAsync() {
|
||||
try {
|
||||
BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, {
|
||||
minimumInterval: 60 * 1, // 1 minutes
|
||||
stopOnTerminate: false, // android only,
|
||||
startOnBoot: false, // android only
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error registering background fetch task", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function unregisterBackgroundFetchAsync() {
|
||||
try {
|
||||
BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_TASK);
|
||||
} catch (error) {
|
||||
console.log("Error unregistering background fetch task", error);
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,11 @@ export async function getAllJobsByDeviceId({
|
||||
},
|
||||
});
|
||||
if (statusResponse.status !== 200) {
|
||||
console.error(
|
||||
statusResponse.status,
|
||||
statusResponse.data,
|
||||
statusResponse.statusText
|
||||
);
|
||||
throw new Error("Failed to fetch job status");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user