import { useMemo } from 'react' import { useNavigate } from 'react-router-dom' import { motion } from 'framer-motion' import { ArrowRight, Star, MoonStars } from '../../lib/icons' import { useTmdbDiscoverMovies, useTmdbDiscoverTv, useTmdbTrending } from '../../hooks/use-tmdb' import { useLibraryByTmdbId, useLibraryGenreDistribution } from '../../hooks/use-jellyfin' import { usePreferencesStore } from '../../stores/preferences-store' import { filterToMissing } from '../../pages/discover/helpers' import { tmdbMovieGenreId, tmdbTvGenreId } from '../../lib/tmdb-genres' const TMDB_IMG = 'https://image.tmdb.org/t/p' interface Props { kind: 'movie' | 'tv' } /** * Personalized "tonight" pick. Reads the user's library genre * distribution, finds their top genre, and picks one highly-rated * unowned title from that genre as a featured spotlight. * * Falls back to TMDB trending-day for cold-start users (small or no * library), so the component always renders something useful. * * The card mirrors the older SpotlightHero visually but reframes the * copy: this isn't "what's hot on TMDB", it's "we read what you * actually watch". */ export default function TonightHero({ kind }: Props) { const navigate = useNavigate() const hideAdult = usePreferencesStore(s => s.hideAdult) const lib = useLibraryByTmdbId() const dist = useLibraryGenreDistribution() // Top genre across the user's library, if we have enough material to // call it a real signal. Below 30 items we treat the user as // cold-start and fall back to a non-personalized pick. const topGenre = useMemo(() => { const data = dist.data if (!data || data.total < 30) return null const sorted = [...data.counts.entries()].sort((a, b) => b[1] - a[1]) return sorted[0]?.[0] || null }, [dist.data]) const genreId = topGenre ? kind === 'movie' ? tmdbMovieGenreId(topGenre) : tmdbTvGenreId(topGenre) : null const personalizedParams = genreId ? { with_genres: String(genreId), 'vote_count.gte': '2000', 'vote_average.gte': '7.5', sort_by: 'popularity.desc', } : ({} as Record) const personalMovies = useTmdbDiscoverMovies(kind === 'movie' && genreId ? personalizedParams : {}) const personalTv = useTmdbDiscoverTv(kind === 'tv' && genreId ? personalizedParams : {}) const personalData = kind === 'movie' ? personalMovies.data : personalTv.data // Cold-start fallback: trending-day, same as the old Spotlight. const trending = useTmdbTrending(kind, 'day') const pick = useMemo(() => { // Personalized pool wins when present + non-empty. const personalRaw = (personalData?.results || []).map(m => ({ ...m, media_type: kind })) const personalPool = filterToMissing(personalRaw, lib.data, hideAdult, m => !!(m as any).adult) .filter((m: any) => m.backdrop_path && m.overview) if (personalPool.length > 0) { return { item: personalPool[0], reason: 'personalized' as const, genre: topGenre } } // Fallback: trending-day filtered to unowned. const trendRaw = trending.data?.results || [] const trendPool = filterToMissing(trendRaw, lib.data, hideAdult, m => !!(m as any).adult) .filter((m: any) => m.backdrop_path && m.overview) if (trendPool.length > 0) { return { item: trendPool[0], reason: 'trending' as const, genre: null } } return null }, [personalData, trending.data, lib.data, hideAdult, kind, topGenre]) if (!pick) return null const p = pick.item as any const title: string = p.title || p.name || '' const overview: string = p.overview || '' const backdrop = `${TMDB_IMG}/w1280${p.backdrop_path}` const year = (p.release_date || p.first_air_date || '').slice(0, 4) const rating = typeof p.vote_average === 'number' ? p.vote_average.toFixed(1) : null const mediaType: 'movie' | 'tv' = (p.media_type === 'tv' || p.first_air_date) ? 'tv' : 'movie' function open() { navigate(`/item/tmdb-${mediaType}-${p.id}`) } const eyebrowText = pick.reason === 'personalized' ? `Tonight - because you watch a lot of ${pick.genre?.toLowerCase()}` : 'Tonight - trending picks' return (
) }