Fix empty series episodes: never block Stremio meta on TMDB enrich or bad air dates.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
9c3fd94f03
commit
52e2be7296
2 changed files with 95 additions and 71 deletions
|
|
@ -54,6 +54,13 @@ function needsEpisodeEnrichment(ep: {
|
|||
return false;
|
||||
}
|
||||
|
||||
function parseAirDate(raw?: string): Date | undefined {
|
||||
if (!raw || !/^\d{4}-\d{2}-\d{2}/.test(raw)) return undefined;
|
||||
const d = new Date(raw);
|
||||
if (Number.isNaN(d.getTime())) return undefined;
|
||||
return d;
|
||||
}
|
||||
|
||||
export class MetadataService {
|
||||
private movieCache = new Map<number, TmdbMeta>();
|
||||
private seriesCache = new Map<number, TmdbMeta>();
|
||||
|
|
@ -365,47 +372,58 @@ export class MetadataService {
|
|||
/** Fill episode titles/stills from TMDB for a series (lazy, used by Stremio meta). */
|
||||
async enrichSeriesEpisodes(seriesId: string): Promise<void> {
|
||||
if (!this.tmdbApiKey) return;
|
||||
const series = await prisma.series.findUnique({
|
||||
where: { id: seriesId },
|
||||
include: {
|
||||
episodes: {
|
||||
select: {
|
||||
id: true,
|
||||
seasonNumber: true,
|
||||
episodeNumber: true,
|
||||
title: true,
|
||||
overview: true,
|
||||
stillUrl: true,
|
||||
try {
|
||||
const series = await prisma.series.findUnique({
|
||||
where: { id: seriesId },
|
||||
include: {
|
||||
episodes: {
|
||||
select: {
|
||||
id: true,
|
||||
seasonNumber: true,
|
||||
episodeNumber: true,
|
||||
title: true,
|
||||
overview: true,
|
||||
stillUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!series?.tmdbId || series.episodes.length === 0) return;
|
||||
});
|
||||
if (!series?.tmdbId || series.episodes.length === 0) return;
|
||||
|
||||
const seasons = new Set(
|
||||
series.episodes.filter((ep) => needsEpisodeEnrichment(ep)).map((ep) => ep.seasonNumber)
|
||||
);
|
||||
for (const seasonNumber of seasons) {
|
||||
const key = `${series.id}:${seasonNumber}`;
|
||||
if (this.enrichedSeriesSeasons.has(key)) continue;
|
||||
const metas = await this.fetchTmdbSeasonEpisodes(series.tmdbId, seasonNumber);
|
||||
if (!metas.length) continue;
|
||||
for (const ep of series.episodes.filter((e) => e.seasonNumber === seasonNumber)) {
|
||||
const meta = metas.find((m) => m.episodeNumber === ep.episodeNumber);
|
||||
if (!meta) continue;
|
||||
await prisma.episode.update({
|
||||
where: { id: ep.id },
|
||||
data: {
|
||||
title: meta.title,
|
||||
overview: meta.overview ?? undefined,
|
||||
stillUrl: meta.stillUrl ?? undefined,
|
||||
runtime: meta.runtime ?? undefined,
|
||||
tmdbId: meta.tmdbId ?? undefined,
|
||||
airDate: meta.airDate ? new Date(meta.airDate) : undefined,
|
||||
},
|
||||
});
|
||||
const seasons = new Set(
|
||||
series.episodes.filter((ep) => needsEpisodeEnrichment(ep)).map((ep) => ep.seasonNumber)
|
||||
);
|
||||
for (const seasonNumber of seasons) {
|
||||
const key = `${series.id}:${seasonNumber}`;
|
||||
if (this.enrichedSeriesSeasons.has(key)) continue;
|
||||
const metas = await this.fetchTmdbSeasonEpisodes(series.tmdbId, seasonNumber);
|
||||
if (!metas.length) {
|
||||
this.enrichedSeriesSeasons.add(key);
|
||||
continue;
|
||||
}
|
||||
for (const ep of series.episodes.filter((e) => e.seasonNumber === seasonNumber)) {
|
||||
const meta = metas.find((m) => m.episodeNumber === ep.episodeNumber);
|
||||
if (!meta) continue;
|
||||
try {
|
||||
await prisma.episode.update({
|
||||
where: { id: ep.id },
|
||||
data: {
|
||||
title: meta.title,
|
||||
overview: meta.overview ?? undefined,
|
||||
stillUrl: meta.stillUrl ?? undefined,
|
||||
runtime: meta.runtime ?? undefined,
|
||||
tmdbId: meta.tmdbId ?? undefined,
|
||||
airDate: parseAirDate(meta.airDate),
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// Column/migration mismatch or concurrent update — skip this episode.
|
||||
}
|
||||
}
|
||||
this.enrichedSeriesSeasons.add(key);
|
||||
}
|
||||
this.enrichedSeriesSeasons.add(key);
|
||||
} catch {
|
||||
// Never fail the caller (Stremio meta) because of enrichment.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -414,20 +432,24 @@ export class MetadataService {
|
|||
episode: EpisodeRow
|
||||
): Promise<EpisodeRow> {
|
||||
if (!needsEpisodeEnrichment(episode)) return episode;
|
||||
const metas = await this.fetchTmdbSeasonEpisodes(seriesTmdbId, episode.seasonNumber);
|
||||
const meta = metas.find((m) => m.episodeNumber === episode.episodeNumber);
|
||||
if (!meta) return episode;
|
||||
return prisma.episode.update({
|
||||
where: { id: episode.id },
|
||||
data: {
|
||||
title: meta.title,
|
||||
overview: meta.overview ?? undefined,
|
||||
stillUrl: meta.stillUrl ?? undefined,
|
||||
runtime: meta.runtime ?? undefined,
|
||||
tmdbId: meta.tmdbId ?? undefined,
|
||||
airDate: meta.airDate ? new Date(meta.airDate) : undefined,
|
||||
},
|
||||
});
|
||||
try {
|
||||
const metas = await this.fetchTmdbSeasonEpisodes(seriesTmdbId, episode.seasonNumber);
|
||||
const meta = metas.find((m) => m.episodeNumber === episode.episodeNumber);
|
||||
if (!meta) return episode;
|
||||
return await prisma.episode.update({
|
||||
where: { id: episode.id },
|
||||
data: {
|
||||
title: meta.title,
|
||||
overview: meta.overview ?? undefined,
|
||||
stillUrl: meta.stillUrl ?? undefined,
|
||||
runtime: meta.runtime ?? undefined,
|
||||
tmdbId: meta.tmdbId ?? undefined,
|
||||
airDate: parseAirDate(meta.airDate),
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return episode;
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchTmdbSeasonEpisodes(
|
||||
|
|
|
|||
|
|
@ -322,13 +322,6 @@ async function getSeriesMeta(id: string, metadata: MetadataService) {
|
|||
where: {
|
||||
OR: [{ imdbId: id }, { id: unwrapLocalId(id, "series") }],
|
||||
},
|
||||
});
|
||||
if (!series) return null;
|
||||
|
||||
await metadata.enrichSeriesEpisodes(series.id);
|
||||
|
||||
const refreshed = await prisma.series.findFirst({
|
||||
where: { id: series.id },
|
||||
include: {
|
||||
seasons: {
|
||||
include: {
|
||||
|
|
@ -341,37 +334,46 @@ async function getSeriesMeta(id: string, metadata: MetadataService) {
|
|||
},
|
||||
},
|
||||
});
|
||||
if (!refreshed) return null;
|
||||
if (!series) return null;
|
||||
|
||||
const sid = seriesId(refreshed.imdbId, refreshed.id);
|
||||
const videos = refreshed.seasons.flatMap((season) =>
|
||||
// Enrich in background — never block or break episode listing on TMDB/DB issues.
|
||||
void metadata.enrichSeriesEpisodes(series.id).catch(() => undefined);
|
||||
|
||||
const sid = seriesId(series.imdbId, series.id);
|
||||
const videos = series.seasons.flatMap((season) =>
|
||||
season.episodes
|
||||
.filter((ep) => ep.mediaFiles.length > 0)
|
||||
.map((ep) => ({
|
||||
id: `${sid}:${ep.seasonNumber}:${ep.episodeNumber}`,
|
||||
title: ep.title ?? `Episode ${ep.episodeNumber}`,
|
||||
title: ep.title && !/^episode\s+\d+$/i.test(ep.title) ? ep.title : `Episode ${ep.episodeNumber}`,
|
||||
season: ep.seasonNumber,
|
||||
episode: ep.episodeNumber,
|
||||
overview: ep.overview ?? undefined,
|
||||
thumbnail: ep.stillUrl ?? undefined,
|
||||
released: ep.airDate?.toISOString().slice(0, 10),
|
||||
runtime: ep.runtime ? `${ep.runtime} min` : undefined,
|
||||
thumbnail: ep.stillUrl || undefined,
|
||||
released: formatMetaDate(ep.airDate),
|
||||
}))
|
||||
);
|
||||
|
||||
return {
|
||||
id: sid,
|
||||
type: "series" as const,
|
||||
name: refreshed.title,
|
||||
poster: refreshed.posterUrl ?? undefined,
|
||||
background: refreshed.backdropUrl ?? undefined,
|
||||
description: refreshed.overview ?? undefined,
|
||||
releaseInfo: refreshed.year ? String(refreshed.year) : undefined,
|
||||
genres: refreshed.genres,
|
||||
name: series.title,
|
||||
poster: series.posterUrl ?? undefined,
|
||||
background: series.backdropUrl ?? undefined,
|
||||
description: series.overview ?? undefined,
|
||||
releaseInfo: series.year ? String(series.year) : undefined,
|
||||
genres: series.genres,
|
||||
videos,
|
||||
};
|
||||
}
|
||||
|
||||
function formatMetaDate(value: Date | null | undefined): string | undefined {
|
||||
if (!value) return undefined;
|
||||
const t = value.getTime();
|
||||
if (Number.isNaN(t)) return undefined;
|
||||
return value.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
async function getStreams(
|
||||
type: string,
|
||||
id: string,
|
||||
|
|
|
|||
Loading…
Reference in a new issue