diff --git a/src/backend/metadata/traktApi.ts b/src/backend/metadata/traktApi.ts index 66365cec..94dc67ca 100644 --- a/src/backend/metadata/traktApi.ts +++ b/src/backend/metadata/traktApi.ts @@ -1,94 +1,76 @@ +import { SimpleCache } from "@/utils/cache"; + import { getMediaDetails } from "./tmdb"; -import { MWMediaType } from "./types/mw"; import { TMDBContentTypes, TMDBMovieData } from "./types/tmdb"; - -export interface TraktLatestResponse { - movie_tmdb_ids: number[]; - tv_tmdb_ids: number[]; - count: number; -} - -export interface TraktReleaseResponse { - tmdb_id: number; - title: string; - year?: number; - type: "movie" | "episode"; - season?: number; - episode?: number; - quality?: string; - source?: string; - group?: string; - theatrical_release_date?: string; - digital_release_date?: string; -} - -export interface PaginatedTraktResponse { - tmdb_ids: number[]; - hasMore: boolean; - totalCount: number; -} - -export type TraktContentType = "movie" | "episode"; +import type { + CuratedMovieList, + TraktListResponse, + TraktNetworkResponse, + TraktReleaseResponse, +} from "./types/trakt"; export const TRAKT_BASE_URL = "https://fed-airdate.pstream.mov"; -export interface TraktDiscoverResponse { - movie_tmdb_ids: number[]; - tv_tmdb_ids: number[]; - count: number; +// Map provider names to their Trakt endpoints +export const PROVIDER_TO_TRAKT_MAP = { + "8": "netflixmovies", // Netflix Movies + "8tv": "netflixtv", // Netflix TV Shows + "2": "applemovie", // Apple TV+ Movies + "2tv": "appletv", // Apple TV+ (both) + "10": "primemovies", // Prime Video Movies + "10tv": "primetv", // Prime Video TV Shows + "15": "hulumovies", // Hulu Movies + "15tv": "hulutv", // Hulu TV Shows + "337": "disneymovies", // Disney+ Movies + "337tv": "disneytv", // Disney+ TV Shows + "1899": "hbomovies", // Max Movies + "1899tv": "hbotv", // Max TV Shows + "531": "paramountmovies", // Paramount+ Movies + "531tv": "paramounttv", // Paramount+ TV Shows +} as const; + +// Map provider names to their image filenames +export const PROVIDER_TO_IMAGE_MAP: Record = { + Max: "max", + "Prime Video": "prime", + Netflix: "netflix", + "Disney+": "disney", + Hulu: "hulu", + "Apple TV+": "appletv", + "Paramount+": "paramount", +}; + +// Cache for Trakt API responses +interface TraktCacheKey { + endpoint: string; } -export interface TraktNetworkResponse { - type: string; - platforms: string[]; - count: number; -} - -export interface CuratedMovieList { - listName: string; - listSlug: string; - tmdbIds: number[]; - count: number; -} - -// Pagination utility -export function paginateResults( - results: TraktLatestResponse, - page: number, - pageSize: number = 20, - contentType: "movie" | "tv" | "both" = "both", -): PaginatedTraktResponse { - let tmdbIds: number[]; - - if (contentType === "movie") { - tmdbIds = results.movie_tmdb_ids; - } else if (contentType === "tv") { - tmdbIds = results.tv_tmdb_ids; - } else { - // For 'both', combine movies and TV shows - tmdbIds = [...results.movie_tmdb_ids, ...results.tv_tmdb_ids]; - } - - const startIndex = (page - 1) * pageSize; - const endIndex = startIndex + pageSize; - const paginatedIds = tmdbIds.slice(startIndex, endIndex); - - return { - tmdb_ids: paginatedIds, - hasMore: endIndex < tmdbIds.length, - totalCount: tmdbIds.length, - }; -} +const traktCache = new SimpleCache(); +traktCache.setCompare((a, b) => a.endpoint === b.endpoint); +traktCache.initialize(); // Base function to fetch from Trakt API -async function fetchFromTrakt( +async function fetchFromTrakt( endpoint: string, ): Promise { + // Check cache first + const cacheKey: TraktCacheKey = { endpoint }; + const cachedResult = traktCache.get(cacheKey); + if (cachedResult) { + return cachedResult as T; + } + + // Make the API request const response = await fetch(`${TRAKT_BASE_URL}${endpoint}`); if (!response.ok) { throw new Error(`Failed to fetch from ${endpoint}: ${response.statusText}`); } - return response.json(); + const result = await response.json(); + + // Cache the result for 1 hour (3600 seconds) + traktCache.set(cacheKey, result, 3600); + + return result as T; } // Release details @@ -101,11 +83,25 @@ export async function getReleaseDetails( if (season !== undefined && episode !== undefined) { url += `/${season}/${episode}`; } + + // Check cache first + const cacheKey: TraktCacheKey = { endpoint: url }; + const cachedResult = traktCache.get(cacheKey); + if (cachedResult) { + return cachedResult as TraktReleaseResponse; + } + + // Make the API request const response = await fetch(`${TRAKT_BASE_URL}${url}`); if (!response.ok) { throw new Error(`Failed to fetch release details: ${response.statusText}`); } - return response.json(); + const result = await response.json(); + + // Cache the result for 1 hour (3600 seconds) + traktCache.set(cacheKey, result, 3600); + + return result as TraktReleaseResponse; } // Latest releases @@ -115,32 +111,29 @@ export const getLatestTVReleases = () => fetchFromTrakt("/latesttv"); // Streaming service releases export const getAppleTVReleases = () => fetchFromTrakt("/appletv"); +export const getAppleMovieReleases = () => fetchFromTrakt("/applemovie"); export const getNetflixMovies = () => fetchFromTrakt("/netflixmovies"); export const getNetflixTVShows = () => fetchFromTrakt("/netflixtv"); -export const getPrimeReleases = () => fetchFromTrakt("/prime"); -export const getHuluReleases = () => fetchFromTrakt("/hulu"); -export const getDisneyReleases = () => fetchFromTrakt("/disney"); -export const getHBOReleases = () => fetchFromTrakt("/hbo"); - -// Genre-specific releases -export const getActionReleases = () => fetchFromTrakt("/action"); -export const getDramaReleases = () => fetchFromTrakt("/drama"); +export const getPrimeMovies = () => fetchFromTrakt("/primemovies"); +export const getPrimeTVShows = () => fetchFromTrakt("/primetv"); +export const getHuluMovies = () => fetchFromTrakt("/hulumovies"); +export const getHuluTVShows = () => fetchFromTrakt("/hulutv"); +export const getDisneyMovies = () => fetchFromTrakt("/disneymovies"); +export const getDisneyTVShows = () => fetchFromTrakt("/disneytv"); +export const getHBOMovies = () => fetchFromTrakt("/hbomovies"); +export const getHBOTVShows = () => fetchFromTrakt("/hbotv"); +export const getParamountMovies = () => fetchFromTrakt("/paramountmovies"); +export const getParamountTVShows = () => fetchFromTrakt("/paramounttv"); // Popular content export const getPopularTVShows = () => fetchFromTrakt("/populartv"); export const getPopularMovies = () => fetchFromTrakt("/popularmovies"); -// Discovery content +// Discovery content used for the featured carousel export const getDiscoverContent = () => - fetchFromTrakt("/discover"); + fetchFromTrakt("/discover"); -// Get only discover movies -export const getDiscoverMovies = async (): Promise => { - const response = await fetchFromTrakt("/discover"); - return response.movie_tmdb_ids; -}; - -// Network content +// Network information export const getNetworkContent = (tmdbId: string) => fetchFromTrakt(`/network/${tmdbId}`); @@ -153,7 +146,7 @@ export const getLGBTQContent = () => fetchFromTrakt("/LGBTQ"); export const getMindfuckMovies = () => fetchFromTrakt("/mindfuck"); export const getTrueStoryMovies = () => fetchFromTrakt("/truestory"); export const getHalloweenMovies = () => fetchFromTrakt("/halloween"); -// export const getGreatestTVShows = () => fetchFromTrakt("/greatesttv"); // We only have movies set up. TODO add a type for tv and add more tv routes. +// export const getGreatestTVShows = () => fetchFromTrakt("/greatesttv"); // We only have movies set up. TODO add more tv routes for curated lists so we can have a new page. // Get all curated movie lists export const getCuratedMovieLists = async (): Promise => { @@ -258,39 +251,3 @@ export const getMovieDetailsForIds = async ( return movieDetails; }; - -// Type conversion utilities -export function convertToMediaType(type: TraktContentType): MWMediaType { - return type === "movie" ? MWMediaType.MOVIE : MWMediaType.SERIES; -} - -export function convertFromMediaType(type: MWMediaType): TraktContentType { - return type === MWMediaType.MOVIE ? "movie" : "episode"; -} - -// Map provider names to their Trakt endpoints -export const PROVIDER_TO_TRAKT_MAP = { - "8": "netflix", // Netflix - "2": "appletv", // Apple TV+ - "10": "prime", // Prime Video - "15": "hulu", // Hulu - "337": "disney", // Disney+ - "1899": "hbo", // Max -} as const; - -// Map genres to their Trakt endpoints -export const GENRE_TO_TRAKT_MAP = { - "28": "action", // Action - "18": "drama", // Drama -} as const; - -// Map provider names to their image filenames -export const PROVIDER_TO_IMAGE_MAP: Record = { - Max: "max", - "Prime Video": "prime", - Netflix: "netflix", - "Disney+": "disney", - Hulu: "hulu", - "Apple TV+": "appletv", - "Paramount+": "paramount", -}; diff --git a/src/backend/metadata/traktFunctions.ts b/src/backend/metadata/traktFunctions.ts new file mode 100644 index 00000000..d1ef952b --- /dev/null +++ b/src/backend/metadata/traktFunctions.ts @@ -0,0 +1,30 @@ +import type { PaginatedTraktResponse, TraktListResponse } from "./types/trakt"; + +// Pagination utility +export function paginateResults( + results: TraktListResponse, + page: number, + pageSize: number = 20, + contentType: "movie" | "tv" | "both" = "both", +): PaginatedTraktResponse { + let tmdbIds: number[]; + + if (contentType === "movie") { + tmdbIds = results.movie_tmdb_ids; + } else if (contentType === "tv") { + tmdbIds = results.tv_tmdb_ids; + } else { + // For 'both', combine movies and TV shows + tmdbIds = [...results.movie_tmdb_ids, ...results.tv_tmdb_ids]; + } + + const startIndex = (page - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedIds = tmdbIds.slice(startIndex, endIndex); + + return { + tmdb_ids: paginatedIds, + hasMore: endIndex < tmdbIds.length, + totalCount: tmdbIds.length, + }; +} diff --git a/src/backend/metadata/types/trakt.ts b/src/backend/metadata/types/trakt.ts new file mode 100644 index 00000000..f223e66c --- /dev/null +++ b/src/backend/metadata/types/trakt.ts @@ -0,0 +1,40 @@ +export interface TraktListResponse { + movie_tmdb_ids: number[]; + tv_tmdb_ids: number[]; + count: number; +} + +export interface TraktReleaseResponse { + tmdb_id: number; + title: string; + year?: number; + type: "movie" | "episode"; + season?: number; + episode?: number; + quality?: string; + source?: string; + group?: string; + theatrical_release_date?: string; + digital_release_date?: string; +} + +export interface PaginatedTraktResponse { + tmdb_ids: number[]; + hasMore: boolean; + totalCount: number; +} + +export type TraktContentType = "movie" | "episode"; + +export interface TraktNetworkResponse { + type: string; + platforms: string[]; + count: number; +} + +export interface CuratedMovieList { + listName: string; + listSlug: string; + tmdbIds: number[]; + count: number; +} diff --git a/src/components/overlays/detailsModal/components/sections/DetailsBody.tsx b/src/components/overlays/detailsModal/components/sections/DetailsBody.tsx index c9643cb5..ed2259f9 100644 --- a/src/components/overlays/detailsModal/components/sections/DetailsBody.tsx +++ b/src/components/overlays/detailsModal/components/sections/DetailsBody.tsx @@ -2,10 +2,8 @@ import classNames from "classnames"; import { t } from "i18next"; import { useEffect, useState } from "react"; -import { - TraktReleaseResponse, - getReleaseDetails, -} from "@/backend/metadata/traktApi"; +import { getReleaseDetails } from "@/backend/metadata/traktApi"; +import type { TraktReleaseResponse } from "@/backend/metadata/types/trakt"; import { Button } from "@/components/buttons/Button"; import { IconPatch } from "@/components/buttons/IconPatch"; import { GroupDropdown } from "@/components/form/GroupDropdown"; diff --git a/src/pages/discover/AllMovieLists.tsx b/src/pages/discover/AllMovieLists.tsx index 7d663267..66014ae9 100644 --- a/src/pages/discover/AllMovieLists.tsx +++ b/src/pages/discover/AllMovieLists.tsx @@ -3,11 +3,11 @@ import { useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { - CuratedMovieList, getCuratedMovieLists, getMovieDetailsForIds, } from "@/backend/metadata/traktApi"; import { TMDBMovieData } from "@/backend/metadata/types/tmdb"; +import type { CuratedMovieList } from "@/backend/metadata/types/trakt"; import { Icon, Icons } from "@/components/Icon"; import { WideContainer } from "@/components/layout/WideContainer"; import { MediaCard } from "@/components/media/MediaCard"; diff --git a/src/pages/discover/components/FeaturedCarousel.tsx b/src/pages/discover/components/FeaturedCarousel.tsx index a20e85ad..90287d1f 100644 --- a/src/pages/discover/components/FeaturedCarousel.tsx +++ b/src/pages/discover/components/FeaturedCarousel.tsx @@ -7,11 +7,11 @@ import { useWindowSize } from "react-use"; import { isExtensionActive } from "@/backend/extension/messaging"; import { get, getMediaLogo } from "@/backend/metadata/tmdb"; import { - TraktReleaseResponse, getDiscoverContent, getReleaseDetails, } from "@/backend/metadata/traktApi"; import { TMDBContentTypes } from "@/backend/metadata/types/tmdb"; +import type { TraktReleaseResponse } from "@/backend/metadata/types/trakt"; import { Button } from "@/components/buttons/Button"; import { Icon, Icons } from "@/components/Icon"; import { Movie, TVShow } from "@/pages/discover/common"; diff --git a/src/pages/discover/hooks/useDiscoverMedia.ts b/src/pages/discover/hooks/useDiscoverMedia.ts index 330abe26..764da15b 100644 --- a/src/pages/discover/hooks/useDiscoverMedia.ts +++ b/src/pages/discover/hooks/useDiscoverMedia.ts @@ -3,259 +3,65 @@ import { useTranslation } from "react-i18next"; import { get } from "@/backend/metadata/tmdb"; import { - GENRE_TO_TRAKT_MAP, PROVIDER_TO_TRAKT_MAP, - TraktLatestResponse, - getActionReleases, + getAppleMovieReleases, getAppleTVReleases, - getDisneyReleases, - getDramaReleases, - getHBOReleases, - getHuluReleases, + getDisneyMovies, + getDisneyTVShows, + getHBOMovies, + getHBOTVShows, + getHuluMovies, + getHuluTVShows, getLatest4KReleases, getLatestReleases, getLatestTVReleases, getNetflixMovies, getNetflixTVShows, - getPrimeReleases, - paginateResults, + getParamountMovies, + getParamountTVShows, + getPrimeMovies, + getPrimeTVShows, } from "@/backend/metadata/traktApi"; +import { paginateResults } from "@/backend/metadata/traktFunctions"; +import type { TraktListResponse } from "@/backend/metadata/types/trakt"; +import { + EDITOR_PICKS_MOVIES, + EDITOR_PICKS_TV_SHOWS, + MOVIE_PROVIDERS, + TV_PROVIDERS, +} from "@/pages/discover/types/discover"; +import type { + DiscoverContentType, + DiscoverMedia, + Genre, + MediaType, + Provider, + UseDiscoverMediaProps, + UseDiscoverMediaReturn, +} from "@/pages/discover/types/discover"; import { conf } from "@/setup/config"; import { useLanguageStore } from "@/stores/language"; import { getTmdbLanguageCode } from "@/utils/language"; -// Shuffle array utility -const shuffleArray = (array: T[]): T[] => { - const shuffled = [...array]; - for (let i = shuffled.length - 1; i > 0; i -= 1) { - const j = Math.floor(Math.random() * (i + 1)); - [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; - } - return shuffled; +// Re-export types for backward compatibility +export type { + DiscoverContentType, + DiscoverMedia, + Genre, + MediaType, + Provider, + UseDiscoverMediaProps, + UseDiscoverMediaReturn, }; -// Editor Picks lists -export const EDITOR_PICKS_MOVIES = shuffleArray([ - { id: 9342, type: "movie" }, // The Mask of Zorro - { id: 293, type: "movie" }, // A River Runs Through It - { id: 370172, type: "movie" }, // No Time To Die - { id: 661374, type: "movie" }, // The Glass Onion - { id: 207, type: "movie" }, // Dead Poets Society - { id: 378785, type: "movie" }, // The Best of the Blues Brothers - { id: 335984, type: "movie" }, // Blade Runner 2049 - { id: 13353, type: "movie" }, // It's the Great Pumpkin, Charlie Brown - { id: 27205, type: "movie" }, // Inception - { id: 106646, type: "movie" }, // The Wolf of Wall Street - { id: 334533, type: "movie" }, // Captain Fantastic - { id: 693134, type: "movie" }, // Dune: Part Two - { id: 765245, type: "movie" }, // Swan Song - { id: 264660, type: "movie" }, // Ex Machina - { id: 92591, type: "movie" }, // Bernie - { id: 976893, type: "movie" }, // Perfect Days - { id: 13187, type: "movie" }, // A Charlie Brown Christmas - { id: 11527, type: "movie" }, // Excalibur - { id: 120, type: "movie" }, // LOTR: The Fellowship of the Ring - { id: 157336, type: "movie" }, // Interstellar - { id: 762, type: "movie" }, // Monty Python and the Holy Grail - { id: 666243, type: "movie" }, // The Witcher: Nightmare of the Wolf - { id: 545611, type: "movie" }, // Everything Everywhere All at Once - { id: 329, type: "movie" }, // Jurrassic Park - { id: 330459, type: "movie" }, // Rogue One: A Star Wars Story - { id: 279, type: "movie" }, // Amadeus - { id: 823219, type: "movie" }, // Flow - { id: 22, type: "movie" }, // Pirates of the Caribbean: The Curse of the Black Pearl - { id: 18971, type: "movie" }, // Rosencrantz and Guildenstern Are Dead - { id: 26388, type: "movie" }, // Buried - { id: 152601, type: "movie" }, // Her - { id: 11886, type: "movie" }, // Robin Hood - { id: 1362, type: "movie" }, // The Hobbit 1977 - { id: 578, type: "movie" }, // Jaws - { id: 78, type: "movie" }, // Blade Runner - { id: 348, type: "movie" }, // Alien - { id: 198184, type: "movie" }, // Chappie - { id: 405774, type: "movie" }, // Bird Box - { id: 333339, type: "movie" }, // Ready Player One -]); +// Re-export constants for backward compatibility +export { + EDITOR_PICKS_MOVIES, + EDITOR_PICKS_TV_SHOWS, + MOVIE_PROVIDERS, + TV_PROVIDERS, +}; -export const EDITOR_PICKS_TV_SHOWS = shuffleArray([ - { id: 456, type: "show" }, // The Simpsons - { id: 73021, type: "show" }, // Disenchantment - { id: 1434, type: "show" }, // Family Guy - { id: 1695, type: "show" }, // Monk - { id: 1408, type: "show" }, // House - { id: 93740, type: "show" }, // Foundation - { id: 60625, type: "show" }, // Rick and Morty - { id: 1396, type: "show" }, // Breaking Bad - { id: 44217, type: "show" }, // Vikings - { id: 90228, type: "show" }, // Dune Prophecy - { id: 13916, type: "show" }, // Death Note - { id: 71912, type: "show" }, // The Witcher - { id: 61222, type: "show" }, // Bojack Horseman - { id: 93405, type: "show" }, // Squid Game - { id: 87108, type: "show" }, // Chernobyl - { id: 105248, type: "show" }, // Cyberpunk: Edgerunners - { id: 82738, type: "show" }, // IRODUKU: The World in Colors - { id: 615, type: "show" }, // Futurama - { id: 4625, type: "show" }, // The New Batman Adventures - { id: 513, type: "show" }, // Batman Beyond - { id: 110948, type: "show" }, // The Snoopy Show - { id: 110492, type: "show" }, // Peacemaker - { id: 125988, type: "show" }, // Silo - { id: 87917, type: "show" }, // For All Mankind - { id: 42009, type: "show" }, // Black Mirror - { id: 86831, type: "show" }, // Love, Death & Robots - { id: 261579, type: "show" }, // Secret Level -]); - -/** - * The type of content to fetch from various endpoints - */ -export type DiscoverContentType = - | "popular" - | "topRated" - | "onTheAir" - | "nowPlaying" - | "latest" - | "latest4k" - | "latesttv" - | "genre" - | "provider" - | "editorPicks" - | "recommendations"; - -/** - * The type of media to fetch (movie or TV show) - */ -export type MediaType = "movie" | "tv"; - -/** - * Props for the useDiscoverMedia hook - */ -export interface UseDiscoverMediaProps { - /** The type of content to fetch */ - contentType: DiscoverContentType; - /** Whether to fetch movies or TV shows */ - mediaType: MediaType; - /** ID used for genre, provider, or recommendations */ - id?: string; - /** Fallback content type if primary fails */ - fallbackType?: DiscoverContentType; - /** Page number for paginated results */ - page?: number; - /** Genre name for display in title */ - genreName?: string; - /** Provider name for display in title */ - providerName?: string; - /** Media title for recommendations display */ - mediaTitle?: string; - /** Whether this is for a carousel view (limits results) */ - isCarouselView?: boolean; -} - -/** - * Media item returned from discover endpoints - */ -export interface DiscoverMedia { - /** TMDB ID of the media */ - id: number; - /** Title for movies */ - title: string; - /** Title for TV shows */ - name?: string; - /** Poster image path */ - poster_path: string; - /** Backdrop image path */ - backdrop_path: string; - /** Release date for movies */ - release_date?: string; - /** First air date for TV shows */ - first_air_date?: string; - /** Media overview/description */ - overview: string; - /** Average vote score (0-10) */ - vote_average: number; - /** Number of votes */ - vote_count: number; - /** Type of media (movie or show) */ - type?: "movie" | "show"; -} - -/** - * Return type of the useDiscoverMedia hook - */ -export interface UseDiscoverMediaReturn { - /** Array of media items */ - media: DiscoverMedia[]; - /** Whether media is currently being fetched */ - isLoading: boolean; - /** Error message if fetch failed */ - error: string | null; - /** Whether there are more pages to load */ - hasMore: boolean; - /** Function to refetch the current media */ - refetch: () => Promise; - /** Localized section title for the media carousel */ - sectionTitle: string; -} - -/** - * Provider interface for streaming services - */ -export interface Provider { - /** Provider name (e.g., "Netflix", "Hulu") */ - name: string; - /** Provider ID from TMDB */ - id: string; -} - -/** - * Genre interface for media categorization - */ -export interface Genre { - /** Genre ID from TMDB */ - id: number; - /** Genre name (e.g., "Action", "Drama") */ - name: string; -} - -// Static provider lists -export const MOVIE_PROVIDERS: Provider[] = [ - { name: "Netflix", id: "8" }, - { name: "Apple TV+", id: "2" }, - { name: "Amazon Prime Video", id: "10" }, - { name: "Hulu", id: "15" }, - { name: "Disney Plus", id: "337" }, - { name: "Max", id: "1899" }, - { name: "Paramount Plus", id: "531" }, - { name: "Shudder", id: "99" }, - { name: "Crunchyroll", id: "283" }, - { name: "fuboTV", id: "257" }, - { name: "AMC+", id: "526" }, - { name: "Starz", id: "43" }, - { name: "Lifetime", id: "157" }, - { name: "National Geographic", id: "1964" }, -]; - -export const TV_PROVIDERS: Provider[] = [ - { name: "Netflix", id: "8" }, - { name: "Apple TV+", id: "350" }, - { name: "Amazon Prime Video", id: "10" }, - { name: "Paramount Plus", id: "531" }, - { name: "Hulu", id: "15" }, - { name: "Max", id: "1899" }, - { name: "Adult Swim", id: "318" }, - { name: "Disney Plus", id: "337" }, - { name: "Crunchyroll", id: "283" }, - { name: "fuboTV", id: "257" }, - { name: "Shudder", id: "99" }, - { name: "Discovery +", id: "520" }, - { name: "National Geographic", id: "1964" }, - { name: "Fox", id: "328" }, -]; - -/** - * Hook for managing providers and genres - */ export function useDiscoverOptions(mediaType: MediaType) { const [genres, setGenres] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -364,10 +170,10 @@ export function useDiscoverMedia({ ); const fetchTraktMedia = useCallback( - async (traktFunction: () => Promise) => { + async (traktFunction: () => Promise) => { try { // Create a timeout promise - const timeoutPromise = new Promise((_, reject) => { + const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error("Trakt request timed out")), 3000); }); @@ -430,30 +236,43 @@ export function useDiscoverMedia({ // Get Trakt function for provider const getTraktProviderFunction = useCallback( (providerId: string) => { + // Create the key based on provider ID and media type + const key = mediaType === "tv" ? `${providerId}tv` : providerId; const trakt = - PROVIDER_TO_TRAKT_MAP[providerId as keyof typeof PROVIDER_TO_TRAKT_MAP]; + PROVIDER_TO_TRAKT_MAP[key as keyof typeof PROVIDER_TO_TRAKT_MAP]; + if (!trakt) return null; - // Handle TV vs Movies for Netflix - if (trakt === "netflix" && mediaType === "tv") { - return getNetflixTVShows; - } - if (trakt === "netflix" && mediaType === "movie") { - return getNetflixMovies; - } - - // Map provider to corresponding Trakt function + // Map trakt endpoint to corresponding function switch (trakt) { case "appletv": return getAppleTVReleases; - case "prime": - return getPrimeReleases; - case "hulu": - return getHuluReleases; - case "disney": - return getDisneyReleases; - case "hbo": - return getHBOReleases; + case "applemovie": + return getAppleMovieReleases; + case "netflixmovies": + return getNetflixMovies; + case "netflixtv": + return getNetflixTVShows; + case "primemovies": + return getPrimeMovies; + case "primetv": + return getPrimeTVShows; + case "hulumovies": + return getHuluMovies; + case "hulutv": + return getHuluTVShows; + case "disneymovies": + return getDisneyMovies; + case "disneytv": + return getDisneyTVShows; + case "hbomovies": + return getHBOMovies; + case "hbotv": + return getHBOTVShows; + case "paramountmovies": + return getParamountMovies; + case "paramounttv": + return getParamountTVShows; default: return null; } @@ -461,22 +280,6 @@ export function useDiscoverMedia({ [mediaType], ); - // Get Trakt function for genre - const getTraktGenreFunction = useCallback((genreId: string) => { - const trakt = - GENRE_TO_TRAKT_MAP[genreId as keyof typeof GENRE_TO_TRAKT_MAP]; - if (!trakt) return null; - - switch (trakt) { - case "action": - return getActionReleases; - case "drama": - return getDramaReleases; - default: - return null; - } - }, []); - const fetchEditorPicks = useCallback(async () => { const picks = mediaType === "movie" ? EDITOR_PICKS_MOVIES : EDITOR_PICKS_TV_SHOWS; @@ -515,7 +318,6 @@ export function useDiscoverMedia({ const attemptFetch = async (type: DiscoverContentType) => { let data; - let traktGenreFunction; let traktProviderFunction; // Map content types to their endpoints and handling logic @@ -566,46 +368,15 @@ export function useDiscoverMedia({ case "genre": if (!id) throw new Error("Genre ID is required"); - // Try to use Trakt genre endpoint if available - traktGenreFunction = getTraktGenreFunction(id); - if (traktGenreFunction) { - try { - data = await fetchTraktMedia(traktGenreFunction); - setSectionTitle( - mediaType === "movie" - ? t("discover.carousel.title.movies", { category: genreName }) - : t("discover.carousel.title.tvshows", { - category: genreName, - }), - ); - } catch (traktErr) { - console.error( - "Trakt genre fetch failed, falling back to TMDB:", - traktErr, - ); - // Fall back to TMDB - data = await fetchTMDBMedia(`/discover/${mediaType}`, { - with_genres: id, - }); - setSectionTitle( - mediaType === "movie" - ? t("discover.carousel.title.movies", { category: genreName }) - : t("discover.carousel.title.tvshows", { - category: genreName, - }), - ); - } - } else { - // Use TMDB if no Trakt endpoint exists for this genre - data = await fetchTMDBMedia(`/discover/${mediaType}`, { - with_genres: id, - }); - setSectionTitle( - mediaType === "movie" - ? t("discover.carousel.title.movies", { category: genreName }) - : t("discover.carousel.title.tvshows", { category: genreName }), - ); - } + // Use TMDB for genres (Trakt genre endpoints removed) + data = await fetchTMDBMedia(`/discover/${mediaType}`, { + with_genres: id, + }); + setSectionTitle( + mediaType === "movie" + ? t("discover.carousel.title.movies", { category: genreName }) + : t("discover.carousel.title.tvshows", { category: genreName }), + ); break; case "provider": @@ -732,7 +503,6 @@ export function useDiscoverMedia({ fetchEditorPicks, t, page, - getTraktGenreFunction, getTraktProviderFunction, ]); diff --git a/src/pages/discover/types/discover.ts b/src/pages/discover/types/discover.ts new file mode 100644 index 00000000..536576dd --- /dev/null +++ b/src/pages/discover/types/discover.ts @@ -0,0 +1,185 @@ +export type DiscoverContentType = + | "popular" + | "topRated" + | "onTheAir" + | "nowPlaying" + | "latest" + | "latest4k" + | "latesttv" + | "genre" + | "provider" + | "editorPicks" + | "recommendations"; + +export type MediaType = "movie" | "tv"; + +export interface UseDiscoverMediaProps { + contentType: DiscoverContentType; + mediaType: MediaType; + id?: string; + fallbackType?: DiscoverContentType; + page?: number; + genreName?: string; + providerName?: string; + mediaTitle?: string; + isCarouselView?: boolean; +} + +export interface DiscoverMedia { + id: number; + title: string; + name?: string; + poster_path: string; + backdrop_path: string; + release_date?: string; + first_air_date?: string; + overview: string; + vote_average: number; + vote_count: number; + type?: "movie" | "show"; +} + +export interface UseDiscoverMediaReturn { + media: DiscoverMedia[]; + isLoading: boolean; + error: string | null; + hasMore: boolean; + refetch: () => Promise; + sectionTitle: string; +} + +export interface Provider { + name: string; + id: string; +} + +export interface Genre { + id: number; + name: string; +} + +// Shuffle array utility +const shuffleArray = (array: T[]): T[] => { + const shuffled = [...array]; + for (let i = shuffled.length - 1; i > 0; i -= 1) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; + } + return shuffled; +}; + +// Editor Picks data +export interface EditorPick { + id: number; + type: "movie" | "show"; +} + +const MOVIES_DATA: EditorPick[] = [ + { id: 9342, type: "movie" }, // The Mask of Zorro + { id: 293, type: "movie" }, // A River Runs Through It + { id: 370172, type: "movie" }, // No Time To Die + { id: 661374, type: "movie" }, // The Glass Onion + { id: 207, type: "movie" }, // Dead Poets Society + { id: 378785, type: "movie" }, // The Best of the Blues Brothers + { id: 335984, type: "movie" }, // Blade Runner 2049 + { id: 13353, type: "movie" }, // It's the Great Pumpkin, Charlie Brown + { id: 27205, type: "movie" }, // Inception + { id: 106646, type: "movie" }, // The Wolf of Wall Street + { id: 334533, type: "movie" }, // Captain Fantastic + { id: 693134, type: "movie" }, // Dune: Part Two + { id: 765245, type: "movie" }, // Swan Song + { id: 264660, type: "movie" }, // Ex Machina + { id: 92591, type: "movie" }, // Bernie + { id: 976893, type: "movie" }, // Perfect Days + { id: 13187, type: "movie" }, // A Charlie Brown Christmas + { id: 11527, type: "movie" }, // Excalibur + { id: 120, type: "movie" }, // LOTR: The Fellowship of the Ring + { id: 157336, type: "movie" }, // Interstellar + { id: 762, type: "movie" }, // Monty Python and the Holy Grail + { id: 666243, type: "movie" }, // The Witcher: Nightmare of the Wolf + { id: 545611, type: "movie" }, // Everything Everywhere All at Once + { id: 329, type: "movie" }, // Jurrassic Park + { id: 330459, type: "movie" }, // Rogue One: A Star Wars Story + { id: 279, type: "movie" }, // Amadeus + { id: 823219, type: "movie" }, // Flow + { id: 22, type: "movie" }, // Pirates of the Caribbean: The Curse of the Black Pearl + { id: 18971, type: "movie" }, // Rosencrantz and Guildenstern Are Dead + { id: 26388, type: "movie" }, // Buried + { id: 152601, type: "movie" }, // Her + { id: 11886, type: "movie" }, // Robin Hood + { id: 1362, type: "movie" }, // The Hobbit 1977 + { id: 578, type: "movie" }, // Jaws + { id: 78, type: "movie" }, // Blade Runner + { id: 348, type: "movie" }, // Alien + { id: 198184, type: "movie" }, // Chappie + { id: 405774, type: "movie" }, // Bird Box + { id: 333339, type: "movie" }, // Ready Player One +]; + +const TV_SHOWS_DATA: EditorPick[] = [ + { id: 456, type: "show" }, // The Simpsons + { id: 73021, type: "show" }, // Disenchantment + { id: 1434, type: "show" }, // Family Guy + { id: 1695, type: "show" }, // Monk + { id: 1408, type: "show" }, // House + { id: 93740, type: "show" }, // Foundation + { id: 60625, type: "show" }, // Rick and Morty + { id: 1396, type: "show" }, // Breaking Bad + { id: 44217, type: "show" }, // Vikings + { id: 90228, type: "show" }, // Dune Prophecy + { id: 13916, type: "show" }, // Death Note + { id: 71912, type: "show" }, // The Witcher + { id: 61222, type: "show" }, // Bojack Horseman + { id: 93405, type: "show" }, // Squid Game + { id: 87108, type: "show" }, // Chernobyl + { id: 105248, type: "show" }, // Cyberpunk: Edgerunners + { id: 82738, type: "show" }, // IRODUKU: The World in Colors + { id: 615, type: "show" }, // Futurama + { id: 4625, type: "show" }, // The New Batman Adventures + { id: 513, type: "show" }, // Batman Beyond + { id: 110948, type: "show" }, // The Snoopy Show + { id: 110492, type: "show" }, // Peacemaker + { id: 125988, type: "show" }, // Silo + { id: 87917, type: "show" }, // For All Mankind + { id: 42009, type: "show" }, // Black Mirror + { id: 86831, type: "show" }, // Love, Death & Robots + { id: 261579, type: "show" }, // Secret Level +]; + +export const EDITOR_PICKS_MOVIES = shuffleArray(MOVIES_DATA); +export const EDITOR_PICKS_TV_SHOWS = shuffleArray(TV_SHOWS_DATA); + +// Static provider lists +export const MOVIE_PROVIDERS: Provider[] = [ + { name: "Netflix", id: "8" }, + { name: "Apple TV+", id: "2" }, + { name: "Amazon Prime Video", id: "10" }, + { name: "Hulu", id: "15" }, + { name: "Disney Plus", id: "337" }, + { name: "Max", id: "1899" }, + { name: "Paramount Plus", id: "531" }, + { name: "Shudder", id: "99" }, + { name: "Crunchyroll", id: "283" }, + { name: "fuboTV", id: "257" }, + { name: "AMC+", id: "526" }, + { name: "Starz", id: "43" }, + { name: "Lifetime", id: "157" }, + { name: "National Geographic", id: "1964" }, +]; + +export const TV_PROVIDERS: Provider[] = [ + { name: "Netflix", id: "8" }, + { name: "Apple TV+", id: "350" }, + { name: "Amazon Prime Video", id: "10" }, + { name: "Paramount Plus", id: "531" }, + { name: "Hulu", id: "15" }, + { name: "Max", id: "1899" }, + { name: "Adult Swim", id: "318" }, + { name: "Disney Plus", id: "337" }, + { name: "Crunchyroll", id: "283" }, + { name: "fuboTV", id: "257" }, + { name: "Shudder", id: "99" }, + { name: "Discovery +", id: "520" }, + { name: "National Geographic", id: "1964" }, + { name: "Fox", id: "328" }, +];