Fix series episode list: private IDs, unique released, dual episode lookup.
Bump addon to 1.4.0 so Stremio refreshes catalog away from IMDb/Cinemeta merges. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0ebaaa1d99
commit
1c76b0cd32
2 changed files with 58 additions and 36 deletions
|
|
@ -99,8 +99,16 @@ async function main() {
|
|||
WHERE e.season_id = s.id
|
||||
AND e.series_id IS DISTINCT FROM s.series_id
|
||||
`);
|
||||
await prisma.$executeRawUnsafe(`
|
||||
UPDATE episodes e
|
||||
SET season_id = s.id
|
||||
FROM seasons s
|
||||
WHERE s.series_id = e.series_id
|
||||
AND s.season_number = e.season_number
|
||||
AND e.season_id IS DISTINCT FROM s.id
|
||||
`);
|
||||
} catch (err) {
|
||||
console.warn("Could not repair episode series_id drift:", err);
|
||||
console.warn("Could not repair episode series/season drift:", err);
|
||||
}
|
||||
|
||||
const shutdown = async () => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const SKIP_EXTRA = [{ name: "skip", isRequired: false }];
|
|||
|
||||
const MANIFEST = {
|
||||
id: "com.mediacluster.library",
|
||||
version: "1.3.1",
|
||||
version: "1.4.0",
|
||||
name: "Media Cluster",
|
||||
description: "Private distributed media library",
|
||||
catalogs: [
|
||||
|
|
@ -63,20 +63,26 @@ function paramId(raw: string): string {
|
|||
return decodeURIComponent(raw).replace(/\.json$/i, "");
|
||||
}
|
||||
|
||||
/** Stremio requires Video.released (ISO-8601); missing/invalid videos are dropped. */
|
||||
/**
|
||||
* Stremio requires Video.released (ISO-8601); missing/invalid videos are dropped.
|
||||
* Always unique per S/E — identical `released` values make some clients collapse the list.
|
||||
*/
|
||||
function videoReleased(
|
||||
airDate: Date | null | undefined,
|
||||
year: number | null | undefined,
|
||||
season: number,
|
||||
episode: number
|
||||
): string {
|
||||
const s = Math.max(0, season);
|
||||
const e = Math.max(0, episode);
|
||||
if (airDate && !Number.isNaN(airDate.getTime())) {
|
||||
return airDate.toISOString();
|
||||
const d = new Date(airDate.getTime());
|
||||
d.setUTCHours(12, s, Math.min(e, 59), 0);
|
||||
return d.toISOString();
|
||||
}
|
||||
// Unique synthetic date per SxxExx so clients never collapse episodes.
|
||||
const base = year && year > 1900 ? year : 2000;
|
||||
const d = new Date(Date.UTC(base, 0, 1, 12, 0, 0));
|
||||
d.setUTCDate(d.getUTCDate() + Math.max(0, season) * 40 + Math.max(0, episode));
|
||||
d.setUTCDate(d.getUTCDate() + s * 40 + e);
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
|
|
@ -395,7 +401,7 @@ async function getSeriesMeta(
|
|||
mediaFiles: Array<{ id: string }>;
|
||||
};
|
||||
|
||||
async function loadEpisodes(onlyAvailable: boolean, withStill: boolean): Promise<EpRow[]> {
|
||||
async function loadEpisodes(onlyAvailable: boolean): Promise<EpRow[]> {
|
||||
const mediaFilter = onlyAvailable
|
||||
? { some: { available: true as const } }
|
||||
: { some: {} };
|
||||
|
|
@ -403,25 +409,12 @@ async function getSeriesMeta(
|
|||
? { where: { available: true as const }, take: 1 as const, select: { id: true as const } }
|
||||
: { take: 1 as const, select: { id: true as const } };
|
||||
|
||||
if (withStill) {
|
||||
return prisma.episode.findMany({
|
||||
where: { season: { seriesId: series.id }, mediaFiles: mediaFilter },
|
||||
orderBy: [{ seasonNumber: "asc" }, { episodeNumber: "asc" }],
|
||||
select: {
|
||||
id: true,
|
||||
seasonNumber: true,
|
||||
episodeNumber: true,
|
||||
title: true,
|
||||
overview: true,
|
||||
airDate: true,
|
||||
stillUrl: true,
|
||||
mediaFiles: mediaSelect,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Match on episode.seriesId OR season.seriesId — either side can drift after merges.
|
||||
const rows = await prisma.episode.findMany({
|
||||
where: { season: { seriesId: series.id }, mediaFiles: mediaFilter },
|
||||
where: {
|
||||
OR: [{ seriesId: series.id }, { season: { seriesId: series.id } }],
|
||||
mediaFiles: mediaFilter,
|
||||
},
|
||||
orderBy: [{ seasonNumber: "asc" }, { episodeNumber: "asc" }],
|
||||
select: {
|
||||
id: true,
|
||||
|
|
@ -433,19 +426,28 @@ async function getSeriesMeta(
|
|||
mediaFiles: mediaSelect,
|
||||
},
|
||||
});
|
||||
return rows.map((ep) => ({ ...ep, stillUrl: null }));
|
||||
return rows.map((ep) => ({ ...ep, stillUrl: null as string | null }));
|
||||
}
|
||||
|
||||
let episodes: EpRow[] = [];
|
||||
try {
|
||||
episodes = await loadEpisodes(true, true);
|
||||
if (episodes.length === 0) episodes = await loadEpisodes(false, true);
|
||||
episodes = await loadEpisodes(true);
|
||||
if (episodes.length === 0) episodes = await loadEpisodes(false);
|
||||
} catch {
|
||||
episodes = [];
|
||||
}
|
||||
|
||||
// Stills are optional — never let still_url break or filter the episode list.
|
||||
const stillById = new Map<string, string | null>();
|
||||
if (episodes.length > 0) {
|
||||
try {
|
||||
episodes = await loadEpisodes(true, false);
|
||||
if (episodes.length === 0) episodes = await loadEpisodes(false, false);
|
||||
const artRows = await prisma.episode.findMany({
|
||||
where: { id: { in: episodes.map((ep) => ep.id) } },
|
||||
select: { id: true, stillUrl: true },
|
||||
});
|
||||
for (const row of artRows) stillById.set(row.id, row.stillUrl);
|
||||
} catch {
|
||||
episodes = [];
|
||||
// ignore missing column / errors
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -465,9 +467,12 @@ async function getSeriesMeta(
|
|||
const videos = [];
|
||||
for (const ep of episodes) {
|
||||
if (ep.mediaFiles.length === 0) continue;
|
||||
// Skip specials (S00) — Stremio often hides/misplaces them; keep numbered seasons.
|
||||
if (ep.seasonNumber <= 0 || ep.episodeNumber <= 0) continue;
|
||||
const key = `${ep.seasonNumber}:${ep.episodeNumber}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
const still = stillById.get(ep.id) ?? ep.stillUrl;
|
||||
videos.push({
|
||||
id: `${sid}:${ep.seasonNumber}:${ep.episodeNumber}`,
|
||||
title:
|
||||
|
|
@ -477,13 +482,21 @@ async function getSeriesMeta(
|
|||
season: ep.seasonNumber,
|
||||
episode: ep.episodeNumber,
|
||||
overview: ep.overview || undefined,
|
||||
thumbnail: ep.stillUrl || undefined,
|
||||
// Prefer Metahub-sized stills only when we have them; omit rather than break listing.
|
||||
thumbnail: still || undefined,
|
||||
released: videoReleased(ep.airDate, series.year, ep.seasonNumber, ep.episodeNumber),
|
||||
available: true,
|
||||
});
|
||||
}
|
||||
|
||||
log?.info?.(
|
||||
{ seriesId: series.id, sid, videoCount: videos.length, requestId: id },
|
||||
{
|
||||
seriesId: series.id,
|
||||
sid,
|
||||
videoCount: videos.length,
|
||||
episodeRows: episodes.length,
|
||||
requestId: id,
|
||||
},
|
||||
"series meta"
|
||||
);
|
||||
|
||||
|
|
@ -498,9 +511,10 @@ async function getSeriesMeta(
|
|||
releaseInfo: series.year ? `${series.year}-` : undefined,
|
||||
genres: series.genres,
|
||||
videos,
|
||||
behaviorHints: videos[0]
|
||||
? { defaultVideoId: videos[0].id, hasScheduledVideos: false }
|
||||
: undefined,
|
||||
behaviorHints: {
|
||||
hasScheduledVideos: false,
|
||||
...(videos[0] ? { defaultVideoId: videos[0].id } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue