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,
|
overview: tmdb.overview,
|
||||||
posterUrl: tmdb.posterUrl,
|
posterUrl: tmdb.posterUrl,
|
||||||
backdropUrl: tmdb.backdropUrl,
|
backdropUrl: tmdb.backdropUrl,
|
||||||
logoUrl: tmdb.logoUrl,
|
|
||||||
tmdbId: tmdb.id,
|
tmdbId: tmdb.id,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
|
|
@ -297,10 +296,14 @@ export class MetadataService {
|
||||||
overview: tmdb.overview,
|
overview: tmdb.overview,
|
||||||
posterUrl: tmdb.posterUrl,
|
posterUrl: tmdb.posterUrl,
|
||||||
backdropUrl: tmdb.backdropUrl,
|
backdropUrl: tmdb.backdropUrl,
|
||||||
logoUrl: tmdb.logoUrl,
|
|
||||||
year: tmdb.year,
|
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);
|
await this.absorbOrphanSeries(series, title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -389,12 +392,16 @@ export class MetadataService {
|
||||||
data: {
|
data: {
|
||||||
posterUrl: tmdb.posterUrl ?? series.posterUrl,
|
posterUrl: tmdb.posterUrl ?? series.posterUrl,
|
||||||
backdropUrl: tmdb.backdropUrl ?? series.backdropUrl,
|
backdropUrl: tmdb.backdropUrl ?? series.backdropUrl,
|
||||||
logoUrl: tmdb.logoUrl ?? series.logoUrl,
|
|
||||||
overview: tmdb.overview ?? series.overview,
|
overview: tmdb.overview ?? series.overview,
|
||||||
title: tmdb.title || series.title,
|
title: tmdb.title || series.title,
|
||||||
year: tmdb.year ?? series.year,
|
year: tmdb.year ?? series.year,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (tmdb.logoUrl) {
|
||||||
|
await prisma.series
|
||||||
|
.update({ where: { id: seriesId }, data: { logoUrl: tmdb.logoUrl } })
|
||||||
|
.catch(() => undefined);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore — meta listing must not depend on this
|
// ignore — meta listing must not depend on this
|
||||||
}
|
}
|
||||||
|
|
@ -832,9 +839,15 @@ export class MetadataService {
|
||||||
overview: tmdb.overview,
|
overview: tmdb.overview,
|
||||||
posterUrl: tmdb.posterUrl,
|
posterUrl: tmdb.posterUrl,
|
||||||
backdropUrl: tmdb.backdropUrl,
|
backdropUrl: tmdb.backdropUrl,
|
||||||
logoUrl: tmdb.logoUrl,
|
|
||||||
year: tmdb.year,
|
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) {
|
async function getSeriesMeta(id: string, metadata: MetadataService) {
|
||||||
// seasons → episodes (same path as catalog). stillUrl/logoUrl optional.
|
// Core listing query: NEVER select stillUrl/logoUrl (optional columns broke meta before).
|
||||||
const series = await loadSeriesMetaRow(id);
|
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;
|
if (!series) return null;
|
||||||
|
|
||||||
void metadata.ensureSeriesArtwork(series.id).catch(() => undefined);
|
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);
|
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)
|
season.episodes.filter((ep) => ep.mediaFiles.length > 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (episodeRows.length === 0) {
|
if (episodes.length === 0) {
|
||||||
const fallback = await loadSeriesMetaRow(id, false);
|
const fallback = await prisma.series.findFirst({
|
||||||
episodeRows =
|
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) =>
|
fallback?.seasons.flatMap((season) =>
|
||||||
season.episodes.filter((ep) => ep.mediaFiles.length > 0)
|
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}`,
|
id: `${sid}:${ep.seasonNumber}:${ep.episodeNumber}`,
|
||||||
title:
|
title:
|
||||||
ep.title && !/^episode\s+\d+$/i.test(ep.title)
|
ep.title && !/^episode\s+\d+$/i.test(ep.title)
|
||||||
|
|
@ -348,7 +432,7 @@ async function getSeriesMeta(id: string, metadata: MetadataService) {
|
||||||
season: ep.seasonNumber,
|
season: ep.seasonNumber,
|
||||||
episode: ep.episodeNumber,
|
episode: ep.episodeNumber,
|
||||||
overview: ep.overview ?? undefined,
|
overview: ep.overview ?? undefined,
|
||||||
thumbnail: ep.stillUrl || undefined,
|
thumbnail: stillByEpisodeId.get(ep.id),
|
||||||
released: formatMetaDate(ep.airDate),
|
released: formatMetaDate(ep.airDate),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -358,7 +442,7 @@ async function getSeriesMeta(id: string, metadata: MetadataService) {
|
||||||
name: series.title,
|
name: series.title,
|
||||||
poster: series.posterUrl ?? undefined,
|
poster: series.posterUrl ?? undefined,
|
||||||
background: series.backdropUrl ?? undefined,
|
background: series.backdropUrl ?? undefined,
|
||||||
logo: series.logoUrl ?? undefined,
|
logo,
|
||||||
description: series.overview ?? undefined,
|
description: series.overview ?? undefined,
|
||||||
releaseInfo: series.year ? String(series.year) : undefined,
|
releaseInfo: series.year ? String(series.year) : undefined,
|
||||||
genres: series.genres,
|
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 {
|
function formatMetaDate(value: Date | null | undefined): string | undefined {
|
||||||
if (!value) return undefined;
|
if (!value) return undefined;
|
||||||
const t = value.getTime();
|
const t = value.getTime();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue