pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor discover and trakt

Pas 6f343727 3ac78601

+431 -451
+86 -129
src/backend/metadata/traktApi.ts
··· 1 + import { SimpleCache } from "@/utils/cache"; 2 + 1 3 import { getMediaDetails } from "./tmdb"; 2 - import { MWMediaType } from "./types/mw"; 3 4 import { TMDBContentTypes, TMDBMovieData } from "./types/tmdb"; 4 - 5 - export interface TraktLatestResponse { 6 - movie_tmdb_ids: number[]; 7 - tv_tmdb_ids: number[]; 8 - count: number; 9 - } 10 - 11 - export interface TraktReleaseResponse { 12 - tmdb_id: number; 13 - title: string; 14 - year?: number; 15 - type: "movie" | "episode"; 16 - season?: number; 17 - episode?: number; 18 - quality?: string; 19 - source?: string; 20 - group?: string; 21 - theatrical_release_date?: string; 22 - digital_release_date?: string; 23 - } 24 - 25 - export interface PaginatedTraktResponse { 26 - tmdb_ids: number[]; 27 - hasMore: boolean; 28 - totalCount: number; 29 - } 30 - 31 - export type TraktContentType = "movie" | "episode"; 5 + import type { 6 + CuratedMovieList, 7 + TraktListResponse, 8 + TraktNetworkResponse, 9 + TraktReleaseResponse, 10 + } from "./types/trakt"; 32 11 33 12 export const TRAKT_BASE_URL = "https://fed-airdate.pstream.mov"; 34 13 35 - export interface TraktDiscoverResponse { 36 - movie_tmdb_ids: number[]; 37 - tv_tmdb_ids: number[]; 38 - count: number; 39 - } 14 + // Map provider names to their Trakt endpoints 15 + export const PROVIDER_TO_TRAKT_MAP = { 16 + "8": "netflixmovies", // Netflix Movies 17 + "8tv": "netflixtv", // Netflix TV Shows 18 + "2": "applemovie", // Apple TV+ Movies 19 + "2tv": "appletv", // Apple TV+ (both) 20 + "10": "primemovies", // Prime Video Movies 21 + "10tv": "primetv", // Prime Video TV Shows 22 + "15": "hulumovies", // Hulu Movies 23 + "15tv": "hulutv", // Hulu TV Shows 24 + "337": "disneymovies", // Disney+ Movies 25 + "337tv": "disneytv", // Disney+ TV Shows 26 + "1899": "hbomovies", // Max Movies 27 + "1899tv": "hbotv", // Max TV Shows 28 + "531": "paramountmovies", // Paramount+ Movies 29 + "531tv": "paramounttv", // Paramount+ TV Shows 30 + } as const; 40 31 41 - export interface TraktNetworkResponse { 42 - type: string; 43 - platforms: string[]; 44 - count: number; 45 - } 32 + // Map provider names to their image filenames 33 + export const PROVIDER_TO_IMAGE_MAP: Record<string, string> = { 34 + Max: "max", 35 + "Prime Video": "prime", 36 + Netflix: "netflix", 37 + "Disney+": "disney", 38 + Hulu: "hulu", 39 + "Apple TV+": "appletv", 40 + "Paramount+": "paramount", 41 + }; 46 42 47 - export interface CuratedMovieList { 48 - listName: string; 49 - listSlug: string; 50 - tmdbIds: number[]; 51 - count: number; 43 + // Cache for Trakt API responses 44 + interface TraktCacheKey { 45 + endpoint: string; 52 46 } 53 47 54 - // Pagination utility 55 - export function paginateResults( 56 - results: TraktLatestResponse, 57 - page: number, 58 - pageSize: number = 20, 59 - contentType: "movie" | "tv" | "both" = "both", 60 - ): PaginatedTraktResponse { 61 - let tmdbIds: number[]; 62 - 63 - if (contentType === "movie") { 64 - tmdbIds = results.movie_tmdb_ids; 65 - } else if (contentType === "tv") { 66 - tmdbIds = results.tv_tmdb_ids; 67 - } else { 68 - // For 'both', combine movies and TV shows 69 - tmdbIds = [...results.movie_tmdb_ids, ...results.tv_tmdb_ids]; 70 - } 71 - 72 - const startIndex = (page - 1) * pageSize; 73 - const endIndex = startIndex + pageSize; 74 - const paginatedIds = tmdbIds.slice(startIndex, endIndex); 75 - 76 - return { 77 - tmdb_ids: paginatedIds, 78 - hasMore: endIndex < tmdbIds.length, 79 - totalCount: tmdbIds.length, 80 - }; 81 - } 48 + const traktCache = new SimpleCache<TraktCacheKey, any>(); 49 + traktCache.setCompare((a, b) => a.endpoint === b.endpoint); 50 + traktCache.initialize(); 82 51 83 52 // Base function to fetch from Trakt API 84 - async function fetchFromTrakt<T = TraktLatestResponse>( 53 + async function fetchFromTrakt<T = TraktListResponse>( 85 54 endpoint: string, 86 55 ): Promise<T> { 56 + // Check cache first 57 + const cacheKey: TraktCacheKey = { endpoint }; 58 + const cachedResult = traktCache.get(cacheKey); 59 + if (cachedResult) { 60 + return cachedResult as T; 61 + } 62 + 63 + // Make the API request 87 64 const response = await fetch(`${TRAKT_BASE_URL}${endpoint}`); 88 65 if (!response.ok) { 89 66 throw new Error(`Failed to fetch from ${endpoint}: ${response.statusText}`); 90 67 } 91 - return response.json(); 68 + const result = await response.json(); 69 + 70 + // Cache the result for 1 hour (3600 seconds) 71 + traktCache.set(cacheKey, result, 3600); 72 + 73 + return result as T; 92 74 } 93 75 94 76 // Release details ··· 101 83 if (season !== undefined && episode !== undefined) { 102 84 url += `/${season}/${episode}`; 103 85 } 86 + 87 + // Check cache first 88 + const cacheKey: TraktCacheKey = { endpoint: url }; 89 + const cachedResult = traktCache.get(cacheKey); 90 + if (cachedResult) { 91 + return cachedResult as TraktReleaseResponse; 92 + } 93 + 94 + // Make the API request 104 95 const response = await fetch(`${TRAKT_BASE_URL}${url}`); 105 96 if (!response.ok) { 106 97 throw new Error(`Failed to fetch release details: ${response.statusText}`); 107 98 } 108 - return response.json(); 99 + const result = await response.json(); 100 + 101 + // Cache the result for 1 hour (3600 seconds) 102 + traktCache.set(cacheKey, result, 3600); 103 + 104 + return result as TraktReleaseResponse; 109 105 } 110 106 111 107 // Latest releases ··· 115 111 116 112 // Streaming service releases 117 113 export const getAppleTVReleases = () => fetchFromTrakt("/appletv"); 114 + export const getAppleMovieReleases = () => fetchFromTrakt("/applemovie"); 118 115 export const getNetflixMovies = () => fetchFromTrakt("/netflixmovies"); 119 116 export const getNetflixTVShows = () => fetchFromTrakt("/netflixtv"); 120 - export const getPrimeReleases = () => fetchFromTrakt("/prime"); 121 - export const getHuluReleases = () => fetchFromTrakt("/hulu"); 122 - export const getDisneyReleases = () => fetchFromTrakt("/disney"); 123 - export const getHBOReleases = () => fetchFromTrakt("/hbo"); 124 - 125 - // Genre-specific releases 126 - export const getActionReleases = () => fetchFromTrakt("/action"); 127 - export const getDramaReleases = () => fetchFromTrakt("/drama"); 117 + export const getPrimeMovies = () => fetchFromTrakt("/primemovies"); 118 + export const getPrimeTVShows = () => fetchFromTrakt("/primetv"); 119 + export const getHuluMovies = () => fetchFromTrakt("/hulumovies"); 120 + export const getHuluTVShows = () => fetchFromTrakt("/hulutv"); 121 + export const getDisneyMovies = () => fetchFromTrakt("/disneymovies"); 122 + export const getDisneyTVShows = () => fetchFromTrakt("/disneytv"); 123 + export const getHBOMovies = () => fetchFromTrakt("/hbomovies"); 124 + export const getHBOTVShows = () => fetchFromTrakt("/hbotv"); 125 + export const getParamountMovies = () => fetchFromTrakt("/paramountmovies"); 126 + export const getParamountTVShows = () => fetchFromTrakt("/paramounttv"); 128 127 129 128 // Popular content 130 129 export const getPopularTVShows = () => fetchFromTrakt("/populartv"); 131 130 export const getPopularMovies = () => fetchFromTrakt("/popularmovies"); 132 131 133 - // Discovery content 132 + // Discovery content used for the featured carousel 134 133 export const getDiscoverContent = () => 135 - fetchFromTrakt<TraktDiscoverResponse>("/discover"); 134 + fetchFromTrakt<TraktListResponse>("/discover"); 136 135 137 - // Get only discover movies 138 - export const getDiscoverMovies = async (): Promise<number[]> => { 139 - const response = await fetchFromTrakt<TraktDiscoverResponse>("/discover"); 140 - return response.movie_tmdb_ids; 141 - }; 142 - 143 - // Network content 136 + // Network information 144 137 export const getNetworkContent = (tmdbId: string) => 145 138 fetchFromTrakt<TraktNetworkResponse>(`/network/${tmdbId}`); 146 139 ··· 153 146 export const getMindfuckMovies = () => fetchFromTrakt("/mindfuck"); 154 147 export const getTrueStoryMovies = () => fetchFromTrakt("/truestory"); 155 148 export const getHalloweenMovies = () => fetchFromTrakt("/halloween"); 156 - // export const getGreatestTVShows = () => fetchFromTrakt("/greatesttv"); // We only have movies set up. TODO add a type for tv and add more tv routes. 149 + // 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. 157 150 158 151 // Get all curated movie lists 159 152 export const getCuratedMovieLists = async (): Promise<CuratedMovieList[]> => { ··· 258 251 259 252 return movieDetails; 260 253 }; 261 - 262 - // Type conversion utilities 263 - export function convertToMediaType(type: TraktContentType): MWMediaType { 264 - return type === "movie" ? MWMediaType.MOVIE : MWMediaType.SERIES; 265 - } 266 - 267 - export function convertFromMediaType(type: MWMediaType): TraktContentType { 268 - return type === MWMediaType.MOVIE ? "movie" : "episode"; 269 - } 270 - 271 - // Map provider names to their Trakt endpoints 272 - export const PROVIDER_TO_TRAKT_MAP = { 273 - "8": "netflix", // Netflix 274 - "2": "appletv", // Apple TV+ 275 - "10": "prime", // Prime Video 276 - "15": "hulu", // Hulu 277 - "337": "disney", // Disney+ 278 - "1899": "hbo", // Max 279 - } as const; 280 - 281 - // Map genres to their Trakt endpoints 282 - export const GENRE_TO_TRAKT_MAP = { 283 - "28": "action", // Action 284 - "18": "drama", // Drama 285 - } as const; 286 - 287 - // Map provider names to their image filenames 288 - export const PROVIDER_TO_IMAGE_MAP: Record<string, string> = { 289 - Max: "max", 290 - "Prime Video": "prime", 291 - Netflix: "netflix", 292 - "Disney+": "disney", 293 - Hulu: "hulu", 294 - "Apple TV+": "appletv", 295 - "Paramount+": "paramount", 296 - };
+30
src/backend/metadata/traktFunctions.ts
··· 1 + import type { PaginatedTraktResponse, TraktListResponse } from "./types/trakt"; 2 + 3 + // Pagination utility 4 + export function paginateResults( 5 + results: TraktListResponse, 6 + page: number, 7 + pageSize: number = 20, 8 + contentType: "movie" | "tv" | "both" = "both", 9 + ): PaginatedTraktResponse { 10 + let tmdbIds: number[]; 11 + 12 + if (contentType === "movie") { 13 + tmdbIds = results.movie_tmdb_ids; 14 + } else if (contentType === "tv") { 15 + tmdbIds = results.tv_tmdb_ids; 16 + } else { 17 + // For 'both', combine movies and TV shows 18 + tmdbIds = [...results.movie_tmdb_ids, ...results.tv_tmdb_ids]; 19 + } 20 + 21 + const startIndex = (page - 1) * pageSize; 22 + const endIndex = startIndex + pageSize; 23 + const paginatedIds = tmdbIds.slice(startIndex, endIndex); 24 + 25 + return { 26 + tmdb_ids: paginatedIds, 27 + hasMore: endIndex < tmdbIds.length, 28 + totalCount: tmdbIds.length, 29 + }; 30 + }
+40
src/backend/metadata/types/trakt.ts
··· 1 + export interface TraktListResponse { 2 + movie_tmdb_ids: number[]; 3 + tv_tmdb_ids: number[]; 4 + count: number; 5 + } 6 + 7 + export interface TraktReleaseResponse { 8 + tmdb_id: number; 9 + title: string; 10 + year?: number; 11 + type: "movie" | "episode"; 12 + season?: number; 13 + episode?: number; 14 + quality?: string; 15 + source?: string; 16 + group?: string; 17 + theatrical_release_date?: string; 18 + digital_release_date?: string; 19 + } 20 + 21 + export interface PaginatedTraktResponse { 22 + tmdb_ids: number[]; 23 + hasMore: boolean; 24 + totalCount: number; 25 + } 26 + 27 + export type TraktContentType = "movie" | "episode"; 28 + 29 + export interface TraktNetworkResponse { 30 + type: string; 31 + platforms: string[]; 32 + count: number; 33 + } 34 + 35 + export interface CuratedMovieList { 36 + listName: string; 37 + listSlug: string; 38 + tmdbIds: number[]; 39 + count: number; 40 + }
+2 -4
src/components/overlays/detailsModal/components/sections/DetailsBody.tsx
··· 2 2 import { t } from "i18next"; 3 3 import { useEffect, useState } from "react"; 4 4 5 - import { 6 - TraktReleaseResponse, 7 - getReleaseDetails, 8 - } from "@/backend/metadata/traktApi"; 5 + import { getReleaseDetails } from "@/backend/metadata/traktApi"; 6 + import type { TraktReleaseResponse } from "@/backend/metadata/types/trakt"; 9 7 import { Button } from "@/components/buttons/Button"; 10 8 import { IconPatch } from "@/components/buttons/IconPatch"; 11 9 import { GroupDropdown } from "@/components/form/GroupDropdown";
+1 -1
src/pages/discover/AllMovieLists.tsx
··· 3 3 import { useNavigate } from "react-router-dom"; 4 4 5 5 import { 6 - CuratedMovieList, 7 6 getCuratedMovieLists, 8 7 getMovieDetailsForIds, 9 8 } from "@/backend/metadata/traktApi"; 10 9 import { TMDBMovieData } from "@/backend/metadata/types/tmdb"; 10 + import type { CuratedMovieList } from "@/backend/metadata/types/trakt"; 11 11 import { Icon, Icons } from "@/components/Icon"; 12 12 import { WideContainer } from "@/components/layout/WideContainer"; 13 13 import { MediaCard } from "@/components/media/MediaCard";
+1 -1
src/pages/discover/components/FeaturedCarousel.tsx
··· 7 7 import { isExtensionActive } from "@/backend/extension/messaging"; 8 8 import { get, getMediaLogo } from "@/backend/metadata/tmdb"; 9 9 import { 10 - TraktReleaseResponse, 11 10 getDiscoverContent, 12 11 getReleaseDetails, 13 12 } from "@/backend/metadata/traktApi"; 14 13 import { TMDBContentTypes } from "@/backend/metadata/types/tmdb"; 14 + import type { TraktReleaseResponse } from "@/backend/metadata/types/trakt"; 15 15 import { Button } from "@/components/buttons/Button"; 16 16 import { Icon, Icons } from "@/components/Icon"; 17 17 import { Movie, TVShow } from "@/pages/discover/common";
+86 -316
src/pages/discover/hooks/useDiscoverMedia.ts
··· 3 3 4 4 import { get } from "@/backend/metadata/tmdb"; 5 5 import { 6 - GENRE_TO_TRAKT_MAP, 7 6 PROVIDER_TO_TRAKT_MAP, 8 - TraktLatestResponse, 9 - getActionReleases, 7 + getAppleMovieReleases, 10 8 getAppleTVReleases, 11 - getDisneyReleases, 12 - getDramaReleases, 13 - getHBOReleases, 14 - getHuluReleases, 9 + getDisneyMovies, 10 + getDisneyTVShows, 11 + getHBOMovies, 12 + getHBOTVShows, 13 + getHuluMovies, 14 + getHuluTVShows, 15 15 getLatest4KReleases, 16 16 getLatestReleases, 17 17 getLatestTVReleases, 18 18 getNetflixMovies, 19 19 getNetflixTVShows, 20 - getPrimeReleases, 21 - paginateResults, 20 + getParamountMovies, 21 + getParamountTVShows, 22 + getPrimeMovies, 23 + getPrimeTVShows, 22 24 } from "@/backend/metadata/traktApi"; 25 + import { paginateResults } from "@/backend/metadata/traktFunctions"; 26 + import type { TraktListResponse } from "@/backend/metadata/types/trakt"; 27 + import { 28 + EDITOR_PICKS_MOVIES, 29 + EDITOR_PICKS_TV_SHOWS, 30 + MOVIE_PROVIDERS, 31 + TV_PROVIDERS, 32 + } from "@/pages/discover/types/discover"; 33 + import type { 34 + DiscoverContentType, 35 + DiscoverMedia, 36 + Genre, 37 + MediaType, 38 + Provider, 39 + UseDiscoverMediaProps, 40 + UseDiscoverMediaReturn, 41 + } from "@/pages/discover/types/discover"; 23 42 import { conf } from "@/setup/config"; 24 43 import { useLanguageStore } from "@/stores/language"; 25 44 import { getTmdbLanguageCode } from "@/utils/language"; 26 45 27 - // Shuffle array utility 28 - const shuffleArray = <T>(array: T[]): T[] => { 29 - const shuffled = [...array]; 30 - for (let i = shuffled.length - 1; i > 0; i -= 1) { 31 - const j = Math.floor(Math.random() * (i + 1)); 32 - [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; 33 - } 34 - return shuffled; 46 + // Re-export types for backward compatibility 47 + export type { 48 + DiscoverContentType, 49 + DiscoverMedia, 50 + Genre, 51 + MediaType, 52 + Provider, 53 + UseDiscoverMediaProps, 54 + UseDiscoverMediaReturn, 35 55 }; 36 56 37 - // Editor Picks lists 38 - export const EDITOR_PICKS_MOVIES = shuffleArray([ 39 - { id: 9342, type: "movie" }, // The Mask of Zorro 40 - { id: 293, type: "movie" }, // A River Runs Through It 41 - { id: 370172, type: "movie" }, // No Time To Die 42 - { id: 661374, type: "movie" }, // The Glass Onion 43 - { id: 207, type: "movie" }, // Dead Poets Society 44 - { id: 378785, type: "movie" }, // The Best of the Blues Brothers 45 - { id: 335984, type: "movie" }, // Blade Runner 2049 46 - { id: 13353, type: "movie" }, // It's the Great Pumpkin, Charlie Brown 47 - { id: 27205, type: "movie" }, // Inception 48 - { id: 106646, type: "movie" }, // The Wolf of Wall Street 49 - { id: 334533, type: "movie" }, // Captain Fantastic 50 - { id: 693134, type: "movie" }, // Dune: Part Two 51 - { id: 765245, type: "movie" }, // Swan Song 52 - { id: 264660, type: "movie" }, // Ex Machina 53 - { id: 92591, type: "movie" }, // Bernie 54 - { id: 976893, type: "movie" }, // Perfect Days 55 - { id: 13187, type: "movie" }, // A Charlie Brown Christmas 56 - { id: 11527, type: "movie" }, // Excalibur 57 - { id: 120, type: "movie" }, // LOTR: The Fellowship of the Ring 58 - { id: 157336, type: "movie" }, // Interstellar 59 - { id: 762, type: "movie" }, // Monty Python and the Holy Grail 60 - { id: 666243, type: "movie" }, // The Witcher: Nightmare of the Wolf 61 - { id: 545611, type: "movie" }, // Everything Everywhere All at Once 62 - { id: 329, type: "movie" }, // Jurrassic Park 63 - { id: 330459, type: "movie" }, // Rogue One: A Star Wars Story 64 - { id: 279, type: "movie" }, // Amadeus 65 - { id: 823219, type: "movie" }, // Flow 66 - { id: 22, type: "movie" }, // Pirates of the Caribbean: The Curse of the Black Pearl 67 - { id: 18971, type: "movie" }, // Rosencrantz and Guildenstern Are Dead 68 - { id: 26388, type: "movie" }, // Buried 69 - { id: 152601, type: "movie" }, // Her 70 - { id: 11886, type: "movie" }, // Robin Hood 71 - { id: 1362, type: "movie" }, // The Hobbit 1977 72 - { id: 578, type: "movie" }, // Jaws 73 - { id: 78, type: "movie" }, // Blade Runner 74 - { id: 348, type: "movie" }, // Alien 75 - { id: 198184, type: "movie" }, // Chappie 76 - { id: 405774, type: "movie" }, // Bird Box 77 - { id: 333339, type: "movie" }, // Ready Player One 78 - ]); 79 - 80 - export const EDITOR_PICKS_TV_SHOWS = shuffleArray([ 81 - { id: 456, type: "show" }, // The Simpsons 82 - { id: 73021, type: "show" }, // Disenchantment 83 - { id: 1434, type: "show" }, // Family Guy 84 - { id: 1695, type: "show" }, // Monk 85 - { id: 1408, type: "show" }, // House 86 - { id: 93740, type: "show" }, // Foundation 87 - { id: 60625, type: "show" }, // Rick and Morty 88 - { id: 1396, type: "show" }, // Breaking Bad 89 - { id: 44217, type: "show" }, // Vikings 90 - { id: 90228, type: "show" }, // Dune Prophecy 91 - { id: 13916, type: "show" }, // Death Note 92 - { id: 71912, type: "show" }, // The Witcher 93 - { id: 61222, type: "show" }, // Bojack Horseman 94 - { id: 93405, type: "show" }, // Squid Game 95 - { id: 87108, type: "show" }, // Chernobyl 96 - { id: 105248, type: "show" }, // Cyberpunk: Edgerunners 97 - { id: 82738, type: "show" }, // IRODUKU: The World in Colors 98 - { id: 615, type: "show" }, // Futurama 99 - { id: 4625, type: "show" }, // The New Batman Adventures 100 - { id: 513, type: "show" }, // Batman Beyond 101 - { id: 110948, type: "show" }, // The Snoopy Show 102 - { id: 110492, type: "show" }, // Peacemaker 103 - { id: 125988, type: "show" }, // Silo 104 - { id: 87917, type: "show" }, // For All Mankind 105 - { id: 42009, type: "show" }, // Black Mirror 106 - { id: 86831, type: "show" }, // Love, Death & Robots 107 - { id: 261579, type: "show" }, // Secret Level 108 - ]); 109 - 110 - /** 111 - * The type of content to fetch from various endpoints 112 - */ 113 - export type DiscoverContentType = 114 - | "popular" 115 - | "topRated" 116 - | "onTheAir" 117 - | "nowPlaying" 118 - | "latest" 119 - | "latest4k" 120 - | "latesttv" 121 - | "genre" 122 - | "provider" 123 - | "editorPicks" 124 - | "recommendations"; 125 - 126 - /** 127 - * The type of media to fetch (movie or TV show) 128 - */ 129 - export type MediaType = "movie" | "tv"; 130 - 131 - /** 132 - * Props for the useDiscoverMedia hook 133 - */ 134 - export interface UseDiscoverMediaProps { 135 - /** The type of content to fetch */ 136 - contentType: DiscoverContentType; 137 - /** Whether to fetch movies or TV shows */ 138 - mediaType: MediaType; 139 - /** ID used for genre, provider, or recommendations */ 140 - id?: string; 141 - /** Fallback content type if primary fails */ 142 - fallbackType?: DiscoverContentType; 143 - /** Page number for paginated results */ 144 - page?: number; 145 - /** Genre name for display in title */ 146 - genreName?: string; 147 - /** Provider name for display in title */ 148 - providerName?: string; 149 - /** Media title for recommendations display */ 150 - mediaTitle?: string; 151 - /** Whether this is for a carousel view (limits results) */ 152 - isCarouselView?: boolean; 153 - } 154 - 155 - /** 156 - * Media item returned from discover endpoints 157 - */ 158 - export interface DiscoverMedia { 159 - /** TMDB ID of the media */ 160 - id: number; 161 - /** Title for movies */ 162 - title: string; 163 - /** Title for TV shows */ 164 - name?: string; 165 - /** Poster image path */ 166 - poster_path: string; 167 - /** Backdrop image path */ 168 - backdrop_path: string; 169 - /** Release date for movies */ 170 - release_date?: string; 171 - /** First air date for TV shows */ 172 - first_air_date?: string; 173 - /** Media overview/description */ 174 - overview: string; 175 - /** Average vote score (0-10) */ 176 - vote_average: number; 177 - /** Number of votes */ 178 - vote_count: number; 179 - /** Type of media (movie or show) */ 180 - type?: "movie" | "show"; 181 - } 182 - 183 - /** 184 - * Return type of the useDiscoverMedia hook 185 - */ 186 - export interface UseDiscoverMediaReturn { 187 - /** Array of media items */ 188 - media: DiscoverMedia[]; 189 - /** Whether media is currently being fetched */ 190 - isLoading: boolean; 191 - /** Error message if fetch failed */ 192 - error: string | null; 193 - /** Whether there are more pages to load */ 194 - hasMore: boolean; 195 - /** Function to refetch the current media */ 196 - refetch: () => Promise<void>; 197 - /** Localized section title for the media carousel */ 198 - sectionTitle: string; 199 - } 57 + // Re-export constants for backward compatibility 58 + export { 59 + EDITOR_PICKS_MOVIES, 60 + EDITOR_PICKS_TV_SHOWS, 61 + MOVIE_PROVIDERS, 62 + TV_PROVIDERS, 63 + }; 200 64 201 - /** 202 - * Provider interface for streaming services 203 - */ 204 - export interface Provider { 205 - /** Provider name (e.g., "Netflix", "Hulu") */ 206 - name: string; 207 - /** Provider ID from TMDB */ 208 - id: string; 209 - } 210 - 211 - /** 212 - * Genre interface for media categorization 213 - */ 214 - export interface Genre { 215 - /** Genre ID from TMDB */ 216 - id: number; 217 - /** Genre name (e.g., "Action", "Drama") */ 218 - name: string; 219 - } 220 - 221 - // Static provider lists 222 - export const MOVIE_PROVIDERS: Provider[] = [ 223 - { name: "Netflix", id: "8" }, 224 - { name: "Apple TV+", id: "2" }, 225 - { name: "Amazon Prime Video", id: "10" }, 226 - { name: "Hulu", id: "15" }, 227 - { name: "Disney Plus", id: "337" }, 228 - { name: "Max", id: "1899" }, 229 - { name: "Paramount Plus", id: "531" }, 230 - { name: "Shudder", id: "99" }, 231 - { name: "Crunchyroll", id: "283" }, 232 - { name: "fuboTV", id: "257" }, 233 - { name: "AMC+", id: "526" }, 234 - { name: "Starz", id: "43" }, 235 - { name: "Lifetime", id: "157" }, 236 - { name: "National Geographic", id: "1964" }, 237 - ]; 238 - 239 - export const TV_PROVIDERS: Provider[] = [ 240 - { name: "Netflix", id: "8" }, 241 - { name: "Apple TV+", id: "350" }, 242 - { name: "Amazon Prime Video", id: "10" }, 243 - { name: "Paramount Plus", id: "531" }, 244 - { name: "Hulu", id: "15" }, 245 - { name: "Max", id: "1899" }, 246 - { name: "Adult Swim", id: "318" }, 247 - { name: "Disney Plus", id: "337" }, 248 - { name: "Crunchyroll", id: "283" }, 249 - { name: "fuboTV", id: "257" }, 250 - { name: "Shudder", id: "99" }, 251 - { name: "Discovery +", id: "520" }, 252 - { name: "National Geographic", id: "1964" }, 253 - { name: "Fox", id: "328" }, 254 - ]; 255 - 256 - /** 257 - * Hook for managing providers and genres 258 - */ 259 65 export function useDiscoverOptions(mediaType: MediaType) { 260 66 const [genres, setGenres] = useState<Genre[]>([]); 261 67 const [isLoading, setIsLoading] = useState(false); ··· 364 170 ); 365 171 366 172 const fetchTraktMedia = useCallback( 367 - async (traktFunction: () => Promise<TraktLatestResponse>) => { 173 + async (traktFunction: () => Promise<TraktListResponse>) => { 368 174 try { 369 175 // Create a timeout promise 370 - const timeoutPromise = new Promise<TraktLatestResponse>((_, reject) => { 176 + const timeoutPromise = new Promise<TraktListResponse>((_, reject) => { 371 177 setTimeout(() => reject(new Error("Trakt request timed out")), 3000); 372 178 }); 373 179 ··· 430 236 // Get Trakt function for provider 431 237 const getTraktProviderFunction = useCallback( 432 238 (providerId: string) => { 239 + // Create the key based on provider ID and media type 240 + const key = mediaType === "tv" ? `${providerId}tv` : providerId; 433 241 const trakt = 434 - PROVIDER_TO_TRAKT_MAP[providerId as keyof typeof PROVIDER_TO_TRAKT_MAP]; 435 - if (!trakt) return null; 242 + PROVIDER_TO_TRAKT_MAP[key as keyof typeof PROVIDER_TO_TRAKT_MAP]; 436 243 437 - // Handle TV vs Movies for Netflix 438 - if (trakt === "netflix" && mediaType === "tv") { 439 - return getNetflixTVShows; 440 - } 441 - if (trakt === "netflix" && mediaType === "movie") { 442 - return getNetflixMovies; 443 - } 244 + if (!trakt) return null; 444 245 445 - // Map provider to corresponding Trakt function 246 + // Map trakt endpoint to corresponding function 446 247 switch (trakt) { 447 248 case "appletv": 448 249 return getAppleTVReleases; 449 - case "prime": 450 - return getPrimeReleases; 451 - case "hulu": 452 - return getHuluReleases; 453 - case "disney": 454 - return getDisneyReleases; 455 - case "hbo": 456 - return getHBOReleases; 250 + case "applemovie": 251 + return getAppleMovieReleases; 252 + case "netflixmovies": 253 + return getNetflixMovies; 254 + case "netflixtv": 255 + return getNetflixTVShows; 256 + case "primemovies": 257 + return getPrimeMovies; 258 + case "primetv": 259 + return getPrimeTVShows; 260 + case "hulumovies": 261 + return getHuluMovies; 262 + case "hulutv": 263 + return getHuluTVShows; 264 + case "disneymovies": 265 + return getDisneyMovies; 266 + case "disneytv": 267 + return getDisneyTVShows; 268 + case "hbomovies": 269 + return getHBOMovies; 270 + case "hbotv": 271 + return getHBOTVShows; 272 + case "paramountmovies": 273 + return getParamountMovies; 274 + case "paramounttv": 275 + return getParamountTVShows; 457 276 default: 458 277 return null; 459 278 } 460 279 }, 461 280 [mediaType], 462 281 ); 463 - 464 - // Get Trakt function for genre 465 - const getTraktGenreFunction = useCallback((genreId: string) => { 466 - const trakt = 467 - GENRE_TO_TRAKT_MAP[genreId as keyof typeof GENRE_TO_TRAKT_MAP]; 468 - if (!trakt) return null; 469 - 470 - switch (trakt) { 471 - case "action": 472 - return getActionReleases; 473 - case "drama": 474 - return getDramaReleases; 475 - default: 476 - return null; 477 - } 478 - }, []); 479 282 480 283 const fetchEditorPicks = useCallback(async () => { 481 284 const picks = ··· 515 318 516 319 const attemptFetch = async (type: DiscoverContentType) => { 517 320 let data; 518 - let traktGenreFunction; 519 321 let traktProviderFunction; 520 322 521 323 // Map content types to their endpoints and handling logic ··· 566 368 case "genre": 567 369 if (!id) throw new Error("Genre ID is required"); 568 370 569 - // Try to use Trakt genre endpoint if available 570 - traktGenreFunction = getTraktGenreFunction(id); 571 - if (traktGenreFunction) { 572 - try { 573 - data = await fetchTraktMedia(traktGenreFunction); 574 - setSectionTitle( 575 - mediaType === "movie" 576 - ? t("discover.carousel.title.movies", { category: genreName }) 577 - : t("discover.carousel.title.tvshows", { 578 - category: genreName, 579 - }), 580 - ); 581 - } catch (traktErr) { 582 - console.error( 583 - "Trakt genre fetch failed, falling back to TMDB:", 584 - traktErr, 585 - ); 586 - // Fall back to TMDB 587 - data = await fetchTMDBMedia(`/discover/${mediaType}`, { 588 - with_genres: id, 589 - }); 590 - setSectionTitle( 591 - mediaType === "movie" 592 - ? t("discover.carousel.title.movies", { category: genreName }) 593 - : t("discover.carousel.title.tvshows", { 594 - category: genreName, 595 - }), 596 - ); 597 - } 598 - } else { 599 - // Use TMDB if no Trakt endpoint exists for this genre 600 - data = await fetchTMDBMedia(`/discover/${mediaType}`, { 601 - with_genres: id, 602 - }); 603 - setSectionTitle( 604 - mediaType === "movie" 605 - ? t("discover.carousel.title.movies", { category: genreName }) 606 - : t("discover.carousel.title.tvshows", { category: genreName }), 607 - ); 608 - } 371 + // Use TMDB for genres (Trakt genre endpoints removed) 372 + data = await fetchTMDBMedia(`/discover/${mediaType}`, { 373 + with_genres: id, 374 + }); 375 + setSectionTitle( 376 + mediaType === "movie" 377 + ? t("discover.carousel.title.movies", { category: genreName }) 378 + : t("discover.carousel.title.tvshows", { category: genreName }), 379 + ); 609 380 break; 610 381 611 382 case "provider": ··· 732 503 fetchEditorPicks, 733 504 t, 734 505 page, 735 - getTraktGenreFunction, 736 506 getTraktProviderFunction, 737 507 ]); 738 508
+185
src/pages/discover/types/discover.ts
··· 1 + export type DiscoverContentType = 2 + | "popular" 3 + | "topRated" 4 + | "onTheAir" 5 + | "nowPlaying" 6 + | "latest" 7 + | "latest4k" 8 + | "latesttv" 9 + | "genre" 10 + | "provider" 11 + | "editorPicks" 12 + | "recommendations"; 13 + 14 + export type MediaType = "movie" | "tv"; 15 + 16 + export interface UseDiscoverMediaProps { 17 + contentType: DiscoverContentType; 18 + mediaType: MediaType; 19 + id?: string; 20 + fallbackType?: DiscoverContentType; 21 + page?: number; 22 + genreName?: string; 23 + providerName?: string; 24 + mediaTitle?: string; 25 + isCarouselView?: boolean; 26 + } 27 + 28 + export interface DiscoverMedia { 29 + id: number; 30 + title: string; 31 + name?: string; 32 + poster_path: string; 33 + backdrop_path: string; 34 + release_date?: string; 35 + first_air_date?: string; 36 + overview: string; 37 + vote_average: number; 38 + vote_count: number; 39 + type?: "movie" | "show"; 40 + } 41 + 42 + export interface UseDiscoverMediaReturn { 43 + media: DiscoverMedia[]; 44 + isLoading: boolean; 45 + error: string | null; 46 + hasMore: boolean; 47 + refetch: () => Promise<void>; 48 + sectionTitle: string; 49 + } 50 + 51 + export interface Provider { 52 + name: string; 53 + id: string; 54 + } 55 + 56 + export interface Genre { 57 + id: number; 58 + name: string; 59 + } 60 + 61 + // Shuffle array utility 62 + const shuffleArray = <T>(array: T[]): T[] => { 63 + const shuffled = [...array]; 64 + for (let i = shuffled.length - 1; i > 0; i -= 1) { 65 + const j = Math.floor(Math.random() * (i + 1)); 66 + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; 67 + } 68 + return shuffled; 69 + }; 70 + 71 + // Editor Picks data 72 + export interface EditorPick { 73 + id: number; 74 + type: "movie" | "show"; 75 + } 76 + 77 + const MOVIES_DATA: EditorPick[] = [ 78 + { id: 9342, type: "movie" }, // The Mask of Zorro 79 + { id: 293, type: "movie" }, // A River Runs Through It 80 + { id: 370172, type: "movie" }, // No Time To Die 81 + { id: 661374, type: "movie" }, // The Glass Onion 82 + { id: 207, type: "movie" }, // Dead Poets Society 83 + { id: 378785, type: "movie" }, // The Best of the Blues Brothers 84 + { id: 335984, type: "movie" }, // Blade Runner 2049 85 + { id: 13353, type: "movie" }, // It's the Great Pumpkin, Charlie Brown 86 + { id: 27205, type: "movie" }, // Inception 87 + { id: 106646, type: "movie" }, // The Wolf of Wall Street 88 + { id: 334533, type: "movie" }, // Captain Fantastic 89 + { id: 693134, type: "movie" }, // Dune: Part Two 90 + { id: 765245, type: "movie" }, // Swan Song 91 + { id: 264660, type: "movie" }, // Ex Machina 92 + { id: 92591, type: "movie" }, // Bernie 93 + { id: 976893, type: "movie" }, // Perfect Days 94 + { id: 13187, type: "movie" }, // A Charlie Brown Christmas 95 + { id: 11527, type: "movie" }, // Excalibur 96 + { id: 120, type: "movie" }, // LOTR: The Fellowship of the Ring 97 + { id: 157336, type: "movie" }, // Interstellar 98 + { id: 762, type: "movie" }, // Monty Python and the Holy Grail 99 + { id: 666243, type: "movie" }, // The Witcher: Nightmare of the Wolf 100 + { id: 545611, type: "movie" }, // Everything Everywhere All at Once 101 + { id: 329, type: "movie" }, // Jurrassic Park 102 + { id: 330459, type: "movie" }, // Rogue One: A Star Wars Story 103 + { id: 279, type: "movie" }, // Amadeus 104 + { id: 823219, type: "movie" }, // Flow 105 + { id: 22, type: "movie" }, // Pirates of the Caribbean: The Curse of the Black Pearl 106 + { id: 18971, type: "movie" }, // Rosencrantz and Guildenstern Are Dead 107 + { id: 26388, type: "movie" }, // Buried 108 + { id: 152601, type: "movie" }, // Her 109 + { id: 11886, type: "movie" }, // Robin Hood 110 + { id: 1362, type: "movie" }, // The Hobbit 1977 111 + { id: 578, type: "movie" }, // Jaws 112 + { id: 78, type: "movie" }, // Blade Runner 113 + { id: 348, type: "movie" }, // Alien 114 + { id: 198184, type: "movie" }, // Chappie 115 + { id: 405774, type: "movie" }, // Bird Box 116 + { id: 333339, type: "movie" }, // Ready Player One 117 + ]; 118 + 119 + const TV_SHOWS_DATA: EditorPick[] = [ 120 + { id: 456, type: "show" }, // The Simpsons 121 + { id: 73021, type: "show" }, // Disenchantment 122 + { id: 1434, type: "show" }, // Family Guy 123 + { id: 1695, type: "show" }, // Monk 124 + { id: 1408, type: "show" }, // House 125 + { id: 93740, type: "show" }, // Foundation 126 + { id: 60625, type: "show" }, // Rick and Morty 127 + { id: 1396, type: "show" }, // Breaking Bad 128 + { id: 44217, type: "show" }, // Vikings 129 + { id: 90228, type: "show" }, // Dune Prophecy 130 + { id: 13916, type: "show" }, // Death Note 131 + { id: 71912, type: "show" }, // The Witcher 132 + { id: 61222, type: "show" }, // Bojack Horseman 133 + { id: 93405, type: "show" }, // Squid Game 134 + { id: 87108, type: "show" }, // Chernobyl 135 + { id: 105248, type: "show" }, // Cyberpunk: Edgerunners 136 + { id: 82738, type: "show" }, // IRODUKU: The World in Colors 137 + { id: 615, type: "show" }, // Futurama 138 + { id: 4625, type: "show" }, // The New Batman Adventures 139 + { id: 513, type: "show" }, // Batman Beyond 140 + { id: 110948, type: "show" }, // The Snoopy Show 141 + { id: 110492, type: "show" }, // Peacemaker 142 + { id: 125988, type: "show" }, // Silo 143 + { id: 87917, type: "show" }, // For All Mankind 144 + { id: 42009, type: "show" }, // Black Mirror 145 + { id: 86831, type: "show" }, // Love, Death & Robots 146 + { id: 261579, type: "show" }, // Secret Level 147 + ]; 148 + 149 + export const EDITOR_PICKS_MOVIES = shuffleArray(MOVIES_DATA); 150 + export const EDITOR_PICKS_TV_SHOWS = shuffleArray(TV_SHOWS_DATA); 151 + 152 + // Static provider lists 153 + export const MOVIE_PROVIDERS: Provider[] = [ 154 + { name: "Netflix", id: "8" }, 155 + { name: "Apple TV+", id: "2" }, 156 + { name: "Amazon Prime Video", id: "10" }, 157 + { name: "Hulu", id: "15" }, 158 + { name: "Disney Plus", id: "337" }, 159 + { name: "Max", id: "1899" }, 160 + { name: "Paramount Plus", id: "531" }, 161 + { name: "Shudder", id: "99" }, 162 + { name: "Crunchyroll", id: "283" }, 163 + { name: "fuboTV", id: "257" }, 164 + { name: "AMC+", id: "526" }, 165 + { name: "Starz", id: "43" }, 166 + { name: "Lifetime", id: "157" }, 167 + { name: "National Geographic", id: "1964" }, 168 + ]; 169 + 170 + export const TV_PROVIDERS: Provider[] = [ 171 + { name: "Netflix", id: "8" }, 172 + { name: "Apple TV+", id: "350" }, 173 + { name: "Amazon Prime Video", id: "10" }, 174 + { name: "Paramount Plus", id: "531" }, 175 + { name: "Hulu", id: "15" }, 176 + { name: "Max", id: "1899" }, 177 + { name: "Adult Swim", id: "318" }, 178 + { name: "Disney Plus", id: "337" }, 179 + { name: "Crunchyroll", id: "283" }, 180 + { name: "fuboTV", id: "257" }, 181 + { name: "Shudder", id: "99" }, 182 + { name: "Discovery +", id: "520" }, 183 + { name: "National Geographic", id: "1964" }, 184 + { name: "Fox", id: "328" }, 185 + ];