mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-19 02:34:17 +01:00
refactor(casting): extract useCastPlayerItem hook
This commit is contained in:
@@ -39,6 +39,7 @@ import { ChromecastSettingsMenu } from "@/components/chromecast/ChromecastSettin
|
|||||||
import { useChromecastSegments } from "@/components/chromecast/hooks/useChromecastSegments";
|
import { useChromecastSegments } from "@/components/chromecast/hooks/useChromecastSegments";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { useCasting } from "@/hooks/useCasting";
|
import { useCasting } from "@/hooks/useCasting";
|
||||||
|
import { useCastPlayerItem } from "@/hooks/useCastPlayerItem";
|
||||||
import { useCastSelection } from "@/hooks/useCastSelection";
|
import { useCastSelection } from "@/hooks/useCastSelection";
|
||||||
import { useTrickplay } from "@/hooks/useTrickplay";
|
import { useTrickplay } from "@/hooks/useTrickplay";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
@@ -87,35 +88,12 @@ export default function CastingPlayerScreen() {
|
|||||||
// Last stable playback position (seconds), for resuming across reloads.
|
// Last stable playback position (seconds), for resuming across reloads.
|
||||||
const resumePositionRef = useRef(0);
|
const resumePositionRef = useRef(0);
|
||||||
|
|
||||||
// Fetch full item data from Jellyfin by ID
|
// Fetch full item data from Jellyfin by ID and derive the effective item
|
||||||
const [fetchedItem, setFetchedItem] = useState<BaseItemDto | null>(null);
|
const { fetchedItem, currentItem } = useCastPlayerItem({
|
||||||
|
api,
|
||||||
useEffect(() => {
|
user,
|
||||||
const controller = new AbortController();
|
mediaStatus,
|
||||||
|
});
|
||||||
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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Sync refs whenever mediaStatus provides a new position
|
// Sync refs whenever mediaStatus provides a new position
|
||||||
@@ -150,46 +128,6 @@ export default function CastingPlayerScreen() {
|
|||||||
}
|
}
|
||||||
}, [mediaStatus?.streamPosition, mediaStatus?.playerState]);
|
}, [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
|
// Derive state from raw Chromecast hooks
|
||||||
const progress = liveProgress; // Use live-updating progress
|
const progress = liveProgress; // Use live-updating progress
|
||||||
const duration = mediaStatus?.mediaInfo?.streamDuration ?? 0;
|
const duration = mediaStatus?.mediaInfo?.streamDuration ?? 0;
|
||||||
|
|||||||
94
hooks/useCastPlayerItem.ts
Normal file
94
hooks/useCastPlayerItem.ts
Normal file
@@ -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<BaseItemDto | null>(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 };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user