forked from Ninjalama/streamyfin_mirror
feat: recently added notifications
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { SettingsIndex } from "@/components/settings/SettingsIndex";
|
||||
import { HomeIndex } from "@/components/settings/HomeIndex";
|
||||
|
||||
export default function page() {
|
||||
return <SettingsIndex />;
|
||||
return <HomeIndex />;
|
||||
}
|
||||
|
||||
@@ -4,14 +4,21 @@ import i18n from "@/i18n";
|
||||
import { DownloadProvider } from "@/providers/DownloadProvider";
|
||||
import {
|
||||
getOrSetDeviceId,
|
||||
getServerUrlFromStorage,
|
||||
getTokenFromStorage,
|
||||
getUserFromStorage,
|
||||
JellyfinProvider,
|
||||
} 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_RECENTLY_ADDED,
|
||||
registerBackgroundFetchAsyncRecentlyAdded,
|
||||
unregisterBackgroundFetchAsyncRecentlyAdded,
|
||||
} from "@/utils/background-tasks";
|
||||
import { LogProvider, writeToLog } from "@/utils/log";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
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 "react-native-reanimated";
|
||||
import { Toaster } from "sonner-native";
|
||||
import { Jellyfin } from "@jellyfin/sdk";
|
||||
import { fetchAndStoreRecentlyAdded } from "@/utils/recently-added-notifications";
|
||||
|
||||
if (!Platform.isTV) {
|
||||
Notifications.setNotificationHandler({
|
||||
@@ -97,6 +106,23 @@ function useNotificationObserver() {
|
||||
}
|
||||
|
||||
if (!Platform.isTV) {
|
||||
TaskManager.defineTask(BACKGROUND_FETCH_TASK_RECENTLY_ADDED, async () => {
|
||||
const token = getTokenFromStorage();
|
||||
const url = getServerUrlFromStorage();
|
||||
const user = getUserFromStorage();
|
||||
|
||||
console.log(
|
||||
"TaskManager ~ trigger ~ recently added notifications:",
|
||||
token,
|
||||
url,
|
||||
user?.Id
|
||||
);
|
||||
|
||||
if (!token || !url || !user?.Id) return;
|
||||
|
||||
fetchAndStoreRecentlyAdded(user.Id, url, token);
|
||||
});
|
||||
|
||||
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
|
||||
console.log("TaskManager ~ trigger");
|
||||
|
||||
@@ -278,6 +304,12 @@ function Layout() {
|
||||
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
||||
);
|
||||
}
|
||||
|
||||
if (settings.recentlyAddedNotifications === true) {
|
||||
registerBackgroundFetchAsyncRecentlyAdded();
|
||||
} else {
|
||||
unregisterBackgroundFetchAsyncRecentlyAdded();
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -53,7 +53,7 @@ type MediaListSection = {
|
||||
|
||||
type Section = ScrollingCollectionListSection | MediaListSection;
|
||||
|
||||
export const SettingsIndex = () => {
|
||||
export const HomeIndex = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useTranslation();
|
||||
@@ -164,6 +164,7 @@ export const OtherSettings: React.FC = () => {
|
||||
title={t("home.settings.other.hide_libraries")}
|
||||
showArrow
|
||||
/>
|
||||
|
||||
<ListItem
|
||||
title={t("home.settings.other.default_quality")}
|
||||
disabled={pluginSettings?.defaultBitrate?.locked}
|
||||
@@ -173,7 +174,6 @@ export const OtherSettings: React.FC = () => {
|
||||
disabled={pluginSettings?.defaultBitrate?.locked}
|
||||
keyExtractor={(item) => item.key}
|
||||
titleExtractor={(item) => item.key}
|
||||
selected={settings.defaultBitrate}
|
||||
title={
|
||||
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
||||
<Text className="mr-1 text-[#8E8D91]">
|
||||
@@ -202,6 +202,14 @@ export const OtherSettings: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem title={"Recently added notifications"}>
|
||||
<Switch
|
||||
value={settings.recentlyAddedNotifications}
|
||||
onValueChange={(recentlyAddedNotifications) =>
|
||||
updateSettings({ recentlyAddedNotifications })
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListGroup>
|
||||
</DisabledSetting>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,8 @@ import { toast } from "sonner-native";
|
||||
import { ListGroup } from "../list/ListGroup";
|
||||
import { ListItem } from "../list/ListItem";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
import { RECENTLY_ADDED_SENT_NOTIFICATIONS_ITEM_IDS_KEY } from "@/utils/recently-added-notifications";
|
||||
|
||||
export const StorageSettings = () => {
|
||||
const { deleteAllFiles, appSizeUsage } = useDownload();
|
||||
@@ -109,6 +111,15 @@ export const StorageSettings = () => {
|
||||
title={t("home.settings.storage.delete_all_downloaded_files")}
|
||||
/>
|
||||
</ListGroup>
|
||||
<ListGroup>
|
||||
<ListItem
|
||||
textColor="red"
|
||||
onPress={() => {
|
||||
storage.set(RECENTLY_ADDED_SENT_NOTIFICATIONS_ITEM_IDS_KEY, "[]");
|
||||
}}
|
||||
title={"Reset recently added notifications"}
|
||||
/>
|
||||
</ListGroup>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -145,6 +145,7 @@ export type Settings = {
|
||||
safeAreaInControlsEnabled: boolean;
|
||||
jellyseerrServerUrl?: string;
|
||||
hiddenLibraries?: string[];
|
||||
recentlyAddedNotifications: boolean;
|
||||
};
|
||||
|
||||
export interface Lockable<T> {
|
||||
@@ -198,6 +199,7 @@ const defaultValues: Settings = {
|
||||
safeAreaInControlsEnabled: true,
|
||||
jellyseerrServerUrl: undefined,
|
||||
hiddenLibraries: [],
|
||||
recentlyAddedNotifications: true,
|
||||
};
|
||||
|
||||
const loadSettings = (): Partial<Settings> => {
|
||||
|
||||
@@ -8,7 +8,7 @@ export const BACKGROUND_FETCH_TASK = "background-fetch";
|
||||
export async function registerBackgroundFetchAsync() {
|
||||
try {
|
||||
BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, {
|
||||
minimumInterval: 60 * 1, // 1 minutes
|
||||
minimumInterval: 3 * 1, // 1 minutes
|
||||
stopOnTerminate: false, // android only,
|
||||
startOnBoot: false, // android only
|
||||
});
|
||||
@@ -24,3 +24,26 @@ export async function unregisterBackgroundFetchAsync() {
|
||||
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: 60 * 60, // 60 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);
|
||||
}
|
||||
}
|
||||
|
||||
168
utils/recently-added-notifications.ts
Normal file
168
utils/recently-added-notifications.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { Api, Jellyfin } from "@jellyfin/sdk";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getItemsApi, getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
import * as Notifications from "expo-notifications";
|
||||
import { Platform } from "react-native";
|
||||
import { getAuthHeaders } from "./jellyfin/jellyfin";
|
||||
import { getOrSetDeviceId } from "./device";
|
||||
import { getDeviceName } from "react-native-device-info";
|
||||
|
||||
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
|
||||
) {
|
||||
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.26.1" },
|
||||
deviceInfo: {
|
||||
name: deviceName,
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
const api = jellyfin?.createApi(basePath, token);
|
||||
|
||||
const response1 = await getItemsApi(api).getItems({
|
||||
userId: userId,
|
||||
limit: 5,
|
||||
recursive: true,
|
||||
includeItemTypes: ["Episode"],
|
||||
sortOrder: ["Descending"],
|
||||
sortBy: ["DateCreated"],
|
||||
});
|
||||
const response2 = await getItemsApi(api).getItems({
|
||||
userId: userId,
|
||||
limit: 5,
|
||||
recursive: true,
|
||||
includeItemTypes: ["Movie"],
|
||||
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 newIds: string[] = [];
|
||||
const items = [...newEpisodes, ...newMovies];
|
||||
|
||||
// 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;
|
||||
} 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])])
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching recently added items:", error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user