From e1fe20db86efddf4e375521be146f88997f7e803 Mon Sep 17 00:00:00 2001 From: sarendsen Date: Tue, 14 Jan 2025 07:56:32 +0100 Subject: [PATCH 1/9] wip --- app/(auth)/(tabs)/(home)/index.tsx | 18 ++++++++- components/settings/OtherSettings.tsx | 58 +++++++++++++++------------ utils/atoms/settings.ts | 12 +++--- 3 files changed, 57 insertions(+), 31 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx index 0675f12f..0b018087 100644 --- a/app/(auth)/(tabs)/(home)/index.tsx +++ b/app/(auth)/(tabs)/(home)/index.tsx @@ -59,7 +59,13 @@ export default function index() { const user = useAtomValue(userAtom); const [loading, setLoading] = useState(false); - const [settings, _] = useSettings(); + const [ + settings, + updateSettings, + pluginSettings, + setPluginSettings, + refreshStreamyfinPluginSettings, + ] = useSettings(); const [isConnected, setIsConnected] = useState(null); const [loadingRetry, setLoadingRetry] = useState(false); @@ -69,6 +75,14 @@ export default function index() { const insets = useSafeAreaInsets(); + useEffect(() => { + const interval = setInterval(async () => { + await refreshStreamyfinPluginSettings(); + }, 60 * 10 * 1000); // 10 min + + return () => clearInterval(interval); + }, []); + useEffect(() => { const hasDownloads = downloadedFiles && downloadedFiles.length > 0; navigation.setOptions({ @@ -110,6 +124,7 @@ export default function index() { cleanCacheDirectory().catch((e) => console.error("Something went wrong cleaning cache directory") ); + return () => { unsubscribe(); }; @@ -154,6 +169,7 @@ export default function index() { const refetch = useCallback(async () => { setLoading(true); + await refreshStreamyfinPluginSettings(); await invalidateCache(); setLoading(false); }, []); diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx index dcea3f26..7c49a100 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -9,7 +9,7 @@ import * as BackgroundFetch from "expo-background-fetch"; import { useRouter } from "expo-router"; import * as ScreenOrientation from "expo-screen-orientation"; import * as TaskManager from "expo-task-manager"; -import React, {useEffect, useMemo} from "react"; +import React, { useEffect, useMemo } from "react"; import { Linking, Switch, TouchableOpacity } from "react-native"; import { toast } from "sonner-native"; import { Text } from "../common/Text"; @@ -52,28 +52,28 @@ export const OtherSettings: React.FC = () => { /********************** *********************/ - const disabled = useMemo(() => ( - pluginSettings?.autoRotate?.locked === true && - pluginSettings?.defaultVideoOrientation?.locked === true && - pluginSettings?.safeAreaInControlsEnabled?.locked === true && - pluginSettings?.showCustomMenuLinks?.locked === true && - pluginSettings?.hiddenLibraries?.locked === true && - pluginSettings?.disableHapticFeedback?.locked === true - ), [pluginSettings]); + const disabled = useMemo( + () => + pluginSettings?.autoRotate?.locked === true && + pluginSettings?.defaultVideoOrientation?.locked === true && + pluginSettings?.safeAreaInControlsEnabled?.locked === true && + pluginSettings?.showCustomMenuLinks?.locked === true && + pluginSettings?.hiddenLibraries?.locked === true && + pluginSettings?.disableHapticFeedback?.locked === true, + [pluginSettings] + ); const orientations = [ ScreenOrientation.OrientationLock.DEFAULT, ScreenOrientation.OrientationLock.PORTRAIT_UP, ScreenOrientation.OrientationLock.LANDSCAPE_LEFT, - ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT - ] + ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT, + ]; if (!settings) return null; return ( - + { updateSettings({autoRotate: value})} + onValueChange={(value) => updateSettings({ autoRotate: value })} /> - ScreenOrientationEnum[item] + disabled={ + pluginSettings?.defaultVideoOrientation?.locked || + settings.autoRotate } + keyExtractor={String} + titleExtractor={(item) => ScreenOrientationEnum[item]} title={ {ScreenOrientationEnum[settings.defaultVideoOrientation]} - + } label="Orientation" onSelected={(defaultVideoOrientation) => - updateSettings({defaultVideoOrientation}) + updateSettings({ defaultVideoOrientation }) } /> @@ -120,7 +128,7 @@ export const OtherSettings: React.FC = () => { value={settings.safeAreaInControlsEnabled} disabled={pluginSettings?.safeAreaInControlsEnabled?.locked} onValueChange={(value) => - updateSettings({safeAreaInControlsEnabled: value}) + updateSettings({ safeAreaInControlsEnabled: value }) } /> @@ -138,7 +146,7 @@ export const OtherSettings: React.FC = () => { value={settings.showCustomMenuLinks} disabled={pluginSettings?.showCustomMenuLinks?.locked} onValueChange={(value) => - updateSettings({showCustomMenuLinks: value}) + updateSettings({ showCustomMenuLinks: value }) } /> @@ -155,7 +163,7 @@ export const OtherSettings: React.FC = () => { value={settings.disableHapticFeedback} disabled={pluginSettings?.disableHapticFeedback?.locked} onValueChange={(disableHapticFeedback) => - updateSettings({disableHapticFeedback}) + updateSettings({ disableHapticFeedback }) } /> diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index a16c51a6..a4d1a135 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -221,11 +221,13 @@ export const useSettings = () => { const refreshStreamyfinPluginSettings = useCallback(async () => { if (!api) return; - const settings = await api - .getStreamyfinPluginConfig() - .then(({ data }) => data?.settings); - - writeInfoLog(`Got remote settings: ${JSON.stringify(settings)}`); + const settings = await api.getStreamyfinPluginConfig().then( + ({ data }) => { + writeInfoLog(`Got remote settings`); + return data?.settings; + }, + (err) => undefined + ); setPluginSettings(settings); return settings; From a24b126539df8d4f15f8180f4a1e7ac5b241f9e0 Mon Sep 17 00:00:00 2001 From: sarendsen Date: Tue, 14 Jan 2025 09:24:31 +0100 Subject: [PATCH 2/9] wip --- app/(auth)/(tabs)/(home)/index.tsx | 2 +- utils/atoms/settings.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx index 0b018087..5992f4a4 100644 --- a/app/(auth)/(tabs)/(home)/index.tsx +++ b/app/(auth)/(tabs)/(home)/index.tsx @@ -205,7 +205,7 @@ export default function index() { ); let sections: Section[] = []; - if (settings?.home === null || settings?.home?.sections === null) { + if (!settings?.home || !settings?.home?.sections) { sections = useMemo(() => { if (!api || !user?.Id) return []; diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index a4d1a135..9583bf68 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -189,7 +189,14 @@ const loadSettings = (): Settings => { } }; +const EXCLUDE_FROM_SAVE = ["home"]; + const saveSettings = (settings: Settings) => { + Object.keys(settings).forEach((key) => { + if (EXCLUDE_FROM_SAVE.includes(key)) { + delete settings[key as keyof Settings]; + } + }); const jsonValue = JSON.stringify(settings); storage.set("settings", jsonValue); }; @@ -277,6 +284,7 @@ export const useSettings = () => { if (Object.keys(unlockedPluginDefaults).length > 0) { updateSettings(unlockedPluginDefaults); } + return { ..._settings, ...overrideSettings, From e7128afb32cf051e583236692a0eb3c4466fa54c Mon Sep 17 00:00:00 2001 From: sarendsen Date: Tue, 14 Jan 2025 09:48:17 +0100 Subject: [PATCH 3/9] wip --- app/(auth)/(tabs)/(home)/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx index 5992f4a4..c6d88470 100644 --- a/app/(auth)/(tabs)/(home)/index.tsx +++ b/app/(auth)/(tabs)/(home)/index.tsx @@ -321,7 +321,7 @@ export default function index() { ( await getItemsApi(api).getItems({ userId: user?.Id, - limit: section.items?.limit || 20, + limit: section.items?.limit || 25, recursive: true, includeItemTypes: section.items?.includeItemTypes, sortBy: section.items?.sortBy, From aa20d9c7016d09e67bb31fc3ceb2d584f40c7ae4 Mon Sep 17 00:00:00 2001 From: sarendsen Date: Tue, 14 Jan 2025 10:31:16 +0100 Subject: [PATCH 4/9] wip --- app/(auth)/(tabs)/(home)/index.tsx | 8 -------- providers/JellyfinProvider.tsx | 11 +++++++++++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx index c6d88470..f2f69853 100644 --- a/app/(auth)/(tabs)/(home)/index.tsx +++ b/app/(auth)/(tabs)/(home)/index.tsx @@ -75,14 +75,6 @@ export default function index() { const insets = useSafeAreaInsets(); - useEffect(() => { - const interval = setInterval(async () => { - await refreshStreamyfinPluginSettings(); - }, 60 * 10 * 1000); // 10 min - - return () => clearInterval(interval); - }, []); - useEffect(() => { const hasDownloads = downloadedFiles && downloadedFiles.length > 0; navigation.setOptions({ diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 4455dfe1..9bb19cd4 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -174,6 +174,17 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ useInterval(pollQuickConnect, isPolling ? 1000 : null); + useInterval(refreshStreamyfinPluginSettings, 60 * 5 * 1000); // 5 min + + /************* ✨ Codeium Command ⭐ *************/ + /** + * Discovers and retrieves a list of recommended Jellyfin server candidates based on the provided URL. + * + * @param url - The URL used to discover server candidates. + * @returns A promise that resolves to an array of servers with their addresses. + */ + + /****** c70f9dcb-e9fa-4929-8ff2-39aba2e996c8 *******/ const discoverServers = async (url: string): Promise => { const servers = await jellyfin?.discovery.getRecommendedServerCandidates( url From e6f290b85f8abbef5b066ef47cbb8873252f1d9c Mon Sep 17 00:00:00 2001 From: sarendsen Date: Tue, 14 Jan 2025 10:35:21 +0100 Subject: [PATCH 5/9] wip --- providers/JellyfinProvider.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 9bb19cd4..1e2b6da8 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -174,6 +174,12 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ useInterval(pollQuickConnect, isPolling ? 1000 : null); + useEffect(() => { + (async () => { + await refreshStreamyfinPluginSettings(); + })(); + }, []); + useInterval(refreshStreamyfinPluginSettings, 60 * 5 * 1000); // 5 min /************* ✨ Codeium Command ⭐ *************/ From ed5403e597242d67ffa3cbe8e7ba134f67c5edcb Mon Sep 17 00:00:00 2001 From: sarendsen Date: Tue, 14 Jan 2025 10:37:20 +0100 Subject: [PATCH 6/9] wip --- providers/JellyfinProvider.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 1e2b6da8..01a901a2 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -182,15 +182,6 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ useInterval(refreshStreamyfinPluginSettings, 60 * 5 * 1000); // 5 min - /************* ✨ Codeium Command ⭐ *************/ - /** - * Discovers and retrieves a list of recommended Jellyfin server candidates based on the provided URL. - * - * @param url - The URL used to discover server candidates. - * @returns A promise that resolves to an array of servers with their addresses. - */ - - /****** c70f9dcb-e9fa-4929-8ff2-39aba2e996c8 *******/ const discoverServers = async (url: string): Promise => { const servers = await jellyfin?.discovery.getRecommendedServerCandidates( url From 0ff0fab3f41cd3a5265f2311f0e23768fd463080 Mon Sep 17 00:00:00 2001 From: sarendsen Date: Wed, 15 Jan 2025 00:47:10 +0100 Subject: [PATCH 7/9] fix: fix horizontal shows --- components/ContinueWatchingPoster.tsx | 5 +++++ components/home/ScrollingCollectionList.tsx | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/components/ContinueWatchingPoster.tsx b/components/ContinueWatchingPoster.tsx index eb000d45..a011d23e 100644 --- a/components/ContinueWatchingPoster.tsx +++ b/components/ContinueWatchingPoster.tsx @@ -49,6 +49,11 @@ const ContinueWatchingPoster: React.FC = ({ else return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`; } + + if (item.ImageTags?.["Thumb"]) + return `${api?.basePath}/Items/${item.Id}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ImageTags?.["Thumb"]}`; + else + return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`; }, [item]); const progress = useMemo(() => { diff --git a/components/home/ScrollingCollectionList.tsx b/components/home/ScrollingCollectionList.tsx index 17b4ec77..48c4c234 100644 --- a/components/home/ScrollingCollectionList.tsx +++ b/components/home/ScrollingCollectionList.tsx @@ -104,7 +104,12 @@ export const ScrollingCollectionList: React.FC = ({ {item.Type === "Movie" && orientation === "vertical" && ( )} - {item.Type === "Series" && } + {item.Type === "Series" && orientation === "vertical" && ( + + )} + {item.Type === "Series" && orientation === "horizontal" && ( + + )} {item.Type === "Program" && ( )} From 7a30a633358e0d1c859df3de0a6dbe6ec2a03725 Mon Sep 17 00:00:00 2001 From: lostb1t Date: Wed, 15 Jan 2025 09:13:03 +0100 Subject: [PATCH 8/9] Update README.md --- README.md | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c1abe08e..533dd65f 100644 --- a/README.md +++ b/README.md @@ -32,22 +32,17 @@ Downloading works by using ffmpeg to convert an HLS stream into a video file on Chromecast support is still in development, and we're working on improving it. Currently, it supports casting videos and audio, but we're working on adding support for subtitles and other features. -## Plugins +### Streamyfin Plugin -In Streamyfin we have built-in support for a few plugins. These plugins are not required to use Streamyfin, but they add some extra functionality. +The Jellyfin Plugin for Streamyfin is a plugin you install into Jellyfin that hold all settings for the client Streamyfin. This allows you to syncronize settings accross all your users, like: -### Collection rows +- Auto log in to Jellyseerr without the user having to do anythin +- Choose the default languages +- Set download method and search provider +- Customize homescreen +- And more... -Jellyfin collections can be shown as rows or carousel on the home screen. -The following tags can be added to a collection to provide this functionality. - -Available tags: - -- sf_promoted: will make the collection a row at home -- sf_carousel: will make the collection a carousel on home. - -A plugin exists to create collections based on external sources like mdblist. This make the automatic process of managing collections such as trending, most watched, etc. -See [Collection Import Plugin](https://github.com/lostb1t/jellyfin-plugin-collection-import) for more info. +[Streamyfin Plugin](https://github.com/streamyfin/jellyfin-plugin-streamyfin) ### Jellysearch From 72b9675df4a39bbd8f6ad5a942ef15e186af609e Mon Sep 17 00:00:00 2001 From: sarendsen Date: Thu, 16 Jan 2025 10:36:20 +0100 Subject: [PATCH 9/9] feat: Implement nextup for custom home --- app/(auth)/(tabs)/(home)/index.tsx | 25 +++++++++++++++++++------ utils/atoms/settings.ts | 8 ++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx index f2f69853..f94393e7 100644 --- a/app/(auth)/(tabs)/(home)/index.tsx +++ b/app/(auth)/(tabs)/(home)/index.tsx @@ -308,10 +308,10 @@ export default function index() { const section = settings.home?.sections[key]; ss.push({ title: key, - queryKey: ["home", key, user?.Id], - queryFn: async () => - ( - await getItemsApi(api).getItems({ + queryKey: ["home", key], + queryFn: async () => { + if (section.items) { + const response = await getItemsApi(api).getItems({ userId: user?.Id, limit: section.items?.limit || 25, recursive: true, @@ -320,8 +320,21 @@ export default function index() { sortOrder: section.items?.sortOrder, filters: section.items?.filters, parentId: section.items?.parentId, - }) - ).data.Items || [], + }); + return response.data.Items || []; + } else if (section.nextUp) { + const response = await getTvShowsApi(api).getNextUp({ + userId: user?.Id, + fields: ["MediaSourceCount"], + limit: section.items?.limit || 25, + enableImageTypes: ["Primary", "Backdrop", "Thumb"], + enableResumable: section.items?.enableResumable || false, + enableRewatching: section.items?.enableRewatching || false, + }); + return response.data.Items || []; + } + return []; + }, type: "ScrollingCollectionList", orientation: section?.orientation || "vertical", }); diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 9583bf68..191e42f4 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -80,6 +80,7 @@ export type Home = { export type HomeSection = { orientation?: "horizontal" | "vertical"; items?: HomeSectionItemResolver; + nextUp?: HomeSectionNextUpResolver; }; export type HomeSectionItemResolver = { @@ -92,6 +93,13 @@ export type HomeSectionItemResolver = { filters?: Array; }; +export type HomeSectionNextUpResolver = { + parentId?: string; + limit?: number; + enableResumable?: boolean; + enableRewatching?: boolean; +}; + export type Settings = { home?: Home | null; autoRotate?: boolean;