mirror of
https://github.com/p-stream/p-stream.git
synced 2026-05-18 23:32:03 +00:00
Merge branch 'p-stream:production' into production
This commit is contained in:
commit
375fb7fc24
10 changed files with 453 additions and 438 deletions
|
|
@ -8,6 +8,16 @@
|
||||||
<lastBuildDate>Mon, 29 Sep 2025 18:00:00 MST</lastBuildDate>
|
<lastBuildDate>Mon, 29 Sep 2025 18:00:00 MST</lastBuildDate>
|
||||||
<atom:link href="https://pstream.mov/notifications.xml" rel="self" type="application/rss+xml" />
|
<atom:link href="https://pstream.mov/notifications.xml" rel="self" type="application/rss+xml" />
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<guid>notification-047</guid>
|
||||||
|
<title>Halloween Movies List Added!</title>
|
||||||
|
<description>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! 🎃👻
|
||||||
|
</description>
|
||||||
|
<link>https://pstream.mov/discover/all</link>
|
||||||
|
<pubDate>Fri, 31 Oct 2025 16:40:00 MST</pubDate>
|
||||||
|
<category>Feature</category>
|
||||||
|
</item>
|
||||||
|
|
||||||
<item>
|
<item>
|
||||||
<guid>notification-046</guid>
|
<guid>notification-046</guid>
|
||||||
<title>P-Stream v5.2.1 released!</title>
|
<title>P-Stream v5.2.1 released!</title>
|
||||||
|
|
|
||||||
|
|
@ -1,81 +1,76 @@
|
||||||
|
import { SimpleCache } from "@/utils/cache";
|
||||||
|
|
||||||
import { getMediaDetails } from "./tmdb";
|
import { getMediaDetails } from "./tmdb";
|
||||||
import { MWMediaType } from "./types/mw";
|
|
||||||
import { TMDBContentTypes, TMDBMovieData } from "./types/tmdb";
|
import { TMDBContentTypes, TMDBMovieData } from "./types/tmdb";
|
||||||
|
import type {
|
||||||
export interface TraktLatestResponse {
|
CuratedMovieList,
|
||||||
tmdb_ids: number[];
|
TraktListResponse,
|
||||||
count: number;
|
TraktNetworkResponse,
|
||||||
}
|
TraktReleaseResponse,
|
||||||
|
} from "./types/trakt";
|
||||||
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 const TRAKT_BASE_URL = "https://fed-airdate.pstream.mov";
|
export const TRAKT_BASE_URL = "https://fed-airdate.pstream.mov";
|
||||||
|
|
||||||
export interface TraktDiscoverResponse {
|
// Map provider names to their Trakt endpoints
|
||||||
movie_tmdb_ids: number[];
|
export const PROVIDER_TO_TRAKT_MAP = {
|
||||||
tv_tmdb_ids: number[];
|
"8": "netflixmovies", // Netflix Movies
|
||||||
count: number;
|
"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 {
|
const traktCache = new SimpleCache<TraktCacheKey, any>();
|
||||||
type: string;
|
traktCache.setCompare((a, b) => a.endpoint === b.endpoint);
|
||||||
platforms: string[];
|
traktCache.initialize();
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base function to fetch from Trakt API
|
// Base function to fetch from Trakt API
|
||||||
async function fetchFromTrakt<T = TraktLatestResponse>(
|
async function fetchFromTrakt<T = TraktListResponse>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
): Promise<T> {
|
): 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}`);
|
const response = await fetch(`${TRAKT_BASE_URL}${endpoint}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch from ${endpoint}: ${response.statusText}`);
|
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
|
// Release details
|
||||||
|
|
@ -88,11 +83,25 @@ export async function getReleaseDetails(
|
||||||
if (season !== undefined && episode !== undefined) {
|
if (season !== undefined && episode !== undefined) {
|
||||||
url += `/${season}/${episode}`;
|
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}`);
|
const response = await fetch(`${TRAKT_BASE_URL}${url}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch release details: ${response.statusText}`);
|
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
|
// Latest releases
|
||||||
|
|
@ -102,26 +111,29 @@ export const getLatestTVReleases = () => fetchFromTrakt("/latesttv");
|
||||||
|
|
||||||
// Streaming service releases
|
// Streaming service releases
|
||||||
export const getAppleTVReleases = () => fetchFromTrakt("/appletv");
|
export const getAppleTVReleases = () => fetchFromTrakt("/appletv");
|
||||||
|
export const getAppleMovieReleases = () => fetchFromTrakt("/applemovie");
|
||||||
export const getNetflixMovies = () => fetchFromTrakt("/netflixmovies");
|
export const getNetflixMovies = () => fetchFromTrakt("/netflixmovies");
|
||||||
export const getNetflixTVShows = () => fetchFromTrakt("/netflixtv");
|
export const getNetflixTVShows = () => fetchFromTrakt("/netflixtv");
|
||||||
export const getPrimeReleases = () => fetchFromTrakt("/prime");
|
export const getPrimeMovies = () => fetchFromTrakt("/primemovies");
|
||||||
export const getHuluReleases = () => fetchFromTrakt("/hulu");
|
export const getPrimeTVShows = () => fetchFromTrakt("/primetv");
|
||||||
export const getDisneyReleases = () => fetchFromTrakt("/disney");
|
export const getHuluMovies = () => fetchFromTrakt("/hulumovies");
|
||||||
export const getHBOReleases = () => fetchFromTrakt("/hbo");
|
export const getHuluTVShows = () => fetchFromTrakt("/hulutv");
|
||||||
|
export const getDisneyMovies = () => fetchFromTrakt("/disneymovies");
|
||||||
// Genre-specific releases
|
export const getDisneyTVShows = () => fetchFromTrakt("/disneytv");
|
||||||
export const getActionReleases = () => fetchFromTrakt("/action");
|
export const getHBOMovies = () => fetchFromTrakt("/hbomovies");
|
||||||
export const getDramaReleases = () => fetchFromTrakt("/drama");
|
export const getHBOTVShows = () => fetchFromTrakt("/hbotv");
|
||||||
|
export const getParamountMovies = () => fetchFromTrakt("/paramountmovies");
|
||||||
|
export const getParamountTVShows = () => fetchFromTrakt("/paramounttv");
|
||||||
|
|
||||||
// Popular content
|
// Popular content
|
||||||
export const getPopularTVShows = () => fetchFromTrakt("/populartv");
|
export const getPopularTVShows = () => fetchFromTrakt("/populartv");
|
||||||
export const getPopularMovies = () => fetchFromTrakt("/popularmovies");
|
export const getPopularMovies = () => fetchFromTrakt("/popularmovies");
|
||||||
|
|
||||||
// Discovery content
|
// Discovery content used for the featured carousel
|
||||||
export const getDiscoverContent = () =>
|
export const getDiscoverContent = () =>
|
||||||
fetchFromTrakt<TraktDiscoverResponse>("/discover");
|
fetchFromTrakt<TraktListResponse>("/discover");
|
||||||
|
|
||||||
// Network content
|
// Network information
|
||||||
export const getNetworkContent = (tmdbId: string) =>
|
export const getNetworkContent = (tmdbId: string) =>
|
||||||
fetchFromTrakt<TraktNetworkResponse>(`/network/${tmdbId}`);
|
fetchFromTrakt<TraktNetworkResponse>(`/network/${tmdbId}`);
|
||||||
|
|
||||||
|
|
@ -133,11 +145,17 @@ export const getNeverHeardMovies = () => fetchFromTrakt("/never");
|
||||||
export const getLGBTQContent = () => fetchFromTrakt("/LGBTQ");
|
export const getLGBTQContent = () => fetchFromTrakt("/LGBTQ");
|
||||||
export const getMindfuckMovies = () => fetchFromTrakt("/mindfuck");
|
export const getMindfuckMovies = () => fetchFromTrakt("/mindfuck");
|
||||||
export const getTrueStoryMovies = () => fetchFromTrakt("/truestory");
|
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
|
// Get all curated movie lists
|
||||||
export const getCuratedMovieLists = async (): Promise<CuratedMovieList[]> => {
|
export const getCuratedMovieLists = async (): Promise<CuratedMovieList[]> => {
|
||||||
const listConfigs = [
|
const listConfigs = [
|
||||||
|
{
|
||||||
|
name: "Halloween Movies",
|
||||||
|
slug: "halloween",
|
||||||
|
endpoint: "/halloween",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Letterboxd Top 250 Narrative Feature Films",
|
name: "Letterboxd Top 250 Narrative Feature Films",
|
||||||
slug: "narrative",
|
slug: "narrative",
|
||||||
|
|
@ -188,8 +206,8 @@ export const getCuratedMovieLists = async (): Promise<CuratedMovieList[]> => {
|
||||||
lists.push({
|
lists.push({
|
||||||
listName: config.name,
|
listName: config.name,
|
||||||
listSlug: config.slug,
|
listSlug: config.slug,
|
||||||
tmdbIds: response.tmdb_ids.slice(0, 30), // Limit to first 30 items
|
tmdbIds: response.movie_tmdb_ids.slice(0, 30), // Limit to first 30 items
|
||||||
count: Math.min(response.count, 30), // Update count to reflect the limit
|
count: Math.min(response.movie_tmdb_ids.length, 30), // Update count to reflect the limit
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to fetch ${config.name}:`, error);
|
console.error(`Failed to fetch ${config.name}:`, error);
|
||||||
|
|
@ -233,39 +251,3 @@ export const getMovieDetailsForIds = async (
|
||||||
|
|
||||||
return movieDetails;
|
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 { t } from "i18next";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import {
|
import { getReleaseDetails } from "@/backend/metadata/traktApi";
|
||||||
TraktReleaseResponse,
|
import type { TraktReleaseResponse } from "@/backend/metadata/types/trakt";
|
||||||
getReleaseDetails,
|
|
||||||
} from "@/backend/metadata/traktApi";
|
|
||||||
import { Button } from "@/components/buttons/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { GroupDropdown } from "@/components/form/GroupDropdown";
|
import { GroupDropdown } from "@/components/form/GroupDropdown";
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import { WorkerTestPart } from "@/pages/parts/admin/WorkerTestPart";
|
||||||
|
|
||||||
import { BackendTestPart } from "../parts/admin/BackendTestPart";
|
import { BackendTestPart } from "../parts/admin/BackendTestPart";
|
||||||
import { EmbedOrderPart } from "../parts/admin/EmbedOrderPart";
|
import { EmbedOrderPart } from "../parts/admin/EmbedOrderPart";
|
||||||
import { ProgressCleanupPart } from "../parts/admin/ProgressCleanupPart";
|
|
||||||
|
|
||||||
export function AdminPage() {
|
export function AdminPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -51,7 +50,7 @@ export function AdminPage() {
|
||||||
disabledEmbeds={embedOrderState.disabledEmbeds}
|
disabledEmbeds={embedOrderState.disabledEmbeds}
|
||||||
setDisabledEmbeds={embedOrderState.setDisabledEmbeds}
|
setDisabledEmbeds={embedOrderState.setDisabledEmbeds}
|
||||||
/>
|
/>
|
||||||
<ProgressCleanupPart />
|
{/* <ProgressCleanupPart /> */}
|
||||||
</ThinContainer>
|
</ThinContainer>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ import { useEffect, useRef, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CuratedMovieList,
|
|
||||||
getCuratedMovieLists,
|
getCuratedMovieLists,
|
||||||
getMovieDetailsForIds,
|
getMovieDetailsForIds,
|
||||||
} from "@/backend/metadata/traktApi";
|
} from "@/backend/metadata/traktApi";
|
||||||
import { TMDBMovieData } from "@/backend/metadata/types/tmdb";
|
import { TMDBMovieData } from "@/backend/metadata/types/tmdb";
|
||||||
|
import type { CuratedMovieList } from "@/backend/metadata/types/trakt";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { WideContainer } from "@/components/layout/WideContainer";
|
import { WideContainer } from "@/components/layout/WideContainer";
|
||||||
import { MediaCard } from "@/components/media/MediaCard";
|
import { MediaCard } from "@/components/media/MediaCard";
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ import { useWindowSize } from "react-use";
|
||||||
import { isExtensionActive } from "@/backend/extension/messaging";
|
import { isExtensionActive } from "@/backend/extension/messaging";
|
||||||
import { get, getMediaLogo } from "@/backend/metadata/tmdb";
|
import { get, getMediaLogo } from "@/backend/metadata/tmdb";
|
||||||
import {
|
import {
|
||||||
TraktReleaseResponse,
|
|
||||||
getDiscoverContent,
|
getDiscoverContent,
|
||||||
getReleaseDetails,
|
getReleaseDetails,
|
||||||
} from "@/backend/metadata/traktApi";
|
} from "@/backend/metadata/traktApi";
|
||||||
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
|
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
|
||||||
|
import type { TraktReleaseResponse } from "@/backend/metadata/types/trakt";
|
||||||
import { Button } from "@/components/buttons/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { Movie, TVShow } from "@/pages/discover/common";
|
import { Movie, TVShow } from "@/pages/discover/common";
|
||||||
|
|
|
||||||
|
|
@ -3,259 +3,65 @@ import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { get } from "@/backend/metadata/tmdb";
|
import { get } from "@/backend/metadata/tmdb";
|
||||||
import {
|
import {
|
||||||
GENRE_TO_TRAKT_MAP,
|
|
||||||
PROVIDER_TO_TRAKT_MAP,
|
PROVIDER_TO_TRAKT_MAP,
|
||||||
TraktLatestResponse,
|
getAppleMovieReleases,
|
||||||
getActionReleases,
|
|
||||||
getAppleTVReleases,
|
getAppleTVReleases,
|
||||||
getDisneyReleases,
|
getDisneyMovies,
|
||||||
getDramaReleases,
|
getDisneyTVShows,
|
||||||
getHBOReleases,
|
getHBOMovies,
|
||||||
getHuluReleases,
|
getHBOTVShows,
|
||||||
|
getHuluMovies,
|
||||||
|
getHuluTVShows,
|
||||||
getLatest4KReleases,
|
getLatest4KReleases,
|
||||||
getLatestReleases,
|
getLatestReleases,
|
||||||
getLatestTVReleases,
|
getLatestTVReleases,
|
||||||
getNetflixMovies,
|
getNetflixMovies,
|
||||||
getNetflixTVShows,
|
getNetflixTVShows,
|
||||||
getPrimeReleases,
|
getParamountMovies,
|
||||||
paginateResults,
|
getParamountTVShows,
|
||||||
|
getPrimeMovies,
|
||||||
|
getPrimeTVShows,
|
||||||
} from "@/backend/metadata/traktApi";
|
} 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 { conf } from "@/setup/config";
|
||||||
import { useLanguageStore } from "@/stores/language";
|
import { useLanguageStore } from "@/stores/language";
|
||||||
import { getTmdbLanguageCode } from "@/utils/language";
|
import { getTmdbLanguageCode } from "@/utils/language";
|
||||||
|
|
||||||
// Shuffle array utility
|
// Re-export types for backward compatibility
|
||||||
const shuffleArray = <T>(array: T[]): T[] => {
|
export type {
|
||||||
const shuffled = [...array];
|
DiscoverContentType,
|
||||||
for (let i = shuffled.length - 1; i > 0; i -= 1) {
|
DiscoverMedia,
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
Genre,
|
||||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
MediaType,
|
||||||
}
|
Provider,
|
||||||
return shuffled;
|
UseDiscoverMediaProps,
|
||||||
|
UseDiscoverMediaReturn,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Editor Picks lists
|
// Re-export constants for backward compatibility
|
||||||
export const EDITOR_PICKS_MOVIES = shuffleArray([
|
export {
|
||||||
{ id: 9342, type: "movie" }, // The Mask of Zorro
|
EDITOR_PICKS_MOVIES,
|
||||||
{ id: 293, type: "movie" }, // A River Runs Through It
|
EDITOR_PICKS_TV_SHOWS,
|
||||||
{ id: 370172, type: "movie" }, // No Time To Die
|
MOVIE_PROVIDERS,
|
||||||
{ id: 661374, type: "movie" }, // The Glass Onion
|
TV_PROVIDERS,
|
||||||
{ 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
|
|
||||||
]);
|
|
||||||
|
|
||||||
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) {
|
export function useDiscoverOptions(mediaType: MediaType) {
|
||||||
const [genres, setGenres] = useState<Genre[]>([]);
|
const [genres, setGenres] = useState<Genre[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
@ -364,10 +170,10 @@ export function useDiscoverMedia({
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchTraktMedia = useCallback(
|
const fetchTraktMedia = useCallback(
|
||||||
async (traktFunction: () => Promise<TraktLatestResponse>) => {
|
async (traktFunction: () => Promise<TraktListResponse>) => {
|
||||||
try {
|
try {
|
||||||
// Create a timeout promise
|
// 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);
|
setTimeout(() => reject(new Error("Trakt request timed out")), 3000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -380,6 +186,7 @@ export function useDiscoverMedia({
|
||||||
response,
|
response,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
|
mediaType === "movie" ? "movie" : mediaType === "tv" ? "tv" : "both",
|
||||||
);
|
);
|
||||||
|
|
||||||
// For carousel views, we only need to fetch details for displayed items
|
// For carousel views, we only need to fetch details for displayed items
|
||||||
|
|
@ -429,30 +236,43 @@ export function useDiscoverMedia({
|
||||||
// Get Trakt function for provider
|
// Get Trakt function for provider
|
||||||
const getTraktProviderFunction = useCallback(
|
const getTraktProviderFunction = useCallback(
|
||||||
(providerId: string) => {
|
(providerId: string) => {
|
||||||
|
// Create the key based on provider ID and media type
|
||||||
|
const key = mediaType === "tv" ? `${providerId}tv` : providerId;
|
||||||
const trakt =
|
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;
|
if (!trakt) return null;
|
||||||
|
|
||||||
// Handle TV vs Movies for Netflix
|
// Map trakt endpoint to corresponding function
|
||||||
if (trakt === "netflix" && mediaType === "tv") {
|
|
||||||
return getNetflixTVShows;
|
|
||||||
}
|
|
||||||
if (trakt === "netflix" && mediaType === "movie") {
|
|
||||||
return getNetflixMovies;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map provider to corresponding Trakt function
|
|
||||||
switch (trakt) {
|
switch (trakt) {
|
||||||
case "appletv":
|
case "appletv":
|
||||||
return getAppleTVReleases;
|
return getAppleTVReleases;
|
||||||
case "prime":
|
case "applemovie":
|
||||||
return getPrimeReleases;
|
return getAppleMovieReleases;
|
||||||
case "hulu":
|
case "netflixmovies":
|
||||||
return getHuluReleases;
|
return getNetflixMovies;
|
||||||
case "disney":
|
case "netflixtv":
|
||||||
return getDisneyReleases;
|
return getNetflixTVShows;
|
||||||
case "hbo":
|
case "primemovies":
|
||||||
return getHBOReleases;
|
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:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -460,22 +280,6 @@ export function useDiscoverMedia({
|
||||||
[mediaType],
|
[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 fetchEditorPicks = useCallback(async () => {
|
||||||
const picks =
|
const picks =
|
||||||
mediaType === "movie" ? EDITOR_PICKS_MOVIES : EDITOR_PICKS_TV_SHOWS;
|
mediaType === "movie" ? EDITOR_PICKS_MOVIES : EDITOR_PICKS_TV_SHOWS;
|
||||||
|
|
@ -514,7 +318,6 @@ export function useDiscoverMedia({
|
||||||
|
|
||||||
const attemptFetch = async (type: DiscoverContentType) => {
|
const attemptFetch = async (type: DiscoverContentType) => {
|
||||||
let data;
|
let data;
|
||||||
let traktGenreFunction;
|
|
||||||
let traktProviderFunction;
|
let traktProviderFunction;
|
||||||
|
|
||||||
// Map content types to their endpoints and handling logic
|
// Map content types to their endpoints and handling logic
|
||||||
|
|
@ -565,46 +368,15 @@ export function useDiscoverMedia({
|
||||||
case "genre":
|
case "genre":
|
||||||
if (!id) throw new Error("Genre ID is required");
|
if (!id) throw new Error("Genre ID is required");
|
||||||
|
|
||||||
// Try to use Trakt genre endpoint if available
|
// Use TMDB for genres (Trakt genre endpoints removed)
|
||||||
traktGenreFunction = getTraktGenreFunction(id);
|
data = await fetchTMDBMedia(`/discover/${mediaType}`, {
|
||||||
if (traktGenreFunction) {
|
with_genres: id,
|
||||||
try {
|
});
|
||||||
data = await fetchTraktMedia(traktGenreFunction);
|
setSectionTitle(
|
||||||
setSectionTitle(
|
mediaType === "movie"
|
||||||
mediaType === "movie"
|
? t("discover.carousel.title.movies", { category: genreName })
|
||||||
? t("discover.carousel.title.movies", { category: genreName })
|
: t("discover.carousel.title.tvshows", { 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 }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "provider":
|
case "provider":
|
||||||
|
|
@ -731,7 +503,6 @@ export function useDiscoverMedia({
|
||||||
fetchEditorPicks,
|
fetchEditorPicks,
|
||||||
t,
|
t,
|
||||||
page,
|
page,
|
||||||
getTraktGenreFunction,
|
|
||||||
getTraktProviderFunction,
|
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