refactor discover and trakt

This commit is contained in:
Pas 2025-10-31 21:38:12 -06:00
parent 3ac786011f
commit 6f3437277d
8 changed files with 433 additions and 453 deletions

View file

@ -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<string, string> = {
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<TraktCacheKey, any>();
traktCache.setCompare((a, b) => a.endpoint === b.endpoint);
traktCache.initialize();
// Base function to fetch from Trakt API
async function fetchFromTrakt<T = TraktLatestResponse>(
async function fetchFromTrakt<T = TraktListResponse>(
endpoint: string,
): Promise<T> {
// 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<TraktDiscoverResponse>("/discover");
fetchFromTrakt<TraktListResponse>("/discover");
// Get only discover movies
export const getDiscoverMovies = async (): Promise<number[]> => {
const response = await fetchFromTrakt<TraktDiscoverResponse>("/discover");
return response.movie_tmdb_ids;
};
// Network content
// Network information
export const getNetworkContent = (tmdbId: string) =>
fetchFromTrakt<TraktNetworkResponse>(`/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<CuratedMovieList[]> => {
@ -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<string, string> = {
Max: "max",
"Prime Video": "prime",
Netflix: "netflix",
"Disney+": "disney",
Hulu: "hulu",
"Apple TV+": "appletv",
"Paramount+": "paramount",
};

View file

@ -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,
};
}

View file

@ -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;
}

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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 = <T>(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<void>;
/** 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<Genre[]>([]);
const [isLoading, setIsLoading] = useState(false);
@ -364,10 +170,10 @@ export function useDiscoverMedia({
);
const fetchTraktMedia = useCallback(
async (traktFunction: () => Promise<TraktLatestResponse>) => {
async (traktFunction: () => Promise<TraktListResponse>) => {
try {
// Create a timeout promise
const timeoutPromise = new Promise<TraktLatestResponse>((_, reject) => {
const timeoutPromise = new Promise<TraktListResponse>((_, 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,
]);

View file

@ -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<void>;
sectionTitle: string;
}
export interface Provider {
name: string;
id: string;
}
export interface Genre {
id: number;
name: string;
}
// Shuffle array utility
const shuffleArray = <T>(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" },
];