mirror of
https://github.com/p-stream/p-stream.git
synced 2026-01-11 20:10:32 +00:00
remove trailer button and move the imdb trailers to the new carousel
This commit is contained in:
parent
e7e49f81cc
commit
24413a805d
8 changed files with 79 additions and 47 deletions
BIN
public/thumbnail-placeholder.png
Normal file
BIN
public/thumbnail-placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
|
|
@ -423,6 +423,7 @@
|
||||||
"airs": "Airs",
|
"airs": "Airs",
|
||||||
"endsAt": "Ends at {{time}}",
|
"endsAt": "Ends at {{time}}",
|
||||||
"trailer": "Trailer",
|
"trailer": "Trailer",
|
||||||
|
"trailers": "Trailers",
|
||||||
"similar": "Similar",
|
"similar": "Similar",
|
||||||
"collection": {
|
"collection": {
|
||||||
"movies": "Movies",
|
"movies": "Movies",
|
||||||
|
|
|
||||||
|
|
@ -391,6 +391,7 @@ export interface TMDBVideo {
|
||||||
type: string;
|
type: string;
|
||||||
official: boolean;
|
official: boolean;
|
||||||
published_at: string;
|
published_at: string;
|
||||||
|
thumbnail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TMDBVideosResponse {
|
export interface TMDBVideosResponse {
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,14 @@ import { TMDBContentTypes, TMDBVideo } from "@/backend/metadata/types/tmdb";
|
||||||
interface TrailerCarouselProps {
|
interface TrailerCarouselProps {
|
||||||
mediaId: string;
|
mediaId: string;
|
||||||
mediaType: TMDBContentTypes;
|
mediaType: TMDBContentTypes;
|
||||||
onTrailerClick: (videoKey: string) => void;
|
imdbData?: any;
|
||||||
|
onTrailerClick: (videoKey: string, isImdbTrailer?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TrailerCarousel({
|
export function TrailerCarousel({
|
||||||
mediaId,
|
mediaId,
|
||||||
mediaType,
|
mediaType,
|
||||||
|
imdbData,
|
||||||
onTrailerClick,
|
onTrailerClick,
|
||||||
}: TrailerCarouselProps) {
|
}: TrailerCarouselProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -36,38 +38,73 @@ export function TrailerCarousel({
|
||||||
loadVideos();
|
loadVideos();
|
||||||
}, [mediaId, mediaType]);
|
}, [mediaId, mediaType]);
|
||||||
|
|
||||||
if (videos.length === 0) return null;
|
// Combine TMDB videos and IMDb trailer
|
||||||
|
const allTrailers = [
|
||||||
|
...videos,
|
||||||
|
...(imdbData?.trailer_url
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "imdb-trailer",
|
||||||
|
key: imdbData.trailer_url,
|
||||||
|
name: "IMDb Trailer",
|
||||||
|
site: "IMDb",
|
||||||
|
size: 1080,
|
||||||
|
type: "Trailer",
|
||||||
|
official: true,
|
||||||
|
published_at: new Date().toISOString(),
|
||||||
|
thumbnail: imdbData.trailer_thumbnail,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (allTrailers.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 pt-8">
|
<div className="space-y-4 pt-8">
|
||||||
<h3 className="text-lg font-semibold text-white/90">
|
<h3 className="text-lg font-semibold text-white/90">
|
||||||
{t("details.trailers", "Trailers")}
|
{t("details.trailers")}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex overflow-x-auto scrollbar-none pb-4 gap-4">
|
<div className="flex overflow-x-auto scrollbar-none pb-4 gap-4">
|
||||||
{videos.map((video) => (
|
{allTrailers.map((video) => {
|
||||||
<button
|
const isImdbTrailer = video.id === "imdb-trailer";
|
||||||
key={video.id}
|
let thumbnailUrl: string;
|
||||||
type="button"
|
|
||||||
onClick={() => onTrailerClick(video.key)}
|
if (isImdbTrailer) {
|
||||||
className="flex-shrink-0 hover:opacity-80 transition-opacity rounded-lg overflow-hidden"
|
// Use IMDb thumbnail if available, otherwise use a generic trailer placeholder
|
||||||
>
|
thumbnailUrl = video.thumbnail || "/thumbnail-placeholder.png";
|
||||||
<div className="relative h-52 w-96 overflow-hidden bg-black/60">
|
} else {
|
||||||
<img
|
// Use YouTube thumbnail for TMDB videos
|
||||||
src={`https://img.youtube.com/vi/${video.key}/hqdefault.jpg`}
|
thumbnailUrl = `https://img.youtube.com/vi/${video.key}/hqdefault.jpg`;
|
||||||
alt={video.name}
|
}
|
||||||
className="h-full w-full object-cover"
|
|
||||||
loading="lazy"
|
return (
|
||||||
/>
|
<button
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-black/60 via-transparent to-transparent" />
|
key={video.id}
|
||||||
<div className="absolute top-3 left-3 right-3">
|
type="button"
|
||||||
<h4 className="text-white font-medium text-sm leading-tight line-clamp-2 text-left">
|
onClick={() => onTrailerClick(video.key, isImdbTrailer)}
|
||||||
{video.name}
|
className="flex-shrink-0 hover:opacity-80 transition-opacity rounded-lg overflow-hidden"
|
||||||
</h4>
|
>
|
||||||
{/* <p className="text-white/80 text-xs mt-1">{video.type}</p> */}
|
<div className="relative h-52 w-96 overflow-hidden bg-black/60">
|
||||||
|
<img
|
||||||
|
src={thumbnailUrl}
|
||||||
|
alt={video.name}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-b from-black/60 via-transparent to-transparent" />
|
||||||
|
<div className="absolute top-3 left-3 right-3">
|
||||||
|
<h4 className="text-white font-medium text-sm leading-tight line-clamp-2 text-left">
|
||||||
|
{video.name}
|
||||||
|
</h4>
|
||||||
|
{/* <p className="text-white/80 text-xs mt-1 text-left">
|
||||||
|
{isImdbTrailer ? "IMDb Trailer" : video.type}
|
||||||
|
</p> */}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
</button>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,6 @@ export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
|
||||||
<DetailsBody
|
<DetailsBody
|
||||||
data={data}
|
data={data}
|
||||||
onPlayClick={handlePlayClick}
|
onPlayClick={handlePlayClick}
|
||||||
onTrailerClick={() => setShowTrailer(true)}
|
|
||||||
onShareClick={handleShareClick}
|
onShareClick={handleShareClick}
|
||||||
showProgress={showProgress}
|
showProgress={showProgress}
|
||||||
voteAverage={data.voteAverage}
|
voteAverage={data.voteAverage}
|
||||||
|
|
@ -379,8 +378,16 @@ export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
|
||||||
? TMDBContentTypes.MOVIE
|
? TMDBContentTypes.MOVIE
|
||||||
: TMDBContentTypes.TV
|
: TMDBContentTypes.TV
|
||||||
}
|
}
|
||||||
onTrailerClick={(videoKey) => {
|
imdbData={imdbData}
|
||||||
const trailerUrl = `https://www.youtube.com/embed/${videoKey}?autoplay=1&rel=0`;
|
onTrailerClick={(videoKey, isImdbTrailer) => {
|
||||||
|
let trailerUrl: string;
|
||||||
|
if (isImdbTrailer) {
|
||||||
|
// IMDb trailer is already a full URL
|
||||||
|
trailerUrl = videoKey;
|
||||||
|
} else {
|
||||||
|
// TMDB trailer needs to be converted to YouTube embed URL
|
||||||
|
trailerUrl = `https://www.youtube.com/embed/${videoKey}?autoplay=1&rel=0`;
|
||||||
|
}
|
||||||
setShowTrailer(true);
|
setShowTrailer(true);
|
||||||
setImdbData((prev: any) => ({
|
setImdbData((prev: any) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import { DetailsBodyProps } from "../../types";
|
||||||
export function DetailsBody({
|
export function DetailsBody({
|
||||||
data,
|
data,
|
||||||
onPlayClick,
|
onPlayClick,
|
||||||
onTrailerClick,
|
|
||||||
onShareClick,
|
onShareClick,
|
||||||
showProgress,
|
showProgress,
|
||||||
voteAverage,
|
voteAverage,
|
||||||
|
|
@ -232,19 +231,6 @@ export function DetailsBody({
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
{imdbData?.trailer_url && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onTrailerClick}
|
|
||||||
className="p-2 opacity-75 transition-opacity duration-300 hover:scale-110 hover:cursor-pointer hover:opacity-95"
|
|
||||||
title={t("details.trailer")}
|
|
||||||
>
|
|
||||||
<IconPatch
|
|
||||||
icon={Icons.FILM}
|
|
||||||
className="transition-transform duration-300 hover:scale-110 hover:cursor-pointer"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<MediaBookmarkButton
|
<MediaBookmarkButton
|
||||||
media={{
|
media={{
|
||||||
id: data.id?.toString() || "",
|
id: data.id?.toString() || "",
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,6 @@ export interface EpisodeCarouselProps {
|
||||||
export interface DetailsBodyProps {
|
export interface DetailsBodyProps {
|
||||||
data: DetailsContent;
|
data: DetailsContent;
|
||||||
onPlayClick: () => void;
|
onPlayClick: () => void;
|
||||||
onTrailerClick: () => void;
|
|
||||||
onShareClick: () => void;
|
onShareClick: () => void;
|
||||||
showProgress: ShowProgressResult | null;
|
showProgress: ShowProgressResult | null;
|
||||||
voteAverage?: number;
|
voteAverage?: number;
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ interface IMDbMetadata {
|
||||||
plot?: string;
|
plot?: string;
|
||||||
poster_url?: string;
|
poster_url?: string;
|
||||||
trailer_url?: string;
|
trailer_url?: string;
|
||||||
|
trailer_thumbnail?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
genre?: string[];
|
genre?: string[];
|
||||||
cast?: string[];
|
cast?: string[];
|
||||||
|
|
@ -251,9 +252,9 @@ export async function scrapeIMDb(
|
||||||
metadata.imdb_rating = aboveTheFold.ratingsSummary?.aggregateRating || null;
|
metadata.imdb_rating = aboveTheFold.ratingsSummary?.aggregateRating || null;
|
||||||
metadata.votes = aboveTheFold.ratingsSummary?.voteCount || null;
|
metadata.votes = aboveTheFold.ratingsSummary?.voteCount || null;
|
||||||
metadata.poster_url = aboveTheFold.primaryImage?.url || "";
|
metadata.poster_url = aboveTheFold.primaryImage?.url || "";
|
||||||
metadata.trailer_url =
|
const trailerNode = aboveTheFold.primaryVideos?.edges?.[0]?.node;
|
||||||
aboveTheFold.primaryVideos?.edges?.[0]?.node?.playbackURLs?.[0]?.url ||
|
metadata.trailer_url = trailerNode?.playbackURLs?.[0]?.url || "";
|
||||||
"";
|
metadata.trailer_thumbnail = trailerNode?.thumbnail?.url || "";
|
||||||
|
|
||||||
// Extract arrays
|
// Extract arrays
|
||||||
metadata.genre = aboveTheFold.genres?.genres?.map((g: any) => g.text) || [];
|
metadata.genre = aboveTheFold.genres?.genres?.map((g: any) => g.text) || [];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue