diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 9eb17fd38..c65d7ffac 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -77,6 +77,12 @@ export async function getMetaFromId( id: string, seasonId?: string, ): Promise { + if (type === MWMediaType.SERIES) { + const { getImdbOverride } = await import("./imdbMetadataProvider"); + const override = await getImdbOverride(id, seasonId); + if (override) return override; + } + const details = await getMediaDetails(id, mediaTypeToTMDB(type)); if (!details) return null; diff --git a/src/backend/metadata/imdbMetadataProvider.ts b/src/backend/metadata/imdbMetadataProvider.ts new file mode 100644 index 000000000..cd28ed04f --- /dev/null +++ b/src/backend/metadata/imdbMetadataProvider.ts @@ -0,0 +1,148 @@ +import { DetailedMeta, MWMediaType } from "./types/mw"; +import { TMDBEpisodeShort } from "./types/tmdb"; +import overrides from "./overrides.json"; + +const API_BASE = "https://api.balloonerismm.workers.dev"; + +const overrideMap: Record = overrides; + +export function hasImdbOverride(tmdbId: string): boolean { + return tmdbId in overrideMap; +} + +function getImdbIdForTmdb(tmdbId: string): string | null { + return overrideMap[tmdbId] ?? null; +} + +interface BalloonShowResponse { + id: string; + name: string; + original_name: string; + overview: string; + first_air_date: string; + poster_path: string | null; + number_of_seasons: number | null; + seasons: { season_number: number; label: string }[]; +} + +interface BalloonEpisode { + air_date: string; + episode_number: number; + id: string; + name: string; + overview: string; + runtime: number | null; + season_number: number; + still_path: string | null; +} + +interface BalloonSeasonResponse { + episodes: BalloonEpisode[]; + name: string; + season_number: number; +} + +async function fetchShowInfo(imdbId: string): Promise { + const res = await fetch(`${API_BASE}/tv/${imdbId}`); + if (!res.ok) throw new Error(`Show API returned ${res.status}`); + return res.json(); +} + +async function fetchSeasonData( + imdbId: string, + seasonNumber: number, +): Promise { + const res = await fetch( + `${API_BASE}/tv/${imdbId}/season/${seasonNumber}`, + ); + if (!res.ok) throw new Error(`Season API returned ${res.status}`); + return res.json(); +} + +export async function getImdbOverride( + tmdbId: string, + seasonId?: string, +): Promise { + const imdbId = getImdbIdForTmdb(tmdbId); + if (!imdbId) return null; + + try { + const show = await fetchShowInfo(imdbId); + + const seasons = show.seasons + .filter((s) => s.season_number > 0) + .map((s) => ({ + id: `${tmdbId}-s${s.season_number}`, + number: s.season_number, + title: `Season ${s.season_number}`, + })); + + if (seasons.length === 0) return null; + + let selectedSeasonNumber = seasons[0].number; + if (seasonId) { + const found = seasons.find((s) => s.id === seasonId); + if (found) selectedSeasonNumber = found.number; + } + + const seasonData = await fetchSeasonData(imdbId, selectedSeasonNumber); + const selectedSeason = seasons.find( + (s) => s.number === selectedSeasonNumber, + )!; + if (seasonData.name) { + selectedSeason.title = seasonData.name; + } + + return { + meta: { + type: MWMediaType.SERIES, + title: show.name, + originalTitle: show.original_name || undefined, + id: tmdbId, + year: show.first_air_date?.split("-")[0], + poster: show.poster_path || undefined, + overview: show.overview || undefined, + seasons, + seasonData: { + id: selectedSeason.id, + number: selectedSeason.number, + title: selectedSeason.title, + episodes: seasonData.episodes.map((ep) => ({ + id: `${tmdbId}-s${selectedSeasonNumber}-e${ep.episode_number}`, + number: ep.episode_number, + title: ep.name, + air_date: ep.air_date || "", + still_path: ep.still_path, + overview: ep.overview || "", + })), + }, + }, + imdbId, + tmdbId, + }; + } catch { + return null; + } +} + +export async function getImdbEpisodes( + tmdbId: string, + seasonNumber: number, +): Promise { + const imdbId = getImdbIdForTmdb(tmdbId); + if (!imdbId) return null; + + try { + const seasonData = await fetchSeasonData(imdbId, seasonNumber); + return seasonData.episodes.map((ep) => ({ + id: ep.episode_number, + episode_number: ep.episode_number, + title: ep.name, + air_date: ep.air_date || "", + still_path: ep.still_path, + overview: ep.overview || "", + })); + } catch { + return null; + } +} diff --git a/src/backend/metadata/overrides.json b/src/backend/metadata/overrides.json new file mode 100644 index 000000000..92a432b6a --- /dev/null +++ b/src/backend/metadata/overrides.json @@ -0,0 +1,3 @@ +{ + "71446": "tt6468322" +} diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index 1b39f3b33..cad099460 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -9,6 +9,7 @@ import { MediaItem } from "@/utils/mediaTypes"; import { getProxyUrls } from "@/utils/proxyUrls"; import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types/mw"; +import { getImdbEpisodes } from "./imdbMetadataProvider"; import { ExternalIdMovieSearchResult, TMDBContentTypes, @@ -527,6 +528,9 @@ export async function getEpisodes( id: string, season: number, ): Promise { + const overrideEps = await getImdbEpisodes(id, season); + if (overrideEps) return overrideEps; + const data = await get(`/tv/${id}/season/${season}`); return data.episodes.map((e) => ({ id: e.id, diff --git a/src/components/player/atoms/Episodes.tsx b/src/components/player/atoms/Episodes.tsx index 8f1843a57..82740bb7d 100644 --- a/src/components/player/atoms/Episodes.tsx +++ b/src/components/player/atoms/Episodes.tsx @@ -169,7 +169,7 @@ function EpisodeItem({
{episode.still_path ? ( {episode.title}