diff --git a/app/_layout.tsx b/app/_layout.tsx index ad84ba0c..e3bc69d2 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -2,30 +2,26 @@ import "@/augmentations"; import { Platform } from "react-native"; import i18n from "@/i18n"; import { DownloadProvider } from "@/providers/DownloadProvider"; -import { - getOrSetDeviceId, - getTokenFromStorage, - JellyfinProvider, -} from "@/providers/JellyfinProvider"; +import { getOrSetDeviceId, getTokenFromStorage, JellyfinProvider, apiAtom } from "@/providers/JellyfinProvider"; import { JobQueueProvider } from "@/providers/JobQueueProvider"; import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider"; import { WebSocketProvider } from "@/providers/WebSocketProvider"; import { Settings, useSettings } from "@/utils/atoms/settings"; -import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks"; +import { + BACKGROUND_FETCH_TASK, + BACKGROUND_FETCH_TASK_SESSIONS, + registerBackgroundFetchAsyncSessions, +} from "@/utils/background-tasks"; import { LogProvider, writeToLog } from "@/utils/log"; import { storage } from "@/utils/mmkv"; 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"; -const BackGroundDownloader = !Platform.isTV - ? require("@kesha-antonov/react-native-background-downloader") - : null; +const BackGroundDownloader = !Platform.isTV ? require("@kesha-antonov/react-native-background-downloader") : null; import { DarkTheme, ThemeProvider } from "@react-navigation/native"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -const BackgroundFetch = !Platform.isTV - ? require("expo-background-fetch") - : null; +const BackgroundFetch = !Platform.isTV ? require("expo-background-fetch") : null; import * as FileSystem from "expo-file-system"; const Notifications = !Platform.isTV ? require("expo-notifications") : null; import { router, Stack } from "expo-router"; @@ -41,6 +37,10 @@ import { SystemBars } from "react-native-edge-to-edge"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import "react-native-reanimated"; import { Toaster } from "sonner-native"; +import { useAtom } from "jotai"; +import { userAtom } from "@/providers/JellyfinProvider"; +import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; +import { store } from "@/utils/store"; if (!Platform.isTV) { Notifications.setNotificationHandler({ @@ -74,20 +74,16 @@ function useNotificationObserver() { } } - Notifications.getLastNotificationResponseAsync().then( - (response: { notification: any }) => { - if (!isMounted || !response?.notification) { - return; - } - redirect(response?.notification); + Notifications.getLastNotificationResponseAsync().then((response: { notification: any }) => { + if (!isMounted || !response?.notification) { + return; } - ); + redirect(response?.notification); + }); - const subscription = Notifications.addNotificationResponseReceivedListener( - (response: { notification: any }) => { - redirect(response.notification); - } - ); + const subscription = Notifications.addNotificationResponseReceivedListener((response: { notification: any }) => { + redirect(response.notification); + }); return () => { isMounted = false; @@ -97,6 +93,22 @@ function useNotificationObserver() { } if (!Platform.isTV) { + TaskManager.defineTask(BACKGROUND_FETCH_TASK_SESSIONS, async () => { + console.log("TaskManager ~ sessions trigger"); + + const api = store.get(apiAtom); + if (api === null || api === undefined) return; + + const response = await getSessionApi(api).getSessions({ + activeWithinSeconds: 360, + }); + + const result = response.data.filter((s) => s.NowPlayingItem); + Notifications.setBadgeCountAsync(result.length); + + return BackgroundFetch.BackgroundFetchResult.NewData; + }); + TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => { console.log("TaskManager ~ trigger"); @@ -109,15 +121,13 @@ if (!Platform.isTV) { const settings: Partial = JSON.parse(settingsData); const url = settings?.optimizedVersionsServerUrl; - if (!settings?.autoDownload || !url) - return BackgroundFetch.BackgroundFetchResult.NoData; + if (!settings?.autoDownload || !url) return BackgroundFetch.BackgroundFetchResult.NoData; const token = getTokenFromStorage(); const deviceId = getOrSetDeviceId(); const baseDirectory = FileSystem.documentDirectory; - if (!token || !deviceId || !baseDirectory) - return BackgroundFetch.BackgroundFetchResult.NoData; + if (!token || !deviceId || !baseDirectory) return BackgroundFetch.BackgroundFetchResult.NoData; const jobs = await getAllJobsByDeviceId({ deviceId, @@ -194,9 +204,7 @@ if (!Platform.isTV) { const checkAndRequestPermissions = async () => { try { - const hasAskedBefore = storage.getString( - "hasAskedForNotificationPermission" - ); + const hasAskedBefore = storage.getString("hasAskedForNotificationPermission"); if (hasAskedBefore !== "true") { const { status } = await Notifications.requestPermissionsAsync(); @@ -214,11 +222,7 @@ const checkAndRequestPermissions = async () => { console.log("Already asked for notification permissions before."); } } catch (error) { - writeToLog( - "ERROR", - "Error checking/requesting notification permissions:", - error - ); + writeToLog("ERROR", "Error checking/requesting notification permissions:", error); console.error("Error checking/requesting notification permissions:", error); } }; @@ -253,12 +257,11 @@ const queryClient = new QueryClient({ function Layout() { const [settings] = useSettings(); + const [user] = useAtom(userAtom); const appState = useRef(AppState.currentState); useEffect(() => { - i18n.changeLanguage( - settings?.preferedLanguage ?? getLocales()[0].languageCode ?? "en" - ); + i18n.changeLanguage(settings?.preferedLanguage ?? getLocales()[0].languageCode ?? "en"); }, [settings?.preferedLanguage, i18n]); if (!Platform.isTV) { @@ -266,6 +269,11 @@ function Layout() { useEffect(() => { checkAndRequestPermissions(); + (async () => { + if (!Platform.isTV && user && user.Policy?.IsAdministrator) { + registerBackgroundFetchAsyncSessions(); + } + })(); }, []); useEffect(() => { @@ -275,24 +283,16 @@ function Layout() { ScreenOrientation.unlockAsync(); } else { // If the user has auto rotate disabled, lock the orientation to portrait - ScreenOrientation.lockAsync( - ScreenOrientation.OrientationLock.PORTRAIT_UP - ); + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); } }, [settings]); useEffect(() => { - const subscription = AppState.addEventListener( - "change", - (nextAppState) => { - if ( - appState.current.match(/inactive|background/) && - nextAppState === "active" - ) { - BackGroundDownloader.checkForExistingDownloads(); - } + const subscription = AppState.addEventListener("change", (nextAppState) => { + if (appState.current.match(/inactive|background/) && nextAppState === "active") { + BackGroundDownloader.checkForExistingDownloads(); } - ); + }); BackGroundDownloader.checkForExistingDownloads(); @@ -369,9 +369,7 @@ function Layout() { function saveDownloadedItemInfo(item: BaseItemDto) { try { const downloadedItems = storage.getString("downloadedItems"); - let items: BaseItemDto[] = downloadedItems - ? JSON.parse(downloadedItems) - : []; + let items: BaseItemDto[] = downloadedItems ? JSON.parse(downloadedItems) : []; const existingItemIndex = items.findIndex((i) => i.Id === item.Id); if (existingItemIndex !== -1) { diff --git a/hooks/useSessions.ts b/hooks/useSessions.ts index 2a2f9b09..738131fe 100644 --- a/hooks/useSessions.ts +++ b/hooks/useSessions.ts @@ -3,6 +3,8 @@ import { apiAtom } from "@/providers/JellyfinProvider"; import { useAtom } from "jotai"; import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; import { userAtom } from "@/providers/JellyfinProvider"; +import { Platform } from "react-native"; +const Notifications = !Platform.isTV ? require("expo-notifications") : null; export interface useSessionsProps { refetchInterval: number; @@ -22,9 +24,13 @@ export const useSessions = ({ refetchInterval = 5 * 1000, activeWithinSeconds = const response = await getSessionApi(api).getSessions({ activeWithinSeconds: activeWithinSeconds, }); - return response.data + + const result = response.data .filter((s) => s.NowPlayingItem) .sort((a, b) => (b.NowPlayingItem?.Name ?? "").localeCompare(a.NowPlayingItem?.Name ?? "")); + + Notifications.setBadgeCountAsync(result.length); + return result }, refetchInterval: refetchInterval, }); diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index ea473d43..37b52346 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -3,6 +3,7 @@ import { useInterval } from "@/hooks/useInterval"; import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr"; import { useSettings } from "@/utils/atoms/settings"; import { storage } from "@/utils/mmkv"; +import { store } from "@/utils/store"; import { Api, Jellyfin } from "@jellyfin/sdk"; import { UserDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getUserApi } from "@jellyfin/sdk/lib/utils/api"; @@ -165,6 +166,10 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ await refreshStreamyfinPluginSettings(); })(); }, []); + + useEffect(() => { + store.set(apiAtom, api); + }, [api]); useInterval(pollQuickConnect, isPolling ? 1000 : null); useInterval(refreshStreamyfinPluginSettings, 60 * 5 * 1000); // 5 min diff --git a/utils/background-tasks.ts b/utils/background-tasks.ts index 0e9a408a..b066692a 100644 --- a/utils/background-tasks.ts +++ b/utils/background-tasks.ts @@ -24,3 +24,26 @@ export async function unregisterBackgroundFetchAsync() { console.log("Error unregistering background fetch task", error); } } + +export const BACKGROUND_FETCH_TASK_SESSIONS = + "background-fetch-sessions"; + +export async function registerBackgroundFetchAsyncSessions() { + try { + BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK_SESSIONS, { + minimumInterval: 1 * 60, // 1 minutes + stopOnTerminate: false, // android only, + startOnBoot: true, // android only + }); + } catch (error) { + console.log("Error registering background fetch task", error); + } +} + +export async function unregisterBackgroundFetchAsyncSessions() { + try { + BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_TASK_SESSIONS); + } catch (error) { + console.log("Error unregistering background fetch task", error); + } +} \ No newline at end of file diff --git a/utils/store.ts b/utils/store.ts new file mode 100644 index 00000000..09a7aa5b --- /dev/null +++ b/utils/store.ts @@ -0,0 +1,3 @@ +import { createStore } from 'jotai'; + +export const store = createStore();