Decouple series episode listing from logo/still columns so meta cannot go empty again.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a0785962ab
commit
189448ebdb
2 changed files with 110 additions and 133 deletions
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, string>();
|
||||
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<SeriesMetaRow | null> {
|
||||
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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue