mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
wip
This commit is contained in:
5
app.json
5
app.json
@@ -43,11 +43,6 @@
|
||||
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"
|
||||
]
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
"output": "static",
|
||||
"favicon": "./assets/images/favicon.png"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
"expo-font",
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useAtom } from "jotai";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
RefreshControl,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
@@ -345,7 +346,8 @@ export default function index() {
|
||||
contentContainerStyle={{
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
paddingBottom: insets.bottom,
|
||||
paddingBottom:
|
||||
Platform.OS === "android" ? insets.bottom + 65 : insets.bottom,
|
||||
}}
|
||||
className="flex flex-col space-y-4"
|
||||
>
|
||||
|
||||
@@ -105,7 +105,6 @@ export default function settings() {
|
||||
Haptics.notificationAsync(
|
||||
Haptics.NotificationFeedbackType.Success
|
||||
);
|
||||
toast.success("All files deleted");
|
||||
} catch (e) {
|
||||
Haptics.notificationAsync(
|
||||
Haptics.NotificationFeedbackType.Error
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { DownloadProvider } from "@/providers/DownloadProvider";
|
||||
import {
|
||||
getOrSetDeviceId,
|
||||
getServerUrlFromStorage,
|
||||
getTokenFromStoraage,
|
||||
JellyfinProvider,
|
||||
} from "@/providers/JellyfinProvider";
|
||||
@@ -9,36 +8,36 @@ import { JobQueueProvider } from "@/providers/JobQueueProvider";
|
||||
import { PlaybackProvider } from "@/providers/PlaybackProvider";
|
||||
import { orientationAtom } from "@/utils/atoms/orientation";
|
||||
import { Settings, useSettings } from "@/utils/atoms/settings";
|
||||
import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks";
|
||||
import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
|
||||
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
|
||||
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import {
|
||||
checkForExistingDownloads,
|
||||
completeHandler,
|
||||
download,
|
||||
} from "@kesha-antonov/react-native-background-downloader";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { DarkTheme, ThemeProvider } from "@react-navigation/native";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import * as BackgroundFetch from "expo-background-fetch";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import { useFonts } from "expo-font";
|
||||
import { useKeepAwake } from "expo-keep-awake";
|
||||
import * as Linking from "expo-linking";
|
||||
import * as Notifications from "expo-notifications";
|
||||
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";
|
||||
import * as TaskManager from "expo-task-manager";
|
||||
import { Provider as JotaiProvider, useAtom } from "jotai";
|
||||
import { useEffect, useRef } from "react";
|
||||
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();
|
||||
|
||||
@@ -145,16 +144,6 @@ TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
|
||||
})
|
||||
.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);
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { useDownload } from "@/providers/DownloadProvider";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
|
||||
import {
|
||||
DefaultLanguageOption,
|
||||
DownloadOptions,
|
||||
ScreenOrientationEnum,
|
||||
useSettings,
|
||||
} from "@/utils/atoms/settings";
|
||||
BACKGROUND_FETCH_TASK,
|
||||
registerBackgroundFetchAsync,
|
||||
unregisterBackgroundFetchAsync,
|
||||
} from "@/utils/background-tasks";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import * as BackgroundFetch from "expo-background-fetch";
|
||||
import * as ScreenOrientation from "expo-screen-orientation";
|
||||
import * as TaskManager from "expo-task-manager";
|
||||
import { useAtom } from "jotai";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Linking,
|
||||
@@ -16,23 +21,13 @@ import {
|
||||
View,
|
||||
ViewProps,
|
||||
} from "react-native";
|
||||
import { toast } from "sonner-native";
|
||||
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||
import { Button } from "../Button";
|
||||
import { Input } from "../common/Input";
|
||||
import { Text } from "../common/Text";
|
||||
import { Loader } from "../Loader";
|
||||
import { Input } from "../common/Input";
|
||||
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 {}
|
||||
|
||||
@@ -52,40 +47,24 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
||||
/********************
|
||||
* 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);
|
||||
await BackgroundFetch.getStatusAsync();
|
||||
await TaskManager.isTaskRegisteredAsync(BACKGROUND_FETCH_TASK);
|
||||
};
|
||||
|
||||
const toggleFetchTask = async () => {
|
||||
if (isRegistered) {
|
||||
console.log("Unregistering task");
|
||||
await unregisterBackgroundFetchAsync();
|
||||
updateSettings({
|
||||
autoDownload: false,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (settings?.autoDownload) {
|
||||
registerBackgroundFetchAsync();
|
||||
} else {
|
||||
console.log("Registering task");
|
||||
await registerBackgroundFetchAsync();
|
||||
updateSettings({
|
||||
autoDownload: true,
|
||||
});
|
||||
unregisterBackgroundFetchAsync();
|
||||
}
|
||||
|
||||
checkStatusAsync();
|
||||
};
|
||||
}, [settings?.autoDownload]);
|
||||
/**********************
|
||||
*********************/
|
||||
|
||||
@@ -571,14 +550,10 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
||||
finished optimizing on the server.
|
||||
</Text>
|
||||
</View>
|
||||
{isRegistered === null ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : (
|
||||
<Switch
|
||||
value={isRegistered}
|
||||
onValueChange={(value) => toggleFetchTask()}
|
||||
/>
|
||||
)}
|
||||
<Switch
|
||||
value={settings.autoDownload}
|
||||
onValueChange={(value) => updateSettings({ autoDownload: value })}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
pointerEvents={
|
||||
@@ -612,6 +587,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
||||
color="purple"
|
||||
className="h-12 mt-2"
|
||||
onPress={() => {
|
||||
toast.success("Saved");
|
||||
updateSettings({
|
||||
optimizedVersionsServerUrl:
|
||||
optimizedVersionsServerUrl.length === 0
|
||||
|
||||
3
eas.json
3
eas.json
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 9.1.0"
|
||||
"version": ">= 9.1.0",
|
||||
"appVersionSource": "local"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
|
||||
@@ -37,6 +37,7 @@ import React, {
|
||||
import { AppState, AppStateStatus } from "react-native";
|
||||
import { toast } from "sonner-native";
|
||||
import { apiAtom } from "./JellyfinProvider";
|
||||
import * as Notifications from "expo-notifications";
|
||||
|
||||
function onAppStateChange(status: AppStateStatus) {
|
||||
focusManager.setFocused(status === "active");
|
||||
@@ -93,7 +94,20 @@ function useDownloadProvider() {
|
||||
url,
|
||||
});
|
||||
|
||||
jobs.forEach((job) => {
|
||||
// Local downloading processes that are still valid
|
||||
const downloadingProcesses = processes
|
||||
.filter((p) => p.status === "downloading")
|
||||
.filter((p) => jobs.some((j) => j.id === p.id));
|
||||
|
||||
const updatedProcesses = jobs.filter(
|
||||
(j) => !downloadingProcesses.some((p) => p.id === j.id)
|
||||
);
|
||||
|
||||
setProcesses([...updatedProcesses, ...downloadingProcesses]);
|
||||
|
||||
// Go though new jobs and compare them to old jobs
|
||||
// if new job is now completed, start download.
|
||||
for (let job of jobs) {
|
||||
const process = processes.find((p) => p.id === job.id);
|
||||
if (
|
||||
process &&
|
||||
@@ -112,20 +126,19 @@ function useDownloadProvider() {
|
||||
},
|
||||
},
|
||||
});
|
||||
Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: job.item.Name,
|
||||
body: `${job.item.Name} is ready to be downloaded`,
|
||||
data: {
|
||||
url: `/downloads`,
|
||||
},
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Local downloading processes that are still valid
|
||||
const downloadingProcesses = processes
|
||||
.filter((p) => p.status === "downloading")
|
||||
.filter((p) => jobs.some((j) => j.id === p.id));
|
||||
|
||||
const updatedProcesses = jobs.filter(
|
||||
(j) => !downloadingProcesses.some((p) => p.id === j.id)
|
||||
);
|
||||
|
||||
setProcesses([...updatedProcesses, ...downloadingProcesses]);
|
||||
}
|
||||
|
||||
return jobs;
|
||||
},
|
||||
@@ -137,19 +150,7 @@ function useDownloadProvider() {
|
||||
useEffect(() => {
|
||||
const checkIfShouldStartDownload = async () => {
|
||||
if (processes.length === 0) return;
|
||||
const tasks = await checkForExistingDownloads();
|
||||
// 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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
await checkForExistingDownloads();
|
||||
};
|
||||
|
||||
checkIfShouldStartDownload();
|
||||
@@ -178,6 +179,7 @@ function useDownloadProvider() {
|
||||
async (process: JobStatus) => {
|
||||
if (!process?.item.Id || !authHeader) throw new Error("No item id");
|
||||
|
||||
console.log("[0] Setting process to downloading");
|
||||
setProcesses((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === process.id
|
||||
@@ -352,38 +354,71 @@ function useDownloadProvider() {
|
||||
|
||||
const deleteAllFiles = async (): Promise<void> => {
|
||||
try {
|
||||
const baseDirectory = FileSystem.documentDirectory;
|
||||
await deleteLocalFiles();
|
||||
await removeDownloadedItemsFromStorage();
|
||||
await cancelAllServerJobs();
|
||||
queryClient.invalidateQueries({ queryKey: ["downloadedItems"] });
|
||||
toast.success("All files, folders, and jobs deleted successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to delete all files, folders, and jobs:", error);
|
||||
toast.error("An error occurred while deleting files and jobs");
|
||||
}
|
||||
};
|
||||
|
||||
if (!baseDirectory) {
|
||||
throw new Error("Base directory not found");
|
||||
}
|
||||
const deleteLocalFiles = async (): Promise<void> => {
|
||||
const baseDirectory = FileSystem.documentDirectory;
|
||||
if (!baseDirectory) {
|
||||
throw new Error("Base directory not found");
|
||||
}
|
||||
|
||||
const dirContents = await FileSystem.readDirectoryAsync(baseDirectory);
|
||||
|
||||
for (const item of dirContents) {
|
||||
const itemPath = `${baseDirectory}${item}`;
|
||||
const itemInfo = await FileSystem.getInfoAsync(itemPath);
|
||||
|
||||
if (itemInfo.exists) {
|
||||
const dirContents = await FileSystem.readDirectoryAsync(baseDirectory);
|
||||
for (const item of dirContents) {
|
||||
const itemPath = `${baseDirectory}${item}`;
|
||||
const itemInfo = await FileSystem.getInfoAsync(itemPath);
|
||||
if (itemInfo.exists) {
|
||||
if (itemInfo.isDirectory) {
|
||||
await FileSystem.deleteAsync(itemPath, { idempotent: true });
|
||||
} else {
|
||||
await FileSystem.deleteAsync(itemPath, { idempotent: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeDownloadedItemsFromStorage = async (): Promise<void> => {
|
||||
try {
|
||||
await AsyncStorage.removeItem("downloadedItems");
|
||||
|
||||
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"] });
|
||||
|
||||
toast.success("All files and folders deleted successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to delete all files and folders:", error);
|
||||
console.error(
|
||||
"Failed to remove downloadedItems from AsyncStorage:",
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const cancelAllServerJobs = async (): Promise<void> => {
|
||||
if (!authHeader) {
|
||||
throw new Error("No auth header available");
|
||||
}
|
||||
if (!settings?.optimizedVersionsServerUrl) {
|
||||
throw new Error("No server URL configured");
|
||||
}
|
||||
|
||||
const deviceId = await getOrSetDeviceId();
|
||||
if (!deviceId) {
|
||||
throw new Error("Failed to get device ID");
|
||||
}
|
||||
|
||||
try {
|
||||
await cancelAllJobs({
|
||||
authHeader,
|
||||
url: settings.optimizedVersionsServerUrl,
|
||||
deviceId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to cancel all server jobs:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user