pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

make details modal load 10x faster

make each component load individually instead of waiting for them all

Pas 4976d388 54e26c8a

+108 -26
+36 -13
src/backend/metadata/tmdb.ts
··· 345 345 ? TMDBShowData 346 346 : never; 347 347 348 + export async function getSeasonDetails( 349 + id: string, 350 + season: number, 351 + ): Promise< 352 + Array<{ 353 + id: number; 354 + name: string; 355 + episode_number: number; 356 + overview: string; 357 + still_path: string | null; 358 + air_date: string; 359 + season_number: number; 360 + }> 361 + > { 362 + const seasonData = await get<TMDBSeason>(`/tv/${id}/season/${season}`); 363 + return seasonData.episodes.map((episode) => ({ 364 + id: episode.id, 365 + name: episode.name, 366 + episode_number: episode.episode_number, 367 + overview: episode.overview, 368 + still_path: episode.still_path, 369 + air_date: episode.air_date, 370 + season_number: season, 371 + })); 372 + } 373 + 348 374 export async function getMediaDetails< 349 375 T extends TMDBContentTypes, 350 376 TReturn = MediaDetailReturn<T>, 351 - >(id: string, type: T): Promise<TReturn> { 377 + >(id: string, type: T, fetchEpisodes: boolean = true): Promise<TReturn> { 352 378 if (type === TMDBContentTypes.MOVIE) { 353 379 return get<TReturn>(`/movie/${id}`, { 354 380 append_to_response: "external_ids,credits,release_dates", ··· 359 385 append_to_response: "external_ids,credits,content_ratings", 360 386 }); 361 387 388 + if (!fetchEpisodes) { 389 + return { 390 + ...showData, 391 + episodes: [], 392 + } as TReturn; 393 + } 394 + 362 395 // Fetch episodes for each season 363 396 const showDetails = showData as TMDBShowData; 364 397 const allEpisodesBySeason = new Array(showDetails.seasons.length); ··· 375 408 const item = seasonsQueue.shift(); 376 409 if (!item) break; 377 410 const { season, index } = item; 378 - const seasonData = await get<TMDBSeason>( 379 - `/tv/${id}/season/${season.season_number}`, 380 - ); 381 - allEpisodesBySeason[index] = seasonData.episodes.map((episode) => ({ 382 - id: episode.id, 383 - name: episode.name, 384 - episode_number: episode.episode_number, 385 - overview: episode.overview, 386 - still_path: episode.still_path, 387 - air_date: episode.air_date, 388 - season_number: season.season_number, 389 - })); 411 + const episodes = await getSeasonDetails(id, season.season_number); 412 + allEpisodesBySeason[index] = episodes; 390 413 } 391 414 }, 392 415 );
+15 -11
src/components/overlays/detailsModal/components/carousels/EpisodeCarousel.tsx
··· 9 9 import { Modal, ModalCard, useModal } from "@/components/overlays/Modal"; 10 10 import { hasAired } from "@/components/player/utils/aired"; 11 11 import { useBookmarkStore } from "@/stores/bookmarks"; 12 - import { getProgressPercentage, useProgressStore } from "@/stores/progress"; 12 + // eslint-disable-next-line import/no-cycle 13 + import { 14 + ProgressEpisodeItem, 15 + getProgressPercentage, 16 + useProgressStore, 17 + } from "@/stores/progress"; 13 18 14 19 import { EpisodeCarouselProps } from "../../types"; 15 20 ··· 256 261 const watchedStats = useMemo(() => { 257 262 if (!mediaId || !totalEpisodes) return { watched: 0, percentage: 0 }; 258 263 264 + const item = progress[mediaId.toString()]; 265 + if (!item?.episodes) return { watched: 0, percentage: 0 }; 266 + 259 267 let watchedCount = 0; 260 - episodes.forEach((episode) => { 261 - const episodeProgress = 262 - progress[mediaId.toString()]?.episodes?.[episode.id]; 263 - const percentage = episodeProgress 264 - ? getProgressPercentage( 265 - episodeProgress.progress.watched, 266 - episodeProgress.progress.duration, 267 - ) 268 - : 0; 268 + Object.values<ProgressEpisodeItem>(item.episodes).forEach((episode) => { 269 + const percentage = getProgressPercentage( 270 + episode.progress.watched, 271 + episode.progress.duration, 272 + ); 269 273 if (percentage > 90) { 270 274 watchedCount += 1; 271 275 } ··· 274 278 const percentage = Math.round((watchedCount / totalEpisodes) * 100); 275 279 276 280 return { watched: watchedCount, percentage }; 277 - }, [episodes, progress, mediaId, totalEpisodes]); 281 + }, [progress, mediaId, totalEpisodes]); 278 282 279 283 // Load favorite episodes when favorites is selected 280 284 useEffect(() => {
+56 -1
src/components/overlays/detailsModal/components/layout/DetailsContent.tsx
··· 2 2 import { useEffect, useMemo, useRef, useState } from "react"; 3 3 import { useCopyToClipboard } from "react-use"; 4 4 5 + import { getSeasonDetails } from "@/backend/metadata/tmdb"; 5 6 import { getNetworkContent } from "@/backend/metadata/traktApi"; 6 7 import { TMDBContentTypes } from "@/backend/metadata/types/tmdb"; 7 8 import { Icon, Icons } from "@/components/Icon"; ··· 33 34 const [showTrailer, setShowTrailer] = useState(false); 34 35 const [showCollection, setShowCollection] = useState(false); 35 36 const [selectedSeason, setSelectedSeason] = useState<number>(1); 37 + const [fetchedSeasons, setFetchedSeasons] = useState<Record<number, any[]>>( 38 + {}, 39 + ); 40 + const [loadingSeasons, setLoadingSeasons] = useState<Record<number, boolean>>( 41 + {}, 42 + ); 36 43 const [, copyToClipboard] = useCopyToClipboard(); 37 44 const [hasCopiedShare, setHasCopiedShare] = useState(false); 38 45 const [logoHeight, setLogoHeight] = useState<number>(0); ··· 68 75 setSelectedSeason(showProgress.season.number); 69 76 } 70 77 }, [showProgress]); 78 + 79 + // Fetch episodes for selected season 80 + useEffect(() => { 81 + const fetchSeason = async (seasonNumber: number) => { 82 + if ( 83 + !data.id || 84 + seasonNumber === -1 || 85 + fetchedSeasons[seasonNumber] || 86 + loadingSeasons[seasonNumber] 87 + ) 88 + return; 89 + 90 + setLoadingSeasons((prev) => ({ ...prev, [seasonNumber]: true })); 91 + try { 92 + const episodes = await getSeasonDetails( 93 + data.id.toString(), 94 + seasonNumber, 95 + ); 96 + setFetchedSeasons((prev) => ({ ...prev, [seasonNumber]: episodes })); 97 + } catch (err) { 98 + console.error("Failed to fetch season details:", err); 99 + } finally { 100 + setLoadingSeasons((prev) => ({ ...prev, [seasonNumber]: false })); 101 + } 102 + }; 103 + 104 + if (data.type === "show") { 105 + if (selectedSeason !== -1) { 106 + fetchSeason(selectedSeason); 107 + } else if (data.seasonData?.seasons) { 108 + // Fetch all seasons for favorites 109 + data.seasonData.seasons.forEach((season) => { 110 + fetchSeason(season.season_number); 111 + }); 112 + } 113 + } 114 + }, [ 115 + data.id, 116 + data.type, 117 + selectedSeason, 118 + fetchedSeasons, 119 + loadingSeasons, 120 + data.seasonData, 121 + ]); 122 + 123 + const allEpisodes = useMemo(() => { 124 + return Object.values(fetchedSeasons).flat(); 125 + }, [fetchedSeasons]); 71 126 72 127 // Add effect to measure logo height 73 128 useEffect(() => { ··· 403 458 {/* Episodes Carousel for TV Shows */} 404 459 {data.type === "show" && data.seasonData && !minimal && ( 405 460 <EpisodeCarousel 406 - episodes={data.seasonData.episodes} 461 + episodes={allEpisodes} 407 462 showProgress={showProgress} 408 463 progress={progress} 409 464 selectedSeason={selectedSeason}
+1 -1
src/components/overlays/detailsModal/components/layout/DetailsModal.tsx
··· 55 55 try { 56 56 const type = 57 57 data.type === "movie" ? TMDBContentTypes.MOVIE : TMDBContentTypes.TV; 58 - const details = await getMediaDetails(data.id.toString(), type); 58 + const details = await getMediaDetails(data.id.toString(), type, false); 59 59 const backdropUrl = getMediaBackdrop(details.backdrop_path); 60 60 const logoUrl = await getMediaLogo(data.id.toString(), type); 61 61 if (type === TMDBContentTypes.MOVIE) {