mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-28 13:08:42 +00:00
add trackt latest and 4k
This commit is contained in:
parent
3713a85720
commit
5cc4485629
5 changed files with 409 additions and 62 deletions
|
|
@ -901,6 +901,12 @@
|
|||
"movies": "{{category}} Movies",
|
||||
"tvshows": "{{category}} Shows",
|
||||
"inCinemas": "In Cinemas",
|
||||
"popular": "Most Popular",
|
||||
"nowPlaying": "In Cinemas",
|
||||
"topRated": "Top Rated",
|
||||
"latestReleases": "Latest Releases",
|
||||
"4kReleases": "4K Releases",
|
||||
"onTheAir": "On The Air",
|
||||
"popularOn": "Popular {{type}} on {{provider}}",
|
||||
"editorPicksMovies": "Editor Picks Movies",
|
||||
"editorPicksShows": "Editor Picks Shows",
|
||||
|
|
|
|||
75
src/backend/metadata/traktApi.ts
Normal file
75
src/backend/metadata/traktApi.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { MWMediaType } from "./types/mw";
|
||||
|
||||
export interface TraktLatestResponse {
|
||||
tmdb_ids: number[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface TraktReleaseResponse {
|
||||
tmdb_id: number;
|
||||
title: string;
|
||||
year?: number;
|
||||
type: "movie" | "episode";
|
||||
season?: number;
|
||||
episode?: number;
|
||||
quality?: string;
|
||||
source?: string;
|
||||
group?: string;
|
||||
}
|
||||
|
||||
export type TraktContentType = "movie" | "episode";
|
||||
|
||||
export const TRAKT_BASE_URL = "https://airdate.up.railway.app";
|
||||
|
||||
export async function getLatestReleases(): Promise<TraktLatestResponse> {
|
||||
const response = await fetch(`${TRAKT_BASE_URL}/latest`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch latest releases: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getLatest4KReleases(): Promise<TraktLatestResponse> {
|
||||
const response = await fetch(`${TRAKT_BASE_URL}/latest4k`);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch latest 4K releases: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getReleaseDetails(
|
||||
id: string,
|
||||
season?: number,
|
||||
episode?: number,
|
||||
): Promise<TraktReleaseResponse> {
|
||||
let url = `${TRAKT_BASE_URL}/release/${id}`;
|
||||
if (season !== undefined && episode !== undefined) {
|
||||
url += `/${season}/${episode}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch release details: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getAppleTVReleases(): Promise<TraktLatestResponse> {
|
||||
const response = await fetch(`${TRAKT_BASE_URL}/appletv`);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch Apple TV releases: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function convertToMediaType(type: TraktContentType): MWMediaType {
|
||||
return type === "movie" ? MWMediaType.MOVIE : MWMediaType.SERIES;
|
||||
}
|
||||
|
||||
export function convertFromMediaType(type: MWMediaType): TraktContentType {
|
||||
return type === MWMediaType.MOVIE ? "movie" : "episode";
|
||||
}
|
||||
|
|
@ -5,6 +5,10 @@ import { useNavigate, useParams } from "react-router-dom";
|
|||
import { useWindowSize } from "react-use";
|
||||
|
||||
import { get } from "@/backend/metadata/tmdb";
|
||||
import {
|
||||
getLatest4KReleases,
|
||||
getLatestReleases,
|
||||
} from "@/backend/metadata/traktApi";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Dropdown, OptionItem } from "@/components/form/Dropdown";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
|
|
@ -22,7 +26,7 @@ import { ProgressMediaItem, useProgressStore } from "@/stores/progress";
|
|||
import { getTmdbLanguageCode } from "@/utils/language";
|
||||
import { MediaItem } from "@/utils/mediaTypes";
|
||||
|
||||
import { Genre, categories, tvCategories } from "./common";
|
||||
import { Genre, Movie, categories, tvCategories } from "./common";
|
||||
import {
|
||||
EDITOR_PICKS_MOVIES,
|
||||
EDITOR_PICKS_TV_SHOWS,
|
||||
|
|
@ -123,6 +127,18 @@ export function MoreContent({ onShowDetails }: MoreContentProps) {
|
|||
const isTVShow = mediaType === "tv";
|
||||
let endpoint = "";
|
||||
|
||||
// Map category URLs to their corresponding TMDB endpoints
|
||||
const categoryEndpointMap: { [key: string]: string } = {
|
||||
// Movie categories
|
||||
"now-playing-movie": "/movie/now_playing",
|
||||
"top-rated-movie": "/movie/top_rated",
|
||||
"most-popular-movie": "/movie/popular",
|
||||
// TV categories
|
||||
"on-the-air-tv": "/tv/on_the_air",
|
||||
"top-rated-tv": "/tv/top_rated",
|
||||
"most-popular-tv": "/tv/popular",
|
||||
};
|
||||
|
||||
// Handle recommendations separately
|
||||
if (contentType === "recommendations") {
|
||||
// Get title from progress store instead of fetching details
|
||||
|
|
@ -141,10 +157,21 @@ export function MoreContent({ onShowDetails }: MoreContentProps) {
|
|||
},
|
||||
);
|
||||
|
||||
const processedResults = results.results.map((item: any) => {
|
||||
const isItemTVShow = Boolean(item.first_air_date);
|
||||
return {
|
||||
...item,
|
||||
type: isItemTVShow ? "show" : "movie",
|
||||
// Keep both dates in the raw data
|
||||
first_air_date: item.first_air_date,
|
||||
release_date: item.release_date,
|
||||
};
|
||||
});
|
||||
|
||||
if (append) {
|
||||
setMedias((prev) => [...prev, ...results.results]);
|
||||
setMedias((prev) => [...prev, ...processedResults]);
|
||||
} else {
|
||||
setMedias(results.results);
|
||||
setMedias(processedResults);
|
||||
}
|
||||
setHasMore(page < results.total_pages);
|
||||
setCurrentPage(page);
|
||||
|
|
@ -153,35 +180,90 @@ export function MoreContent({ onShowDetails }: MoreContentProps) {
|
|||
|
||||
// Handle editor picks separately
|
||||
if (category?.includes("editor-picks")) {
|
||||
const editorPicks = isTVShow
|
||||
const isEditorPicksTV = category.includes("tv");
|
||||
const editorPicks = isEditorPicksTV
|
||||
? EDITOR_PICKS_TV_SHOWS
|
||||
: EDITOR_PICKS_MOVIES;
|
||||
|
||||
// Fetch details for all editor picks
|
||||
const promises = editorPicks.map((item) =>
|
||||
get<any>(`/${isTVShow ? "tv" : "movie"}/${item.id}`, {
|
||||
get<any>(`/${isEditorPicksTV ? "tv" : "movie"}/${item.id}`, {
|
||||
api_key: conf().TMDB_READ_API_KEY,
|
||||
language: formattedLanguage,
|
||||
}),
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
setMedias(results);
|
||||
const validResults = results.map((item) => ({
|
||||
...item,
|
||||
type: isEditorPicksTV ? "show" : "movie",
|
||||
release_date: isEditorPicksTV
|
||||
? item.first_air_date
|
||||
: item.release_date,
|
||||
}));
|
||||
setMedias(validResults);
|
||||
setHasMore(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the correct endpoint based on the type
|
||||
if (contentType === "category") {
|
||||
const categoryList = isTVShow ? tvCategories : categories;
|
||||
const categoryData = categoryList.find((c) => c.urlPath === id);
|
||||
if (categoryData) {
|
||||
endpoint = categoryData.endpoint;
|
||||
} else {
|
||||
endpoint = isTVShow ? "/discover/tv" : "/discover/movie";
|
||||
// Handle Trakt categories separately
|
||||
if (
|
||||
category?.includes("latest-releases") ||
|
||||
category?.includes("4k-releases")
|
||||
) {
|
||||
try {
|
||||
const traktFunction = category?.includes("latest-releases")
|
||||
? getLatestReleases
|
||||
: getLatest4KReleases;
|
||||
|
||||
const traktData = await traktFunction();
|
||||
const moviePromises = traktData.tmdb_ids
|
||||
.slice((page - 1) * 20, page * 20)
|
||||
.map((tmdbId: number) =>
|
||||
get<any>(`/movie/${tmdbId}`, {
|
||||
api_key: conf().TMDB_READ_API_KEY,
|
||||
language: formattedLanguage,
|
||||
}).catch(() => null),
|
||||
);
|
||||
|
||||
const results = await Promise.all(moviePromises);
|
||||
const validMovies = results
|
||||
.filter((movie: any): movie is Movie => movie !== null)
|
||||
.map((movie: Movie) => {
|
||||
const isItemTVShow = Boolean(movie.first_air_date);
|
||||
return {
|
||||
...movie,
|
||||
type: isItemTVShow ? "show" : "movie",
|
||||
// Keep both dates in the raw data
|
||||
first_air_date: movie.first_air_date,
|
||||
release_date: movie.release_date,
|
||||
};
|
||||
});
|
||||
|
||||
if (append) {
|
||||
setMedias((prev) => [...prev, ...validMovies]);
|
||||
} else {
|
||||
setMedias(validMovies);
|
||||
}
|
||||
setHasMore(traktData.tmdb_ids.length > page * 20);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ${category}:`, error);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
// Determine the correct endpoint based on the category
|
||||
if (category && categoryEndpointMap[category]) {
|
||||
endpoint = categoryEndpointMap[category];
|
||||
} else if (contentType === "provider") {
|
||||
endpoint = isTVShow ? "/discover/tv" : "/discover/movie";
|
||||
} else if (contentType === "genre") {
|
||||
endpoint = isTVShow ? "/discover/tv" : "/discover/movie";
|
||||
}
|
||||
|
||||
if (!endpoint) {
|
||||
console.error("No endpoint found for category:", category);
|
||||
return;
|
||||
}
|
||||
|
||||
const allResults: any[] = [];
|
||||
|
|
@ -203,7 +285,18 @@ export function MoreContent({ onShowDetails }: MoreContentProps) {
|
|||
}
|
||||
|
||||
const data = await get<any>(endpoint, params);
|
||||
allResults.push(...data.results);
|
||||
const processedResults = data.results.map((item: any) => {
|
||||
const isItemTVShow = Boolean(item.first_air_date);
|
||||
return {
|
||||
...item,
|
||||
type: isItemTVShow ? "show" : "movie",
|
||||
// Keep both dates in the raw data
|
||||
first_air_date: item.first_air_date,
|
||||
release_date: item.release_date,
|
||||
};
|
||||
});
|
||||
|
||||
allResults.push(...processedResults);
|
||||
|
||||
// Check if we've reached the end
|
||||
if (currentPageNum >= data.total_pages) {
|
||||
|
|
@ -259,12 +352,36 @@ export function MoreContent({ onShowDetails }: MoreContentProps) {
|
|||
});
|
||||
}
|
||||
|
||||
if (category === "editor-picks-tv" || category === "editor-picks-movie") {
|
||||
return category === "editor-picks-tv"
|
||||
if (category?.includes("editor-picks")) {
|
||||
return category.includes("tv")
|
||||
? t("discover.carousel.title.editorPicksShows")
|
||||
: t("discover.carousel.title.editorPicksMovies");
|
||||
}
|
||||
|
||||
if (category?.includes("latest-releases")) {
|
||||
return t("discover.carousel.title.latestReleases");
|
||||
}
|
||||
|
||||
if (category?.includes("4k-releases")) {
|
||||
return t("discover.carousel.title.4kReleases");
|
||||
}
|
||||
|
||||
// Map category URLs to their display titles
|
||||
const categoryTitleMap: { [key: string]: string } = {
|
||||
// Movie categories
|
||||
"now-playing-movie": t("discover.carousel.title.nowPlaying"),
|
||||
"top-rated-movie": t("discover.carousel.title.topRated"),
|
||||
"most-popular-movie": t("discover.carousel.title.popular"),
|
||||
// TV categories
|
||||
"on-the-air-tv": t("discover.carousel.title.onTheAir"),
|
||||
"top-rated-tv": t("discover.carousel.title.topRated"),
|
||||
"most-popular-tv": t("discover.carousel.title.popular"),
|
||||
};
|
||||
|
||||
if (category && categoryTitleMap[category]) {
|
||||
return categoryTitleMap[category];
|
||||
}
|
||||
|
||||
if (!contentType || !id) return "";
|
||||
|
||||
if (contentType === "provider") {
|
||||
|
|
@ -620,34 +737,52 @@ export function MoreContent({ onShowDetails }: MoreContentProps) {
|
|||
{renderGenreButtons()}
|
||||
|
||||
<div className="grid grid-cols-2 gap-8 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-6 3xl:grid-cols-8 4xl:grid-cols-10 pt-8">
|
||||
{medias.map((media) => (
|
||||
<div
|
||||
key={media.id}
|
||||
style={{ userSelect: "none" }}
|
||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
>
|
||||
<MediaCard
|
||||
media={{
|
||||
id: media.id.toString(),
|
||||
title: media.title || media.name || "",
|
||||
poster: `https://image.tmdb.org/t/p/w342${media.poster_path}`,
|
||||
type: mediaType === "tv" ? "show" : "movie",
|
||||
year:
|
||||
mediaType === "tv"
|
||||
? media.first_air_date
|
||||
? parseInt(media.first_air_date.split("-")[0], 10)
|
||||
: undefined
|
||||
: media.release_date
|
||||
? parseInt(media.release_date.split("-")[0], 10)
|
||||
: undefined,
|
||||
}}
|
||||
onShowDetails={handleShowDetails}
|
||||
linkable={!category?.includes("upcoming")}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{medias.map((media) => {
|
||||
// Determine if this is a TV show based on the presence of first_air_date
|
||||
const isTVShow = Boolean(media.first_air_date);
|
||||
const releaseDate = isTVShow
|
||||
? media.first_air_date
|
||||
: media.release_date;
|
||||
const year = releaseDate
|
||||
? parseInt(releaseDate.split("-")[0], 10)
|
||||
: undefined;
|
||||
|
||||
const mediaItem: MediaItem = {
|
||||
id: media.id.toString(),
|
||||
title: media.title || media.name || "",
|
||||
poster: `https://image.tmdb.org/t/p/w342${media.poster_path}`,
|
||||
type: isTVShow ? "show" : "movie",
|
||||
year,
|
||||
release_date: releaseDate ? new Date(releaseDate) : undefined,
|
||||
};
|
||||
|
||||
console.log("Final MediaCard item:", {
|
||||
id: mediaItem.id,
|
||||
title: mediaItem.title,
|
||||
year: mediaItem.year,
|
||||
release_date: mediaItem.release_date?.toISOString(),
|
||||
type: mediaItem.type,
|
||||
raw_first_air_date: media.first_air_date,
|
||||
raw_release_date: media.release_date,
|
||||
isTVShow,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={media.id}
|
||||
style={{ userSelect: "none" }}
|
||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
>
|
||||
<MediaCard
|
||||
media={mediaItem}
|
||||
onShowDetails={handleShowDetails}
|
||||
linkable={!category?.includes("upcoming")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{hasMore && (
|
||||
<div className="flex justify-center mt-8">
|
||||
|
|
|
|||
|
|
@ -147,6 +147,14 @@ export function MediaCarousel({
|
|||
: t("discover.carousel.title.editorPicksMovies");
|
||||
}
|
||||
|
||||
if (categoryName === "Latest Releases") {
|
||||
return t("discover.carousel.title.latestReleases");
|
||||
}
|
||||
|
||||
if (categoryName === "4K Releases") {
|
||||
return t("discover.carousel.title.4kReleases");
|
||||
}
|
||||
|
||||
if (
|
||||
categoryName.includes("Movies on") ||
|
||||
categoryName.includes("Shows on")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import { useEffect, useRef, useState } from "react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { get } from "@/backend/metadata/tmdb";
|
||||
import {
|
||||
getLatest4KReleases,
|
||||
getLatestReleases,
|
||||
} from "@/backend/metadata/traktApi";
|
||||
import { WideContainer } from "@/components/layout/WideContainer";
|
||||
import { DetailsModal } from "@/components/overlays/details/DetailsModal";
|
||||
import { useModal } from "@/components/overlays/Modal";
|
||||
|
|
@ -163,6 +167,11 @@ export function DiscoverContent() {
|
|||
const [selectedTVSource, setSelectedTVSource] = useState<string>("");
|
||||
const progressStore = useProgressStore();
|
||||
const { t } = useTranslation();
|
||||
const [latestReleases, setLatestReleases] = useState<Movie[]>([]);
|
||||
const [latest4KReleases, setLatest4KReleases] = useState<Movie[]>([]);
|
||||
const [isLoadingLatest, setIsLoadingLatest] = useState(false);
|
||||
const [isLoading4K, setIsLoading4K] = useState(false);
|
||||
const [isTraktAvailable, setIsTraktAvailable] = useState(false);
|
||||
|
||||
const carouselRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
|
||||
|
||||
|
|
@ -485,6 +494,98 @@ export function DiscoverContent() {
|
|||
t,
|
||||
]);
|
||||
|
||||
// Fetch latest releases from Trakt
|
||||
useEffect(() => {
|
||||
const fetchLatestReleases = async () => {
|
||||
if (!isMoviesTab) return;
|
||||
setIsLoadingLatest(true);
|
||||
try {
|
||||
const traktData = await getLatestReleases();
|
||||
const moviePromises = traktData.tmdb_ids.slice(0, 20).map(
|
||||
(id) =>
|
||||
get<any>(`/movie/${id}`, {
|
||||
api_key: conf().TMDB_READ_API_KEY,
|
||||
language: formattedLanguage,
|
||||
}).catch(() => null), // Handle failed TMDB fetches gracefully
|
||||
);
|
||||
|
||||
const results = await Promise.all(moviePromises);
|
||||
const validMovies = results
|
||||
.filter((movie): movie is Movie => movie !== null)
|
||||
.map((movie) => ({
|
||||
...movie,
|
||||
type: "movie" as const,
|
||||
}));
|
||||
setLatestReleases(validMovies);
|
||||
setIsTraktAvailable(true);
|
||||
} catch (error) {
|
||||
console.error("Error fetching latest releases:", error);
|
||||
setIsTraktAvailable(false);
|
||||
// Fallback to TMDB if Trakt fails
|
||||
const data = await get<any>("/movie/now_playing", {
|
||||
api_key: conf().TMDB_READ_API_KEY,
|
||||
language: formattedLanguage,
|
||||
});
|
||||
setLatestReleases(
|
||||
data.results.map((movie: any) => ({
|
||||
...movie,
|
||||
type: "movie" as const,
|
||||
})),
|
||||
);
|
||||
} finally {
|
||||
setIsLoadingLatest(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchLatestReleases();
|
||||
}, [isMoviesTab, formattedLanguage]);
|
||||
|
||||
// Fetch 4K releases from Trakt
|
||||
useEffect(() => {
|
||||
const fetch4KReleases = async () => {
|
||||
if (!isMoviesTab) return;
|
||||
setIsLoading4K(true);
|
||||
try {
|
||||
const traktData = await getLatest4KReleases();
|
||||
const moviePromises = traktData.tmdb_ids.slice(0, 20).map(
|
||||
(id) =>
|
||||
get<any>(`/movie/${id}`, {
|
||||
api_key: conf().TMDB_READ_API_KEY,
|
||||
language: formattedLanguage,
|
||||
}).catch(() => null), // Handle failed TMDB fetches gracefully
|
||||
);
|
||||
|
||||
const results = await Promise.all(moviePromises);
|
||||
const validMovies = results
|
||||
.filter((movie): movie is Movie => movie !== null)
|
||||
.map((movie) => ({
|
||||
...movie,
|
||||
type: "movie" as const,
|
||||
}));
|
||||
setLatest4KReleases(validMovies);
|
||||
setIsTraktAvailable(true);
|
||||
} catch (error) {
|
||||
console.error("Error fetching 4K releases:", error);
|
||||
setIsTraktAvailable(false);
|
||||
// Fallback to TMDB if Trakt fails
|
||||
const data = await get<any>("/movie/popular", {
|
||||
api_key: conf().TMDB_READ_API_KEY,
|
||||
language: formattedLanguage,
|
||||
});
|
||||
setLatest4KReleases(
|
||||
data.results.map((movie: any) => ({
|
||||
...movie,
|
||||
type: "movie" as const,
|
||||
})),
|
||||
);
|
||||
} finally {
|
||||
setIsLoading4K(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetch4KReleases();
|
||||
}, [isMoviesTab, formattedLanguage]);
|
||||
|
||||
const handleShowDetails = async (media: MediaItem | FeaturedMedia) => {
|
||||
setDetailsData({
|
||||
id: Number(media.id),
|
||||
|
|
@ -513,14 +614,25 @@ export function DiscoverContent() {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* In Cinemas */}
|
||||
<LazyMediaCarousel
|
||||
category={categories[0].name}
|
||||
isTVShow={false}
|
||||
carouselRefs={carouselRefs}
|
||||
onShowDetails={handleShowDetails}
|
||||
moreContent
|
||||
/>
|
||||
{/* Latest Releases or In Cinemas */}
|
||||
{isTraktAvailable ? (
|
||||
<LazyMediaCarousel
|
||||
medias={isLoadingLatest ? undefined : latestReleases}
|
||||
category="Latest Releases"
|
||||
isTVShow={false}
|
||||
carouselRefs={carouselRefs}
|
||||
onShowDetails={handleShowDetails}
|
||||
moreContent
|
||||
/>
|
||||
) : (
|
||||
<LazyMediaCarousel
|
||||
category={categories[0].name}
|
||||
isTVShow={false}
|
||||
carouselRefs={carouselRefs}
|
||||
onShowDetails={handleShowDetails}
|
||||
moreContent
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Top Rated */}
|
||||
<LazyMediaCarousel
|
||||
|
|
@ -531,14 +643,25 @@ export function DiscoverContent() {
|
|||
moreContent
|
||||
/>
|
||||
|
||||
{/* Popular */}
|
||||
<LazyMediaCarousel
|
||||
category={categories[2].name}
|
||||
isTVShow={false}
|
||||
carouselRefs={carouselRefs}
|
||||
onShowDetails={handleShowDetails}
|
||||
moreContent
|
||||
/>
|
||||
{/* 4K Releases or Popular */}
|
||||
{isTraktAvailable ? (
|
||||
<LazyMediaCarousel
|
||||
medias={isLoading4K ? undefined : latest4KReleases}
|
||||
category="4K Releases"
|
||||
isTVShow={false}
|
||||
carouselRefs={carouselRefs}
|
||||
onShowDetails={handleShowDetails}
|
||||
moreContent
|
||||
/>
|
||||
) : (
|
||||
<LazyMediaCarousel
|
||||
category={categories[2].name}
|
||||
isTVShow={false}
|
||||
carouselRefs={carouselRefs}
|
||||
onShowDetails={handleShowDetails}
|
||||
moreContent
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Provider Movies */}
|
||||
<LazyMediaCarousel
|
||||
|
|
|
|||
Loading…
Reference in a new issue