Repair Cinemeta/IMDb linking after path migration and harden library sync.
This commit is contained in:
parent
68440f4081
commit
887f750f92
5 changed files with 285 additions and 61 deletions
|
|
@ -49,6 +49,7 @@ export default function DashboardPage() {
|
|||
const [error, setError] = useState("");
|
||||
const [dedupeMsg, setDedupeMsg] = useState("");
|
||||
const [imdbMsg, setImdbMsg] = useState("");
|
||||
const [repairMsg, setRepairMsg] = useState("");
|
||||
|
||||
const load = useCallback(() => {
|
||||
fetch("/api/v1/admin/dashboard", { credentials: "include" })
|
||||
|
|
@ -69,6 +70,31 @@ export default function DashboardPage() {
|
|||
load();
|
||||
}, [load]);
|
||||
|
||||
async function repairLibrary() {
|
||||
if (
|
||||
!confirm(
|
||||
"Bibliotheek herstellen na pad-migratie?\n\n• Duplicaten samenvoegen naar IMDb/TMDB-records\n• Dode (unavailable) bestanden verwijderen\n• Ontbrekende IMDb-IDs backfillen\n\nDit kan enkele minuten duren."
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setRepairMsg("Bezig…");
|
||||
const res = await fetch("/api/v1/admin/library/repair", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok) {
|
||||
setRepairMsg(json.error?.message ?? "Mislukt");
|
||||
return;
|
||||
}
|
||||
const imdb = json.imdb ?? {};
|
||||
setRepairMsg(
|
||||
`Klaar: ${json.moviesMerged ?? 0} film-merges, ${json.seriesMerged ?? 0} serie-merges, ${json.staleFilesRemoved ?? 0} dode files weg · IMDb films +${imdb.movies?.updated ?? 0}, series +${imdb.series?.updated ?? 0}. Herlaad daarna de Stremio-addon.`
|
||||
);
|
||||
load();
|
||||
}
|
||||
|
||||
async function backfillImdb() {
|
||||
if (
|
||||
!confirm(
|
||||
|
|
@ -180,6 +206,26 @@ export default function DashboardPage() {
|
|||
</div>
|
||||
<div className="card" style={{ marginTop: "1.5rem" }}>
|
||||
<h2 style={{ fontSize: "1rem", color: "#fafafa", textTransform: "none" }}>
|
||||
Herstel na pad-wijziging
|
||||
</h2>
|
||||
<p style={{ color: "#a1a1aa", fontSize: "0.875rem", margin: "0.75rem 0" }}>
|
||||
Gebruik dit als Cinemeta geen streams meer toont na een Unraid pad-wijziging
|
||||
(oude /media/movies-bestanden + dubbele titels zonder IMDb).
|
||||
</p>
|
||||
<button onClick={repairLibrary}>Bibliotheek herstellen</button>
|
||||
{repairMsg && (
|
||||
<p style={{ color: "#a1a1aa", fontSize: "0.875rem", marginTop: "0.75rem" }}>
|
||||
{repairMsg}
|
||||
</p>
|
||||
)}
|
||||
<h2
|
||||
style={{
|
||||
fontSize: "1rem",
|
||||
color: "#fafafa",
|
||||
textTransform: "none",
|
||||
marginTop: "1.5rem",
|
||||
}}
|
||||
>
|
||||
Bibliotheek opruimen
|
||||
</h2>
|
||||
<p style={{ color: "#a1a1aa", fontSize: "0.875rem", margin: "0.75rem 0" }}>
|
||||
|
|
|
|||
|
|
@ -45,11 +45,7 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
prisma.movie.count({ where: { mediaFiles: { some: { available: true } } } }),
|
||||
prisma.series.count({
|
||||
where: {
|
||||
seasons: {
|
||||
some: {
|
||||
episodes: { some: { mediaFiles: { some: { available: true } } } },
|
||||
},
|
||||
},
|
||||
episodes: { some: { mediaFiles: { some: { available: true } } } },
|
||||
},
|
||||
}),
|
||||
prisma.mediaFile.count(),
|
||||
|
|
@ -463,6 +459,11 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
return result;
|
||||
});
|
||||
|
||||
app.post("/api/v1/admin/library/repair", { preHandler: requireAdmin }, async () => {
|
||||
const result = await metadata.repairLibraryAfterPathMigration();
|
||||
return result;
|
||||
});
|
||||
|
||||
app.get("/api/v1/admin/library/shelves", { preHandler: requireAdmin }, async () => {
|
||||
const { ensureDefaultShelves } = await import("../library/shelves");
|
||||
await ensureDefaultShelves();
|
||||
|
|
|
|||
|
|
@ -58,12 +58,28 @@ export class LibrarySyncService {
|
|||
const keepIds = options.fileIdsSoFar;
|
||||
const existing = await prisma.mediaFile.findMany({
|
||||
where: { nodeId },
|
||||
select: { id: true, localFileId: true },
|
||||
select: { id: true, localFileId: true, available: true },
|
||||
});
|
||||
const availableCount = existing.filter((r) => r.available).length;
|
||||
const toDisable = existing
|
||||
.filter((row) => !keepIds.has(row.localFileId))
|
||||
.map((row) => row.id);
|
||||
|
||||
// Guard: incomplete sync (scan still running) must not wipe the library.
|
||||
if (
|
||||
availableCount > 500 &&
|
||||
keepIds.size < Math.max(100, Math.floor(availableCount * 0.5))
|
||||
) {
|
||||
console.warn(
|
||||
`Skipping library prune for ${nodeId}: sync has ${keepIds.size} files but ${availableCount} are available (likely incomplete scan)`
|
||||
);
|
||||
await prisma.node.update({
|
||||
where: { id: nodeId },
|
||||
data: { lastRevision: nodeRevision },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < toDisable.length; i += PRUNE_CHUNK) {
|
||||
const chunk = toDisable.slice(i, i + PRUNE_CHUNK);
|
||||
await prisma.mediaFile.updateMany({
|
||||
|
|
|
|||
|
|
@ -104,6 +104,45 @@ function normalizeSeriesKey(title: string): string {
|
|||
.replace(/[^a-z0-9]+/g, "");
|
||||
}
|
||||
|
||||
type MoviePick = {
|
||||
id: string;
|
||||
imdbId: string | null;
|
||||
tmdbId: number | null;
|
||||
createdAt: Date;
|
||||
_count: { mediaFiles: number };
|
||||
};
|
||||
|
||||
type SeriesPick = {
|
||||
id: string;
|
||||
imdbId: string | null;
|
||||
tmdbId: number | null;
|
||||
createdAt: Date;
|
||||
_count: { episodes: number };
|
||||
};
|
||||
|
||||
/** Prefer IMDb → TMDB → most files → oldest (stable Cinemeta identity). */
|
||||
function pickBestMovie(group: MoviePick[]): MoviePick {
|
||||
return [...group].sort((a, b) => {
|
||||
if (!!a.imdbId !== !!b.imdbId) return a.imdbId ? -1 : 1;
|
||||
if ((a.tmdbId != null) !== (b.tmdbId != null)) return a.tmdbId != null ? -1 : 1;
|
||||
if (a._count.mediaFiles !== b._count.mediaFiles) {
|
||||
return b._count.mediaFiles - a._count.mediaFiles;
|
||||
}
|
||||
return a.createdAt.getTime() - b.createdAt.getTime();
|
||||
})[0];
|
||||
}
|
||||
|
||||
function pickBestSeries(group: SeriesPick[]): SeriesPick {
|
||||
return [...group].sort((a, b) => {
|
||||
if (!!a.imdbId !== !!b.imdbId) return a.imdbId ? -1 : 1;
|
||||
if ((a.tmdbId != null) !== (b.tmdbId != null)) return a.tmdbId != null ? -1 : 1;
|
||||
if (a._count.episodes !== b._count.episodes) {
|
||||
return b._count.episodes - a._count.episodes;
|
||||
}
|
||||
return a.createdAt.getTime() - b.createdAt.getTime();
|
||||
})[0];
|
||||
}
|
||||
|
||||
export class MetadataService {
|
||||
private movieCache = new Map<number, TmdbMeta>();
|
||||
private seriesCache = new Map<number, TmdbMeta>();
|
||||
|
|
@ -267,6 +306,7 @@ export class MetadataService {
|
|||
for (const group of movieGroups.values()) {
|
||||
if (group.length < 2) continue;
|
||||
const canonical =
|
||||
group.find((m) => m.imdbId != null) ??
|
||||
group.find((m) => m.tmdbId != null) ??
|
||||
group.find((m) => m._count.mediaFiles > 0) ??
|
||||
group[0];
|
||||
|
|
@ -299,7 +339,10 @@ export class MetadataService {
|
|||
}
|
||||
for (const group of seriesGroups.values()) {
|
||||
if (group.length < 2) continue;
|
||||
const canonical = group.find((s) => s.tmdbId != null) ?? group[0];
|
||||
const canonical =
|
||||
group.find((s) => s.imdbId != null) ??
|
||||
group.find((s) => s.tmdbId != null) ??
|
||||
group[0];
|
||||
for (const orphan of group) {
|
||||
if (orphan.id === canonical.id) continue;
|
||||
await this.mergeSeriesInto(canonical.id, orphan.id);
|
||||
|
|
@ -382,26 +425,36 @@ export class MetadataService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Pull duplicate series rows (same TMDB/IMDb/normalized title) into one canonical
|
||||
* so Stremio meta sees every episode file, not just the TMDB shell with one match.
|
||||
* Pull duplicate series rows into the best canonical (prefer IMDb/TMDB)
|
||||
* so Cinemeta tt-streams and local catalogs share one record with all files.
|
||||
*/
|
||||
async consolidateSeries(seriesId: string): Promise<number> {
|
||||
const canonical = await prisma.series.findUnique({ where: { id: seriesId } });
|
||||
if (!canonical) return 0;
|
||||
async consolidateSeries(seriesId: string): Promise<{ merged: number; canonicalId: string }> {
|
||||
const seed = await prisma.series.findUnique({ where: { id: seriesId } });
|
||||
if (!seed) return { merged: 0, canonicalId: seriesId };
|
||||
|
||||
const all = await prisma.series.findMany({
|
||||
select: { id: true, title: true, tmdbId: true, imdbId: true },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
tmdbId: true,
|
||||
imdbId: true,
|
||||
createdAt: true,
|
||||
_count: { select: { episodes: true } },
|
||||
},
|
||||
});
|
||||
const norm = normalizeSeriesKey(canonical.title);
|
||||
const orphans = all.filter((s) => {
|
||||
if (s.id === canonical.id) return false;
|
||||
if (canonical.tmdbId != null && s.tmdbId === canonical.tmdbId) return true;
|
||||
if (canonical.imdbId && s.imdbId === canonical.imdbId) return true;
|
||||
const norm = normalizeSeriesKey(seed.title);
|
||||
const group = all.filter((s) => {
|
||||
if (s.id === seed.id) return true;
|
||||
if (seed.tmdbId != null && s.tmdbId === seed.tmdbId) return true;
|
||||
if (seed.imdbId && s.imdbId === seed.imdbId) return true;
|
||||
if (s.tmdbId != null && seed.tmdbId != null && s.tmdbId === seed.tmdbId) return true;
|
||||
return normalizeSeriesKey(s.title) === norm;
|
||||
});
|
||||
|
||||
const canonical = pickBestSeries(group);
|
||||
let merged = 0;
|
||||
for (const orphan of orphans) {
|
||||
for (const orphan of group) {
|
||||
if (orphan.id === canonical.id) continue;
|
||||
try {
|
||||
await this.mergeSeriesInto(canonical.id, orphan.id);
|
||||
merged += 1;
|
||||
|
|
@ -409,7 +462,7 @@ export class MetadataService {
|
|||
// unique conflicts / race — skip
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
return { merged, canonicalId: canonical.id };
|
||||
}
|
||||
|
||||
private async matchMovie(nodeId: string, file: MediaFileInfo): Promise<string | null> {
|
||||
|
|
@ -767,39 +820,121 @@ export class MetadataService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Pull duplicate movie rows into one canonical so streams see every file.
|
||||
* Pull duplicate movie rows into the best canonical (prefer IMDb/TMDB holders)
|
||||
* so Cinemeta tt-lookups see the new available files after a path migration.
|
||||
*/
|
||||
async consolidateMovies(movieId: string): Promise<number> {
|
||||
const canonical = await prisma.movie.findUnique({ where: { id: movieId } });
|
||||
if (!canonical) return 0;
|
||||
async consolidateMovies(movieId: string): Promise<{ merged: number; canonicalId: string }> {
|
||||
const seed = await prisma.movie.findUnique({ where: { id: movieId } });
|
||||
if (!seed) return { merged: 0, canonicalId: movieId };
|
||||
|
||||
const all = await prisma.movie.findMany({
|
||||
select: { id: true, title: true, year: true, tmdbId: true, imdbId: true },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
year: true,
|
||||
tmdbId: true,
|
||||
imdbId: true,
|
||||
createdAt: true,
|
||||
_count: { select: { mediaFiles: true } },
|
||||
},
|
||||
});
|
||||
const norm = normalizeSeriesKey(canonical.title);
|
||||
const orphans = all.filter((m) => {
|
||||
if (m.id === canonical.id) return false;
|
||||
if (canonical.tmdbId != null && m.tmdbId === canonical.tmdbId) return true;
|
||||
if (canonical.imdbId && m.imdbId === canonical.imdbId) return true;
|
||||
const norm = normalizeSeriesKey(seed.title);
|
||||
const group = all.filter((m) => {
|
||||
if (m.id === seed.id) return true;
|
||||
if (seed.tmdbId != null && m.tmdbId === seed.tmdbId) return true;
|
||||
if (seed.imdbId && m.imdbId === seed.imdbId) return true;
|
||||
if (normalizeSeriesKey(m.title) !== norm) return false;
|
||||
if (canonical.year && m.year && canonical.year !== m.year) return false;
|
||||
if (seed.year && m.year && seed.year !== m.year) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const canonical = pickBestMovie(group);
|
||||
let merged = 0;
|
||||
for (const orphan of orphans) {
|
||||
for (const orphan of group) {
|
||||
if (orphan.id === canonical.id) continue;
|
||||
try {
|
||||
await prisma.mediaFile.updateMany({
|
||||
where: { movieId: orphan.id },
|
||||
data: { movieId: canonical.id },
|
||||
});
|
||||
// Prefer keeping IMDb/TMDB metadata on the survivor
|
||||
if ((!canonical.imdbId && orphan.imdbId) || (!canonical.tmdbId && orphan.tmdbId)) {
|
||||
await prisma.movie
|
||||
.update({
|
||||
where: { id: canonical.id },
|
||||
data: {
|
||||
...(canonical.imdbId ? {} : orphan.imdbId ? { imdbId: orphan.imdbId } : {}),
|
||||
...(canonical.tmdbId != null
|
||||
? {}
|
||||
: orphan.tmdbId != null
|
||||
? { tmdbId: orphan.tmdbId }
|
||||
: {}),
|
||||
},
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}
|
||||
await prisma.movie.delete({ where: { id: orphan.id } });
|
||||
merged += 1;
|
||||
} catch {
|
||||
// skip conflicts
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
return { merged, canonicalId: canonical.id };
|
||||
}
|
||||
|
||||
/**
|
||||
* After path migrations: merge duplicates toward IMDb holders, drop dead
|
||||
* unavailable files, then backfill missing IMDb ids.
|
||||
*/
|
||||
async repairLibraryAfterPathMigration(): Promise<{
|
||||
moviesMerged: number;
|
||||
seriesMerged: number;
|
||||
staleFilesRemoved: number;
|
||||
imdb: Awaited<ReturnType<MetadataService["backfillAllImdbIds"]>>;
|
||||
}> {
|
||||
let moviesMerged = 0;
|
||||
let seriesMerged = 0;
|
||||
|
||||
const movies = await prisma.movie.findMany({
|
||||
select: { id: true },
|
||||
orderBy: { createdAt: "asc" },
|
||||
});
|
||||
const seenMovies = new Set<string>();
|
||||
for (const m of movies) {
|
||||
if (seenMovies.has(m.id)) continue;
|
||||
if (!(await prisma.movie.findUnique({ where: { id: m.id }, select: { id: true } }))) {
|
||||
continue;
|
||||
}
|
||||
const { merged, canonicalId } = await this.consolidateMovies(m.id);
|
||||
seenMovies.add(canonicalId);
|
||||
moviesMerged += merged;
|
||||
}
|
||||
|
||||
const seriesList = await prisma.series.findMany({
|
||||
select: { id: true },
|
||||
orderBy: { createdAt: "asc" },
|
||||
});
|
||||
const seenSeries = new Set<string>();
|
||||
for (const s of seriesList) {
|
||||
if (seenSeries.has(s.id)) continue;
|
||||
if (!(await prisma.series.findUnique({ where: { id: s.id }, select: { id: true } }))) {
|
||||
continue;
|
||||
}
|
||||
const { merged, canonicalId } = await this.consolidateSeries(s.id);
|
||||
seenSeries.add(canonicalId);
|
||||
seriesMerged += merged;
|
||||
}
|
||||
|
||||
// Unavailable rows left after the /media/movies → /mnt/user migration
|
||||
const staleFilesRemoved = (
|
||||
await prisma.mediaFile.deleteMany({
|
||||
where: { available: false },
|
||||
})
|
||||
).count;
|
||||
|
||||
const imdb = await this.backfillAllImdbIds({ delayMs: 80 });
|
||||
|
||||
return { moviesMerged, seriesMerged, staleFilesRemoved, imdb };
|
||||
}
|
||||
|
||||
/** Backfill IMDb id from TMDB so catalog can use tt + Cinemeta. */
|
||||
|
|
@ -1056,20 +1191,32 @@ export class MetadataService {
|
|||
title: { equals: title, mode: "insensitive" },
|
||||
year,
|
||||
},
|
||||
orderBy: { tmdbId: { sort: "desc", nulls: "last" } },
|
||||
orderBy: [
|
||||
{ imdbId: { sort: "desc", nulls: "last" } },
|
||||
{ tmdbId: { sort: "desc", nulls: "last" } },
|
||||
{ createdAt: "asc" },
|
||||
],
|
||||
});
|
||||
if (withYear) return withYear;
|
||||
}
|
||||
return prisma.movie.findFirst({
|
||||
where: { title: { equals: title, mode: "insensitive" } },
|
||||
orderBy: { tmdbId: { sort: "desc", nulls: "last" } },
|
||||
orderBy: [
|
||||
{ imdbId: { sort: "desc", nulls: "last" } },
|
||||
{ tmdbId: { sort: "desc", nulls: "last" } },
|
||||
{ createdAt: "asc" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async findExistingSeries(title: string) {
|
||||
return prisma.series.findFirst({
|
||||
where: { title: { equals: title, mode: "insensitive" } },
|
||||
orderBy: { tmdbId: { sort: "desc", nulls: "last" } },
|
||||
orderBy: [
|
||||
{ imdbId: { sort: "desc", nulls: "last" } },
|
||||
{ tmdbId: { sort: "desc", nulls: "last" } },
|
||||
{ createdAt: "asc" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ async function buildManifest(tokenId: string | null, config: Config) {
|
|||
const types = new Set(["movie", "series", ...iptv.types]);
|
||||
return {
|
||||
id: "com.mediacluster.library",
|
||||
version: "1.6.0",
|
||||
version: "1.6.1",
|
||||
name: "Media Cluster",
|
||||
description: "Privé bibliotheek + Live TV / IPTV (Xtream) met EPG",
|
||||
catalogs: [...BASE_CATALOGS, ...shelfCatalogs, ...iptv.catalogs],
|
||||
|
|
@ -430,23 +430,17 @@ async function getSeriesCatalog(
|
|||
const shelfSlug = catalogId.startsWith("shelf-") ? catalogId.slice("shelf-".length) : null;
|
||||
const availableFilter = shelfSlug
|
||||
? {
|
||||
seasons: {
|
||||
episodes: {
|
||||
some: {
|
||||
episodes: {
|
||||
some: {
|
||||
mediaFiles: {
|
||||
some: { available: true, shelf: { slug: shelfSlug, kind: "SERIES" as const } },
|
||||
},
|
||||
},
|
||||
mediaFiles: {
|
||||
some: { available: true, shelf: { slug: shelfSlug, kind: "SERIES" as const } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
seasons: {
|
||||
some: {
|
||||
episodes: { some: { mediaFiles: { some: { available: true } } } },
|
||||
},
|
||||
episodes: {
|
||||
some: { mediaFiles: { some: { available: true } } },
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -487,7 +481,7 @@ async function getMovieMeta(
|
|||
log?: { info?: (obj: unknown, msg?: string) => void; warn?: (obj: unknown, msg?: string) => void }
|
||||
) {
|
||||
const localUuid = unwrapLocalId(id, "movie");
|
||||
const movie = await prisma.movie.findFirst({
|
||||
let movie = await prisma.movie.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
...(id.startsWith("tt") ? [{ imdbId: id }] : []),
|
||||
|
|
@ -502,7 +496,11 @@ async function getMovieMeta(
|
|||
}
|
||||
|
||||
try {
|
||||
await metadata.consolidateMovies(movie.id);
|
||||
const { canonicalId } = await metadata.consolidateMovies(movie.id);
|
||||
if (canonicalId !== movie.id) {
|
||||
const moved = await prisma.movie.findUnique({ where: { id: canonicalId } });
|
||||
if (moved) movie = moved;
|
||||
}
|
||||
} catch (err) {
|
||||
log?.warn?.({ err, movieId: movie.id }, "movie meta: consolidate failed");
|
||||
}
|
||||
|
|
@ -562,7 +560,7 @@ async function getSeriesMeta(
|
|||
const localUuid = unwrapLocalId(id, "series");
|
||||
// Resolve by our private id only (mcseries-uuid). Also accept legacy mc:series:uuid / bare uuid.
|
||||
// Do NOT look up by imdbId — that pulls the wrong row after TMDB linking.
|
||||
const series = await prisma.series.findFirst({
|
||||
let series = await prisma.series.findFirst({
|
||||
where: {
|
||||
OR: [{ id: localUuid }, { id }],
|
||||
},
|
||||
|
|
@ -586,8 +584,25 @@ async function getSeriesMeta(
|
|||
|
||||
// Fold duplicate series rows into this one so every matched file shows up.
|
||||
try {
|
||||
const merged = await metadata.consolidateSeries(series.id);
|
||||
if (merged > 0) {
|
||||
const { merged, canonicalId } = await metadata.consolidateSeries(series.id);
|
||||
if (canonicalId !== series.id) {
|
||||
const moved = await prisma.series.findFirst({
|
||||
where: { id: canonicalId },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
year: true,
|
||||
overview: true,
|
||||
posterUrl: true,
|
||||
backdropUrl: true,
|
||||
logoUrl: true,
|
||||
genres: true,
|
||||
imdbId: true,
|
||||
tmdbId: true,
|
||||
},
|
||||
});
|
||||
if (moved) series = moved;
|
||||
} else if (merged > 0) {
|
||||
log?.info?.({ seriesId: series.id, merged }, "series meta: consolidated duplicates");
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
@ -807,9 +822,9 @@ async function getStreams(
|
|||
|
||||
const movieDbId = movie.id;
|
||||
try {
|
||||
await metadata.consolidateMovies(movieDbId);
|
||||
const { canonicalId } = await metadata.consolidateMovies(movieDbId);
|
||||
movie = await prisma.movie.findFirst({
|
||||
where: { id: movieDbId },
|
||||
where: { id: canonicalId },
|
||||
include: { mediaFiles: { include: { node: true } } },
|
||||
});
|
||||
} catch (err) {
|
||||
|
|
@ -821,8 +836,8 @@ async function getStreams(
|
|||
void metadata.ensureMovieImdbId(movie.id).catch(() => undefined);
|
||||
}
|
||||
|
||||
const files = movie.mediaFiles.filter((f) => f.available);
|
||||
const pool = files.length > 0 ? files : movie.mediaFiles;
|
||||
// Never fall back to unavailable legacy paths (/media/movies) — those break playback.
|
||||
const pool = movie.mediaFiles.filter((f) => f.available);
|
||||
|
||||
const streams = [];
|
||||
for (const file of pool) {
|
||||
|
|
@ -891,8 +906,8 @@ async function getStreams(
|
|||
}
|
||||
|
||||
try {
|
||||
await metadata.consolidateSeries(series.id);
|
||||
const refreshed = await prisma.series.findUnique({ where: { id: series.id } });
|
||||
const { canonicalId } = await metadata.consolidateSeries(series.id);
|
||||
const refreshed = await prisma.series.findUnique({ where: { id: canonicalId } });
|
||||
if (refreshed) series = refreshed;
|
||||
} catch (err) {
|
||||
log?.warn?.({ err, seriesId: series.id }, "series stream: consolidate failed");
|
||||
|
|
@ -924,8 +939,7 @@ async function getStreams(
|
|||
return [];
|
||||
}
|
||||
|
||||
const files = episode.mediaFiles.filter((f) => f.available);
|
||||
const pool = files.length > 0 ? files : episode.mediaFiles;
|
||||
const pool = episode.mediaFiles.filter((f) => f.available);
|
||||
|
||||
const streams = [];
|
||||
for (const file of pool) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue