diff --git a/apps/master-api/src/metadata/service.ts b/apps/master-api/src/metadata/service.ts index aaceb3b..f1024dd 100644 --- a/apps/master-api/src/metadata/service.ts +++ b/apps/master-api/src/metadata/service.ts @@ -289,7 +289,6 @@ export class MetadataService { overview: tmdb.overview, posterUrl: tmdb.posterUrl, backdropUrl: tmdb.backdropUrl, - logoUrl: tmdb.logoUrl, tmdbId: tmdb.id, }, update: { @@ -297,10 +296,14 @@ export class MetadataService { overview: tmdb.overview, posterUrl: tmdb.posterUrl, backdropUrl: tmdb.backdropUrl, - logoUrl: tmdb.logoUrl, year: tmdb.year, }, }); + if (tmdb.logoUrl) { + await prisma.series + .update({ where: { id: series.id }, data: { logoUrl: tmdb.logoUrl } }) + .catch(() => undefined); + } await this.absorbOrphanSeries(series, title); } } @@ -389,12 +392,16 @@ export class MetadataService { data: { posterUrl: tmdb.posterUrl ?? series.posterUrl, backdropUrl: tmdb.backdropUrl ?? series.backdropUrl, - logoUrl: tmdb.logoUrl ?? series.logoUrl, overview: tmdb.overview ?? series.overview, title: tmdb.title || series.title, year: tmdb.year ?? series.year, }, }); + if (tmdb.logoUrl) { + await prisma.series + .update({ where: { id: seriesId }, data: { logoUrl: tmdb.logoUrl } }) + .catch(() => undefined); + } } catch { // ignore — meta listing must not depend on this } @@ -832,9 +839,15 @@ export class MetadataService { overview: tmdb.overview, posterUrl: tmdb.posterUrl, backdropUrl: tmdb.backdropUrl, - logoUrl: tmdb.logoUrl, year: tmdb.year, }, + }).then(async (row) => { + if (tmdb.logoUrl) { + await prisma.series + .update({ where: { id: seriesId }, data: { logoUrl: tmdb.logoUrl } }) + .catch(() => undefined); + } + return row; }); } } diff --git a/apps/master-api/src/stremio/routes.ts b/apps/master-api/src/stremio/routes.ts index 2c2df31..ef52afb 100644 --- a/apps/master-api/src/stremio/routes.ts +++ b/apps/master-api/src/stremio/routes.ts @@ -318,8 +318,44 @@ async function getMovieMeta(id: string) { } async function getSeriesMeta(id: string, metadata: MetadataService) { - // seasons → episodes (same path as catalog). stillUrl/logoUrl optional. - const series = await loadSeriesMetaRow(id); + // Core listing query: NEVER select stillUrl/logoUrl (optional columns broke meta before). + const series = await prisma.series.findFirst({ + where: { + OR: [{ imdbId: id }, { id: unwrapLocalId(id, "series") }], + }, + select: { + id: true, + title: true, + year: true, + overview: true, + posterUrl: true, + backdropUrl: true, + genres: true, + imdbId: true, + seasons: { + orderBy: { seasonNumber: "asc" }, + select: { + episodes: { + orderBy: [{ seasonNumber: "asc" }, { episodeNumber: "asc" }], + select: { + id: true, + seasonNumber: true, + episodeNumber: true, + title: true, + overview: true, + airDate: true, + mediaFiles: { + where: { available: true }, + take: 1, + select: { id: true }, + }, + }, + }, + }, + }, + }, + }); + if (!series) return null; void metadata.ensureSeriesArtwork(series.id).catch(() => undefined); @@ -327,19 +363,67 @@ async function getSeriesMeta(id: string, metadata: MetadataService) { const sid = seriesId(series.imdbId, series.id); - let episodeRows = series.seasons.flatMap((season) => + let episodes = series.seasons.flatMap((season) => season.episodes.filter((ep) => ep.mediaFiles.length > 0) ); - if (episodeRows.length === 0) { - const fallback = await loadSeriesMetaRow(id, false); - episodeRows = + if (episodes.length === 0) { + const fallback = await prisma.series.findFirst({ + where: { id: series.id }, + select: { + seasons: { + orderBy: { seasonNumber: "asc" }, + select: { + episodes: { + orderBy: [{ seasonNumber: "asc" }, { episodeNumber: "asc" }], + select: { + id: true, + seasonNumber: true, + episodeNumber: true, + title: true, + overview: true, + airDate: true, + mediaFiles: { take: 1, select: { id: true } }, + }, + }, + }, + }, + }, + }); + episodes = fallback?.seasons.flatMap((season) => season.episodes.filter((ep) => ep.mediaFiles.length > 0) ) ?? []; } - const videos = episodeRows.map((ep) => ({ + // Optional extras — failure must not clear videos. + const stillByEpisodeId = new Map(); + let logo: string | undefined; + try { + const art = await prisma.series.findFirst({ + where: { id: series.id }, + select: { + logoUrl: true, + seasons: { + select: { + episodes: { + select: { id: true, stillUrl: true }, + }, + }, + }, + }, + }); + logo = art?.logoUrl ?? undefined; + for (const season of art?.seasons ?? []) { + for (const ep of season.episodes) { + if (ep.stillUrl) stillByEpisodeId.set(ep.id, ep.stillUrl); + } + } + } catch { + // columns may not exist yet + } + + const videos = episodes.map((ep) => ({ id: `${sid}:${ep.seasonNumber}:${ep.episodeNumber}`, title: ep.title && !/^episode\s+\d+$/i.test(ep.title) @@ -348,7 +432,7 @@ async function getSeriesMeta(id: string, metadata: MetadataService) { season: ep.seasonNumber, episode: ep.episodeNumber, overview: ep.overview ?? undefined, - thumbnail: ep.stillUrl || undefined, + thumbnail: stillByEpisodeId.get(ep.id), released: formatMetaDate(ep.airDate), })); @@ -358,7 +442,7 @@ async function getSeriesMeta(id: string, metadata: MetadataService) { name: series.title, poster: series.posterUrl ?? undefined, background: series.backdropUrl ?? undefined, - logo: series.logoUrl ?? undefined, + logo, description: series.overview ?? undefined, releaseInfo: series.year ? String(series.year) : undefined, genres: series.genres, @@ -366,126 +450,6 @@ async function getSeriesMeta(id: string, metadata: MetadataService) { }; } -type SeriesMetaRow = { - id: string; - title: string; - year: number | null; - overview: string | null; - posterUrl: string | null; - backdropUrl: string | null; - logoUrl: string | null; - genres: string[]; - imdbId: string | null; - seasons: Array<{ - episodes: Array<{ - seasonNumber: number; - episodeNumber: number; - title: string | null; - overview: string | null; - airDate: Date | null; - stillUrl: string | null; - mediaFiles: Array<{ id: string }>; - }>; - }>; -}; - -async function loadSeriesMetaRow( - id: string, - onlyAvailable = true -): Promise { - const where = { - OR: [{ imdbId: id }, { id: unwrapLocalId(id, "series") }], - }; - const mediaFiles = onlyAvailable - ? { where: { available: true as const }, take: 1 as const, select: { id: true as const } } - : { take: 1 as const, select: { id: true as const } }; - - try { - const row = await prisma.series.findFirst({ - where, - select: { - id: true, - title: true, - year: true, - overview: true, - posterUrl: true, - backdropUrl: true, - logoUrl: true, - genres: true, - imdbId: true, - seasons: { - orderBy: { seasonNumber: "asc" as const }, - select: { - episodes: { - orderBy: [ - { seasonNumber: "asc" as const }, - { episodeNumber: "asc" as const }, - ], - select: { - seasonNumber: true, - episodeNumber: true, - title: true, - overview: true, - airDate: true, - stillUrl: true, - mediaFiles, - }, - }, - }, - }, - }, - }); - if (row) return row; - } catch { - // logo_url / still_url missing — fall back without those columns - } - - try { - const row = await prisma.series.findFirst({ - where, - select: { - id: true, - title: true, - year: true, - overview: true, - posterUrl: true, - backdropUrl: true, - genres: true, - imdbId: true, - seasons: { - orderBy: { seasonNumber: "asc" as const }, - select: { - episodes: { - orderBy: [ - { seasonNumber: "asc" as const }, - { episodeNumber: "asc" as const }, - ], - select: { - seasonNumber: true, - episodeNumber: true, - title: true, - overview: true, - airDate: true, - mediaFiles, - }, - }, - }, - }, - }, - }); - if (!row) return null; - return { - ...row, - logoUrl: null, - seasons: row.seasons.map((s) => ({ - episodes: s.episodes.map((ep) => ({ ...ep, stillUrl: null })), - })), - }; - } catch { - return null; - } -} - function formatMetaDate(value: Date | null | undefined): string | undefined { if (!value) return undefined; const t = value.getTime();