forked from Ninjalama/streamyfin_mirror
Compare commits
17 Commits
develop
...
feat/recen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef851ae3f5 | ||
|
|
349899d840 | ||
|
|
e1561dbd82 | ||
|
|
e217fcff36 | ||
|
|
b402cf7f10 | ||
|
|
f3b77b8547 | ||
|
|
c0643f564d | ||
|
|
893cedcf36 | ||
|
|
69db54a66c | ||
|
|
268e93effb | ||
|
|
8fe0089131 | ||
|
|
82ced0f101 | ||
|
|
5447c36bcd | ||
|
|
988eede36f | ||
|
|
a472d48b3b | ||
|
|
1fd4598ba8 | ||
|
|
63ea7d128f |
@@ -1,5 +1,5 @@
|
|||||||
import { SettingsIndex } from "@/components/settings/SettingsIndex";
|
import { HomeIndex } from "@/components/settings/HomeIndex";
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
return <SettingsIndex />;
|
return <HomeIndex />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,21 @@ import { useHaptic } from "@/hooks/useHaptic";
|
|||||||
import { useJellyfin } from "@/providers/JellyfinProvider";
|
import { useJellyfin } from "@/providers/JellyfinProvider";
|
||||||
import { clearLogs } from "@/utils/log";
|
import { clearLogs } from "@/utils/log";
|
||||||
import { storage } from "@/utils/mmkv";
|
import { storage } from "@/utils/mmkv";
|
||||||
|
import { RECENTLY_ADDED_SENT_NOTIFICATIONS_ITEM_IDS_KEY } from "@/utils/recently-added-notifications";
|
||||||
import { useNavigation, useRouter } from "expo-router";
|
import { useNavigation, useRouter } from "expo-router";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import React, { useEffect } from "react";
|
import React, { useCallback, useEffect, useMemo } from "react";
|
||||||
import { ScrollView, TouchableOpacity, View } from "react-native";
|
import {
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Switch,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import * as TaskManager from "expo-task-manager";
|
||||||
|
import { BACKGROUND_FETCH_TASK_RECENTLY_ADDED } from "@/utils/background-tasks";
|
||||||
|
import { RecentlyAddedNotificationsSettings } from "@/components/settings/RecentlyAddedNotifications";
|
||||||
|
|
||||||
export default function settings() {
|
export default function settings() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -91,7 +101,7 @@ export default function settings() {
|
|||||||
/>
|
/>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
|
|
||||||
<View className="mb-4">
|
<View className="">
|
||||||
<ListGroup title={t("home.settings.logs.logs_title")}>
|
<ListGroup title={t("home.settings.logs.logs_title")}>
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={() => router.push("/settings/logs/page")}
|
onPress={() => router.push("/settings/logs/page")}
|
||||||
@@ -106,7 +116,21 @@ export default function settings() {
|
|||||||
</ListGroup>
|
</ListGroup>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<StorageSettings />
|
<RecentlyAddedNotificationsSettings />
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
height: StyleSheet.hairlineWidth,
|
||||||
|
backgroundColor: "white",
|
||||||
|
overflow: "hidden",
|
||||||
|
marginVertical: 16,
|
||||||
|
opacity: 0.3,
|
||||||
|
}}
|
||||||
|
></View>
|
||||||
|
|
||||||
|
<View className="">
|
||||||
|
<StorageSettings />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ import i18n from "@/i18n";
|
|||||||
import { DownloadProvider } from "@/providers/DownloadProvider";
|
import { DownloadProvider } from "@/providers/DownloadProvider";
|
||||||
import {
|
import {
|
||||||
getOrSetDeviceId,
|
getOrSetDeviceId,
|
||||||
|
getServerUrlFromStorage,
|
||||||
getTokenFromStorage,
|
getTokenFromStorage,
|
||||||
|
getUserFromStorage,
|
||||||
JellyfinProvider,
|
JellyfinProvider,
|
||||||
} from "@/providers/JellyfinProvider";
|
} from "@/providers/JellyfinProvider";
|
||||||
import { JobQueueProvider } from "@/providers/JobQueueProvider";
|
import { JobQueueProvider } from "@/providers/JobQueueProvider";
|
||||||
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
|
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
|
||||||
import { WebSocketProvider } from "@/providers/WebSocketProvider";
|
import { WebSocketProvider } from "@/providers/WebSocketProvider";
|
||||||
import { Settings, useSettings } from "@/utils/atoms/settings";
|
import { Settings, useSettings } from "@/utils/atoms/settings";
|
||||||
import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks";
|
import {
|
||||||
|
BACKGROUND_FETCH_TASK,
|
||||||
|
BACKGROUND_FETCH_TASK_RECENTLY_ADDED,
|
||||||
|
registerBackgroundFetchAsyncRecentlyAdded,
|
||||||
|
unregisterBackgroundFetchAsyncRecentlyAdded,
|
||||||
|
} from "@/utils/background-tasks";
|
||||||
import { LogProvider, writeToLog } from "@/utils/log";
|
import { LogProvider, writeToLog } from "@/utils/log";
|
||||||
import { storage } from "@/utils/mmkv";
|
import { storage } from "@/utils/mmkv";
|
||||||
import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
|
import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
|
||||||
@@ -41,6 +48,8 @@ import { SystemBars } from "react-native-edge-to-edge";
|
|||||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||||
import "react-native-reanimated";
|
import "react-native-reanimated";
|
||||||
import { Toaster } from "sonner-native";
|
import { Toaster } from "sonner-native";
|
||||||
|
import { Jellyfin } from "@jellyfin/sdk";
|
||||||
|
import { fetchAndStoreRecentlyAdded } from "@/utils/recently-added-notifications";
|
||||||
|
|
||||||
if (!Platform.isTV) {
|
if (!Platform.isTV) {
|
||||||
Notifications.setNotificationHandler({
|
Notifications.setNotificationHandler({
|
||||||
@@ -97,6 +106,30 @@ function useNotificationObserver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!Platform.isTV) {
|
if (!Platform.isTV) {
|
||||||
|
TaskManager.defineTask(BACKGROUND_FETCH_TASK_RECENTLY_ADDED, async () => {
|
||||||
|
const token = getTokenFromStorage();
|
||||||
|
const url = getServerUrlFromStorage();
|
||||||
|
const user = getUserFromStorage();
|
||||||
|
|
||||||
|
const c = storage.getNumber("notification_send_for_item_ids.count");
|
||||||
|
storage.set("notification_send_for_item_ids.count", (c || 0) + 1);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"TaskManager ~ trigger ~ recently added notifications:",
|
||||||
|
token,
|
||||||
|
url,
|
||||||
|
user?.Id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!token || !url || !user?.Id) return;
|
||||||
|
|
||||||
|
const result = await fetchAndStoreRecentlyAdded(user.Id, url, token);
|
||||||
|
|
||||||
|
if (!result) return BackgroundFetch.BackgroundFetchResult.NoData;
|
||||||
|
|
||||||
|
return BackgroundFetch.BackgroundFetchResult.NewData;
|
||||||
|
});
|
||||||
|
|
||||||
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
|
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
|
||||||
console.log("TaskManager ~ trigger");
|
console.log("TaskManager ~ trigger");
|
||||||
|
|
||||||
@@ -268,6 +301,32 @@ function Layout() {
|
|||||||
checkAndRequestPermissions();
|
checkAndRequestPermissions();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const checkStatusAsync = async () => {
|
||||||
|
if (Platform.isTV) return;
|
||||||
|
|
||||||
|
await BackgroundFetch.getStatusAsync();
|
||||||
|
return await TaskManager.isTaskRegisteredAsync(
|
||||||
|
BACKGROUND_FETCH_TASK_RECENTLY_ADDED
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const isRegistered = await checkStatusAsync();
|
||||||
|
|
||||||
|
if (settings.recentlyAddedNotifications === false && isRegistered) {
|
||||||
|
console.log("unregisterBackgroundFetchAsyncRecentlyAdded");
|
||||||
|
unregisterBackgroundFetchAsyncRecentlyAdded();
|
||||||
|
} else if (
|
||||||
|
settings.recentlyAddedNotifications === true &&
|
||||||
|
!isRegistered
|
||||||
|
) {
|
||||||
|
console.log("registerBackgroundFetchAsyncRecentlyAdded");
|
||||||
|
registerBackgroundFetchAsyncRecentlyAdded();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [settings.recentlyAddedNotifications]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If the user has auto rotate enabled, unlock the orientation
|
// If the user has auto rotate enabled, unlock the orientation
|
||||||
if (settings.autoRotate === true) {
|
if (settings.autoRotate === true) {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ type MediaListSection = {
|
|||||||
|
|
||||||
type Section = ScrollingCollectionListSection | MediaListSection;
|
type Section = ScrollingCollectionListSection | MediaListSection;
|
||||||
|
|
||||||
export const SettingsIndex = () => {
|
export const HomeIndex = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -22,6 +22,7 @@ import { ListItem } from "../list/ListItem";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
import Dropdown from "@/components/common/Dropdown";
|
import Dropdown from "@/components/common/Dropdown";
|
||||||
|
import { RECENTLY_ADDED_SENT_NOTIFICATIONS_ITEM_IDS_KEY } from "@/utils/recently-added-notifications";
|
||||||
|
|
||||||
export const OtherSettings: React.FC = () => {
|
export const OtherSettings: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -164,6 +165,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
title={t("home.settings.other.hide_libraries")}
|
title={t("home.settings.other.hide_libraries")}
|
||||||
showArrow
|
showArrow
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.other.default_quality")}
|
title={t("home.settings.other.default_quality")}
|
||||||
disabled={pluginSettings?.defaultBitrate?.locked}
|
disabled={pluginSettings?.defaultBitrate?.locked}
|
||||||
@@ -173,7 +175,6 @@ export const OtherSettings: React.FC = () => {
|
|||||||
disabled={pluginSettings?.defaultBitrate?.locked}
|
disabled={pluginSettings?.defaultBitrate?.locked}
|
||||||
keyExtractor={(item) => item.key}
|
keyExtractor={(item) => item.key}
|
||||||
titleExtractor={(item) => item.key}
|
titleExtractor={(item) => item.key}
|
||||||
selected={settings.defaultBitrate}
|
|
||||||
title={
|
title={
|
||||||
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
||||||
<Text className="mr-1 text-[#8E8D91]">
|
<Text className="mr-1 text-[#8E8D91]">
|
||||||
|
|||||||
56
components/settings/RecentlyAddedNotifications.tsx
Normal file
56
components/settings/RecentlyAddedNotifications.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import settings from "@/app/(auth)/(tabs)/(home)/settings";
|
||||||
|
import { Switch, View } from "react-native";
|
||||||
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { ListItem } from "../list/ListItem";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import React, { useCallback, useEffect, useMemo } from "react";
|
||||||
|
import { BACKGROUND_FETCH_TASK_RECENTLY_ADDED } from "@/utils/background-tasks";
|
||||||
|
import { storage } from "@/utils/mmkv";
|
||||||
|
import { RECENTLY_ADDED_SENT_NOTIFICATIONS_ITEM_IDS_KEY } from "@/utils/recently-added-notifications";
|
||||||
|
import * as TaskManager from "expo-task-manager";
|
||||||
|
import * as BackgroundFetch from "expo-background-fetch";
|
||||||
|
import { useMMKVNumber } from "react-native-mmkv";
|
||||||
|
|
||||||
|
export const RecentlyAddedNotificationsSettings: React.FC = ({ ...props }) => {
|
||||||
|
const [settings, updateSettings] = useSettings();
|
||||||
|
|
||||||
|
const clearRecentlyAddedNotifications = useCallback(() => {
|
||||||
|
storage.delete(RECENTLY_ADDED_SENT_NOTIFICATIONS_ITEM_IDS_KEY);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const recentlyAddedNotificationsItemIds = useMemo(() => {
|
||||||
|
const s = storage.getString(RECENTLY_ADDED_SENT_NOTIFICATIONS_ITEM_IDS_KEY);
|
||||||
|
if (!s) return [] as string[];
|
||||||
|
try {
|
||||||
|
const t: string[] = JSON.parse(s);
|
||||||
|
return t;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error("Failed to parse recently added notifications item ids");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [triggerCount, setTriggerCount] = useMMKVNumber(
|
||||||
|
"notification_send_for_item_ids.count"
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="mb-4" {...props}>
|
||||||
|
<ListGroup title={"Recently Added Notifications"}>
|
||||||
|
<ListItem title={"Recently added notifications"}>
|
||||||
|
<Switch
|
||||||
|
value={settings.recentlyAddedNotifications}
|
||||||
|
onValueChange={(recentlyAddedNotifications) =>
|
||||||
|
updateSettings({ recentlyAddedNotifications })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem title={`Trigger count (${triggerCount || 0})`} />
|
||||||
|
<ListItem
|
||||||
|
textColor="red"
|
||||||
|
onPress={clearRecentlyAddedNotifications}
|
||||||
|
title={`Reset recently added notifications (${recentlyAddedNotificationsItemIds.length})`}
|
||||||
|
/>
|
||||||
|
</ListGroup>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,6 +8,9 @@ import { toast } from "sonner-native";
|
|||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { storage } from "@/utils/mmkv";
|
||||||
|
import { RECENTLY_ADDED_SENT_NOTIFICATIONS_ITEM_IDS_KEY } from "@/utils/recently-added-notifications";
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
|
||||||
export const StorageSettings = () => {
|
export const StorageSettings = () => {
|
||||||
const { deleteAllFiles, appSizeUsage } = useDownload();
|
const { deleteAllFiles, appSizeUsage } = useDownload();
|
||||||
@@ -41,6 +44,7 @@ export const StorageSettings = () => {
|
|||||||
return ((value / total) * 100).toFixed(2);
|
return ((value / total) * 100).toFixed(2);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<View className="flex flex-col gap-y-1">
|
<View className="flex flex-col gap-y-1">
|
||||||
@@ -109,6 +113,7 @@ export const StorageSettings = () => {
|
|||||||
title={t("home.settings.storage.delete_all_downloaded_files")}
|
title={t("home.settings.storage.delete_all_downloaded_files")}
|
||||||
/>
|
/>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ export type Settings = {
|
|||||||
safeAreaInControlsEnabled: boolean;
|
safeAreaInControlsEnabled: boolean;
|
||||||
jellyseerrServerUrl?: string;
|
jellyseerrServerUrl?: string;
|
||||||
hiddenLibraries?: string[];
|
hiddenLibraries?: string[];
|
||||||
|
recentlyAddedNotifications: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Lockable<T> {
|
export interface Lockable<T> {
|
||||||
@@ -198,6 +199,7 @@ const defaultValues: Settings = {
|
|||||||
safeAreaInControlsEnabled: true,
|
safeAreaInControlsEnabled: true,
|
||||||
jellyseerrServerUrl: undefined,
|
jellyseerrServerUrl: undefined,
|
||||||
hiddenLibraries: [],
|
hiddenLibraries: [],
|
||||||
|
recentlyAddedNotifications: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadSettings = (): Partial<Settings> => {
|
const loadSettings = (): Partial<Settings> => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const BACKGROUND_FETCH_TASK = "background-fetch";
|
|||||||
export async function registerBackgroundFetchAsync() {
|
export async function registerBackgroundFetchAsync() {
|
||||||
try {
|
try {
|
||||||
BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, {
|
BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, {
|
||||||
minimumInterval: 60 * 1, // 1 minutes
|
minimumInterval: 10 * 60, // 10 minutes
|
||||||
stopOnTerminate: false, // android only,
|
stopOnTerminate: false, // android only,
|
||||||
startOnBoot: false, // android only
|
startOnBoot: false, // android only
|
||||||
});
|
});
|
||||||
@@ -24,3 +24,26 @@ export async function unregisterBackgroundFetchAsync() {
|
|||||||
console.log("Error unregistering background fetch task", error);
|
console.log("Error unregistering background fetch task", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const BACKGROUND_FETCH_TASK_RECENTLY_ADDED =
|
||||||
|
"background-fetch-recently-added";
|
||||||
|
|
||||||
|
export async function registerBackgroundFetchAsyncRecentlyAdded() {
|
||||||
|
try {
|
||||||
|
BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK_RECENTLY_ADDED, {
|
||||||
|
minimumInterval: 10 * 60, // 10 minutes
|
||||||
|
stopOnTerminate: false, // android only,
|
||||||
|
startOnBoot: true, // android only
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error registering background fetch task", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unregisterBackgroundFetchAsyncRecentlyAdded() {
|
||||||
|
try {
|
||||||
|
BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_TASK_RECENTLY_ADDED);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error unregistering background fetch task", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
187
utils/recently-added-notifications.ts
Normal file
187
utils/recently-added-notifications.ts
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import { storage } from "@/utils/mmkv";
|
||||||
|
import { Jellyfin } from "@jellyfin/sdk";
|
||||||
|
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
|
import { getItemsApi, getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
|
import * as Notifications from "expo-notifications";
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
import { getDeviceName } from "react-native-device-info";
|
||||||
|
import { getOrSetDeviceId } from "./device";
|
||||||
|
|
||||||
|
export const RECENTLY_ADDED_SENT_NOTIFICATIONS_ITEM_IDS_KEY =
|
||||||
|
"notification_send_for_item_ids";
|
||||||
|
|
||||||
|
const acceptedIemTypes = ["Movie", "Episode", "Series"];
|
||||||
|
|
||||||
|
async function sendNewItemNotification(item: BaseItemDto) {
|
||||||
|
if (Platform.isTV) return;
|
||||||
|
if (!item.Type) return;
|
||||||
|
|
||||||
|
if (!acceptedIemTypes.includes(item.Type)) return;
|
||||||
|
|
||||||
|
if (item.Type === "Movie")
|
||||||
|
await Notifications.scheduleNotificationAsync({
|
||||||
|
content: {
|
||||||
|
title: `New Movie Added`,
|
||||||
|
body: `${item.Name} (${item.ProductionYear})`,
|
||||||
|
},
|
||||||
|
trigger: null,
|
||||||
|
});
|
||||||
|
else if (item.Type === "Episode")
|
||||||
|
await Notifications.scheduleNotificationAsync({
|
||||||
|
content: {
|
||||||
|
title: `New Episode Added`,
|
||||||
|
body: `${item.SeriesName} - ${item.Name}`,
|
||||||
|
},
|
||||||
|
trigger: null,
|
||||||
|
});
|
||||||
|
else if (item.Type === "Series")
|
||||||
|
await Notifications.scheduleNotificationAsync({
|
||||||
|
content: {
|
||||||
|
title: `New Series Added`,
|
||||||
|
body: `${item.Name} (${item.ProductionYear})`,
|
||||||
|
},
|
||||||
|
trigger: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches recently added items from Jellyfin and sends notifications for new content.
|
||||||
|
*
|
||||||
|
* This function performs the following operations:
|
||||||
|
* 1. Retrieves previously notified item IDs from storage
|
||||||
|
* 2. Connects to Jellyfin server using provided credentials
|
||||||
|
* 3. Fetches 5 most recent episodes and 5 most recent movies
|
||||||
|
* 4. Checks for new items that haven't been notified before
|
||||||
|
* 5. Sends notifications for new items
|
||||||
|
* 6. Updates storage with new item IDs
|
||||||
|
*
|
||||||
|
* Note: On first run (when no previous notifications exist), it will store all
|
||||||
|
* current items without sending notifications to avoid mass-notifications.
|
||||||
|
*
|
||||||
|
* @param userId - The Jellyfin user ID to fetch items for
|
||||||
|
* @param basePath - The base URL of the Jellyfin server
|
||||||
|
* @param token - The authentication token for the Jellyfin server
|
||||||
|
*/
|
||||||
|
export async function fetchAndStoreRecentlyAdded(
|
||||||
|
userId: string,
|
||||||
|
basePath: string,
|
||||||
|
token: string
|
||||||
|
): Promise<number> {
|
||||||
|
try {
|
||||||
|
const deviceName = await getDeviceName();
|
||||||
|
const id = getOrSetDeviceId();
|
||||||
|
|
||||||
|
// Get stored items
|
||||||
|
const _alreadySentItemIds = storage.getString(
|
||||||
|
RECENTLY_ADDED_SENT_NOTIFICATIONS_ITEM_IDS_KEY
|
||||||
|
);
|
||||||
|
const alreadySentItemIds: string[] = _alreadySentItemIds
|
||||||
|
? JSON.parse(_alreadySentItemIds)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"fetchAndStoreRecentlyAdded ~ notifications stored:",
|
||||||
|
alreadySentItemIds.length
|
||||||
|
);
|
||||||
|
|
||||||
|
const jellyfin = new Jellyfin({
|
||||||
|
clientInfo: { name: "Streamyfin", version: "0.27.0" },
|
||||||
|
deviceInfo: {
|
||||||
|
name: deviceName,
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = jellyfin?.createApi(basePath, token);
|
||||||
|
|
||||||
|
const response1 = await getItemsApi(api).getItems({
|
||||||
|
userId: userId,
|
||||||
|
limit: 10,
|
||||||
|
recursive: true,
|
||||||
|
includeItemTypes: ["Episode"],
|
||||||
|
sortOrder: ["Descending"],
|
||||||
|
sortBy: ["DateCreated"],
|
||||||
|
});
|
||||||
|
const response2 = await getItemsApi(api).getItems({
|
||||||
|
userId: userId,
|
||||||
|
limit: 10,
|
||||||
|
recursive: true,
|
||||||
|
includeItemTypes: ["Movie"],
|
||||||
|
sortOrder: ["Descending"],
|
||||||
|
sortBy: ["DateCreated"],
|
||||||
|
});
|
||||||
|
const response3 = await getItemsApi(api).getItems({
|
||||||
|
userId: userId,
|
||||||
|
limit: 10,
|
||||||
|
recursive: true,
|
||||||
|
includeItemTypes: ["Series"],
|
||||||
|
sortOrder: ["Descending"],
|
||||||
|
sortBy: ["DateCreated"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const newEpisodes =
|
||||||
|
response1.data.Items?.map((item) => ({
|
||||||
|
Id: item.Id,
|
||||||
|
Name: item.Name,
|
||||||
|
DateCreated: item.DateCreated,
|
||||||
|
Type: item.Type,
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
|
const newMovies =
|
||||||
|
response2.data.Items?.map((item) => ({
|
||||||
|
Id: item.Id,
|
||||||
|
Name: item.Name,
|
||||||
|
DateCreated: item.DateCreated,
|
||||||
|
Type: item.Type,
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
|
const newSeries =
|
||||||
|
response3.data.Items?.map((item) => ({
|
||||||
|
Id: item.Id,
|
||||||
|
Name: item.Name,
|
||||||
|
DateCreated: item.DateCreated,
|
||||||
|
Type: item.Type,
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
|
const newIds: string[] = [];
|
||||||
|
const items = [...newEpisodes, ...newMovies, ...newSeries];
|
||||||
|
|
||||||
|
// Don't send initial mass-notifications if there are no previously sent notifications
|
||||||
|
if (alreadySentItemIds.length === 0) {
|
||||||
|
// Store all items as sent (since these items are already in the users library)
|
||||||
|
storage.set(
|
||||||
|
RECENTLY_ADDED_SENT_NOTIFICATIONS_ITEM_IDS_KEY,
|
||||||
|
JSON.stringify(items.map((item) => item.Id))
|
||||||
|
);
|
||||||
|
return items.length;
|
||||||
|
} else {
|
||||||
|
// Only send notifications for items that haven't been sent yet
|
||||||
|
for (const newItem of items) {
|
||||||
|
const alreadySentNotificationFor = alreadySentItemIds.some(
|
||||||
|
(id) => id === newItem.Id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!alreadySentNotificationFor) {
|
||||||
|
const fullItem = await getUserLibraryApi(api).getItem({
|
||||||
|
itemId: newItem.Id!,
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendNewItemNotification(fullItem.data);
|
||||||
|
newIds.push(newItem.Id!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Store all new items as sent, so that we don't send notifications for them again
|
||||||
|
storage.set(
|
||||||
|
RECENTLY_ADDED_SENT_NOTIFICATIONS_ITEM_IDS_KEY,
|
||||||
|
JSON.stringify([...new Set([...alreadySentItemIds, ...newIds])])
|
||||||
|
);
|
||||||
|
|
||||||
|
return newIds.length;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching recently added items:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user