Update personalRecommendations.ts

This commit is contained in:
Pas 2026-02-15 11:32:11 -07:00
parent 248da37056
commit 5b73ca114e

View file

@ -1,7 +1,9 @@
import { getRelatedMedia } from "@/backend/metadata/tmdb";
import { getMediaDetails, getRelatedMedia } from "@/backend/metadata/tmdb";
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
import type {
TMDBMovieData,
TMDBMovieSearchResult,
TMDBShowData,
TMDBShowSearchResult,
} from "@/backend/metadata/types/tmdb";
import type { DiscoverMedia } from "@/pages/discover/types/discover";
@ -73,9 +75,29 @@ function bookmarkToDiscoverMedia(b: BookmarkSource): DiscoverMedia {
};
}
/**
* Fetches similar items from the fed-similar API
*/
async function fetchFedSimilarItems(
tmdbId: string,
isTVShow: boolean,
): Promise<string[]> {
try {
const endpoint = isTVShow
? `https://fed-similar.up.railway.app/tv/${tmdbId}`
: `https://fed-similar.up.railway.app/movie/${tmdbId}`;
const response = await fetch(endpoint);
if (!response.ok) return [];
const items = await response.json();
return Array.isArray(items) ? items : [];
} catch (error) {
return [];
}
}
/**
* Fetches personal recommendations by:
* 1. Getting related media for up to MAX_HISTORY_FOR_RELATED history items, MAX_CURRENT_FOR_RELATED progress items, and MAX_BOOKMARK_FOR_RELATED bookmark
* 1. Getting related media from fed-similar API and TMDB for history, progress, and bookmark items
* 2. Merging and deduping, excluding items already in history/progress/bookmarks
* 3. Adding up to MAX_BOOKMARK_REMINDERS bookmarked items as "reminders"
*/
@ -123,21 +145,104 @@ export async function fetchPersonalRecommendations(
}
}
const relatedPromises = sourceIds.map((id) =>
// Fetch from both fed-similar API and TMDB
const fedSimilarPromises = sourceIds.map((id) =>
fetchFedSimilarItems(id, isTVShow),
);
const tmdbPromises = sourceIds.map((id) =>
getRelatedMedia(id, type, RELATED_PER_ITEM_LIMIT),
);
const relatedResults = await Promise.allSettled(relatedPromises);
const [fedSimilarResults, tmdbResults] = await Promise.allSettled([
Promise.all(fedSimilarPromises),
Promise.all(tmdbPromises),
]);
const merged: DiscoverMedia[] = [];
const seenIds = new Set<number>([]);
const seenFedSimilarIds = new Set<string>();
for (const result of relatedResults) {
if (result.status !== "fulfilled" || !result.value) continue;
for (const item of result.value) {
const idStr = String(item.id);
if (excludeIds.has(idStr) || seenIds.has(item.id)) continue;
// Process fed-similar results first (higher priority)
if (fedSimilarResults.status === "fulfilled") {
for (const fedSimilarItems of fedSimilarResults.value) {
for (const tmdbId of fedSimilarItems) {
if (excludeIds.has(tmdbId) || seenFedSimilarIds.has(tmdbId)) {
continue;
}
seenFedSimilarIds.add(tmdbId);
}
}
// Fetch full details for fed-similar items
const fedSimilarDetailPromises = Array.from(seenFedSimilarIds)
.slice(0, 20)
.map((tmdbId) => getMediaDetails(tmdbId, type));
const fedSimilarDetails = await Promise.allSettled(
fedSimilarDetailPromises,
);
for (const result of fedSimilarDetails) {
if (result.status !== "fulfilled" || !result.value) continue;
const item = result.value as TMDBMovieData | TMDBShowData;
if (seenIds.has(item.id)) continue;
seenIds.add(item.id);
merged.push(toDiscoverMedia(item, isTVShow));
let searchItem: TMDBMovieSearchResult | TMDBShowSearchResult;
if (isTVShow) {
const showItem = item as TMDBShowData;
searchItem = {
adult: showItem.adult ?? false,
backdrop_path: showItem.backdrop_path ?? "",
id: showItem.id,
name: showItem.name,
original_language: showItem.original_language ?? "",
original_name: showItem.original_name ?? "",
overview: showItem.overview ?? "",
poster_path: showItem.poster_path ?? "",
media_type: TMDBContentTypes.TV,
genre_ids: showItem.genres?.map((g) => g.id) ?? [],
popularity: showItem.popularity ?? 0,
first_air_date: showItem.first_air_date ?? "",
vote_average: showItem.vote_average,
vote_count: showItem.vote_count,
origin_country: showItem.origin_country ?? [],
};
} else {
const movieItem = item as TMDBMovieData;
searchItem = {
adult: movieItem.adult ?? false,
backdrop_path: movieItem.backdrop_path ?? "",
id: movieItem.id,
title: movieItem.title,
original_language: movieItem.original_language ?? "",
original_title: movieItem.original_title ?? "",
overview: movieItem.overview ?? "",
poster_path: movieItem.poster_path ?? "",
media_type: TMDBContentTypes.MOVIE,
genre_ids: movieItem.genres?.map((g) => g.id) ?? [],
popularity: movieItem.popularity ?? 0,
release_date: movieItem.release_date ?? "",
video: movieItem.video ?? false,
vote_average: movieItem.vote_average,
vote_count: movieItem.vote_count,
};
}
merged.push(toDiscoverMedia(searchItem, isTVShow));
}
}
// Process TMDB results (lower priority)
if (tmdbResults.status === "fulfilled") {
for (const result of tmdbResults.value) {
for (const item of result) {
const idStr = String(item.id);
if (excludeIds.has(idStr) || seenIds.has(item.id)) continue;
seenIds.add(item.id);
merged.push(toDiscoverMedia(item, isTVShow));
}
}
}