Speed up episodes carousel loading by making it async

Maps an array to a promise that resolves with an array of results, limiting the number of concurrent operations.
This commit is contained in:
Pas 2026-02-22 22:26:44 -07:00
parent 9aa6e4088a
commit 94f6ecf089
3 changed files with 61 additions and 18 deletions

View file

@ -55,6 +55,24 @@ export function TMDBMediaToMediaItemType(
throw new Error("unsupported type"); throw new Error("unsupported type");
} }
export function formatTMDBEpisode(v: TMDBEpisodeShort): {
id: string;
number: number;
title: string;
air_date: string;
still_path: string | null;
overview: string;
} {
return {
id: v.id.toString(),
number: v.episode_number,
title: v.title,
air_date: v.air_date,
still_path: v.still_path,
overview: v.overview,
};
}
export function formatTMDBMeta( export function formatTMDBMeta(
media: TMDBMediaResult, media: TMDBMediaResult,
season?: TMDBSeasonMetaResult, season?: TMDBSeasonMetaResult,
@ -88,14 +106,7 @@ export function formatTMDBMeta(
title: season.title, title: season.title,
episodes: season.episodes episodes: season.episodes
.sort((a, b) => a.episode_number - b.episode_number) .sort((a, b) => a.episode_number - b.episode_number)
.map((v) => ({ .map(formatTMDBEpisode),
id: v.id.toString(),
number: v.episode_number,
title: v.title,
air_date: v.air_date,
still_path: v.still_path,
overview: v.overview,
})),
} }
: (undefined as any), : (undefined as any),
}; };

View file

@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import { useAsync } from "react-use"; import { useAsync } from "react-use";
import { getMetaFromId } from "@/backend/metadata/getmeta"; import { getMetaFromId } from "@/backend/metadata/getmeta";
import { formatTMDBEpisode, getEpisodes } from "@/backend/metadata/tmdb";
import { MWMediaType, MWSeasonMeta } from "@/backend/metadata/types/mw"; import { MWMediaType, MWSeasonMeta } from "@/backend/metadata/types/mw";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { ProgressRing } from "@/components/layout/ProgressRing"; import { ProgressRing } from "@/components/layout/ProgressRing";
@ -20,6 +21,7 @@ import { PlayerMeta } from "@/stores/player/slices/source";
import { usePlayerStore } from "@/stores/player/store"; import { usePlayerStore } from "@/stores/player/store";
import { usePreferencesStore } from "@/stores/preferences"; import { usePreferencesStore } from "@/stores/preferences";
import { useProgressStore } from "@/stores/progress"; import { useProgressStore } from "@/stores/progress";
import { concurrentMap } from "@/utils/async";
import { scrollToElement } from "@/utils/scroll"; import { scrollToElement } from "@/utils/scroll";
import { hasAired } from "../utils/aired"; import { hasAired } from "../utils/aired";
@ -594,23 +596,21 @@ export function EpisodesView({
if (selectedSeason === "favorites" && meta?.tmdbId && seasons) { if (selectedSeason === "favorites" && meta?.tmdbId && seasons) {
setAllSeasonsLoading(true); setAllSeasonsLoading(true);
const loadAllSeasons = async () => { const loadAllSeasons = async () => {
const seasonPromises = seasons.map(async (season) => { const results = await concurrentMap(seasons, 5, async (season) => {
try { try {
const data = await getMetaFromId( const episodes = await getEpisodes(meta.tmdbId!, season.number);
MWMediaType.SERIES, return {
meta.tmdbId, id: season.id,
season.id, number: season.number,
); title: season.title,
return data?.meta.type === MWMediaType.SERIES episodes: episodes.map(formatTMDBEpisode),
? data.meta.seasonData };
: null;
} catch (error) { } catch (error) {
console.error(`Failed to load season ${season.id}:`, error); console.error(`Failed to load season ${season.id}:`, error);
return null; return null;
} }
}); });
const results = await Promise.all(seasonPromises);
setAllSeasonsData(results.filter(Boolean)); setAllSeasonsData(results.filter(Boolean));
setAllSeasonsLoading(false); setAllSeasonsLoading(false);
}; };

32
src/utils/async.ts Normal file
View file

@ -0,0 +1,32 @@
/**
* Maps an array to a promise that resolves with an array of results,
* limiting the number of concurrent operations.
*
* @param items The array of items to map
* @param concurrency The maximum number of concurrent operations
* @param fn The async function to apply to each item
* @returns A promise that resolves with an array of results
*/
export async function concurrentMap<T, R>(
items: T[],
concurrency: number,
fn: (item: T) => Promise<R>,
): Promise<R[]> {
const results: R[] = new Array(items.length);
const queue = items.map((item, index) => ({ item, index }));
const workers = Array.from(
{ length: Math.min(concurrency, items.length) },
async () => {
while (queue.length > 0) {
const entry = queue.shift();
if (!entry) break;
const { item, index } = entry;
results[index] = await fn(item);
}
},
);
await Promise.all(workers);
return results;
}