Cut full-sync Postgres load: skip no-op updates, batch lookup, one post-startup sync.

Also cap startup IMDb backfill and bump media-node to 1.3.12 so upgrades no longer match the library twice.
This commit is contained in:
Jos Vooges | STH 2026-09-17 03:07:29 +02:00
parent 7e2dd68da9
commit 34130ab412
5 changed files with 105 additions and 32 deletions

View file

@ -610,13 +610,13 @@ async function main() {
await app.listen({ port: config.PORT, host: config.HOST }); await app.listen({ port: config.PORT, host: config.HOST });
console.log(`Master API listening on ${config.HOST}:${config.PORT}`); console.log(`Master API listening on ${config.HOST}:${config.PORT}`);
// Background: fill ALL missing movie + series imdbIds so Cinemeta can resolve local streams. // Background: klein stukje IMDb backfill na start (niet de hele library — dat hield CPU/TMDB bezig).
// Series catalog/meta stay on mcseries-* (tt only used for streams). // Rest via Admin → metadata tools of Stremio lazy backfill.
if (config.TMDB_API_KEY) { if (config.TMDB_API_KEY) {
const metadata = createMetadataService(config); const metadata = createMetadataService(config);
setTimeout(() => { setTimeout(() => {
void metadata void metadata
.backfillAllImdbIds({ delayMs: 120 }) .backfillAllImdbIds({ delayMs: 200, limit: 40 })
.then((r) => { .then((r) => {
if (r.movies.checked > 0 || r.series.checked > 0) { if (r.movies.checked > 0 || r.series.checked > 0) {
console.log( console.log(

View file

@ -53,10 +53,38 @@ export class LibrarySyncService {
fileIdsSoFar: Set<string>; fileIdsSoFar: Set<string>;
} }
): Promise<void> { ): Promise<void> {
// Eén findMany per batch i.p.v. N findUnique — scheelt enorm bij stabiele library.
const existingRows =
files.length > 0
? await prisma.mediaFile.findMany({
where: {
nodeId,
localFileId: { in: files.map((f) => f.localFileId) },
},
select: {
id: true,
localFileId: true,
movieId: true,
episodeId: true,
shelfId: true,
sizeBytes: true,
path: true,
available: true,
container: true,
resolution: true,
videoCodec: true,
audioCodec: true,
releaseName: true,
},
})
: [];
const existingByLocal = new Map(existingRows.map((r) => [r.localFileId, r]));
for (const file of files) { for (const file of files) {
options.fileIdsSoFar.add(file.localFileId); options.fileIdsSoFar.add(file.localFileId);
try { try {
await this.metadata.matchMediaFile(nodeId, file); const preloaded = existingByLocal.get(file.localFileId) ?? null;
await this.metadata.matchMediaFile(nodeId, file, { existing: preloaded });
} catch (err) { } catch (err) {
console.error( console.error(
`Library full sync: failed to match ${file.releaseName} on node ${nodeId}:`, `Library full sync: failed to match ${file.releaseName} on node ${nodeId}:`,

View file

@ -443,21 +443,67 @@ export class MetadataService {
async matchMediaFile( async matchMediaFile(
nodeId: string, nodeId: string,
file: MediaFileInfo file: MediaFileInfo,
options?: {
/** Vooraf geladen rij (full-sync batch); `null` = bestaat niet, weglaten = zelf ophalen. */
existing?: {
id: string;
movieId: string | null;
episodeId: string | null;
shelfId: string | null;
sizeBytes: bigint;
path: string | null;
available: boolean;
container: string | null;
resolution: string | null;
videoCodec: string | null;
audioCodec: string | null;
releaseName: string | null;
} | null;
}
): Promise<string | null> { ): Promise<string | null> {
// Master herparst het pad (Plex S10E01 Title.mkv e.d.) — betrouwbaarder dan alleen node-parse. // Fast path eerst — geen pad-herparse nodig als het bestand al gekoppeld is.
file = this.enrichParsedFromPath(file); const existing =
options && "existing" in options
// Fast path: already matched → only refresh shelf / availability (no TMDB). ? options.existing ?? null
const existing = await prisma.mediaFile.findUnique({ : await prisma.mediaFile.findUnique({
where: { where: {
nodeId_localFileId: { nodeId, localFileId: file.localFileId }, nodeId_localFileId: { nodeId, localFileId: file.localFileId },
}, },
select: { id: true, movieId: true, episodeId: true, shelfId: true, sizeBytes: true, path: true }, select: {
}); id: true,
movieId: true,
episodeId: true,
shelfId: true,
sizeBytes: true,
path: true,
available: true,
container: true,
resolution: true,
videoCodec: true,
audioCodec: true,
releaseName: true,
},
});
if (existing && (existing.movieId || existing.episodeId)) { if (existing && (existing.movieId || existing.episodeId)) {
const nextShelf = file.shelfId || null; const nextShelf = file.shelfId || null;
const sizeBytes = BigInt(file.sizeBytes); const sizeBytes = BigInt(file.sizeBytes);
const unchanged =
existing.available === true &&
existing.sizeBytes === sizeBytes &&
(existing.shelfId ?? null) === nextShelf &&
(file.path == null || file.path === "" || existing.path === file.path) &&
(file.container == null || existing.container === file.container) &&
(file.resolution == null || existing.resolution === file.resolution) &&
(file.videoCodec == null || existing.videoCodec === file.videoCodec) &&
(file.audioCodec == null || existing.audioCodec === file.audioCodec) &&
(file.releaseName == null || existing.releaseName === file.releaseName);
// Full sync van een stabiele library: geen WRITE-storm op Postgres.
if (unchanged) {
return existing.movieId ?? existing.episodeId;
}
const sizeChanged = existing.sizeBytes !== sizeBytes; const sizeChanged = existing.sizeBytes !== sizeBytes;
const discoveredAt = sizeChanged ? new Date() : undefined; const discoveredAt = sizeChanged ? new Date() : undefined;
await prisma.mediaFile.update({ await prisma.mediaFile.update({
@ -486,6 +532,9 @@ export class MetadataService {
return existing.movieId ?? existing.episodeId; return existing.movieId ?? existing.episodeId;
} }
// Alleen herparsen voor nieuwe / unmatched files.
file = this.enrichParsedFromPath(file);
if (file.mediaType === "movie" && file.parsedMovie) { if (file.mediaType === "movie" && file.parsedMovie) {
return this.matchMovie(nodeId, file); return this.matchMovie(nodeId, file);
} }
@ -2604,7 +2653,8 @@ export class MetadataService {
/** Alleen series met TMDB of IMDb — orphan titel-records tellen niet als match. */ /** Alleen series met TMDB of IMDb — orphan titel-records tellen niet als match. */
private async findExistingCanonicalSeries(title: string) { private async findExistingCanonicalSeries(title: string) {
const cleaned = stripTrailingSeasonLabel(title.trim()); const cleaned = stripTrailingSeasonLabel(title.trim());
const direct = await prisma.series.findFirst({ if (!cleaned) return null;
return prisma.series.findFirst({
where: { where: {
title: { equals: cleaned, mode: "insensitive" }, title: { equals: cleaned, mode: "insensitive" },
OR: [{ tmdbId: { not: null } }, { imdbId: { not: null } }], OR: [{ tmdbId: { not: null } }, { imdbId: { not: null } }],
@ -2615,20 +2665,6 @@ export class MetadataService {
{ createdAt: "asc" }, { createdAt: "asc" },
], ],
}); });
if (direct) return direct;
const key = normalizeSeriesKey(cleaned);
if (!key) return null;
const candidates = await prisma.series.findMany({
where: { OR: [{ tmdbId: { not: null } }, { imdbId: { not: null } }] },
orderBy: [
{ imdbId: { sort: "desc", nulls: "last" } },
{ tmdbId: { sort: "desc", nulls: "last" } },
{ createdAt: "asc" },
],
take: 5000,
});
return candidates.find((s) => normalizeSeriesKey(s.title) === key) ?? null;
} }
/** OMDb titel-match voor scanner (na TMDB). Alleen bij confident title match. */ /** OMDb titel-match voor scanner (na TMDB). Alleen bij confident title match. */

View file

@ -24,7 +24,7 @@ import (
"github.com/sthmedia/media-node/internal/streaming" "github.com/sthmedia/media-node/internal/streaming"
) )
var version = "1.3.11" var version = "1.3.12"
func main() { func main() {
if len(os.Args) < 2 { if len(os.Args) < 2 {

View file

@ -183,8 +183,17 @@ func mediaRootsEqual(a, b []config.MediaRoot) bool {
func (s *Scanner) runStartupScan() { func (s *Scanner) runStartupScan() {
log.Println("Startup library inventory scan...") log.Println("Startup library inventory scan...")
s.FullScan() // Geen onScanComplete-sync hier: HELLO wacht op startupScanDone en pusht
// precies één full sync. Anders matcht master de hele library twee keer.
s.mu.Lock() s.mu.Lock()
hook := s.onScanComplete
s.onScanComplete = nil
s.mu.Unlock()
s.FullScan()
s.mu.Lock()
s.onScanComplete = hook
s.startupScanDone = true s.startupScanDone = true
s.mu.Unlock() s.mu.Unlock()
} }