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

remove trailer button and move the imdb trailers to the new carousel

Pas 24413a80 e7e49f81

+79 -47
public/thumbnail-placeholder.png

This is a binary file and will not be displayed.

+1
src/assets/locales/en.json
··· 423 "airs": "Airs", 424 "endsAt": "Ends at {{time}}", 425 "trailer": "Trailer", 426 "similar": "Similar", 427 "collection": { 428 "movies": "Movies",
··· 423 "airs": "Airs", 424 "endsAt": "Ends at {{time}}", 425 "trailer": "Trailer", 426 + "trailers": "Trailers", 427 "similar": "Similar", 428 "collection": { 429 "movies": "Movies",
+1
src/backend/metadata/types/tmdb.ts
··· 391 type: string; 392 official: boolean; 393 published_at: string; 394 } 395 396 export interface TMDBVideosResponse {
··· 391 type: string; 392 official: boolean; 393 published_at: string; 394 + thumbnail?: string; 395 } 396 397 export interface TMDBVideosResponse {
+63 -26
src/components/overlays/detailsModal/components/carousels/TrailerCarousel.tsx
··· 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(); ··· 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 );
··· 7 interface TrailerCarouselProps { 8 mediaId: string; 9 mediaType: TMDBContentTypes; 10 + imdbData?: any; 11 + onTrailerClick: (videoKey: string, isImdbTrailer?: boolean) => void; 12 } 13 14 export function TrailerCarousel({ 15 mediaId, 16 mediaType, 17 + imdbData, 18 onTrailerClick, 19 }: TrailerCarouselProps) { 20 const { t } = useTranslation(); ··· 38 loadVideos(); 39 }, [mediaId, mediaType]); 40 41 + // Combine TMDB videos and IMDb trailer 42 + const allTrailers = [ 43 + ...videos, 44 + ...(imdbData?.trailer_url 45 + ? [ 46 + { 47 + id: "imdb-trailer", 48 + key: imdbData.trailer_url, 49 + name: "IMDb Trailer", 50 + site: "IMDb", 51 + size: 1080, 52 + type: "Trailer", 53 + official: true, 54 + published_at: new Date().toISOString(), 55 + thumbnail: imdbData.trailer_thumbnail, 56 + }, 57 + ] 58 + : []), 59 + ]; 60 + 61 + if (allTrailers.length === 0) return null; 62 63 return ( 64 <div className="space-y-4 pt-8"> 65 <h3 className="text-lg font-semibold text-white/90"> 66 + {t("details.trailers")} 67 </h3> 68 <div className="flex overflow-x-auto scrollbar-none pb-4 gap-4"> 69 + {allTrailers.map((video) => { 70 + const isImdbTrailer = video.id === "imdb-trailer"; 71 + let thumbnailUrl: string; 72 + 73 + if (isImdbTrailer) { 74 + // Use IMDb thumbnail if available, otherwise use a generic trailer placeholder 75 + thumbnailUrl = video.thumbnail || "/thumbnail-placeholder.png"; 76 + } else { 77 + // Use YouTube thumbnail for TMDB videos 78 + thumbnailUrl = `https://img.youtube.com/vi/${video.key}/hqdefault.jpg`; 79 + } 80 + 81 + return ( 82 + <button 83 + key={video.id} 84 + type="button" 85 + onClick={() => onTrailerClick(video.key, isImdbTrailer)} 86 + className="flex-shrink-0 hover:opacity-80 transition-opacity rounded-lg overflow-hidden" 87 + > 88 + <div className="relative h-52 w-96 overflow-hidden bg-black/60"> 89 + <img 90 + src={thumbnailUrl} 91 + alt={video.name} 92 + className="h-full w-full object-cover" 93 + loading="lazy" 94 + /> 95 + <div className="absolute inset-0 bg-gradient-to-b from-black/60 via-transparent to-transparent" /> 96 + <div className="absolute top-3 left-3 right-3"> 97 + <h4 className="text-white font-medium text-sm leading-tight line-clamp-2 text-left"> 98 + {video.name} 99 + </h4> 100 + {/* <p className="text-white/80 text-xs mt-1 text-left"> 101 + {isImdbTrailer ? "IMDb Trailer" : video.type} 102 + </p> */} 103 + </div> 104 </div> 105 + </button> 106 + ); 107 + })} 108 </div> 109 </div> 110 );
+10 -3
src/components/overlays/detailsModal/components/layout/DetailsContent.tsx
··· 268 <DetailsBody 269 data={data} 270 onPlayClick={handlePlayClick} 271 - onTrailerClick={() => setShowTrailer(true)} 272 onShareClick={handleShareClick} 273 showProgress={showProgress} 274 voteAverage={data.voteAverage} ··· 379 ? TMDBContentTypes.MOVIE 380 : TMDBContentTypes.TV 381 } 382 - onTrailerClick={(videoKey) => { 383 - const trailerUrl = `https://www.youtube.com/embed/${videoKey}?autoplay=1&rel=0`; 384 setShowTrailer(true); 385 setImdbData((prev: any) => ({ 386 ...prev,
··· 268 <DetailsBody 269 data={data} 270 onPlayClick={handlePlayClick} 271 onShareClick={handleShareClick} 272 showProgress={showProgress} 273 voteAverage={data.voteAverage} ··· 378 ? TMDBContentTypes.MOVIE 379 : TMDBContentTypes.TV 380 } 381 + imdbData={imdbData} 382 + onTrailerClick={(videoKey, isImdbTrailer) => { 383 + let trailerUrl: string; 384 + if (isImdbTrailer) { 385 + // IMDb trailer is already a full URL 386 + trailerUrl = videoKey; 387 + } else { 388 + // TMDB trailer needs to be converted to YouTube embed URL 389 + trailerUrl = `https://www.youtube.com/embed/${videoKey}?autoplay=1&rel=0`; 390 + } 391 setShowTrailer(true); 392 setImdbData((prev: any) => ({ 393 ...prev,
-14
src/components/overlays/detailsModal/components/sections/DetailsBody.tsx
··· 16 export function DetailsBody({ 17 data, 18 onPlayClick, 19 - onTrailerClick, 20 onShareClick, 21 showProgress, 22 voteAverage, ··· 232 </span> 233 </Button> 234 <div className="flex items-center gap-1 flex-shrink-0"> 235 - {imdbData?.trailer_url && ( 236 - <button 237 - type="button" 238 - onClick={onTrailerClick} 239 - className="p-2 opacity-75 transition-opacity duration-300 hover:scale-110 hover:cursor-pointer hover:opacity-95" 240 - title={t("details.trailer")} 241 - > 242 - <IconPatch 243 - icon={Icons.FILM} 244 - className="transition-transform duration-300 hover:scale-110 hover:cursor-pointer" 245 - /> 246 - </button> 247 - )} 248 <MediaBookmarkButton 249 media={{ 250 id: data.id?.toString() || "",
··· 16 export function DetailsBody({ 17 data, 18 onPlayClick, 19 onShareClick, 20 showProgress, 21 voteAverage, ··· 231 </span> 232 </Button> 233 <div className="flex items-center gap-1 flex-shrink-0"> 234 <MediaBookmarkButton 235 media={{ 236 id: data.id?.toString() || "",
-1
src/components/overlays/detailsModal/types.ts
··· 110 export interface DetailsBodyProps { 111 data: DetailsContent; 112 onPlayClick: () => void; 113 - onTrailerClick: () => void; 114 onShareClick: () => void; 115 showProgress: ShowProgressResult | null; 116 voteAverage?: number;
··· 110 export interface DetailsBodyProps { 111 data: DetailsContent; 112 onPlayClick: () => void; 113 onShareClick: () => void; 114 showProgress: ShowProgressResult | null; 115 voteAverage?: number;
+4 -3
src/utils/imdbScraper.ts
··· 55 plot?: string; 56 poster_url?: string; 57 trailer_url?: string; 58 url?: string; 59 genre?: string[]; 60 cast?: string[]; ··· 251 metadata.imdb_rating = aboveTheFold.ratingsSummary?.aggregateRating || null; 252 metadata.votes = aboveTheFold.ratingsSummary?.voteCount || null; 253 metadata.poster_url = aboveTheFold.primaryImage?.url || ""; 254 - metadata.trailer_url = 255 - aboveTheFold.primaryVideos?.edges?.[0]?.node?.playbackURLs?.[0]?.url || 256 - ""; 257 258 // Extract arrays 259 metadata.genre = aboveTheFold.genres?.genres?.map((g: any) => g.text) || [];
··· 55 plot?: string; 56 poster_url?: string; 57 trailer_url?: string; 58 + trailer_thumbnail?: string; 59 url?: string; 60 genre?: string[]; 61 cast?: string[]; ··· 252 metadata.imdb_rating = aboveTheFold.ratingsSummary?.aggregateRating || null; 253 metadata.votes = aboveTheFold.ratingsSummary?.voteCount || null; 254 metadata.poster_url = aboveTheFold.primaryImage?.url || ""; 255 + const trailerNode = aboveTheFold.primaryVideos?.edges?.[0]?.node; 256 + metadata.trailer_url = trailerNode?.playbackURLs?.[0]?.url || ""; 257 + metadata.trailer_thumbnail = trailerNode?.thumbnail?.url || ""; 258 259 // Extract arrays 260 metadata.genre = aboveTheFold.genres?.genres?.map((g: any) => g.text) || [];