Files
streamyfin/utils/downloads/offline-series.ts
2026-01-11 17:38:41 +01:00

115 lines
3.4 KiB
TypeScript

/**
* Utility functions for querying downloaded series/episode data.
* Centralizes common filtering patterns to reduce duplication.
*/
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import type { DownloadedItem } from "@/providers/Downloads/types";
/** Sort episodes by season then episode number */
const sortByEpisodeOrder = (a: BaseItemDto, b: BaseItemDto) =>
(a.ParentIndexNumber ?? 0) - (b.ParentIndexNumber ?? 0) ||
(a.IndexNumber ?? 0) - (b.IndexNumber ?? 0);
/**
* Get all downloaded episodes for a series, sorted by season and episode number.
*/
export function getDownloadedEpisodesForSeries(
downloadedItems: DownloadedItem[] | undefined,
seriesId: string,
): BaseItemDto[] {
if (!downloadedItems) return [];
return downloadedItems
.filter((f) => f.item.SeriesId === seriesId)
.map((f) => f.item)
.sort(sortByEpisodeOrder);
}
/**
* Get downloaded episodes for a specific season of a series.
*/
export function getDownloadedEpisodesForSeason(
downloadedItems: DownloadedItem[] | undefined,
seriesId: string,
seasonNumber: number,
): BaseItemDto[] {
return getDownloadedEpisodesForSeries(downloadedItems, seriesId).filter(
(ep) => ep.ParentIndexNumber === seasonNumber,
);
}
/**
* Get downloaded episodes by seasonId (for carousel views).
*/
export function getDownloadedEpisodesBySeasonId(
downloadedItems: DownloadedItem[] | undefined,
seasonId: string,
): BaseItemDto[] {
if (!downloadedItems) return [];
return downloadedItems
.filter((f) => f.item.Type === "Episode" && f.item.SeasonId === seasonId)
.map((f) => f.item)
.sort(sortByEpisodeOrder);
}
/**
* Get unique season numbers from downloaded episodes for a series.
*/
export function getDownloadedSeasonNumbers(
downloadedItems: DownloadedItem[] | undefined,
seriesId: string,
): number[] {
const episodes = getDownloadedEpisodesForSeries(downloadedItems, seriesId);
return [
...new Set(
episodes
.map((ep) => ep.ParentIndexNumber)
.filter((n): n is number => n != null),
),
].sort((a, b) => a - b);
}
/**
* Build fake season objects from downloaded episodes.
* Useful for offline mode where we don't have actual season data.
*/
export function buildOfflineSeasons(
downloadedItems: DownloadedItem[] | undefined,
seriesId: string,
): BaseItemDto[] {
const episodes = getDownloadedEpisodesForSeries(downloadedItems, seriesId);
const seasonNumbers = getDownloadedSeasonNumbers(downloadedItems, seriesId);
return seasonNumbers.map((seasonNum) => {
const firstEpisode = episodes.find(
(ep) => ep.ParentIndexNumber === seasonNum,
);
return {
Id: `offline-season-${seasonNum}`,
IndexNumber: seasonNum,
Name: firstEpisode?.SeasonName || `Season ${seasonNum}`,
SeriesId: seriesId,
} as BaseItemDto;
});
}
/**
* Build a series-like object from downloaded episodes.
* Useful for offline mode where we don't have the actual series data.
*/
export function buildOfflineSeriesFromEpisodes(
downloadedItems: DownloadedItem[] | undefined,
seriesId: string,
): BaseItemDto | null {
const episodes = getDownloadedEpisodesForSeries(downloadedItems, seriesId);
if (episodes.length === 0) return null;
const firstEpisode = episodes[0];
return {
Id: seriesId,
Name: firstEpisode.SeriesName,
Type: "Series",
ProductionYear: firstEpisode.ProductionYear,
Overview: firstEpisode.SeriesName,
} as BaseItemDto;
}