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 423 "airs": "Airs", 424 424 "endsAt": "Ends at {{time}}", 425 425 "trailer": "Trailer", 426 + "trailers": "Trailers", 426 427 "similar": "Similar", 427 428 "collection": { 428 429 "movies": "Movies",
+1
src/backend/metadata/types/tmdb.ts
··· 391 391 type: string; 392 392 official: boolean; 393 393 published_at: string; 394 + thumbnail?: string; 394 395 } 395 396 396 397 export interface TMDBVideosResponse {
+63 -26
src/components/overlays/detailsModal/components/carousels/TrailerCarousel.tsx
··· 7 7 interface TrailerCarouselProps { 8 8 mediaId: string; 9 9 mediaType: TMDBContentTypes; 10 - onTrailerClick: (videoKey: string) => void; 10 + imdbData?: any; 11 + onTrailerClick: (videoKey: string, isImdbTrailer?: boolean) => void; 11 12 } 12 13 13 14 export function TrailerCarousel({ 14 15 mediaId, 15 16 mediaType, 17 + imdbData, 16 18 onTrailerClick, 17 19 }: TrailerCarouselProps) { 18 20 const { t } = useTranslation(); ··· 36 38 loadVideos(); 37 39 }, [mediaId, mediaType]); 38 40 39 - if (videos.length === 0) return null; 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; 40 62 41 63 return ( 42 64 <div className="space-y-4 pt-8"> 43 65 <h3 className="text-lg font-semibold text-white/90"> 44 - {t("details.trailers", "Trailers")} 66 + {t("details.trailers")} 45 67 </h3> 46 68 <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> */} 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> 67 104 </div> 68 - </div> 69 - </button> 70 - ))} 105 + </button> 106 + ); 107 + })} 71 108 </div> 72 109 </div> 73 110 );
+10 -3
src/components/overlays/detailsModal/components/layout/DetailsContent.tsx
··· 268 268 <DetailsBody 269 269 data={data} 270 270 onPlayClick={handlePlayClick} 271 - onTrailerClick={() => setShowTrailer(true)} 272 271 onShareClick={handleShareClick} 273 272 showProgress={showProgress} 274 273 voteAverage={data.voteAverage} ··· 379 378 ? TMDBContentTypes.MOVIE 380 379 : TMDBContentTypes.TV 381 380 } 382 - onTrailerClick={(videoKey) => { 383 - const trailerUrl = `https://www.youtube.com/embed/${videoKey}?autoplay=1&rel=0`; 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 + } 384 391 setShowTrailer(true); 385 392 setImdbData((prev: any) => ({ 386 393 ...prev,
-14
src/components/overlays/detailsModal/components/sections/DetailsBody.tsx
··· 16 16 export function DetailsBody({ 17 17 data, 18 18 onPlayClick, 19 - onTrailerClick, 20 19 onShareClick, 21 20 showProgress, 22 21 voteAverage, ··· 232 231 </span> 233 232 </Button> 234 233 <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 234 <MediaBookmarkButton 249 235 media={{ 250 236 id: data.id?.toString() || "",
-1
src/components/overlays/detailsModal/types.ts
··· 110 110 export interface DetailsBodyProps { 111 111 data: DetailsContent; 112 112 onPlayClick: () => void; 113 - onTrailerClick: () => void; 114 113 onShareClick: () => void; 115 114 showProgress: ShowProgressResult | null; 116 115 voteAverage?: number;
+4 -3
src/utils/imdbScraper.ts
··· 55 55 plot?: string; 56 56 poster_url?: string; 57 57 trailer_url?: string; 58 + trailer_thumbnail?: string; 58 59 url?: string; 59 60 genre?: string[]; 60 61 cast?: string[]; ··· 251 252 metadata.imdb_rating = aboveTheFold.ratingsSummary?.aggregateRating || null; 252 253 metadata.votes = aboveTheFold.ratingsSummary?.voteCount || null; 253 254 metadata.poster_url = aboveTheFold.primaryImage?.url || ""; 254 - metadata.trailer_url = 255 - aboveTheFold.primaryVideos?.edges?.[0]?.node?.playbackURLs?.[0]?.url || 256 - ""; 255 + const trailerNode = aboveTheFold.primaryVideos?.edges?.[0]?.node; 256 + metadata.trailer_url = trailerNode?.playbackURLs?.[0]?.url || ""; 257 + metadata.trailer_thumbnail = trailerNode?.thumbnail?.url || ""; 257 258 258 259 // Extract arrays 259 260 metadata.genre = aboveTheFold.genres?.genres?.map((g: any) => g.text) || [];