new trakt /discover endpoint for featured carousel

This commit is contained in:
Pas 2025-06-07 12:34:04 -06:00
parent 0f1ce2811f
commit eaf2399539
2 changed files with 108 additions and 48 deletions

View file

@ -29,6 +29,12 @@ export type TraktContentType = "movie" | "episode";
export const TRAKT_BASE_URL = "https://fed-airdate.pstream.org"; export const TRAKT_BASE_URL = "https://fed-airdate.pstream.org";
export interface TraktDiscoverResponse {
movie_tmdb_ids: number[];
tv_tmdb_ids: number[];
count: number;
}
// Pagination utility // Pagination utility
export function paginateResults( export function paginateResults(
results: TraktLatestResponse, results: TraktLatestResponse,
@ -47,7 +53,9 @@ export function paginateResults(
} }
// Base function to fetch from Trakt API // Base function to fetch from Trakt API
async function fetchFromTrakt(endpoint: string): Promise<TraktLatestResponse> { async function fetchFromTrakt<T = TraktLatestResponse>(
endpoint: string,
): Promise<T> {
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}`);
@ -94,6 +102,10 @@ export const getDramaReleases = () => fetchFromTrakt("/drama");
export const getPopularTVShows = () => fetchFromTrakt("/populartv"); export const getPopularTVShows = () => fetchFromTrakt("/populartv");
export const getPopularMovies = () => fetchFromTrakt("/popularmovies"); export const getPopularMovies = () => fetchFromTrakt("/popularmovies");
// Discovery content
export const getDiscoverContent = () =>
fetchFromTrakt<TraktDiscoverResponse>("/discover");
// Type conversion utilities // Type conversion utilities
export function convertToMediaType(type: TraktContentType): MWMediaType { export function convertToMediaType(type: TraktContentType): MWMediaType {
return type === "movie" ? MWMediaType.MOVIE : MWMediaType.SERIES; return type === "movie" ? MWMediaType.MOVIE : MWMediaType.SERIES;

View file

@ -8,6 +8,7 @@ import { isExtensionActive } from "@/backend/extension/messaging";
import { get, getMediaLogo } from "@/backend/metadata/tmdb"; import { get, getMediaLogo } from "@/backend/metadata/tmdb";
import { import {
TraktReleaseResponse, TraktReleaseResponse,
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";
@ -202,60 +203,107 @@ export function FeaturedCarousel({
logoFetchController.current.abort(); // Cancel any in-progress logo fetches logoFetchController.current.abort(); // Cancel any in-progress logo fetches
} }
try { try {
if (effectiveCategory === "movies") { if (effectiveCategory === "movies" || effectiveCategory === "tvshows") {
// First get the list of popular movies // First try to get IDs from Trakt discover endpoint
const listData = await get<any>("/movie/popular", { try {
api_key: conf().TMDB_READ_API_KEY, const discoverData = await getDiscoverContent();
language: formattedLanguage,
});
// Then fetch full details for each movie to get external_ids let tmdbIds: number[] = [];
const moviePromises = listData.results if (effectiveCategory === "movies") {
.slice(0, FETCH_QUANTITY) tmdbIds = discoverData.movie_tmdb_ids;
.map((movie: any) => } else {
get<any>(`/movie/${movie.id}`, { tmdbIds = discoverData.tv_tmdb_ids;
api_key: conf().TMDB_READ_API_KEY, }
language: formattedLanguage,
append_to_response: "external_ids", // Then fetch full details for each movie/show to get external_ids
}), const detailPromises = tmdbIds.map((id) =>
get<any>(
`/${effectiveCategory === "movies" ? "movie" : "tv"}/${id}`,
{
api_key: conf().TMDB_READ_API_KEY,
language: formattedLanguage,
append_to_response: "external_ids",
},
),
); );
const movieDetails = await Promise.all(moviePromises); const details = await Promise.all(detailPromises);
const allMovies = movieDetails.map((movie) => ({ const mediaItems = details.map((item) => ({
...movie, ...item,
type: "movie" as const, type:
})); effectiveCategory === "movies" ? "movie" : ("show" as const),
}));
// Shuffle // Take the first SLIDE_QUANTITY items
const shuffledMovies = [...allMovies].sort(() => 0.5 - Math.random()); setMedia(mediaItems.slice(0, SLIDE_QUANTITY));
setMedia(shuffledMovies.slice(0, SLIDE_QUANTITY)); } catch (traktError) {
} else if (effectiveCategory === "tvshows") { console.error(
// First get the list of popular shows "Falling back to TMDB method",
const listData = await get<any>("/tv/popular", { "Error fetching from Trakt discover:",
api_key: conf().TMDB_READ_API_KEY, traktError,
language: formattedLanguage,
});
// Then fetch full details for each show to get external_ids
const showPromises = listData.results
.slice(0, FETCH_QUANTITY)
.map((show: any) =>
get<any>(`/tv/${show.id}`, {
api_key: conf().TMDB_READ_API_KEY,
language: formattedLanguage,
append_to_response: "external_ids",
}),
); );
const showDetails = await Promise.all(showPromises); // Fallback to TMDB method
const allShows = showDetails.map((show) => ({ if (effectiveCategory === "movies") {
...show, // First get the list of popular movies
type: "show" as const, const listData = await get<any>("/movie/popular", {
})); api_key: conf().TMDB_READ_API_KEY,
language: formattedLanguage,
});
// Shuffle // Then fetch full details for each movie to get external_ids
const shuffledShows = [...allShows].sort(() => 0.5 - Math.random()); const moviePromises = listData.results
setMedia(shuffledShows.slice(0, SLIDE_QUANTITY)); .slice(0, FETCH_QUANTITY)
.map((movie: any) =>
get<any>(`/movie/${movie.id}`, {
api_key: conf().TMDB_READ_API_KEY,
language: formattedLanguage,
append_to_response: "external_ids",
}),
);
const movieDetails = await Promise.all(moviePromises);
const allMovies = movieDetails.map((movie) => ({
...movie,
type: "movie" as const,
}));
// Shuffle
const shuffledMovies = [...allMovies].sort(
() => 0.5 - Math.random(),
);
setMedia(shuffledMovies.slice(0, SLIDE_QUANTITY));
} else if (effectiveCategory === "tvshows") {
// First get the list of popular shows
const listData = await get<any>("/tv/popular", {
api_key: conf().TMDB_READ_API_KEY,
language: formattedLanguage,
});
// Then fetch full details for each show to get external_ids
const showPromises = listData.results
.slice(0, FETCH_QUANTITY)
.map((show: any) =>
get<any>(`/tv/${show.id}`, {
api_key: conf().TMDB_READ_API_KEY,
language: formattedLanguage,
append_to_response: "external_ids",
}),
);
const showDetails = await Promise.all(showPromises);
const allShows = showDetails.map((show) => ({
...show,
type: "show" as const,
}));
// Shuffle
const shuffledShows = [...allShows].sort(
() => 0.5 - Math.random(),
);
setMedia(shuffledShows.slice(0, SLIDE_QUANTITY));
}
}
} else if (effectiveCategory === "editorpicks") { } else if (effectiveCategory === "editorpicks") {
// Shuffle editor picks Ids // Shuffle editor picks Ids
const allMovieIds = EDITOR_PICKS_MOVIES.map((item) => ({ const allMovieIds = EDITOR_PICKS_MOVIES.map((item) => ({