Add per-node library reset that purges DB content and rescans.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3b9b95469b
commit
308160aa56
2 changed files with 168 additions and 0 deletions
|
|
@ -154,6 +154,35 @@ export default function NodesPage() {
|
|||
alert("Rescan aangevraagd");
|
||||
}
|
||||
|
||||
async function resetLibraryNode(id: string, name: string) {
|
||||
if (
|
||||
!confirm(
|
||||
`Bibliotheek van “${name}” volledig wissen uit de database en opnieuw scannen?\n\n` +
|
||||
`Alle films/series-bestanden van deze node verdwijnen uit Master. ` +
|
||||
`Titels die alleen op deze node stonden worden opgeruimd. Daarna start een volle rescan.\n\n` +
|
||||
`Dit kan niet ongedaan worden gemaakt.`
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Laatste check: echt resetten voor “${name}”?`)) return;
|
||||
|
||||
const res = await fetch(`/api/v1/admin/nodes/${id}/reset-library`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
alert(data.error?.message ?? "Reset mislukt");
|
||||
return;
|
||||
}
|
||||
alert(
|
||||
data.message ??
|
||||
`Klaar: ${data.filesRemoved ?? 0} files, ${data.moviesRemoved ?? 0} films, ${data.seriesRemoved ?? 0} series verwijderd. Rescan gestart.`
|
||||
);
|
||||
loadNodes();
|
||||
}
|
||||
|
||||
async function restartNode(id: string) {
|
||||
if (!confirm("Node herstarten? De container komt terug via restart-policy.")) return;
|
||||
const res = await fetch(`/api/v1/admin/nodes/${id}/restart`, {
|
||||
|
|
@ -554,6 +583,13 @@ export default function NodesPage() {
|
|||
<td style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
||||
<button onClick={() => openEdit(node)}>Edit</button>
|
||||
<button onClick={() => rescanNode(node.id)}>Rescan</button>
|
||||
<button
|
||||
className="danger"
|
||||
onClick={() => resetLibraryNode(node.id, node.name)}
|
||||
title="Wis alle content van deze node uit de DB en scan opnieuw"
|
||||
>
|
||||
Bibliotheek reset
|
||||
</button>
|
||||
{node.version !== targetNodeVersion && (
|
||||
<button
|
||||
onClick={() => upgradeNode(node.id, node.name)}
|
||||
|
|
|
|||
|
|
@ -366,6 +366,138 @@ export async function registerNodeRoutes(app: FastifyInstance) {
|
|||
return { ok: true };
|
||||
});
|
||||
|
||||
/**
|
||||
* Wis alle media van deze node uit de master-DB, ruim wezen-films/series op,
|
||||
* reset revisie en start een volledige rescan (nieuwe match-logica).
|
||||
*/
|
||||
app.post("/api/v1/admin/nodes/:id/reset-library", { preHandler: requireAdmin }, async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const node = await prisma.node.findUnique({ where: { id } });
|
||||
if (!node) throw new AppError("NOT_FOUND", "Node not found", 404);
|
||||
|
||||
const { nodeConnectionManager } = await import("../websocket/manager");
|
||||
if (!nodeConnectionManager.isOnline(id)) {
|
||||
throw new AppError("NODE_OFFLINE", "Node is niet verbonden — start de node eerst", 503);
|
||||
}
|
||||
|
||||
const files = await prisma.mediaFile.findMany({
|
||||
where: { nodeId: id },
|
||||
select: { id: true, movieId: true, episodeId: true },
|
||||
});
|
||||
const fileIds = files.map((f) => f.id);
|
||||
const touchedMovieIds = [...new Set(files.map((f) => f.movieId).filter(Boolean))] as string[];
|
||||
const touchedEpisodeIds = [...new Set(files.map((f) => f.episodeId).filter(Boolean))] as string[];
|
||||
|
||||
// Playback sessions + media files
|
||||
const chunkSize = 500;
|
||||
for (let i = 0; i < fileIds.length; i += chunkSize) {
|
||||
const chunk = fileIds.slice(i, i + chunkSize);
|
||||
await prisma.playbackSession.deleteMany({ where: { mediaFileId: { in: chunk } } });
|
||||
await prisma.mediaFile.deleteMany({ where: { id: { in: chunk } } });
|
||||
}
|
||||
|
||||
// Movies die nergens meer bestanden hebben
|
||||
let moviesRemoved = 0;
|
||||
if (touchedMovieIds.length > 0) {
|
||||
for (const movieId of touchedMovieIds) {
|
||||
const left = await prisma.mediaFile.count({ where: { movieId } });
|
||||
if (left > 0) continue;
|
||||
await prisma.libraryMatch.deleteMany({ where: { movieId } });
|
||||
await prisma.watchProgress.deleteMany({ where: { movieId } });
|
||||
await prisma.viewerFavorite.deleteMany({
|
||||
where: { mediaType: "MOVIE", mediaId: movieId },
|
||||
});
|
||||
await prisma.librarySubtitle.deleteMany({ where: { movieId } });
|
||||
await prisma.movie.delete({ where: { id: movieId } }).catch(() => undefined);
|
||||
moviesRemoved += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Episodes / series opruimen
|
||||
let seriesRemoved = 0;
|
||||
const seriesIds = new Set<string>();
|
||||
if (touchedEpisodeIds.length > 0) {
|
||||
const eps = await prisma.episode.findMany({
|
||||
where: { id: { in: touchedEpisodeIds } },
|
||||
select: { id: true, seriesId: true },
|
||||
});
|
||||
for (const ep of eps) seriesIds.add(ep.seriesId);
|
||||
|
||||
for (const episodeId of touchedEpisodeIds) {
|
||||
const left = await prisma.mediaFile.count({ where: { episodeId } });
|
||||
if (left > 0) continue;
|
||||
await prisma.libraryMatch.deleteMany({ where: { episodeId } });
|
||||
await prisma.watchProgress.deleteMany({ where: { episodeId } });
|
||||
await prisma.episode.delete({ where: { id: episodeId } }).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
for (const seriesId of seriesIds) {
|
||||
const leftFiles = await prisma.mediaFile.count({
|
||||
where: { episode: { seriesId } },
|
||||
});
|
||||
if (leftFiles > 0) continue;
|
||||
await prisma.libraryMatch.deleteMany({ where: { seriesId } });
|
||||
await prisma.watchProgress.deleteMany({ where: { seriesId } });
|
||||
await prisma.viewerFavorite.deleteMany({
|
||||
where: { mediaType: "SERIES", mediaId: seriesId },
|
||||
});
|
||||
await prisma.episode.deleteMany({ where: { seriesId } });
|
||||
await prisma.season.deleteMany({ where: { seriesId } });
|
||||
await prisma.series.delete({ where: { id: seriesId } }).catch(() => undefined);
|
||||
seriesRemoved += 1;
|
||||
}
|
||||
|
||||
// Ook films/series die alleen op deze node stonden maar via andere refs — wezen zonder files
|
||||
const orphanMovies = await prisma.movie.findMany({
|
||||
where: { mediaFiles: { none: {} } },
|
||||
select: { id: true },
|
||||
take: 2000,
|
||||
});
|
||||
for (const m of orphanMovies) {
|
||||
await prisma.libraryMatch.deleteMany({ where: { movieId: m.id } });
|
||||
await prisma.watchProgress.deleteMany({ where: { movieId: m.id } });
|
||||
await prisma.movie.delete({ where: { id: m.id } }).catch(() => undefined);
|
||||
moviesRemoved += 1;
|
||||
}
|
||||
|
||||
const orphanSeries = await prisma.series.findMany({
|
||||
where: { episodes: { none: { mediaFiles: { some: {} } } } },
|
||||
select: { id: true },
|
||||
take: 2000,
|
||||
});
|
||||
for (const s of orphanSeries) {
|
||||
await prisma.libraryMatch.deleteMany({ where: { seriesId: s.id } });
|
||||
await prisma.watchProgress.deleteMany({ where: { seriesId: s.id } });
|
||||
await prisma.episode.deleteMany({ where: { seriesId: s.id } });
|
||||
await prisma.season.deleteMany({ where: { seriesId: s.id } });
|
||||
await prisma.series.delete({ where: { id: s.id } }).catch(() => undefined);
|
||||
seriesRemoved += 1;
|
||||
}
|
||||
|
||||
await prisma.node.update({
|
||||
where: { id },
|
||||
data: { lastRevision: 0, libraryFileCount: 0 },
|
||||
});
|
||||
|
||||
const sent = nodeConnectionManager.sendRescan(id, true);
|
||||
if (!sent) {
|
||||
throw new AppError(
|
||||
"RESCAN_FAILED",
|
||||
"Database gewist, maar rescan kon niet worden gestuurd (node offline?)",
|
||||
503
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
filesRemoved: fileIds.length,
|
||||
moviesRemoved,
|
||||
seriesRemoved,
|
||||
message: `Bibliotheek van ${node.name} gewist (${fileIds.length} files). Volledige rescan gestart.`,
|
||||
};
|
||||
});
|
||||
|
||||
app.post("/api/v1/admin/nodes/:id/restart", { preHandler: requireAdmin }, async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const node = await prisma.node.findUnique({ where: { id } });
|
||||
|
|
|
|||
Loading…
Reference in a new issue