diff --git a/public/notifications.xml b/public/notifications.xml
index 36eb0c8c..f7cb2297 100644
--- a/public/notifications.xml
+++ b/public/notifications.xml
@@ -8,6 +8,16 @@
Mon, 29 Sep 2025 18:00:00 MST
+ -
+ notification-047
+ Halloween Movies List Added!
+ We've added a new list of Halloween movies to the site! You can find it in the "Discover All Lists" page or by pressing the link below! Happy Halloween! 🎃👻
+
+ https://pstream.mov/discover/all
+ Fri, 31 Oct 2025 16:40:00 MST
+ Feature
+
+
-
notification-046
P-Stream v5.2.1 released!
diff --git a/src/backend/metadata/traktApi.ts b/src/backend/metadata/traktApi.ts
index c27c1856..94dc67ca 100644
--- a/src/backend/metadata/traktApi.ts
+++ b/src/backend/metadata/traktApi.ts
@@ -1,81 +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 {
- tmdb_ids: number[];
- count: number;
-}
-
-export interface TraktReleaseResponse {
- tmdb_id: number;
- title: string;
- year?: number;
- type: "movie" | "episode";
- season?: number;
- episode?: number;
- quality?: string;
- source?: string;
- group?: string;
- theatrical_release_date?: string;
- digital_release_date?: string;
-}
-
-export interface PaginatedTraktResponse {
- tmdb_ids: number[];
- hasMore: boolean;
- totalCount: number;
-}
-
-export type TraktContentType = "movie" | "episode";
+import type {
+ CuratedMovieList,
+ TraktListResponse,
+ TraktNetworkResponse,
+ TraktReleaseResponse,
+} from "./types/trakt";
export const TRAKT_BASE_URL = "https://fed-airdate.pstream.mov";
-export interface TraktDiscoverResponse {
- movie_tmdb_ids: number[];
- tv_tmdb_ids: number[];
- count: number;
+// Map provider names to their Trakt endpoints
+export const PROVIDER_TO_TRAKT_MAP = {
+ "8": "netflixmovies", // Netflix Movies
+ "8tv": "netflixtv", // Netflix TV Shows
+ "2": "applemovie", // Apple TV+ Movies
+ "2tv": "appletv", // Apple TV+ (both)
+ "10": "primemovies", // Prime Video Movies
+ "10tv": "primetv", // Prime Video TV Shows
+ "15": "hulumovies", // Hulu Movies
+ "15tv": "hulutv", // Hulu TV Shows
+ "337": "disneymovies", // Disney+ Movies
+ "337tv": "disneytv", // Disney+ TV Shows
+ "1899": "hbomovies", // Max Movies
+ "1899tv": "hbotv", // Max TV Shows
+ "531": "paramountmovies", // Paramount+ Movies
+ "531tv": "paramounttv", // Paramount+ TV Shows
+} as const;
+
+// Map provider names to their image filenames
+export const PROVIDER_TO_IMAGE_MAP: Record = {
+ Max: "max",
+ "Prime Video": "prime",
+ Netflix: "netflix",
+ "Disney+": "disney",
+ Hulu: "hulu",
+ "Apple TV+": "appletv",
+ "Paramount+": "paramount",
+};
+
+// Cache for Trakt API responses
+interface TraktCacheKey {
+ endpoint: string;
}
-export interface TraktNetworkResponse {
- type: string;
- platforms: string[];
- count: number;
-}
-
-export interface CuratedMovieList {
- listName: string;
- listSlug: string;
- tmdbIds: number[];
- count: number;
-}
-
-// Pagination utility
-export function paginateResults(
- results: TraktLatestResponse,
- page: number,
- pageSize: number = 20,
-): PaginatedTraktResponse {
- const startIndex = (page - 1) * pageSize;
- const endIndex = startIndex + pageSize;
- const paginatedIds = results.tmdb_ids.slice(startIndex, endIndex);
-
- return {
- tmdb_ids: paginatedIds,
- hasMore: endIndex < results.tmdb_ids.length,
- totalCount: results.tmdb_ids.length,
- };
-}
+const traktCache = new SimpleCache();
+traktCache.setCompare((a, b) => a.endpoint === b.endpoint);
+traktCache.initialize();
// Base function to fetch from Trakt API
-async function fetchFromTrakt(
+async function fetchFromTrakt(
endpoint: string,
): Promise {
+ // Check cache first
+ const cacheKey: TraktCacheKey = { endpoint };
+ const cachedResult = traktCache.get(cacheKey);
+ if (cachedResult) {
+ return cachedResult as T;
+ }
+
+ // Make the API request
const response = await fetch(`${TRAKT_BASE_URL}${endpoint}`);
if (!response.ok) {
throw new Error(`Failed to fetch from ${endpoint}: ${response.statusText}`);
}
- return response.json();
+ const result = await response.json();
+
+ // Cache the result for 1 hour (3600 seconds)
+ traktCache.set(cacheKey, result, 3600);
+
+ return result as T;
}
// Release details
@@ -88,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
@@ -102,26 +111,29 @@ export const getLatestTVReleases = () => fetchFromTrakt("/latesttv");
// Streaming service releases
export const getAppleTVReleases = () => fetchFromTrakt("/appletv");
+export const getAppleMovieReleases = () => fetchFromTrakt("/applemovie");
export const getNetflixMovies = () => fetchFromTrakt("/netflixmovies");
export const getNetflixTVShows = () => fetchFromTrakt("/netflixtv");
-export const getPrimeReleases = () => fetchFromTrakt("/prime");
-export const getHuluReleases = () => fetchFromTrakt("/hulu");
-export const getDisneyReleases = () => fetchFromTrakt("/disney");
-export const getHBOReleases = () => fetchFromTrakt("/hbo");
-
-// Genre-specific releases
-export const getActionReleases = () => fetchFromTrakt("/action");
-export const getDramaReleases = () => fetchFromTrakt("/drama");
+export const getPrimeMovies = () => fetchFromTrakt("/primemovies");
+export const getPrimeTVShows = () => fetchFromTrakt("/primetv");
+export const getHuluMovies = () => fetchFromTrakt("/hulumovies");
+export const getHuluTVShows = () => fetchFromTrakt("/hulutv");
+export const getDisneyMovies = () => fetchFromTrakt("/disneymovies");
+export const getDisneyTVShows = () => fetchFromTrakt("/disneytv");
+export const getHBOMovies = () => fetchFromTrakt("/hbomovies");
+export const getHBOTVShows = () => fetchFromTrakt("/hbotv");
+export const getParamountMovies = () => fetchFromTrakt("/paramountmovies");
+export const getParamountTVShows = () => fetchFromTrakt("/paramounttv");
// Popular content
export const getPopularTVShows = () => fetchFromTrakt("/populartv");
export const getPopularMovies = () => fetchFromTrakt("/popularmovies");
-// Discovery content
+// Discovery content used for the featured carousel
export const getDiscoverContent = () =>
- fetchFromTrakt("/discover");
+ fetchFromTrakt("/discover");
-// Network content
+// Network information
export const getNetworkContent = (tmdbId: string) =>
fetchFromTrakt(`/network/${tmdbId}`);
@@ -133,11 +145,17 @@ export const getNeverHeardMovies = () => fetchFromTrakt("/never");
export const getLGBTQContent = () => fetchFromTrakt("/LGBTQ");
export const getMindfuckMovies = () => fetchFromTrakt("/mindfuck");
export const getTrueStoryMovies = () => fetchFromTrakt("/truestory");
-// export const getGreatestTVShows = () => fetchFromTrakt("/greatesttv"); // We only have movies set up. TODO add a type for tv and add more tv routes.
+export const getHalloweenMovies = () => fetchFromTrakt("/halloween");
+// 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 => {
const listConfigs = [
+ {
+ name: "Halloween Movies",
+ slug: "halloween",
+ endpoint: "/halloween",
+ },
{
name: "Letterboxd Top 250 Narrative Feature Films",
slug: "narrative",
@@ -188,8 +206,8 @@ export const getCuratedMovieLists = async (): Promise => {
lists.push({
listName: config.name,
listSlug: config.slug,
- tmdbIds: response.tmdb_ids.slice(0, 30), // Limit to first 30 items
- count: Math.min(response.count, 30), // Update count to reflect the limit
+ tmdbIds: response.movie_tmdb_ids.slice(0, 30), // Limit to first 30 items
+ count: Math.min(response.movie_tmdb_ids.length, 30), // Update count to reflect the limit
});
} catch (error) {
console.error(`Failed to fetch ${config.name}:`, error);
@@ -233,39 +251,3 @@ export const getMovieDetailsForIds = async (
return movieDetails;
};
-
-// Type conversion utilities
-export function convertToMediaType(type: TraktContentType): MWMediaType {
- return type === "movie" ? MWMediaType.MOVIE : MWMediaType.SERIES;
-}
-
-export function convertFromMediaType(type: MWMediaType): TraktContentType {
- return type === MWMediaType.MOVIE ? "movie" : "episode";
-}
-
-// Map provider names to their Trakt endpoints
-export const PROVIDER_TO_TRAKT_MAP = {
- "8": "netflix", // Netflix
- "2": "appletv", // Apple TV+
- "10": "prime", // Prime Video
- "15": "hulu", // Hulu
- "337": "disney", // Disney+
- "1899": "hbo", // Max
-} as const;
-
-// Map genres to their Trakt endpoints
-export const GENRE_TO_TRAKT_MAP = {
- "28": "action", // Action
- "18": "drama", // Drama
-} as const;
-
-// Map provider names to their image filenames
-export const PROVIDER_TO_IMAGE_MAP: Record = {
- Max: "max",
- "Prime Video": "prime",
- Netflix: "netflix",
- "Disney+": "disney",
- Hulu: "hulu",
- "Apple TV+": "appletv",
- "Paramount+": "paramount",
-};
diff --git a/src/backend/metadata/traktFunctions.ts b/src/backend/metadata/traktFunctions.ts
new file mode 100644
index 00000000..d1ef952b
--- /dev/null
+++ b/src/backend/metadata/traktFunctions.ts
@@ -0,0 +1,30 @@
+import type { PaginatedTraktResponse, TraktListResponse } from "./types/trakt";
+
+// Pagination utility
+export function paginateResults(
+ results: TraktListResponse,
+ page: number,
+ pageSize: number = 20,
+ contentType: "movie" | "tv" | "both" = "both",
+): PaginatedTraktResponse {
+ let tmdbIds: number[];
+
+ if (contentType === "movie") {
+ tmdbIds = results.movie_tmdb_ids;
+ } else if (contentType === "tv") {
+ tmdbIds = results.tv_tmdb_ids;
+ } else {
+ // For 'both', combine movies and TV shows
+ tmdbIds = [...results.movie_tmdb_ids, ...results.tv_tmdb_ids];
+ }
+
+ const startIndex = (page - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ const paginatedIds = tmdbIds.slice(startIndex, endIndex);
+
+ return {
+ tmdb_ids: paginatedIds,
+ hasMore: endIndex < tmdbIds.length,
+ totalCount: tmdbIds.length,
+ };
+}
diff --git a/src/backend/metadata/types/trakt.ts b/src/backend/metadata/types/trakt.ts
new file mode 100644
index 00000000..f223e66c
--- /dev/null
+++ b/src/backend/metadata/types/trakt.ts
@@ -0,0 +1,40 @@
+export interface TraktListResponse {
+ movie_tmdb_ids: number[];
+ tv_tmdb_ids: number[];
+ count: number;
+}
+
+export interface TraktReleaseResponse {
+ tmdb_id: number;
+ title: string;
+ year?: number;
+ type: "movie" | "episode";
+ season?: number;
+ episode?: number;
+ quality?: string;
+ source?: string;
+ group?: string;
+ theatrical_release_date?: string;
+ digital_release_date?: string;
+}
+
+export interface PaginatedTraktResponse {
+ tmdb_ids: number[];
+ hasMore: boolean;
+ totalCount: number;
+}
+
+export type TraktContentType = "movie" | "episode";
+
+export interface TraktNetworkResponse {
+ type: string;
+ platforms: string[];
+ count: number;
+}
+
+export interface CuratedMovieList {
+ listName: string;
+ listSlug: string;
+ tmdbIds: number[];
+ count: number;
+}
diff --git a/src/components/overlays/detailsModal/components/sections/DetailsBody.tsx b/src/components/overlays/detailsModal/components/sections/DetailsBody.tsx
index c9643cb5..ed2259f9 100644
--- a/src/components/overlays/detailsModal/components/sections/DetailsBody.tsx
+++ b/src/components/overlays/detailsModal/components/sections/DetailsBody.tsx
@@ -2,10 +2,8 @@ import classNames from "classnames";
import { t } from "i18next";
import { useEffect, useState } from "react";
-import {
- TraktReleaseResponse,
- getReleaseDetails,
-} from "@/backend/metadata/traktApi";
+import { getReleaseDetails } from "@/backend/metadata/traktApi";
+import type { TraktReleaseResponse } from "@/backend/metadata/types/trakt";
import { Button } from "@/components/buttons/Button";
import { IconPatch } from "@/components/buttons/IconPatch";
import { GroupDropdown } from "@/components/form/GroupDropdown";
diff --git a/src/pages/admin/AdminPage.tsx b/src/pages/admin/AdminPage.tsx
index 389c3029..e3d39eca 100644
--- a/src/pages/admin/AdminPage.tsx
+++ b/src/pages/admin/AdminPage.tsx
@@ -14,7 +14,6 @@ import { WorkerTestPart } from "@/pages/parts/admin/WorkerTestPart";
import { BackendTestPart } from "../parts/admin/BackendTestPart";
import { EmbedOrderPart } from "../parts/admin/EmbedOrderPart";
-import { ProgressCleanupPart } from "../parts/admin/ProgressCleanupPart";
export function AdminPage() {
const { t } = useTranslation();
@@ -51,7 +50,7 @@ export function AdminPage() {
disabledEmbeds={embedOrderState.disabledEmbeds}
setDisabledEmbeds={embedOrderState.setDisabledEmbeds}
/>
-
+ {/* */}
(array: T[]): T[] => {
- const shuffled = [...array];
- for (let i = shuffled.length - 1; i > 0; i -= 1) {
- const j = Math.floor(Math.random() * (i + 1));
- [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
- }
- return shuffled;
+// Re-export types for backward compatibility
+export type {
+ DiscoverContentType,
+ DiscoverMedia,
+ Genre,
+ MediaType,
+ Provider,
+ UseDiscoverMediaProps,
+ UseDiscoverMediaReturn,
};
-// Editor Picks lists
-export const EDITOR_PICKS_MOVIES = shuffleArray([
- { id: 9342, type: "movie" }, // The Mask of Zorro
- { id: 293, type: "movie" }, // A River Runs Through It
- { id: 370172, type: "movie" }, // No Time To Die
- { id: 661374, type: "movie" }, // The Glass Onion
- { id: 207, type: "movie" }, // Dead Poets Society
- { id: 378785, type: "movie" }, // The Best of the Blues Brothers
- { id: 335984, type: "movie" }, // Blade Runner 2049
- { id: 13353, type: "movie" }, // It's the Great Pumpkin, Charlie Brown
- { id: 27205, type: "movie" }, // Inception
- { id: 106646, type: "movie" }, // The Wolf of Wall Street
- { id: 334533, type: "movie" }, // Captain Fantastic
- { id: 693134, type: "movie" }, // Dune: Part Two
- { id: 765245, type: "movie" }, // Swan Song
- { id: 264660, type: "movie" }, // Ex Machina
- { id: 92591, type: "movie" }, // Bernie
- { id: 976893, type: "movie" }, // Perfect Days
- { id: 13187, type: "movie" }, // A Charlie Brown Christmas
- { id: 11527, type: "movie" }, // Excalibur
- { id: 120, type: "movie" }, // LOTR: The Fellowship of the Ring
- { id: 157336, type: "movie" }, // Interstellar
- { id: 762, type: "movie" }, // Monty Python and the Holy Grail
- { id: 666243, type: "movie" }, // The Witcher: Nightmare of the Wolf
- { id: 545611, type: "movie" }, // Everything Everywhere All at Once
- { id: 329, type: "movie" }, // Jurrassic Park
- { id: 330459, type: "movie" }, // Rogue One: A Star Wars Story
- { id: 279, type: "movie" }, // Amadeus
- { id: 823219, type: "movie" }, // Flow
- { id: 22, type: "movie" }, // Pirates of the Caribbean: The Curse of the Black Pearl
- { id: 18971, type: "movie" }, // Rosencrantz and Guildenstern Are Dead
- { id: 26388, type: "movie" }, // Buried
- { id: 152601, type: "movie" }, // Her
- { id: 11886, type: "movie" }, // Robin Hood
- { id: 1362, type: "movie" }, // The Hobbit 1977
- { id: 578, type: "movie" }, // Jaws
- { id: 78, type: "movie" }, // Blade Runner
- { id: 348, type: "movie" }, // Alien
- { id: 198184, type: "movie" }, // Chappie
- { id: 405774, type: "movie" }, // Bird Box
- { id: 333339, type: "movie" }, // Ready Player One
-]);
+// Re-export constants for backward compatibility
+export {
+ EDITOR_PICKS_MOVIES,
+ EDITOR_PICKS_TV_SHOWS,
+ MOVIE_PROVIDERS,
+ TV_PROVIDERS,
+};
-export const EDITOR_PICKS_TV_SHOWS = shuffleArray([
- { id: 456, type: "show" }, // The Simpsons
- { id: 73021, type: "show" }, // Disenchantment
- { id: 1434, type: "show" }, // Family Guy
- { id: 1695, type: "show" }, // Monk
- { id: 1408, type: "show" }, // House
- { id: 93740, type: "show" }, // Foundation
- { id: 60625, type: "show" }, // Rick and Morty
- { id: 1396, type: "show" }, // Breaking Bad
- { id: 44217, type: "show" }, // Vikings
- { id: 90228, type: "show" }, // Dune Prophecy
- { id: 13916, type: "show" }, // Death Note
- { id: 71912, type: "show" }, // The Witcher
- { id: 61222, type: "show" }, // Bojack Horseman
- { id: 93405, type: "show" }, // Squid Game
- { id: 87108, type: "show" }, // Chernobyl
- { id: 105248, type: "show" }, // Cyberpunk: Edgerunners
- { id: 82738, type: "show" }, // IRODUKU: The World in Colors
- { id: 615, type: "show" }, // Futurama
- { id: 4625, type: "show" }, // The New Batman Adventures
- { id: 513, type: "show" }, // Batman Beyond
- { id: 110948, type: "show" }, // The Snoopy Show
- { id: 110492, type: "show" }, // Peacemaker
- { id: 125988, type: "show" }, // Silo
- { id: 87917, type: "show" }, // For All Mankind
- { id: 42009, type: "show" }, // Black Mirror
- { id: 86831, type: "show" }, // Love, Death & Robots
- { id: 261579, type: "show" }, // Secret Level
-]);
-
-/**
- * The type of content to fetch from various endpoints
- */
-export type DiscoverContentType =
- | "popular"
- | "topRated"
- | "onTheAir"
- | "nowPlaying"
- | "latest"
- | "latest4k"
- | "latesttv"
- | "genre"
- | "provider"
- | "editorPicks"
- | "recommendations";
-
-/**
- * The type of media to fetch (movie or TV show)
- */
-export type MediaType = "movie" | "tv";
-
-/**
- * Props for the useDiscoverMedia hook
- */
-export interface UseDiscoverMediaProps {
- /** The type of content to fetch */
- contentType: DiscoverContentType;
- /** Whether to fetch movies or TV shows */
- mediaType: MediaType;
- /** ID used for genre, provider, or recommendations */
- id?: string;
- /** Fallback content type if primary fails */
- fallbackType?: DiscoverContentType;
- /** Page number for paginated results */
- page?: number;
- /** Genre name for display in title */
- genreName?: string;
- /** Provider name for display in title */
- providerName?: string;
- /** Media title for recommendations display */
- mediaTitle?: string;
- /** Whether this is for a carousel view (limits results) */
- isCarouselView?: boolean;
-}
-
-/**
- * Media item returned from discover endpoints
- */
-export interface DiscoverMedia {
- /** TMDB ID of the media */
- id: number;
- /** Title for movies */
- title: string;
- /** Title for TV shows */
- name?: string;
- /** Poster image path */
- poster_path: string;
- /** Backdrop image path */
- backdrop_path: string;
- /** Release date for movies */
- release_date?: string;
- /** First air date for TV shows */
- first_air_date?: string;
- /** Media overview/description */
- overview: string;
- /** Average vote score (0-10) */
- vote_average: number;
- /** Number of votes */
- vote_count: number;
- /** Type of media (movie or show) */
- type?: "movie" | "show";
-}
-
-/**
- * Return type of the useDiscoverMedia hook
- */
-export interface UseDiscoverMediaReturn {
- /** Array of media items */
- media: DiscoverMedia[];
- /** Whether media is currently being fetched */
- isLoading: boolean;
- /** Error message if fetch failed */
- error: string | null;
- /** Whether there are more pages to load */
- hasMore: boolean;
- /** Function to refetch the current media */
- refetch: () => Promise;
- /** Localized section title for the media carousel */
- sectionTitle: string;
-}
-
-/**
- * Provider interface for streaming services
- */
-export interface Provider {
- /** Provider name (e.g., "Netflix", "Hulu") */
- name: string;
- /** Provider ID from TMDB */
- id: string;
-}
-
-/**
- * Genre interface for media categorization
- */
-export interface Genre {
- /** Genre ID from TMDB */
- id: number;
- /** Genre name (e.g., "Action", "Drama") */
- name: string;
-}
-
-// Static provider lists
-export const MOVIE_PROVIDERS: Provider[] = [
- { name: "Netflix", id: "8" },
- { name: "Apple TV+", id: "2" },
- { name: "Amazon Prime Video", id: "10" },
- { name: "Hulu", id: "15" },
- { name: "Disney Plus", id: "337" },
- { name: "Max", id: "1899" },
- { name: "Paramount Plus", id: "531" },
- { name: "Shudder", id: "99" },
- { name: "Crunchyroll", id: "283" },
- { name: "fuboTV", id: "257" },
- { name: "AMC+", id: "526" },
- { name: "Starz", id: "43" },
- { name: "Lifetime", id: "157" },
- { name: "National Geographic", id: "1964" },
-];
-
-export const TV_PROVIDERS: Provider[] = [
- { name: "Netflix", id: "8" },
- { name: "Apple TV+", id: "350" },
- { name: "Amazon Prime Video", id: "10" },
- { name: "Paramount Plus", id: "531" },
- { name: "Hulu", id: "15" },
- { name: "Max", id: "1899" },
- { name: "Adult Swim", id: "318" },
- { name: "Disney Plus", id: "337" },
- { name: "Crunchyroll", id: "283" },
- { name: "fuboTV", id: "257" },
- { name: "Shudder", id: "99" },
- { name: "Discovery +", id: "520" },
- { name: "National Geographic", id: "1964" },
- { name: "Fox", id: "328" },
-];
-
-/**
- * Hook for managing providers and genres
- */
export function useDiscoverOptions(mediaType: MediaType) {
const [genres, setGenres] = useState([]);
const [isLoading, setIsLoading] = useState(false);
@@ -364,10 +170,10 @@ export function useDiscoverMedia({
);
const fetchTraktMedia = useCallback(
- async (traktFunction: () => Promise) => {
+ async (traktFunction: () => Promise) => {
try {
// Create a timeout promise
- const timeoutPromise = new Promise((_, reject) => {
+ const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Trakt request timed out")), 3000);
});
@@ -380,6 +186,7 @@ export function useDiscoverMedia({
response,
page,
pageSize,
+ mediaType === "movie" ? "movie" : mediaType === "tv" ? "tv" : "both",
);
// For carousel views, we only need to fetch details for displayed items
@@ -429,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;
}
@@ -460,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;
@@ -514,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
@@ -565,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":
@@ -731,7 +503,6 @@ export function useDiscoverMedia({
fetchEditorPicks,
t,
page,
- getTraktGenreFunction,
getTraktProviderFunction,
]);
diff --git a/src/pages/discover/types/discover.ts b/src/pages/discover/types/discover.ts
new file mode 100644
index 00000000..536576dd
--- /dev/null
+++ b/src/pages/discover/types/discover.ts
@@ -0,0 +1,185 @@
+export type DiscoverContentType =
+ | "popular"
+ | "topRated"
+ | "onTheAir"
+ | "nowPlaying"
+ | "latest"
+ | "latest4k"
+ | "latesttv"
+ | "genre"
+ | "provider"
+ | "editorPicks"
+ | "recommendations";
+
+export type MediaType = "movie" | "tv";
+
+export interface UseDiscoverMediaProps {
+ contentType: DiscoverContentType;
+ mediaType: MediaType;
+ id?: string;
+ fallbackType?: DiscoverContentType;
+ page?: number;
+ genreName?: string;
+ providerName?: string;
+ mediaTitle?: string;
+ isCarouselView?: boolean;
+}
+
+export interface DiscoverMedia {
+ id: number;
+ title: string;
+ name?: string;
+ poster_path: string;
+ backdrop_path: string;
+ release_date?: string;
+ first_air_date?: string;
+ overview: string;
+ vote_average: number;
+ vote_count: number;
+ type?: "movie" | "show";
+}
+
+export interface UseDiscoverMediaReturn {
+ media: DiscoverMedia[];
+ isLoading: boolean;
+ error: string | null;
+ hasMore: boolean;
+ refetch: () => Promise;
+ sectionTitle: string;
+}
+
+export interface Provider {
+ name: string;
+ id: string;
+}
+
+export interface Genre {
+ id: number;
+ name: string;
+}
+
+// Shuffle array utility
+const shuffleArray = (array: T[]): T[] => {
+ const shuffled = [...array];
+ for (let i = shuffled.length - 1; i > 0; i -= 1) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
+ }
+ return shuffled;
+};
+
+// Editor Picks data
+export interface EditorPick {
+ id: number;
+ type: "movie" | "show";
+}
+
+const MOVIES_DATA: EditorPick[] = [
+ { id: 9342, type: "movie" }, // The Mask of Zorro
+ { id: 293, type: "movie" }, // A River Runs Through It
+ { id: 370172, type: "movie" }, // No Time To Die
+ { id: 661374, type: "movie" }, // The Glass Onion
+ { id: 207, type: "movie" }, // Dead Poets Society
+ { id: 378785, type: "movie" }, // The Best of the Blues Brothers
+ { id: 335984, type: "movie" }, // Blade Runner 2049
+ { id: 13353, type: "movie" }, // It's the Great Pumpkin, Charlie Brown
+ { id: 27205, type: "movie" }, // Inception
+ { id: 106646, type: "movie" }, // The Wolf of Wall Street
+ { id: 334533, type: "movie" }, // Captain Fantastic
+ { id: 693134, type: "movie" }, // Dune: Part Two
+ { id: 765245, type: "movie" }, // Swan Song
+ { id: 264660, type: "movie" }, // Ex Machina
+ { id: 92591, type: "movie" }, // Bernie
+ { id: 976893, type: "movie" }, // Perfect Days
+ { id: 13187, type: "movie" }, // A Charlie Brown Christmas
+ { id: 11527, type: "movie" }, // Excalibur
+ { id: 120, type: "movie" }, // LOTR: The Fellowship of the Ring
+ { id: 157336, type: "movie" }, // Interstellar
+ { id: 762, type: "movie" }, // Monty Python and the Holy Grail
+ { id: 666243, type: "movie" }, // The Witcher: Nightmare of the Wolf
+ { id: 545611, type: "movie" }, // Everything Everywhere All at Once
+ { id: 329, type: "movie" }, // Jurrassic Park
+ { id: 330459, type: "movie" }, // Rogue One: A Star Wars Story
+ { id: 279, type: "movie" }, // Amadeus
+ { id: 823219, type: "movie" }, // Flow
+ { id: 22, type: "movie" }, // Pirates of the Caribbean: The Curse of the Black Pearl
+ { id: 18971, type: "movie" }, // Rosencrantz and Guildenstern Are Dead
+ { id: 26388, type: "movie" }, // Buried
+ { id: 152601, type: "movie" }, // Her
+ { id: 11886, type: "movie" }, // Robin Hood
+ { id: 1362, type: "movie" }, // The Hobbit 1977
+ { id: 578, type: "movie" }, // Jaws
+ { id: 78, type: "movie" }, // Blade Runner
+ { id: 348, type: "movie" }, // Alien
+ { id: 198184, type: "movie" }, // Chappie
+ { id: 405774, type: "movie" }, // Bird Box
+ { id: 333339, type: "movie" }, // Ready Player One
+];
+
+const TV_SHOWS_DATA: EditorPick[] = [
+ { id: 456, type: "show" }, // The Simpsons
+ { id: 73021, type: "show" }, // Disenchantment
+ { id: 1434, type: "show" }, // Family Guy
+ { id: 1695, type: "show" }, // Monk
+ { id: 1408, type: "show" }, // House
+ { id: 93740, type: "show" }, // Foundation
+ { id: 60625, type: "show" }, // Rick and Morty
+ { id: 1396, type: "show" }, // Breaking Bad
+ { id: 44217, type: "show" }, // Vikings
+ { id: 90228, type: "show" }, // Dune Prophecy
+ { id: 13916, type: "show" }, // Death Note
+ { id: 71912, type: "show" }, // The Witcher
+ { id: 61222, type: "show" }, // Bojack Horseman
+ { id: 93405, type: "show" }, // Squid Game
+ { id: 87108, type: "show" }, // Chernobyl
+ { id: 105248, type: "show" }, // Cyberpunk: Edgerunners
+ { id: 82738, type: "show" }, // IRODUKU: The World in Colors
+ { id: 615, type: "show" }, // Futurama
+ { id: 4625, type: "show" }, // The New Batman Adventures
+ { id: 513, type: "show" }, // Batman Beyond
+ { id: 110948, type: "show" }, // The Snoopy Show
+ { id: 110492, type: "show" }, // Peacemaker
+ { id: 125988, type: "show" }, // Silo
+ { id: 87917, type: "show" }, // For All Mankind
+ { id: 42009, type: "show" }, // Black Mirror
+ { id: 86831, type: "show" }, // Love, Death & Robots
+ { id: 261579, type: "show" }, // Secret Level
+];
+
+export const EDITOR_PICKS_MOVIES = shuffleArray(MOVIES_DATA);
+export const EDITOR_PICKS_TV_SHOWS = shuffleArray(TV_SHOWS_DATA);
+
+// Static provider lists
+export const MOVIE_PROVIDERS: Provider[] = [
+ { name: "Netflix", id: "8" },
+ { name: "Apple TV+", id: "2" },
+ { name: "Amazon Prime Video", id: "10" },
+ { name: "Hulu", id: "15" },
+ { name: "Disney Plus", id: "337" },
+ { name: "Max", id: "1899" },
+ { name: "Paramount Plus", id: "531" },
+ { name: "Shudder", id: "99" },
+ { name: "Crunchyroll", id: "283" },
+ { name: "fuboTV", id: "257" },
+ { name: "AMC+", id: "526" },
+ { name: "Starz", id: "43" },
+ { name: "Lifetime", id: "157" },
+ { name: "National Geographic", id: "1964" },
+];
+
+export const TV_PROVIDERS: Provider[] = [
+ { name: "Netflix", id: "8" },
+ { name: "Apple TV+", id: "350" },
+ { name: "Amazon Prime Video", id: "10" },
+ { name: "Paramount Plus", id: "531" },
+ { name: "Hulu", id: "15" },
+ { name: "Max", id: "1899" },
+ { name: "Adult Swim", id: "318" },
+ { name: "Disney Plus", id: "337" },
+ { name: "Crunchyroll", id: "283" },
+ { name: "fuboTV", id: "257" },
+ { name: "Shudder", id: "99" },
+ { name: "Discovery +", id: "520" },
+ { name: "National Geographic", id: "1964" },
+ { name: "Fox", id: "328" },
+];