From a4bc67bc23c02dd70ad5d73f4b64e9146d79b1f8 Mon Sep 17 00:00:00 2001 From: Lance Chant <13349722+lancechant@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:55:52 +0200 Subject: [PATCH] fix: fixing seerr intergration added an alert if seerr if configured via settings, but not authed Added an alert if the auth for seerr expires it should fire cleaned up a little Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com> --- app/(auth)/(tabs)/(home)/settings.tv.tsx | 47 +++++++++++++-------- app/(auth)/(tabs)/(search)/index.tsx | 52 ++++++++++++++++++++++-- hooks/useJellyseerr.ts | 33 ++++++++++++++- providers/JellyfinProvider.tsx | 21 ++++++---- translations/en.json | 3 ++ 5 files changed, 129 insertions(+), 27 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/settings.tv.tsx b/app/(auth)/(tabs)/(home)/settings.tv.tsx index f57a149b6..bd866580b 100644 --- a/app/(auth)/(tabs)/(home)/settings.tv.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tv.tsx @@ -1,5 +1,5 @@ import { SubtitlePlaybackMode } from "@jellyfin/sdk/lib/generated-client"; -import { useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Directory, Paths } from "expo-file-system"; import { Image } from "expo-image"; import { useAtom } from "jotai"; @@ -7,6 +7,7 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Alert, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { toast } from "sonner-native"; import { Text } from "@/components/common/Text"; import { TVPasswordEntryModal } from "@/components/login/TVPasswordEntryModal"; import { TVPINEntryModal } from "@/components/login/TVPINEntryModal"; @@ -21,8 +22,7 @@ import { TVSettingsToggle, } from "@/components/tv"; import { useScaledTVTypography } from "@/constants/TVTypography"; -import { useJellyseerr } from "@/hooks/useJellyseerr"; -import { useJellyseerrConnect } from "@/hooks/useJellyseerrConnect"; +import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr"; import { useTVOptionModal } from "@/hooks/useTVOptionModal"; import { useTVUserSwitchModal } from "@/hooks/useTVUserSwitchModal"; import { APP_LANGUAGES } from "@/i18n"; @@ -61,9 +61,8 @@ export default function SettingsTV() { const { showUserSwitchModal } = useTVUserSwitchModal(); const typography = useScaledTVTypography(); const queryClient = useQueryClient(); - const { jellyseerrApi, clearAllJellyseerData } = useJellyseerr(); - const { connecting: jellyseerrConnecting, connect: jellyseerrConnect } = - useJellyseerrConnect(); + const { jellyseerrApi, setJellyseerrUser, clearAllJellyseerData } = + useJellyseerr(); // Jellyseerr state const [jellyseerrServerUrl, setJellyseerrServerUrl] = useState( @@ -81,11 +80,27 @@ export default function SettingsTV() { updateSettings({ jellyseerrServerUrl: url || undefined }); }, [jellyseerrServerUrl, updateSettings]); - const handleJellyseerrConnect = useCallback(async () => { - const url = jellyseerrServerUrl.trim(); - if (!url) return; - await jellyseerrConnect(url, jellyseerrPassword); - }, [jellyseerrServerUrl, jellyseerrPassword, jellyseerrConnect]); + const jellyseerrLoginMutation = useMutation({ + mutationFn: async () => { + const url = jellyseerrServerUrl.trim(); + if (!url) throw new Error("Missing server url"); + if (!user?.Name) throw new Error("Missing user info"); + const tempApi = new JellyseerrApi(url); + const testResult = await tempApi.test(); + if (!testResult.isValid) throw new Error("Invalid server url"); + return tempApi.login(user.Name, jellyseerrPassword); + }, + onSuccess: (loggedInUser) => { + setJellyseerrUser(loggedInUser); + updateSettings({ jellyseerrServerUrl: jellyseerrServerUrl.trim() }); + }, + onError: () => { + toast.error(t("jellyseerr.failed_to_login")); + }, + onSettled: () => { + setJellyseerrPassword(""); + }, + }); const handleDisconnectJellyseerr = useCallback(() => { clearAllJellyseerData(); @@ -940,7 +955,7 @@ export default function SettingsTV() { } onChangeText={setJellyseerrServerUrl} onBlur={handleJellyseerrUrlBlur} - disabled={isJellyseerrLocked || jellyseerrConnecting} + disabled={isJellyseerrLocked || jellyseerrLoginMutation.isPending} /> {!isJellyseerrConnected && !isJellyseerrLocked && ( <> @@ -956,17 +971,17 @@ export default function SettingsTV() { } onChangeText={setJellyseerrPassword} secureTextEntry - disabled={jellyseerrConnecting} + disabled={jellyseerrLoginMutation.isPending} /> jellyseerrLoginMutation.mutate()} + disabled={jellyseerrLoginMutation.isPending} /> )} diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 29461b49a..61c9107dd 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -7,7 +7,12 @@ import { useAsyncDebouncer } from "@tanstack/react-pacer"; import { useQuery } from "@tanstack/react-query"; import axios from "axios"; import { Image } from "expo-image"; -import { useLocalSearchParams, useNavigation, useSegments } from "expo-router"; +import { + useIsFocused, + useLocalSearchParams, + useNavigation, + useSegments, +} from "expo-router"; import { useAtom } from "jotai"; import { orderBy, uniqBy } from "lodash"; import { @@ -20,7 +25,13 @@ import { useState, } from "react"; import { useTranslation } from "react-i18next"; -import { Platform, ScrollView, TouchableOpacity, View } from "react-native"; +import { + Alert, + Platform, + ScrollView, + TouchableOpacity, + View, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; import { Text } from "@/components/common/Text"; @@ -41,7 +52,10 @@ import { SearchItemWrapper } from "@/components/search/SearchItemWrapper"; import { SearchTabButtons } from "@/components/search/SearchTabButtons"; import { TVSearchPage } from "@/components/search/TVSearchPage"; import useRouter from "@/hooks/useAppRouter"; -import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { + useJellyseerr, + validateJellyseerrSession, +} from "@/hooks/useJellyseerr"; import { useTVItemActionModal } from "@/hooks/useTVItemActionModal"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; @@ -106,8 +120,40 @@ export default function SearchPage() { const [api] = useAtom(apiAtom); + const isFocused = useIsFocused(); const { settings } = useSettings(); const { jellyseerrApi } = useJellyseerr(); + + // Alert when seerr server is configured but user hasn't connected (only when focused) + useEffect(() => { + if (!isFocused || !settings?.jellyseerrServerUrl || jellyseerrApi) return; + Alert.alert( + t("jellyseerr.connect_to_jellyseerr", "Connect to Jellyseerr"), + t( + "jellyseerr.connect_in_settings", + "Jellyseerr is available. Connect in Settings to enable request features.", + ), + ); + }, []); + + // Validate jellyseerr session when switching to Discover + useEffect(() => { + if ( + searchType !== "Discover" || + !jellyseerrApi || + !settings?.jellyseerrServerUrl + ) + return; + validateJellyseerrSession(settings.jellyseerrServerUrl).then((status) => { + if (status.valid) return; + Alert.alert( + t( + "jellyseerr.session_expired_connect_again", + "Your Jellyseerr session has expired. Please reconnect in Settings.", + ), + ); + }); + }, [searchType, jellyseerrApi, settings?.jellyseerrServerUrl, t]); const [jellyseerrOrderBy, setJellyseerrOrderBy] = useState( JellyseerrSearchSort[ diff --git a/hooks/useJellyseerr.ts b/hooks/useJellyseerr.ts index 4ae918d85..e6d679436 100644 --- a/hooks/useJellyseerr.ts +++ b/hooks/useJellyseerr.ts @@ -70,6 +70,36 @@ export const clearJellyseerrStorageData = () => { storage.remove(JELLYSEERR_COOKIES); }; +export type JellyseerrSessionStatus = + | { valid: true } + | { valid: false; reason: "no_session" | "expired" }; + +export async function validateJellyseerrSession( + serverUrl: string, +): Promise { + const user = storage.get(JELLYSEERR_USER); + const cookies = storage.get(JELLYSEERR_COOKIES); + + console.log( + "Validating Jellyseerr session with server URL:", + serverUrl, + !user, + !cookies, + ); + if (!user || !cookies) { + return { valid: false, reason: "no_session" }; + } + + try { + const api = new JellyseerrApi(serverUrl); + await api.axios.get(Endpoints.API_V1 + Endpoints.STATUS); + return { valid: true }; + } catch { + clearJellyseerrStorageData(); + return { valid: false, reason: "expired" }; + } +} + export enum Endpoints { STATUS = "/status", API_V1 = "/api/v1", @@ -450,7 +480,8 @@ export const useJellyseerr = () => { clearJellyseerrStorageData(); setJellyseerrUser(undefined); updateSettings({ jellyseerrServerUrl: undefined }); - }, []); + queryClient.removeQueries({ queryKey: ["search", "jellyseerr"] }); + }, [queryClient]); const requestMedia = useCallback( (title: string, request: MediaRequestBody, onSuccess?: () => void) => { diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 8608222b8..32e6ab97c 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -247,12 +247,6 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ } }, [api, secret, headers, jellyfin]); - useEffect(() => { - (async () => { - await refreshStreamyfinPluginSettings(); - })(); - }, []); - useEffect(() => { store.set(apiAtom, api); }, [api]); @@ -553,7 +547,20 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ ); // Refresh plugin settings - await refreshStreamyfinPluginSettings(); + const recentPluginSettings = await refreshStreamyfinPluginSettings(); + if (recentPluginSettings?.jellyseerrServerUrl?.value) { + const jellyseerrApi = new JellyseerrApi( + recentPluginSettings.jellyseerrServerUrl.value, + ); + await jellyseerrApi.test().then((result) => { + if (result.isValid && result.requiresPass) { + jellyseerrApi + .login(username, password) + .then(setJellyseerrUser) + .catch(console.error); + } + }); + } } }, onError: (error) => { diff --git a/translations/en.json b/translations/en.json index 733f94b19..0c699bafb 100644 --- a/translations/en.json +++ b/translations/en.json @@ -823,6 +823,9 @@ "request_button": "Request", "are_you_sure_you_want_to_request_all_seasons": "Are you sure you want to request all seasons?", "failed_to_login": "Failed to Login", + "connect_to_jellyseerr": "Connect to Jellyseerr", + "session_expired_connect_again": "Your Jellyseerr session has expired. Please reconnect in Settings.", + "connect_in_settings": "Jellyseerr is available. Connect in Settings to enable request features.", "cast": "Cast", "details": "Details", "status": "Status",