diff --git a/CLAUDE.md b/CLAUDE.md index cc3b0a53..5cfd2990 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Streamyfin is a cross-platform Jellyfin video streaming client built with Expo (React Native). It supports mobile (iOS/Android) and TV platforms, with features including offline downloads, Chromecast support, and Jellyseerr integration. +Streamyfin is a cross-platform Jellyfin video streaming client built with Expo (React Native). It supports mobile (iOS/Android) and TV platforms, with features including offline downloads, Chromecast support, and Seerr integration. ## Development Commands diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/company/[companyId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/company/[companyId].tsx index 359ebfd1..4f129694 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/company/[companyId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/company/[companyId].tsx @@ -57,7 +57,7 @@ export default function CompanyPage() { ), "id", ) ?? [], - [data], + [data, isSeerrMovieOrTvResult], ); const backdrops = useMemo( diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/genre/[genreId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/genre/[genreId].tsx index a61cff40..5858d96e 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/genre/[genreId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/genre/[genreId].tsx @@ -20,7 +20,7 @@ export default function GenrePage() { }; const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ - queryKey: ["seerr", "company", type, genreId], + queryKey: ["seerr", "genre", type, genreId], queryFn: async ({ pageParam }) => { const params: any = { page: Number(pageParam), diff --git a/components/TrackSheet.tsx b/components/TrackSheet.tsx index 4dda0564..8c5f3837 100644 --- a/components/TrackSheet.tsx +++ b/components/TrackSheet.tsx @@ -26,7 +26,7 @@ export const TrackSheet: React.FC = ({ const streams = useMemo( () => source?.MediaStreams?.filter((x) => x.Type === streamType), - [source], + [source, streamType], ); const selectedSteam = useMemo( diff --git a/components/library/LibraryItemCard.tsx b/components/library/LibraryItemCard.tsx index 09de500d..c84364ce 100644 --- a/components/library/LibraryItemCard.tsx +++ b/components/library/LibraryItemCard.tsx @@ -51,7 +51,7 @@ export const LibraryItemCard: React.FC = ({ library, ...props }) => { api, item: library, }), - [library], + [api, library], ); const itemType = useMemo(() => { diff --git a/components/music/MusicAlbumCard.tsx b/components/music/MusicAlbumCard.tsx index 11e78610..773b96c0 100644 --- a/components/music/MusicAlbumCard.tsx +++ b/components/music/MusicAlbumCard.tsx @@ -23,6 +23,7 @@ export const MusicAlbumCard: React.FC = ({ album, width = 130 }) => { ); const handlePress = useCallback(() => { + if (!album.Id) return; router.push(`/music/album/${album.Id}`); }, [router, album.Id]); diff --git a/components/music/MusicAlbumRowCard.tsx b/components/music/MusicAlbumRowCard.tsx index e794a793..ffb4b6cb 100644 --- a/components/music/MusicAlbumRowCard.tsx +++ b/components/music/MusicAlbumRowCard.tsx @@ -24,6 +24,7 @@ export const MusicAlbumRowCard: React.FC = ({ album }) => { ); const handlePress = useCallback(() => { + if (!album.Id) return; router.push(`/music/album/${album.Id}`); }, [router, album.Id]); diff --git a/components/music/MusicArtistCard.tsx b/components/music/MusicArtistCard.tsx index a9bfc61b..f3ccb95b 100644 --- a/components/music/MusicArtistCard.tsx +++ b/components/music/MusicArtistCard.tsx @@ -25,6 +25,7 @@ export const MusicArtistCard: React.FC = ({ artist }) => { ); const handlePress = useCallback(() => { + if (!artist.Id) return; router.push(`/music/artist/${artist.Id}`); }, [router, artist.Id]); diff --git a/components/posters/SeerrPoster.tsx b/components/posters/SeerrPoster.tsx index 399ac386..d9c0c425 100644 --- a/components/posters/SeerrPoster.tsx +++ b/components/posters/SeerrPoster.tsx @@ -170,7 +170,7 @@ const SeerrPoster: React.FC = ({ className='absolute right-1 top-1 text-right bg-black border border-neutral-800/50' text={mediaRequest?.requestedBy.displayName} /> - {requestedSeasons.length > 0 && ( + {(requestedSeasons?.length ?? 0) > 0 && ( } - keyExtractor={(item) => item?.id?.toString()} + keyExtractor={(item) => item?.id?.toString() ?? ""} contentContainerStyle={{ paddingHorizontal: 16 }} renderItem={({ item }) => ( r.iso_3166_1 === region, )?.release_dates as TmdbRelease["release_dates"], - [details], + [details, region], ); // Release date types: @@ -82,7 +82,7 @@ const DetailFacts: React.FC< if (firstAirDate) { return new Date(firstAirDate).toLocaleDateString(locale, dateOpts); } - }, [details]); + }, [details, locale]); const nextAirDate = useMemo(() => { const firstAirDate = (details as TvDetails)?.firstAirDate; @@ -90,7 +90,7 @@ const DetailFacts: React.FC< if (nextAirDate && firstAirDate !== nextAirDate) { return new Date(nextAirDate).toLocaleDateString(locale, dateOpts); } - }, [details]); + }, [details, locale]); const revenue = useMemo( () => @@ -98,7 +98,7 @@ const DetailFacts: React.FC< style: "currency", currency: "USD", }), - [details], + [details, locale], ); const budget = useMemo( @@ -107,7 +107,7 @@ const DetailFacts: React.FC< style: "currency", currency: "USD", }), - [details], + [details, locale], ); const streamingProviders = useMemo( @@ -115,7 +115,7 @@ const DetailFacts: React.FC< details?.watchProviders?.find( (provider) => provider.iso_3166_1 === region, )?.flatrate, - [details], + [details, region], ); const networks = useMemo(() => (details as TvDetails)?.networks, [details]); diff --git a/components/seerr/GridSkeleton.tsx b/components/seerr/GridSkeleton.tsx index 75431cd7..a7890312 100644 --- a/components/seerr/GridSkeleton.tsx +++ b/components/seerr/GridSkeleton.tsx @@ -1,16 +1,10 @@ +import React from "react"; import { View } from "react-native"; -interface Props { - index: number; -} // Dev note might be a good idea to standardize skeletons across the app and have one "file" for it. -export const GridSkeleton: React.FC = ({ index }) => { +export const GridSkeleton = React.memo(() => { return ( - + @@ -18,4 +12,4 @@ export const GridSkeleton: React.FC = ({ index }) => { ); -}; +}); diff --git a/components/seerr/PersonPoster.tsx b/components/seerr/PersonPoster.tsx index 821cf52c..e9b77b55 100644 --- a/components/seerr/PersonPoster.tsx +++ b/components/seerr/PersonPoster.tsx @@ -40,6 +40,8 @@ const PersonPoster: React.FC = ({ ); + + return null; }; export default PersonPoster; diff --git a/components/seerr/RequestModal.tsx b/components/seerr/RequestModal.tsx index 53d23765..b3069458 100644 --- a/components/seerr/RequestModal.tsx +++ b/components/seerr/RequestModal.tsx @@ -38,7 +38,16 @@ const RequestModal = forwardRef< Props & Omit >( ( - { id, title, requestBody, type, isAnime = false, onRequested, onDismiss }, + { + id, + title, + requestBody, + type, + isAnime = false, + is4k, + onRequested, + onDismiss, + }, ref, ) => { const { seerrApi, seerrUser, requestMedia } = useSeerr(); @@ -258,7 +267,8 @@ const RequestModal = forwardRef< const request = useCallback(() => { const body = { - is4k: defaultService?.is4k || defaultServiceDetails?.server.is4k, + is4k: + is4k ?? defaultService?.is4k ?? defaultServiceDetails?.server.is4k, profileId: defaultProfile?.id, rootFolder: defaultFolder?.path, tags: defaultTags.map((t) => t.id), @@ -274,11 +284,18 @@ const RequestModal = forwardRef< onRequested, ); }, [ + is4k, + defaultService?.is4k, + defaultServiceDetails?.server.is4k, requestBody, requestOverrides, defaultProfile, defaultFolder, defaultTags, + requestMedia, + seasonTitle, + title, + onRequested, ]); return ( diff --git a/components/seerr/SeerrIndexPage.tsx b/components/seerr/SeerrIndexPage.tsx index 6ef54fa6..edfd2b0d 100644 --- a/components/seerr/SeerrIndexPage.tsx +++ b/components/seerr/SeerrIndexPage.tsx @@ -113,7 +113,7 @@ export const SeerrIndexPage: React.FC = ({ ], order || "desc", ), - [seerrResults, sortingType, order], + [seerrResults, sortingType, order, searchQuery], ); const seerrTvResults = useMemo( @@ -125,7 +125,7 @@ export const SeerrIndexPage: React.FC = ({ ], order || "desc", ), - [seerrResults, sortingType, order], + [seerrResults, sortingType, order, searchQuery], ); const seerrPersonResults = useMemo( @@ -137,7 +137,7 @@ export const SeerrIndexPage: React.FC = ({ ], order || "desc", ), - [seerrResults, sortingType, order], + [seerrResults, sortingType, order, searchQuery], ); if (!searchQuery.length) diff --git a/components/seerr/discover/CompanySlide.tsx b/components/seerr/discover/CompanySlide.tsx index 58e614fa..f29d849e 100644 --- a/components/seerr/discover/CompanySlide.tsx +++ b/components/seerr/discover/CompanySlide.tsx @@ -26,7 +26,7 @@ const CompanySlide: React.FC< pathname: `/(auth)/(tabs)/${from}/seerr/company/${id}` as any, params: { id, image, name, type: slide.type }, }), - [slide], + [router, from, slide.type], ); return ( diff --git a/components/seerr/discover/Discover.tsx b/components/seerr/discover/Discover.tsx index 7a926eec..bd099359 100644 --- a/components/seerr/discover/Discover.tsx +++ b/components/seerr/discover/Discover.tsx @@ -23,7 +23,6 @@ const Discover: React.FC = ({ sliders }) => { sortBy( (sliders ?? []).filter((s) => s.enabled), "order", - "asc", ), [sliders], ); diff --git a/components/seerr/discover/GenericSlideCard.tsx b/components/seerr/discover/GenericSlideCard.tsx index 5ee68dd9..36b9ab4d 100644 --- a/components/seerr/discover/GenericSlideCard.tsx +++ b/components/seerr/discover/GenericSlideCard.tsx @@ -1,6 +1,6 @@ import { Image, type ImageContentFit } from "expo-image"; import { LinearGradient } from "expo-linear-gradient"; -import type React from "react"; +import React from "react"; import { StyleSheet, View, type ViewProps } from "react-native"; import { Text } from "@/components/common/Text"; @@ -67,4 +67,4 @@ const GenericSlideCard: React.FC< ); -export default GenericSlideCard; +export default React.memo(GenericSlideCard); diff --git a/components/seerr/discover/GenreSlide.tsx b/components/seerr/discover/GenreSlide.tsx index e65534d0..7f1a6a83 100644 --- a/components/seerr/discover/GenreSlide.tsx +++ b/components/seerr/discover/GenreSlide.tsx @@ -23,9 +23,8 @@ const GenreSlide: React.FC = ({ slide, ...props }) => { pathname: `/(auth)/(tabs)/${from}/seerr/genre/${genre.id}` as any, params: { type: slide.type, name: genre.name }, }), - [slide], + [router, from, slide.type], ); - const { data } = useQuery({ queryKey: ["seerr", "discover", slide.type, slide.id], queryFn: async () => { diff --git a/components/seerr/discover/MovieTvSlide.tsx b/components/seerr/discover/MovieTvSlide.tsx index 6291bab1..d698701c 100644 --- a/components/seerr/discover/MovieTvSlide.tsx +++ b/components/seerr/discover/MovieTvSlide.tsx @@ -64,7 +64,7 @@ const MovieTvSlide: React.FC = ({ .flatMap((p) => p?.results.filter((r) => isSeerrMovieOrTvResult(r))), "id", ), - [data], + [data, isSeerrMovieOrTvResult], ); return ( diff --git a/components/seerr/discover/Slide.tsx b/components/seerr/discover/Slide.tsx index 41b4667e..7f794a3c 100644 --- a/components/seerr/discover/Slide.tsx +++ b/components/seerr/discover/Slide.tsx @@ -14,10 +14,7 @@ export interface SlideProps { interface Props extends SlideProps { data: T[]; - renderItem: ( - item: T, - index: number, - ) => React.ComponentType | React.ReactElement | null | undefined; + renderItem: (item: T, index: number) => React.ReactElement | null; keyExtractor: (item: T) => string; onEndReached?: (() => void) | null | undefined; } diff --git a/hooks/useSeerr.ts b/hooks/useSeerr.ts index 679677d4..c0e7ce3a 100644 --- a/hooks/useSeerr.ts +++ b/hooks/useSeerr.ts @@ -424,6 +424,7 @@ export class SeerrApi { }, (error) => { console.error("Seerr request error", error); + return Promise.reject(error); }, ); } @@ -448,7 +449,7 @@ export const useSeerr = () => { clearSeerrStorageData(); setSeerrUser(undefined); updateSettings({ seerrServerUrl: undefined }); - }, []); + }, [setSeerrUser, updateSettings]); const requestMedia = useCallback( (title: string, request: MediaRequestBody, onSuccess?: () => void) => { @@ -474,18 +475,20 @@ export const useSeerr = () => { } }); }, - [seerrApi], + [seerrApi, queryClient], ); - const isSeerrMovieOrTvResult = ( - items: any | null | undefined, - ): items is MovieResult | TvResult => { - return ( - items && - Object.hasOwn(items, "mediaType") && - (items.mediaType === MediaType.MOVIE || items.mediaType === MediaType.TV) - ); - }; + const isSeerrMovieOrTvResult = useCallback( + (items: any | null | undefined): items is MovieResult | TvResult => { + return ( + items && + Object.hasOwn(items, "mediaType") && + (items.mediaType === MediaType.MOVIE || + items.mediaType === MediaType.TV) + ); + }, + [], + ); const getTitle = ( item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast, diff --git a/scripts/typecheck.js b/scripts/typecheck.js index 73914e8c..1771dc60 100644 --- a/scripts/typecheck.js +++ b/scripts/typecheck.js @@ -176,7 +176,7 @@ function runTypeCheck() { } catch (error) { const errorOutput = (error && (error.stderr || error.stdout)) || ""; - // Filter out jellyseerr utils errors - this is a third-party git submodule + // Filter out seerr utils errors - this is a third-party git submodule // that generates a large volume of known type errors const filteredLines = errorOutput.split("\n").filter((line) => { const trimmedLine = line.trim(); diff --git a/translations/en.json b/translations/en.json index 55a6c198..d6ae888a 100644 --- a/translations/en.json +++ b/translations/en.json @@ -719,7 +719,7 @@ "requested_by": "Requested by {{user}}", "unknown_user": "Unknown User", "toasts": { - "seer_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0", + "seerr_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0", "seerr_test_failed": "Seerr test failed. Please try again.", "failed_to_test_seerr_server_url": "Failed to test Seerr server URL", "issue_submitted": "Issue Submitted!",