mirror of
https://github.com/p-stream/p-stream.git
synced 2026-01-11 20:10:32 +00:00
refactor discover and trakt
This commit is contained in:
parent
3ac786011f
commit
6f3437277d
8 changed files with 433 additions and 453 deletions
|
|
@ -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",
|
||||
};
|
||||
|
|
|
|||
30
src/backend/metadata/traktFunctions.ts
Normal file
30
src/backend/metadata/traktFunctions.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
40
src/backend/metadata/types/trakt.ts
Normal file
40
src/backend/metadata/types/trakt.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
|
||||
|
|
|
|||
185
src/pages/discover/types/discover.ts
Normal file
185
src/pages/discover/types/discover.ts
Normal 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" },
|
||||
];
|
||||
Loading…
Reference in a new issue