From db4046267ff72d69a34d5b6cf36a4b50b354a2ca Mon Sep 17 00:00:00 2001
From: herrrta <73949927+herrrta@users.noreply.github.com>
Date: Sun, 5 Jan 2025 02:53:41 -0500
Subject: [PATCH] [Jellyseerr] Add cast/crew results
implements #327
---
.../jellyseerr/[personId].tsx | 206 ++++++++++++++++++
.../jellyseerr/page.tsx | 7 +-
app/(auth)/(tabs)/(search)/_layout.tsx | 1 +
app/(auth)/(tabs)/(search)/index.tsx | 24 +-
components/jellyseerr/Cast.tsx | 34 +++
components/jellyseerr/PersonPoster.tsx | 42 ++++
components/posters/JellyseerrPoster.tsx | 8 +-
components/posters/Poster.tsx | 14 +-
components/series/CastAndCrew.tsx | 2 +-
components/series/CurrentSeries.tsx | 2 +-
components/series/JellyseerrSeasons.tsx | 2 +-
hooks/useJellyseerr.ts | 39 +++-
12 files changed, 354 insertions(+), 27 deletions(-)
create mode 100644 app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/[personId].tsx
create mode 100644 components/jellyseerr/Cast.tsx
create mode 100644 components/jellyseerr/PersonPoster.tsx
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/[personId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/[personId].tsx
new file mode 100644
index 00000000..f2219b26
--- /dev/null
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/[personId].tsx
@@ -0,0 +1,206 @@
+import {router, useLocalSearchParams, useNavigation, useSegments} from "expo-router";
+import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
+import {TouchableOpacity, View} from "react-native";
+import {useQuery} from "@tanstack/react-query";
+import {useJellyseerr} from "@/hooks/useJellyseerr";
+import {useSafeAreaInsets} from "react-native-safe-area-context";
+import {ParallaxScrollView} from "@/components/ParallaxPage";
+import {Text} from "@/components/common/Text";
+import {Animated} from "react-native";
+import {Image} from "expo-image";
+import {OverviewText} from "@/components/OverviewText";
+import {orderBy} from "lodash";
+import {FlashList} from "@shopify/flash-list";
+import {PersonCreditCast} from "@/utils/jellyseerr/server/models/Person";
+import Poster from "@/components/posters/Poster";
+import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon";
+
+const ANIMATION_ENTER = 250
+const ANIMATION_EXIT = 250
+const BACKDROP_DURATION = 5000
+
+export default function page() {
+ const insets = useSafeAreaInsets();
+ const local = useLocalSearchParams();
+ const segments = useSegments();
+ const {jellyseerrApi, jellyseerrUser} = useJellyseerr();
+
+ const { personId } = local as { personId: string; };
+ const from = segments[2];
+
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+
+ const {data, isLoading, isFetching} = useQuery({
+ queryKey: ['jellyseerr', 'person', personId],
+ queryFn: async () => ({
+ details: await jellyseerrApi?.personDetails(personId),
+ combinedCredits: await jellyseerrApi?.personCombinedCredits(personId)
+ }),
+ enabled: !!jellyseerrApi && !!personId
+ });
+
+ const locale = useMemo(() => {
+ return jellyseerrUser?.settings?.locale || 'en'
+ }, [jellyseerrUser]);
+
+ const region = useMemo(
+ () => jellyseerrUser?.settings?.region || 'US',
+ [jellyseerrUser]
+ );
+
+ const castedRoles: PersonCreditCast[] = useMemo(
+ () => orderBy(data?.combinedCredits?.cast, ['voteCount', 'voteAverage'], 'desc'),
+ [data?.combinedCredits]
+ );
+
+ const backdrops = useMemo(
+ () => castedRoles.map(c => c.backdropPath),
+ [data?.combinedCredits]
+ )
+
+ const enterAnimation = useCallback(() =>
+ Animated.timing(fadeAnim, {
+ toValue: 1,
+ duration: ANIMATION_ENTER,
+ useNativeDriver: true,
+ }), [fadeAnim]);
+
+ const exitAnimation = useCallback(() =>
+ Animated.timing(fadeAnim, {
+ toValue: 0,
+ duration: ANIMATION_EXIT,
+ useNativeDriver: true,
+ }), [fadeAnim]);
+
+ useEffect(() => {
+ if (backdrops?.length) {
+ enterAnimation().start()
+ const intervalId = setInterval(
+ () => {
+ exitAnimation().start(end => {
+ if (end.finished)
+ setCurrentIndex((prevIndex) => (prevIndex + 1) % backdrops?.length)
+ })
+
+ },
+ BACKDROP_DURATION
+ );
+
+ return () => clearInterval(intervalId);
+ }
+ }, [backdrops, enterAnimation, exitAnimation, setCurrentIndex, currentIndex]);
+
+ const viewDetails = (credit: PersonCreditCast) => {
+ router.push({
+ //@ts-ignore
+ pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
+ //@ts-ignore
+ params: {
+ ...credit,
+ mediaTitle: credit.title,
+ releaseYear: new Date(credit.releaseDate).getFullYear(),
+ canRequest: "false",
+ posterSrc: jellyseerrApi?.imageProxy(credit.posterPath, 'w300_and_h450_face')
+ }
+ });
+ }
+
+ return (
+
+
+ }
+ logo={
+
+ }
+ >
+
+
+
+
+ {data?.details?.name}
+
+
+ Born {new Date(data?.details?.birthday!!).toLocaleDateString(`${locale}-${region}`, {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })} | {data?.details?.placeOfBirth}
+
+
+
+
+
+
+
+ No results
+
+ }
+ contentInsetAdjustmentBehavior="automatic"
+ ListHeaderComponent={
+ Appearances
+ }
+ renderItem={({item}) =>
+ viewDetails(item)}
+ >
+
+
+ {/*{item.title}*/}
+ as {item.character}
+
+ }
+ keyExtractor={(item) => item.id.toString()}
+ estimatedItemSize={255}
+ numColumns={3}
+ contentContainerStyle={{paddingBottom: 24}}
+ ItemSeparatorComponent={() => (
+
+ )}
+ />
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx
index 31334dcc..805727fd 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx
@@ -30,6 +30,7 @@ import { JellyserrRatings } from "@/components/Ratings";
import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
import DetailFacts from "@/components/jellyseerr/DetailFacts";
import {ItemActions} from "@/components/series/SeriesActions";
+import Cast from "@/components/jellyseerr/Cast";
const Page: React.FC = () => {
const insets = useSafeAreaInsets();
@@ -156,7 +157,7 @@ const Page: React.FC = () => {
height: "100%",
}}
source={{
- uri: `https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${result.backdropPath}`,
+ uri: jellyseerrApi?.imageProxy(result.backdropPath, 'w1920_and_h800_multi_faces'),
}}
/>
) : (
@@ -240,6 +241,10 @@ const Page: React.FC = () => {
className="p-2 border border-neutral-800 bg-neutral-900 rounded-xl"
details={details}
/>
+
diff --git a/app/(auth)/(tabs)/(search)/_layout.tsx b/app/(auth)/(tabs)/(search)/_layout.tsx
index 12cbad20..1119e2a4 100644
--- a/app/(auth)/(tabs)/(search)/_layout.tsx
+++ b/app/(auth)/(tabs)/(search)/_layout.tsx
@@ -36,6 +36,7 @@ export default function SearchLayout() {
}}
/>
+
);
}
diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx
index 7d9ecebe..fcf8119d 100644
--- a/app/(auth)/(tabs)/(search)/index.tsx
+++ b/app/(auth)/(tabs)/(search)/index.tsx
@@ -31,12 +31,13 @@ import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useDebounce } from "use-debounce";
import { useJellyseerr } from "@/hooks/useJellyseerr";
-import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
+import {MovieResult, PersonResult, TvResult} from "@/utils/jellyseerr/server/models/Search";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import { Tag } from "@/components/GenreTags";
import DiscoverSlide from "@/components/jellyseerr/DiscoverSlide";
import { sortBy } from "lodash";
+import PersonPoster from "@/components/jellyseerr/PersonPoster";
type SearchType = "Library" | "Discover";
@@ -191,6 +192,14 @@ export default function search() {
[jellyseerrResults]
);
+ const jellyseerrPersonResults: PersonResult[] | undefined = useMemo(
+ () =>
+ jellyseerrResults?.filter(
+ (r) => r.mediaType === "person"
+ ) as PersonResult[],
+ [jellyseerrResults]
+ );
+
const { data: series, isFetching: l2 } = useQuery({
queryKey: ["search", "series", debouncedSearch],
queryFn: () =>
@@ -486,6 +495,19 @@ export default function search() {
)}
/>
+ (
+
+ )}
+ />
>
)}
diff --git a/components/jellyseerr/Cast.tsx b/components/jellyseerr/Cast.tsx
new file mode 100644
index 00000000..7642382d
--- /dev/null
+++ b/components/jellyseerr/Cast.tsx
@@ -0,0 +1,34 @@
+import {View, ViewProps} from "react-native";
+import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie";
+import {TvDetails} from "@/utils/jellyseerr/server/models/Tv";
+import React from "react";
+import {FlashList} from "@shopify/flash-list";
+import {Text} from "@/components/common/Text";
+import PersonPoster from "@/components/jellyseerr/PersonPoster";
+
+const CastSlide: React.FC<{ details?: MovieDetails | TvDetails } & ViewProps> = ({ details, ...props }) => {
+ return (
+ details?.credits?.cast?.length && details?.credits?.cast?.length > 0 &&
+
+ Cast
+ }
+ estimatedItemSize={15}
+ keyExtractor={item => item?.id?.toString()}
+ renderItem={({item}) =>
+
+ }
+ />
+
+ )
+}
+
+export default CastSlide;
\ No newline at end of file
diff --git a/components/jellyseerr/PersonPoster.tsx b/components/jellyseerr/PersonPoster.tsx
new file mode 100644
index 00000000..57ff9f58
--- /dev/null
+++ b/components/jellyseerr/PersonPoster.tsx
@@ -0,0 +1,42 @@
+import {TouchableOpacity, View, ViewProps} from "react-native";
+import React from "react";
+import {Text} from "@/components/common/Text";
+import Poster from "@/components/posters/Poster";
+import {useRouter, useSegments} from "expo-router";
+import {useJellyseerr} from "@/hooks/useJellyseerr";
+
+interface Props {
+ id: string
+ posterPath?: string
+ name: string
+ subName?: string
+}
+
+const PersonPoster: React.FC = ({
+ id,
+ posterPath,
+ name,
+ subName,
+ ...props
+}) => {
+ const {jellyseerrApi} = useJellyseerr();
+ const router = useRouter();
+ const segments = useSegments();
+ const from = segments[2];
+
+ if (from === "(home)" || from === "(search)" || from === "(libraries)")
+ return (
+ router.push(`/(auth)/(tabs)/${from}/jellyseerr/${id}`)}>
+
+
+ {name}
+ {subName && {subName}}
+
+
+ )
+}
+
+export default PersonPoster;
\ No newline at end of file
diff --git a/components/posters/JellyseerrPoster.tsx b/components/posters/JellyseerrPoster.tsx
index 0de80a75..ad3df50f 100644
--- a/components/posters/JellyseerrPoster.tsx
+++ b/components/posters/JellyseerrPoster.tsx
@@ -20,10 +20,8 @@ const JellyseerrPoster: React.FC = ({
const {jellyseerrUser, jellyseerrApi} = useJellyseerr();
// const imageSource =
- const imageSrc = useMemo(() =>
- item.posterPath ?
- `https://image.tmdb.org/t/p/w300_and_h450_face${item.posterPath}`
- : jellyseerrApi?.axios?.defaults.baseURL + `/images/overseerr_poster_not_found_logo_top.png`,
+ const imageSrc = useMemo(
+ () => jellyseerrApi?.imageProxy(item.posterPath, 'w300_and_h450_face'),
[item, jellyseerrApi]
)
const title = useMemo(() => item.mediaType === MediaType.MOVIE ? item.title : item.name, [item])
@@ -57,7 +55,7 @@ const JellyseerrPoster: React.FC = ({
mediaTitle={title}
releaseYear={releaseYear}
canRequest={canRequest}
- posterSrc={imageSrc}
+ posterSrc={imageSrc!!}
>
diff --git a/components/posters/Poster.tsx b/components/posters/Poster.tsx
index 1787506e..68799f47 100644
--- a/components/posters/Poster.tsx
+++ b/components/posters/Poster.tsx
@@ -1,19 +1,15 @@
-import {
- BaseItemDto,
- BaseItemPerson,
-} from "@jellyfin/sdk/lib/generated-client/models";
import { Image } from "expo-image";
import { View } from "react-native";
type PosterProps = {
- item?: BaseItemDto | BaseItemPerson | null;
+ id?: string | null;
url?: string | null;
showProgress?: boolean;
blurhash?: string | null;
};
-const Poster: React.FC = ({ item, url, blurhash }) => {
- if (!item)
+const Poster: React.FC = ({ id, url, blurhash }) => {
+ if (!id && !url)
return (
= ({ item, url, blurhash }) => {
}
: null
}
- key={item.Id}
- id={item.Id}
+ key={id}
+ id={id!!}
source={
url
? {
diff --git a/components/series/CastAndCrew.tsx b/components/series/CastAndCrew.tsx
index 2b312f0e..01cd1e84 100644
--- a/components/series/CastAndCrew.tsx
+++ b/components/series/CastAndCrew.tsx
@@ -55,7 +55,7 @@ export const CastAndCrew: React.FC = ({ item, loading, ...props }) => {
}}
className="flex flex-col w-28"
>
-
+
{i.Name}
{i.Role}
diff --git a/components/series/CurrentSeries.tsx b/components/series/CurrentSeries.tsx
index e573929a..52851533 100644
--- a/components/series/CurrentSeries.tsx
+++ b/components/series/CurrentSeries.tsx
@@ -29,7 +29,7 @@ export const CurrentSeries: React.FC = ({ item, ...props }) => {
className="flex flex-col space-y-2 w-28"
>
{item.SeriesName}
diff --git a/components/series/JellyseerrSeasons.tsx b/components/series/JellyseerrSeasons.tsx
index d6dedeb6..80bfd254 100644
--- a/components/series/JellyseerrSeasons.tsx
+++ b/components/series/JellyseerrSeasons.tsx
@@ -61,7 +61,7 @@ const RenderItem = ({ item, index }: any) => {
key={item.id}
id={item.id}
source={{
- uri: jellyseerrApi?.tvStillImageProxy(item.stillPath),
+ uri: jellyseerrApi?.imageProxy(item.stillPath),
}}
cachePolicy={"memory-disk"}
contentFit="cover"
diff --git a/hooks/useJellyseerr.ts b/hooks/useJellyseerr.ts
index 8393798d..4546cee1 100644
--- a/hooks/useJellyseerr.ts
+++ b/hooks/useJellyseerr.ts
@@ -28,6 +28,10 @@ import Issue from "@/utils/jellyseerr/server/entity/Issue";
import { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes";
import { writeErrorLog } from "@/utils/log";
import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
+import {
+ CombinedCredit,
+ PersonDetails
+} from "@/utils/jellyseerr/server/models/Person";
interface SearchParams {
query: string;
@@ -55,6 +59,8 @@ export enum Endpoints {
API_V1 = "/api/v1",
SEARCH = "/search",
REQUEST = "/request",
+ PERSON = "/person",
+ COMBINED_CREDITS = "/combined_credits",
MOVIE = "/movie",
RATINGS = "/ratings",
ISSUE = "/issue",
@@ -204,6 +210,22 @@ export class JellyseerrApi {
});
}
+ async personDetails(id: number | string): Promise {
+ return this.axios
+ ?.get(Endpoints.API_V1 + Endpoints.PERSON + `/${id}`)
+ .then((response) => {
+ return response?.data;
+ });
+ }
+
+ async personCombinedCredits(id: number | string): Promise {
+ return this.axios
+ ?.get(Endpoints.API_V1 + Endpoints.PERSON + `/${id}` + Endpoints.COMBINED_CREDITS)
+ .then((response) => {
+ return response?.data;
+ });
+ }
+
async movieRatings(id: number) {
return this.axios
?.get(
@@ -238,14 +260,15 @@ export class JellyseerrApi {
});
}
- tvStillImageProxy(path: string, width: number = 1920, quality: number = 75) {
- return (
- this.axios.defaults.baseURL +
- `/_next/image?` +
- new URLSearchParams(
- `url=https://image.tmdb.org/t/p/original/${path}&w=${width}&q=${quality}`
- ).toString()
- );
+ imageProxy(path?: string, tmdbPath: string = 'original', width: number = 1920, quality: number = 75) {
+ return path ? (
+ this.axios.defaults.baseURL +
+ `/_next/image?` +
+ new URLSearchParams(
+ `url=https://image.tmdb.org/t/p/${tmdbPath}/${path}&w=${width}&q=${quality}`
+ ).toString()
+ ) :
+ this.axios?.defaults.baseURL + `/images/overseerr_poster_not_found_logo_top.png`;
}
async submitIssue(mediaId: number, issueType: IssueType, message: string) {