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");
}
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(
media: TMDBMediaResult,
season?: TMDBSeasonMetaResult,
@ -88,14 +106,7 @@ export function formatTMDBMeta(
title: season.title,
episodes: season.episodes
.sort((a, b) => a.episode_number - b.episode_number)
.map((v) => ({
id: v.id.toString(),
number: v.episode_number,
title: v.title,
air_date: v.air_date,
still_path: v.still_path,
overview: v.overview,
})),
.map(formatTMDBEpisode),
}
: (undefined as any),
};

View file

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