diff --git a/hooks/useCastEpisodes.ts b/hooks/useCastEpisodes.ts index ed93a4fe8..392544fe3 100644 --- a/hooks/useCastEpisodes.ts +++ b/hooks/useCastEpisodes.ts @@ -1,10 +1,11 @@ import type { Api } from "@jellyfin/sdk"; import type { BaseItemDto, UserDto } from "@jellyfin/sdk/lib/generated-client"; -import { getTvShowsApi, getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; +import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; import { useCallback, useEffect, useState } from "react"; import type { Device, RemoteMediaClient } from "react-native-google-cast"; import type { Settings } from "@/utils/atoms/settings"; import { loadCastMedia } from "@/utils/casting/castLoad"; +import { fetchSeriesEpisodes, findNextEpisode } from "@/utils/casting/episodes"; interface UseCastEpisodesParams { api: Api | null; @@ -118,31 +119,24 @@ export function useCastEpisodes({ // Fetch episodes for TV shows useEffect(() => { - if (currentItem?.Type !== "Episode" || !currentItem.SeriesId || !api) + if ( + currentItem?.Type !== "Episode" || + !currentItem.SeriesId || + !api || + !user + ) return; const fetchEpisodes = async () => { try { - const tvShowsApi = getTvShowsApi(api); - // Fetch ALL episodes from ALL seasons by removing seasonId filter - const response = await tvShowsApi.getEpisodes({ - seriesId: currentItem.SeriesId!, - // Don't filter by seasonId - get all seasons - userId: api.accessToken ? undefined : "", - }); - - const episodeList = response.data.Items || []; - setEpisodes(episodeList); - - // Find next episode - const currentIndex = episodeList.findIndex( - (ep) => ep.Id === currentItem.Id, + // Fetch ALL episodes from ALL seasons (no season filter). + const episodeList = await fetchSeriesEpisodes( + api, + user, + currentItem.SeriesId!, ); - if (currentIndex >= 0 && currentIndex < episodeList.length - 1) { - setNextEpisode(episodeList[currentIndex + 1]); - } else { - setNextEpisode(null); - } + setEpisodes(episodeList); + setNextEpisode(findNextEpisode(episodeList, currentItem.Id)); } catch (error) { console.error("Failed to fetch episodes:", error); } @@ -155,6 +149,7 @@ export function useCastEpisodes({ currentItem?.SeasonId, currentItem?.Id, api, + user, ]); return { episodes, nextEpisode, seasonData, loadEpisode, loadingEpisodeId }; diff --git a/utils/casting/episodes.test.ts b/utils/casting/episodes.test.ts new file mode 100644 index 000000000..931dc84d2 --- /dev/null +++ b/utils/casting/episodes.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, test } from "bun:test"; +import { findNextEpisode } from "./episodes"; + +const ep = (id: string) => ({ Id: id }); + +describe("findNextEpisode", () => { + test("returns the episode after the current one", () => { + expect(findNextEpisode([ep("a"), ep("b"), ep("c")], "b")).toEqual(ep("c")); + }); + test("returns null on the last episode", () => { + expect(findNextEpisode([ep("a"), ep("b")], "b")).toBeNull(); + }); + test("returns null when the current id is not found", () => { + expect(findNextEpisode([ep("a"), ep("b")], "x")).toBeNull(); + }); + test("returns null for an empty list", () => { + expect(findNextEpisode([], "a")).toBeNull(); + }); +}); diff --git a/utils/casting/episodes.ts b/utils/casting/episodes.ts new file mode 100644 index 000000000..a6f879c36 --- /dev/null +++ b/utils/casting/episodes.ts @@ -0,0 +1,38 @@ +/** + * Episode-list helpers for the casting player and the autoplay watcher. + */ + +import type { Api } from "@jellyfin/sdk"; +import type { + BaseItemDto, + UserDto, +} from "@jellyfin/sdk/lib/generated-client/models"; +import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; + +/** The episode following `currentId` in `episodes`, or null if none / not found. */ +export const findNextEpisode = ( + episodes: BaseItemDto[], + currentId: string | null | undefined, +): BaseItemDto | null => { + const index = episodes.findIndex((e) => e.Id === currentId); + if (index < 0 || index + 1 >= episodes.length) return null; + return episodes[index + 1]; +}; + +/** + * Fetch every episode of the series that owns the current episode. + * Mirrors the call previously inlined in `useCastEpisodes`: no season filter, + * and the same `userId` quirk (undefined when an access token is present, else + * the empty string) so the request payload stays byte-identical. + */ +export const fetchSeriesEpisodes = async ( + api: Api, + _user: UserDto, + seriesId: string, +): Promise => { + const res = await getTvShowsApi(api).getEpisodes({ + seriesId, + userId: api.accessToken ? undefined : "", + }); + return res.data.Items ?? []; +};