From 94f6ecf089f79de1d700f2022d4f197c453fbb80 Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:26:44 -0700 Subject: [PATCH] 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. --- src/backend/metadata/tmdb.ts | 27 ++++++++++++++------ src/components/player/atoms/Episodes.tsx | 20 +++++++-------- src/utils/async.ts | 32 ++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 18 deletions(-) create mode 100644 src/utils/async.ts diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index ff32ffc5..ebf55b0d 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -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), }; diff --git a/src/components/player/atoms/Episodes.tsx b/src/components/player/atoms/Episodes.tsx index 71cbdc21..8f1843a5 100644 --- a/src/components/player/atoms/Episodes.tsx +++ b/src/components/player/atoms/Episodes.tsx @@ -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); }; diff --git a/src/utils/async.ts b/src/utils/async.ts new file mode 100644 index 00000000..692a058e --- /dev/null +++ b/src/utils/async.ts @@ -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( + items: T[], + concurrency: number, + fn: (item: T) => Promise, +): Promise { + 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; +}