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

Configure Feed

Select the types of activity you want to include in your feed.

add trailer carousel to details modal

Pas 6997acd7 4d5a5151

+126
+15
src/backend/metadata/tmdb.ts
··· 24 24 TMDBSeasonMetaResult, 25 25 TMDBShowData, 26 26 TMDBShowSearchResult, 27 + TMDBVideo, 28 + TMDBVideosResponse, 27 29 } from "./types/tmdb"; 28 30 import { mwFetch } from "../helpers/fetch"; 29 31 ··· 509 511 ): Promise<TMDBCredits> { 510 512 const endpoint = type === TMDBContentTypes.MOVIE ? "movie" : "tv"; 511 513 return get<TMDBCredits>(`/${endpoint}/${id}/credits`); 514 + } 515 + 516 + export async function getMediaVideos( 517 + id: string, 518 + type: TMDBContentTypes, 519 + ): Promise<TMDBVideo[]> { 520 + const endpoint = type === TMDBContentTypes.MOVIE ? "movie" : "tv"; 521 + const data = await get<TMDBVideosResponse>(`/${endpoint}/${id}/videos`); 522 + return data.results.filter( 523 + (video) => 524 + video.site === "YouTube" && 525 + (video.type === "Trailer" || video.type === "Teaser"), 526 + ); 512 527 } 513 528 514 529 export async function getRelatedMedia(
+16
src/backend/metadata/types/tmdb.ts
··· 381 381 id: number; 382 382 profiles: TMDBPersonImage[]; 383 383 } 384 + 385 + export interface TMDBVideo { 386 + id: string; 387 + key: string; 388 + name: string; 389 + site: string; 390 + size: number; 391 + type: string; 392 + official: boolean; 393 + published_at: string; 394 + } 395 + 396 + export interface TMDBVideosResponse { 397 + id: number; 398 + results: TMDBVideo[]; 399 + }
+74
src/components/overlays/detailsModal/components/carousels/TrailerCarousel.tsx
··· 1 + import { useEffect, useState } from "react"; 2 + import { useTranslation } from "react-i18next"; 3 + 4 + import { getMediaVideos } from "@/backend/metadata/tmdb"; 5 + import { TMDBContentTypes, TMDBVideo } from "@/backend/metadata/types/tmdb"; 6 + 7 + interface TrailerCarouselProps { 8 + mediaId: string; 9 + mediaType: TMDBContentTypes; 10 + onTrailerClick: (videoKey: string) => void; 11 + } 12 + 13 + export function TrailerCarousel({ 14 + mediaId, 15 + mediaType, 16 + onTrailerClick, 17 + }: TrailerCarouselProps) { 18 + const { t } = useTranslation(); 19 + const [videos, setVideos] = useState<TMDBVideo[]>([]); 20 + 21 + useEffect(() => { 22 + async function loadVideos() { 23 + try { 24 + const mediaVideos = await getMediaVideos(mediaId, mediaType); 25 + // Sort by official status and then by type (Trailer first, then Teaser) 26 + const sortedVideos = mediaVideos.sort((a, b) => { 27 + if (a.official !== b.official) return b.official ? 1 : -1; 28 + if (a.type !== b.type) return a.type === "Trailer" ? -1 : 1; 29 + return 0; 30 + }); 31 + setVideos(sortedVideos); 32 + } catch (err) { 33 + console.error("Failed to load videos:", err); 34 + } 35 + } 36 + loadVideos(); 37 + }, [mediaId, mediaType]); 38 + 39 + if (videos.length === 0) return null; 40 + 41 + return ( 42 + <div className="space-y-4 pt-8"> 43 + <h3 className="text-lg font-semibold text-white/90"> 44 + {t("details.trailers", "Trailers")} 45 + </h3> 46 + <div className="flex overflow-x-auto scrollbar-none pb-4 gap-4"> 47 + {videos.map((video) => ( 48 + <button 49 + key={video.id} 50 + type="button" 51 + onClick={() => onTrailerClick(video.key)} 52 + className="flex-shrink-0 hover:opacity-80 transition-opacity rounded-lg overflow-hidden" 53 + > 54 + <div className="relative h-52 w-96 overflow-hidden bg-black/60"> 55 + <img 56 + src={`https://img.youtube.com/vi/${video.key}/hqdefault.jpg`} 57 + alt={video.name} 58 + className="h-full w-full object-cover" 59 + loading="lazy" 60 + /> 61 + <div className="absolute inset-0 bg-gradient-to-b from-black/60 via-transparent to-transparent" /> 62 + <div className="absolute top-3 left-3 right-3"> 63 + <h4 className="text-white font-medium text-sm leading-tight line-clamp-2 text-left"> 64 + {video.name} 65 + </h4> 66 + {/* <p className="text-white/80 text-xs mt-1">{video.type}</p> */} 67 + </div> 68 + </div> 69 + </button> 70 + ))} 71 + </div> 72 + </div> 73 + ); 74 + }
+21
src/components/overlays/detailsModal/components/layout/DetailsContent.tsx
··· 16 16 import { DetailsContentProps } from "../../types"; 17 17 import { EpisodeCarousel } from "../carousels/EpisodeCarousel"; 18 18 import { CastCarousel } from "../carousels/PeopleCarousel"; 19 + import { TrailerCarousel } from "../carousels/TrailerCarousel"; 19 20 import { CollectionOverlay } from "../overlays/CollectionOverlay"; 20 21 import { TrailerOverlay } from "../overlays/TrailerOverlay"; 21 22 import { DetailsBody } from "../sections/DetailsBody"; ··· 365 366 ? TMDBContentTypes.MOVIE 366 367 : TMDBContentTypes.TV 367 368 } 369 + /> 370 + )} 371 + 372 + {/* Trailer Carousel */} 373 + {data.id && ( 374 + <TrailerCarousel 375 + mediaId={data.id.toString()} 376 + mediaType={ 377 + data.type === "movie" 378 + ? TMDBContentTypes.MOVIE 379 + : TMDBContentTypes.TV 380 + } 381 + onTrailerClick={(videoKey) => { 382 + const trailerUrl = `https://www.youtube.com/embed/${videoKey}?autoplay=1&rel=0`; 383 + setShowTrailer(true); 384 + setImdbData((prev: any) => ({ 385 + ...prev, 386 + trailer_url: trailerUrl, 387 + })); 388 + }} 368 389 /> 369 390 )} 370 391 </div>