update all movie lists

This commit is contained in:
Pas 2025-08-10 12:44:48 -06:00
parent 88dc5f0890
commit 32d9cd040c
3 changed files with 159 additions and 15 deletions

View file

@ -1,3 +1,4 @@
// This endpoint is not used anymore, but we keep it here for reference or if we feel like fixing the backend
import { conf } from "@/utils/setup/config";
export interface TmdbMovie {

View file

@ -1,4 +1,6 @@
import { getMediaDetails } from "./tmdb";
import { MWMediaType } from "./types/mw";
import { TMDBContentTypes, TMDBMovieData } from "./types/tmdb";
export interface TraktLatestResponse {
tmdb_ids: number[];
@ -41,6 +43,13 @@ export interface TraktNetworkResponse {
count: number;
}
export interface CuratedMovieList {
listName: string;
listSlug: string;
tmdbIds: number[];
count: number;
}
// Pagination utility
export function paginateResults(
results: TraktLatestResponse,
@ -116,6 +125,115 @@ export const getDiscoverContent = () =>
export const getNetworkContent = (tmdbId: string) =>
fetchFromTrakt<TraktNetworkResponse>(`/network/${tmdbId}`);
// Curated movie lists (replacing Letterboxd functionality)
export const getNarrativeMovies = () => fetchFromTrakt("/narrative");
export const getTopMovies = () => fetchFromTrakt("/top");
export const getLifetimeMovies = () => fetchFromTrakt("/lifetime");
export const getNeverHeardMovies = () => fetchFromTrakt("/never");
export const getLGBTQContent = () => fetchFromTrakt("/LGBTQ");
export const getMindfuckMovies = () => fetchFromTrakt("/mindfuck");
export const getTrueStoryMovies = () => fetchFromTrakt("/truestory");
export const getGreatestTVShows = () => fetchFromTrakt("/greatesttv");
// Get all curated movie lists
export const getCuratedMovieLists = async (): Promise<CuratedMovieList[]> => {
const listConfigs = [
{
name: "Letterboxd Top 250 Narrative Feature Films",
slug: "narrative",
endpoint: "/narrative",
},
{
name: "1001 Greatest Movies of All Time",
slug: "top",
endpoint: "/top",
},
{
name: "1001 Movies You Must See Before You Die",
slug: "lifetime",
endpoint: "/lifetime",
},
{
name: "Great Movies You May Have Never Heard Of",
slug: "never",
endpoint: "/never",
},
{
name: "LGBT Movies/Shows",
slug: "LGBTQ",
endpoint: "/LGBTQ",
},
{
name: "Best Mindfuck Movies",
slug: "mindfuck",
endpoint: "/mindfuck",
},
{
name: "Based on a True Story Movies",
slug: "truestory",
endpoint: "/truestory",
},
{
name: "Rolling Stone's 100 Greatest TV Shows",
slug: "greatesttv",
endpoint: "/greatesttv",
},
];
const lists: CuratedMovieList[] = [];
for (const config of listConfigs) {
try {
const response = await fetchFromTrakt(config.endpoint);
lists.push({
listName: config.name,
listSlug: config.slug,
tmdbIds: response.tmdb_ids.slice(0, 30), // Limit to first 30 items
count: Math.min(response.count, 30), // Update count to reflect the limit
});
} catch (error) {
console.error(`Failed to fetch ${config.name}:`, error);
}
}
return lists;
};
// Fetch movie details for multiple TMDB IDs
export const getMovieDetailsForIds = async (
tmdbIds: number[],
limit: number = 50,
): Promise<TMDBMovieData[]> => {
const limitedIds = tmdbIds.slice(0, limit);
const movieDetails: TMDBMovieData[] = [];
// Process in smaller batches to avoid overwhelming the API
const batchSize = 10;
for (let i = 0; i < limitedIds.length; i += batchSize) {
const batch = limitedIds.slice(i, i + batchSize);
const batchPromises = batch.map(async (id) => {
try {
const details = await getMediaDetails(
id.toString(),
TMDBContentTypes.MOVIE,
);
return details as TMDBMovieData;
} catch (error) {
console.error(`Failed to fetch movie details for ID ${id}:`, error);
return null;
}
});
const batchResults = await Promise.all(batchPromises);
const validResults = batchResults.filter(
(result): result is TMDBMovieData => result !== null,
);
movieDetails.push(...validResults);
}
return movieDetails;
};
// Type conversion utilities
export function convertToMediaType(type: TraktContentType): MWMediaType {
return type === "movie" ? MWMediaType.MOVIE : MWMediaType.SERIES;

View file

@ -2,7 +2,12 @@ import { t } from "i18next";
import { useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { TmdbMovie, getLetterboxdLists } from "@/backend/metadata/letterboxd";
import {
CuratedMovieList,
getCuratedMovieLists,
getMovieDetailsForIds,
} from "@/backend/metadata/traktApi";
import { TMDBMovieData } from "@/backend/metadata/types/tmdb";
import { Icon, Icons } from "@/components/Icon";
import { WideContainer } from "@/components/layout/WideContainer";
import { MediaCard } from "@/components/media/MediaCard";
@ -19,29 +24,49 @@ import { MediaCarousel } from "./components/MediaCarousel";
export function DiscoverMore() {
const [detailsData, setDetailsData] = useState<any>();
const [letterboxdLists, setLetterboxdLists] = useState<any[]>([]);
const [curatedLists, setCuratedLists] = useState<CuratedMovieList[]>([]);
const [movieDetails, setMovieDetails] = useState<{
[listSlug: string]: TMDBMovieData[];
}>({});
const detailsModal = useModal("discover-details");
const carouselRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
const navigate = useNavigate();
const { lastView } = useDiscoverStore();
const { isMobile } = useIsMobile();
// Track overflow states for Letterboxd lists
// Track overflow states for curated lists
const [overflowStates, setOverflowStates] = useState<{
[key: string]: boolean;
}>({});
useEffect(() => {
const fetchLetterboxdLists = async () => {
const fetchCuratedLists = async () => {
try {
const response = await getLetterboxdLists();
setLetterboxdLists(response.lists);
const lists = await getCuratedMovieLists();
setCuratedLists(lists);
// Fetch movie details for each list
const details: { [listSlug: string]: TMDBMovieData[] } = {};
for (const list of lists) {
try {
const movies = await getMovieDetailsForIds(list.tmdbIds, 50);
if (movies.length > 0) {
details[list.listSlug] = movies;
}
} catch (error) {
console.error(
`Failed to fetch movies for list ${list.listSlug}:`,
error,
);
}
}
setMovieDetails(details);
} catch (error) {
console.error("Failed to fetch Letterboxd lists:", error);
console.error("Failed to fetch curated lists:", error);
}
};
fetchLetterboxdLists();
fetchCuratedLists();
}, []);
const handleShowDetails = async (media: MediaItem) => {
@ -143,9 +168,9 @@ export function DiscoverMore() {
/>
</div>
{/* Letterboxd Lists */}
{letterboxdLists.map((list) => (
<div key={list.listUrl}>
{/* Curated Movie Lists */}
{curatedLists.map((list) => (
<div key={list.listSlug}>
<div className="flex items-center justify-between ml-2 md:ml-8 mt-2">
<div className="flex flex-col">
<div className="flex items-center gap-4">
@ -158,11 +183,11 @@ export function DiscoverMore() {
<div className="relative overflow-hidden carousel-container md:pb-4">
<div
className="grid grid-flow-col auto-cols-max gap-4 pt-0 overflow-x-scroll scrollbar-none rounded-xl overflow-y-hidden md:pl-8 md:pr-8"
ref={(el) => setCarouselRef(el, list.listUrl)}
ref={(el) => setCarouselRef(el, list.listSlug)}
onWheel={handleWheel}
>
<div className="md:w-12" />
{list.tmdbMovies.map((movie: TmdbMovie) => (
{movieDetails[list.listSlug]?.map((movie: TMDBMovieData) => (
<div
key={movie.id}
className="relative mt-4 group cursor-pointer user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto"
@ -188,9 +213,9 @@ export function DiscoverMore() {
</div>
{!isMobile && (
<CarouselNavButtons
categorySlug={list.listUrl}
categorySlug={list.listSlug}
carouselRefs={carouselRefs}
hasOverflow={overflowStates[list.listUrl]}
hasOverflow={overflowStates[list.listSlug]}
/>
)}
</div>