diff --git a/app/(auth)/casting-player.tsx b/app/(auth)/casting-player.tsx index 48c916532..28916a99c 100644 --- a/app/(auth)/casting-player.tsx +++ b/app/(auth)/casting-player.tsx @@ -39,6 +39,7 @@ import { ChromecastSettingsMenu } from "@/components/chromecast/ChromecastSettin import { useChromecastSegments } from "@/components/chromecast/hooks/useChromecastSegments"; import { Text } from "@/components/common/Text"; import { useCasting } from "@/hooks/useCasting"; +import { useCastPlayerItem } from "@/hooks/useCastPlayerItem"; import { useCastSelection } from "@/hooks/useCastSelection"; import { useTrickplay } from "@/hooks/useTrickplay"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; @@ -87,35 +88,12 @@ export default function CastingPlayerScreen() { // Last stable playback position (seconds), for resuming across reloads. const resumePositionRef = useRef(0); - // Fetch full item data from Jellyfin by ID - const [fetchedItem, setFetchedItem] = useState(null); - - useEffect(() => { - const controller = new AbortController(); - - const fetchItemData = async () => { - const itemId = mediaStatus?.mediaInfo?.contentId; - if (!itemId || !api || !user?.Id) return; - - try { - const res = await getUserLibraryApi(api).getItem( - { itemId, userId: user.Id }, - { signal: controller.signal }, - ); - if (!controller.signal.aborted) { - setFetchedItem(res.data); - } - } catch (error) { - if (error instanceof DOMException && error.name === "AbortError") - return; - console.error("[Casting Player] Failed to fetch item:", error); - } - }; - - fetchItemData(); - - return () => controller.abort(); - }, [mediaStatus?.mediaInfo?.contentId, api, user?.Id]); + // Fetch full item data from Jellyfin by ID and derive the effective item + const { fetchedItem, currentItem } = useCastPlayerItem({ + api, + user, + mediaStatus, + }); useEffect(() => { // Sync refs whenever mediaStatus provides a new position @@ -150,46 +128,6 @@ export default function CastingPlayerScreen() { } }, [mediaStatus?.streamPosition, mediaStatus?.playerState]); - // Extract item from customData, or use fetched item, or create a minimal fallback - const currentItem = useMemo(() => { - // Priority 1: Use fetched item from API (most reliable) - if (fetchedItem) { - return fetchedItem; - } - - // Priority 2: Try customData from mediaStatus - const customData = mediaStatus?.mediaInfo?.customData as BaseItemDto | null; - if ( - customData?.Type && - (customData.ImageTags || customData.MediaSources || customData.Id) - ) { - // Use customData if it has a real Type AND meaningful metadata - // (rules out placeholder objects that lack image tags, media sources, or an ID) - return customData; - } - - // Priority 3: Create minimal fallback while loading - if (mediaStatus?.mediaInfo) { - const { contentId, metadata } = mediaStatus.mediaInfo; - // Derive type from metadata if available, otherwise omit to avoid - // misrepresenting episodes as movies - let metadataType: string | undefined; - if (metadata?.type === "movie") { - metadataType = "Movie"; - } else if (metadata?.type === "tvShow") { - metadataType = "Episode"; - } - return { - Id: contentId, - Name: metadata?.title || "Unknown", - ...(metadataType ? { Type: metadataType } : {}), - ServerId: "", - } as BaseItemDto; - } - - return null; - }, [fetchedItem, mediaStatus?.mediaInfo]); - // Derive state from raw Chromecast hooks const progress = liveProgress; // Use live-updating progress const duration = mediaStatus?.mediaInfo?.streamDuration ?? 0; diff --git a/hooks/useCastPlayerItem.ts b/hooks/useCastPlayerItem.ts new file mode 100644 index 000000000..8f7c460a0 --- /dev/null +++ b/hooks/useCastPlayerItem.ts @@ -0,0 +1,94 @@ +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto, UserDto } from "@jellyfin/sdk/lib/generated-client"; +import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; +import { useEffect, useMemo, useState } from "react"; +import type { MediaStatus } from "react-native-google-cast"; + +interface UseCastPlayerItemParams { + api: Api | null; + user: UserDto | null; + mediaStatus: MediaStatus | null; +} + +interface UseCastPlayerItemResult { + fetchedItem: BaseItemDto | null; + currentItem: BaseItemDto | null; +} + +export function useCastPlayerItem({ + api, + user, + mediaStatus, +}: UseCastPlayerItemParams): UseCastPlayerItemResult { + // Fetch full item data from Jellyfin by ID + const [fetchedItem, setFetchedItem] = useState(null); + + useEffect(() => { + const controller = new AbortController(); + + const fetchItemData = async () => { + const itemId = mediaStatus?.mediaInfo?.contentId; + if (!itemId || !api || !user?.Id) return; + + try { + const res = await getUserLibraryApi(api).getItem( + { itemId, userId: user.Id }, + { signal: controller.signal }, + ); + if (!controller.signal.aborted) { + setFetchedItem(res.data); + } + } catch (error) { + if (error instanceof DOMException && error.name === "AbortError") + return; + console.error("[Casting Player] Failed to fetch item:", error); + } + }; + + fetchItemData(); + + return () => controller.abort(); + }, [mediaStatus?.mediaInfo?.contentId, api, user?.Id]); + + // Extract item from customData, or use fetched item, or create a minimal fallback + const currentItem = useMemo(() => { + // Priority 1: Use fetched item from API (most reliable) + if (fetchedItem) { + return fetchedItem; + } + + // Priority 2: Try customData from mediaStatus + const customData = mediaStatus?.mediaInfo?.customData as BaseItemDto | null; + if ( + customData?.Type && + (customData.ImageTags || customData.MediaSources || customData.Id) + ) { + // Use customData if it has a real Type AND meaningful metadata + // (rules out placeholder objects that lack image tags, media sources, or an ID) + return customData; + } + + // Priority 3: Create minimal fallback while loading + if (mediaStatus?.mediaInfo) { + const { contentId, metadata } = mediaStatus.mediaInfo; + // Derive type from metadata if available, otherwise omit to avoid + // misrepresenting episodes as movies + let metadataType: string | undefined; + if (metadata?.type === "movie") { + metadataType = "Movie"; + } else if (metadata?.type === "tvShow") { + metadataType = "Episode"; + } + return { + Id: contentId, + Name: metadata?.title || "Unknown", + ...(metadataType ? { Type: metadataType } : {}), + ServerId: "", + } as BaseItemDto; + } + + return null; + }, [fetchedItem, mediaStatus?.mediaInfo]); + + return { fetchedItem, currentItem }; +}