hooks for jellyfin data, playback, tmdb, player chrome

This commit is contained in:
2026-03-25 19:11:34 +02:00
parent 996a85de76
commit df17c7ab95
27 changed files with 3066 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
import { useEffect, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { getMovieFull, getTvShowFull } from '../api/tmdb'
import type { BaseItemDto } from '../api/types'
import { getTmdbId } from '../lib/item-types'
/**
* Returns the YouTube key of the first official Trailer/Teaser for the
* given item, lazy-fetched only after the user has hovered the card for
* `delayMs`. Avoids slamming TMDB when users sweep the cursor across a
* grid.
*
* The fetch is gated by an `armed` boolean toggled by the consumer
* (PosterCard's hover effect) so we don't dispatch network requests for
* every card on the screen, only the ones the user lingers on.
*/
export function useHoverTrailer(
item: BaseItemDto | null | undefined,
hovered: boolean,
enabled: boolean,
delayMs = 700,
): { videoKey: string | null; ready: boolean } {
const [armed, setArmed] = useState(false)
// Arm only after the user has stayed on the card for delayMs.
useEffect(() => {
if (!hovered || !enabled) {
setArmed(false)
return
}
const id = setTimeout(() => setArmed(true), delayMs)
return () => clearTimeout(id)
}, [hovered, enabled, delayMs])
const tmdbId = getTmdbId(item)
const numericId = tmdbId ? Number(tmdbId) : null
const isTv = item?.Type === 'Series'
const movieQ = useQuery({
queryKey: ['tmdb', 'movie-full', numericId],
queryFn: () => (numericId ? getMovieFull(numericId) : null),
enabled: !!numericId && !isTv && armed,
staleTime: 24 * 60 * 60 * 1000,
})
const tvQ = useQuery({
queryKey: ['tmdb', 'tv-full', numericId],
queryFn: () => (numericId ? getTvShowFull(numericId) : null),
enabled: !!numericId && isTv && armed,
staleTime: 24 * 60 * 60 * 1000,
})
const videos = (isTv ? tvQ.data?.videos?.results : movieQ.data?.videos?.results) || []
// Prefer official trailers, then teasers; restricted to YouTube since
// we render via the YT iframe player.
const pickPriority = (t: string) => (t === 'Trailer' ? 0 : t === 'Teaser' ? 1 : 2)
const trailer = [...videos]
.filter(v => v.site === 'YouTube' && v.key)
.sort((a, b) => {
if (a.official !== b.official) return a.official ? -1 : 1
return pickPriority(a.type) - pickPriority(b.type)
})[0]
return {
videoKey: trailer?.key || null,
ready: armed && (movieQ.isFetched || tvQ.isFetched),
}
}