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

new trakt /discover endpoint for featured carousel

Pas eaf23995 0f1ce281

+105 -45
+13 -1
src/backend/metadata/traktApi.ts
··· 29 29 30 30 export const TRAKT_BASE_URL = "https://fed-airdate.pstream.org"; 31 31 32 + export interface TraktDiscoverResponse { 33 + movie_tmdb_ids: number[]; 34 + tv_tmdb_ids: number[]; 35 + count: number; 36 + } 37 + 32 38 // Pagination utility 33 39 export function paginateResults( 34 40 results: TraktLatestResponse, ··· 47 53 } 48 54 49 55 // Base function to fetch from Trakt API 50 - async function fetchFromTrakt(endpoint: string): Promise<TraktLatestResponse> { 56 + async function fetchFromTrakt<T = TraktLatestResponse>( 57 + endpoint: string, 58 + ): Promise<T> { 51 59 const response = await fetch(`${TRAKT_BASE_URL}${endpoint}`); 52 60 if (!response.ok) { 53 61 throw new Error(`Failed to fetch from ${endpoint}: ${response.statusText}`); ··· 93 101 // Popular content 94 102 export const getPopularTVShows = () => fetchFromTrakt("/populartv"); 95 103 export const getPopularMovies = () => fetchFromTrakt("/popularmovies"); 104 + 105 + // Discovery content 106 + export const getDiscoverContent = () => 107 + fetchFromTrakt<TraktDiscoverResponse>("/discover"); 96 108 97 109 // Type conversion utilities 98 110 export function convertToMediaType(type: TraktContentType): MWMediaType {
+92 -44
src/pages/discover/components/FeaturedCarousel.tsx
··· 8 8 import { get, getMediaLogo } from "@/backend/metadata/tmdb"; 9 9 import { 10 10 TraktReleaseResponse, 11 + getDiscoverContent, 11 12 getReleaseDetails, 12 13 } from "@/backend/metadata/traktApi"; 13 14 import { TMDBContentTypes } from "@/backend/metadata/types/tmdb"; ··· 202 203 logoFetchController.current.abort(); // Cancel any in-progress logo fetches 203 204 } 204 205 try { 205 - if (effectiveCategory === "movies") { 206 - // First get the list of popular movies 207 - const listData = await get<any>("/movie/popular", { 208 - api_key: conf().TMDB_READ_API_KEY, 209 - language: formattedLanguage, 210 - }); 206 + if (effectiveCategory === "movies" || effectiveCategory === "tvshows") { 207 + // First try to get IDs from Trakt discover endpoint 208 + try { 209 + const discoverData = await getDiscoverContent(); 211 210 212 - // Then fetch full details for each movie to get external_ids 213 - const moviePromises = listData.results 214 - .slice(0, FETCH_QUANTITY) 215 - .map((movie: any) => 216 - get<any>(`/movie/${movie.id}`, { 211 + let tmdbIds: number[] = []; 212 + if (effectiveCategory === "movies") { 213 + tmdbIds = discoverData.movie_tmdb_ids; 214 + } else { 215 + tmdbIds = discoverData.tv_tmdb_ids; 216 + } 217 + 218 + // Then fetch full details for each movie/show to get external_ids 219 + const detailPromises = tmdbIds.map((id) => 220 + get<any>( 221 + `/${effectiveCategory === "movies" ? "movie" : "tv"}/${id}`, 222 + { 223 + api_key: conf().TMDB_READ_API_KEY, 224 + language: formattedLanguage, 225 + append_to_response: "external_ids", 226 + }, 227 + ), 228 + ); 229 + 230 + const details = await Promise.all(detailPromises); 231 + const mediaItems = details.map((item) => ({ 232 + ...item, 233 + type: 234 + effectiveCategory === "movies" ? "movie" : ("show" as const), 235 + })); 236 + 237 + // Take the first SLIDE_QUANTITY items 238 + setMedia(mediaItems.slice(0, SLIDE_QUANTITY)); 239 + } catch (traktError) { 240 + console.error( 241 + "Falling back to TMDB method", 242 + "Error fetching from Trakt discover:", 243 + traktError, 244 + ); 245 + 246 + // Fallback to TMDB method 247 + if (effectiveCategory === "movies") { 248 + // First get the list of popular movies 249 + const listData = await get<any>("/movie/popular", { 217 250 api_key: conf().TMDB_READ_API_KEY, 218 251 language: formattedLanguage, 219 - append_to_response: "external_ids", 220 - }), 221 - ); 252 + }); 222 253 223 - const movieDetails = await Promise.all(moviePromises); 224 - const allMovies = movieDetails.map((movie) => ({ 225 - ...movie, 226 - type: "movie" as const, 227 - })); 254 + // Then fetch full details for each movie to get external_ids 255 + const moviePromises = listData.results 256 + .slice(0, FETCH_QUANTITY) 257 + .map((movie: any) => 258 + get<any>(`/movie/${movie.id}`, { 259 + api_key: conf().TMDB_READ_API_KEY, 260 + language: formattedLanguage, 261 + append_to_response: "external_ids", 262 + }), 263 + ); 228 264 229 - // Shuffle 230 - const shuffledMovies = [...allMovies].sort(() => 0.5 - Math.random()); 231 - setMedia(shuffledMovies.slice(0, SLIDE_QUANTITY)); 232 - } else if (effectiveCategory === "tvshows") { 233 - // First get the list of popular shows 234 - const listData = await get<any>("/tv/popular", { 235 - api_key: conf().TMDB_READ_API_KEY, 236 - language: formattedLanguage, 237 - }); 265 + const movieDetails = await Promise.all(moviePromises); 266 + const allMovies = movieDetails.map((movie) => ({ 267 + ...movie, 268 + type: "movie" as const, 269 + })); 238 270 239 - // Then fetch full details for each show to get external_ids 240 - const showPromises = listData.results 241 - .slice(0, FETCH_QUANTITY) 242 - .map((show: any) => 243 - get<any>(`/tv/${show.id}`, { 271 + // Shuffle 272 + const shuffledMovies = [...allMovies].sort( 273 + () => 0.5 - Math.random(), 274 + ); 275 + setMedia(shuffledMovies.slice(0, SLIDE_QUANTITY)); 276 + } else if (effectiveCategory === "tvshows") { 277 + // First get the list of popular shows 278 + const listData = await get<any>("/tv/popular", { 244 279 api_key: conf().TMDB_READ_API_KEY, 245 280 language: formattedLanguage, 246 - append_to_response: "external_ids", 247 - }), 248 - ); 281 + }); 282 + 283 + // Then fetch full details for each show to get external_ids 284 + const showPromises = listData.results 285 + .slice(0, FETCH_QUANTITY) 286 + .map((show: any) => 287 + get<any>(`/tv/${show.id}`, { 288 + api_key: conf().TMDB_READ_API_KEY, 289 + language: formattedLanguage, 290 + append_to_response: "external_ids", 291 + }), 292 + ); 249 293 250 - const showDetails = await Promise.all(showPromises); 251 - const allShows = showDetails.map((show) => ({ 252 - ...show, 253 - type: "show" as const, 254 - })); 294 + const showDetails = await Promise.all(showPromises); 295 + const allShows = showDetails.map((show) => ({ 296 + ...show, 297 + type: "show" as const, 298 + })); 255 299 256 - // Shuffle 257 - const shuffledShows = [...allShows].sort(() => 0.5 - Math.random()); 258 - setMedia(shuffledShows.slice(0, SLIDE_QUANTITY)); 300 + // Shuffle 301 + const shuffledShows = [...allShows].sort( 302 + () => 0.5 - Math.random(), 303 + ); 304 + setMedia(shuffledShows.slice(0, SLIDE_QUANTITY)); 305 + } 306 + } 259 307 } else if (effectiveCategory === "editorpicks") { 260 308 // Shuffle editor picks Ids 261 309 const allMovieIds = EDITOR_PICKS_MOVIES.map((item) => ({