Add bulk relink of a series folder to the correct TMDB/IMDb show.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a6b0ec15f2
commit
fba94b656c
3 changed files with 515 additions and 3 deletions
|
|
@ -95,6 +95,16 @@ export default function SeriesPage() {
|
||||||
const [linkHits, setLinkHits] = useState<MetaHit[]>([]);
|
const [linkHits, setLinkHits] = useState<MetaHit[]>([]);
|
||||||
const [linkBusy, setLinkBusy] = useState(false);
|
const [linkBusy, setLinkBusy] = useState(false);
|
||||||
const [linkMsg, setLinkMsg] = useState("");
|
const [linkMsg, setLinkMsg] = useState("");
|
||||||
|
const [folderRelink, setFolderRelink] = useState<{
|
||||||
|
nodeId: string;
|
||||||
|
nodeName: string;
|
||||||
|
folder: string;
|
||||||
|
fileCount: number;
|
||||||
|
} | null>(null);
|
||||||
|
const [folderQuery, setFolderQuery] = useState("");
|
||||||
|
const [folderHits, setFolderHits] = useState<MetaHit[]>([]);
|
||||||
|
const [folderBusy, setFolderBusy] = useState(false);
|
||||||
|
const [folderMsg, setFolderMsg] = useState("");
|
||||||
|
|
||||||
function loadSeries(q = search, filterNodeId = nodeId, pathQ = pathSearch) {
|
function loadSeries(q = search, filterNodeId = nodeId, pathQ = pathSearch) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
@ -172,6 +182,126 @@ export default function SeriesPage() {
|
||||||
void searchMeta(s.title);
|
void searchMeta(s.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openFolderRelink(opts: {
|
||||||
|
nodeId: string;
|
||||||
|
nodeName: string;
|
||||||
|
folder: string;
|
||||||
|
fileCount: number;
|
||||||
|
}) {
|
||||||
|
const folderName = opts.folder.split("/").filter(Boolean).pop() || opts.folder;
|
||||||
|
setFolderRelink(opts);
|
||||||
|
setFolderQuery(folderName);
|
||||||
|
setFolderHits([]);
|
||||||
|
setFolderMsg("");
|
||||||
|
void searchFolderMeta(folderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchFolderMeta(q = folderQuery) {
|
||||||
|
const query = q.trim();
|
||||||
|
if (!query) return;
|
||||||
|
setFolderBusy(true);
|
||||||
|
setFolderMsg("");
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/v1/admin/metadata/search?type=series&q=${encodeURIComponent(query)}`,
|
||||||
|
{ credentials: "include" }
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
setFolderMsg(data.error?.message ?? "Zoeken mislukt");
|
||||||
|
setFolderHits([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFolderHits(data.results ?? []);
|
||||||
|
if (!(data.results ?? []).length) setFolderMsg("Geen resultaten");
|
||||||
|
} finally {
|
||||||
|
setFolderBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyFolderRelink(hit: MetaHit) {
|
||||||
|
if (!folderRelink) return;
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Alle ${folderRelink.fileCount} bestanden in\n${folderRelink.folder}\nop ${folderRelink.nodeName}\nherkoppelen naar “${hit.title}${hit.year ? ` (${hit.year})` : ""}”?`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFolderBusy(true);
|
||||||
|
setFolderMsg("Hele map herkoppelen…");
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/v1/admin/library/relink-folder`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
nodeId: folderRelink.nodeId,
|
||||||
|
folder: folderRelink.folder,
|
||||||
|
tmdbId: hit.tmdbId,
|
||||||
|
...(hit.imdbId ? { imdbId: hit.imdbId } : {}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
setFolderMsg(data.error?.message ?? "Herkoppelen mislukt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setMsg(
|
||||||
|
`Map herkoppeld → ${data.seriesTitle}: ${data.moved}/${data.total} bestanden` +
|
||||||
|
(data.failed ? ` (${data.failed} mislukt)` : "")
|
||||||
|
);
|
||||||
|
setFolderRelink(null);
|
||||||
|
setExpanded(data.seriesId ?? expanded);
|
||||||
|
load();
|
||||||
|
} finally {
|
||||||
|
setFolderBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyFolderImdbDirect() {
|
||||||
|
const imdb = folderQuery.trim();
|
||||||
|
if (!/^tt\d+$/i.test(imdb) || !folderRelink) {
|
||||||
|
setFolderMsg("Vul een IMDb-id in (bijv. tt14424422) of zoek eerst op titel.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Alle ${folderRelink.fileCount} bestanden in\n${folderRelink.folder}\nherkoppelen via ${imdb}?`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFolderBusy(true);
|
||||||
|
setFolderMsg("Hele map herkoppelen via IMDb…");
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/v1/admin/library/relink-folder`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
nodeId: folderRelink.nodeId,
|
||||||
|
folder: folderRelink.folder,
|
||||||
|
imdbId: imdb,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
setFolderMsg(data.error?.message ?? "Herkoppelen mislukt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setMsg(
|
||||||
|
`Map herkoppeld → ${data.seriesTitle}: ${data.moved}/${data.total} bestanden` +
|
||||||
|
(data.failed ? ` (${data.failed} mislukt)` : "")
|
||||||
|
);
|
||||||
|
setFolderRelink(null);
|
||||||
|
setExpanded(data.seriesId ?? expanded);
|
||||||
|
load();
|
||||||
|
} finally {
|
||||||
|
setFolderBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function searchMeta(q = linkQuery) {
|
async function searchMeta(q = linkQuery) {
|
||||||
const query = q.trim();
|
const query = q.trim();
|
||||||
if (!query) return;
|
if (!query) return;
|
||||||
|
|
@ -418,9 +548,35 @@ export default function SeriesPage() {
|
||||||
{(n.folders?.length ?? 0) > 0 ? (
|
{(n.folders?.length ?? 0) > 0 ? (
|
||||||
<div className="muted mono tiny" style={{ marginTop: 4, paddingLeft: 4 }}>
|
<div className="muted mono tiny" style={{ marginTop: 4, paddingLeft: 4 }}>
|
||||||
{n.folders!.map((f) => (
|
{n.folders!.map((f) => (
|
||||||
<div key={f.folder}>
|
<div
|
||||||
{f.folder}{" "}
|
key={f.folder}
|
||||||
<span style={{ color: "#52525b" }}>({f.fileCount})</span>
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 8,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{f.folder}{" "}
|
||||||
|
<span style={{ color: "#52525b" }}>({f.fileCount})</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style={{ fontSize: "0.7rem", padding: "2px 8px" }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openFolderRelink({
|
||||||
|
nodeId: n.id,
|
||||||
|
nodeName: n.name,
|
||||||
|
folder: f.folder,
|
||||||
|
fileCount: f.fileCount,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Hele map herkoppelen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -637,6 +793,141 @@ export default function SeriesPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{folderRelink && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
inset: 0,
|
||||||
|
background: "rgba(0,0,0,0.65)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
zIndex: 50,
|
||||||
|
padding: "1rem",
|
||||||
|
}}
|
||||||
|
onClick={() => !folderBusy && setFolderRelink(null)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="card"
|
||||||
|
style={{ width: "min(640px, 100%)", maxHeight: "90vh", overflow: "auto", margin: 0 }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<h2 style={{ fontSize: "1.1rem", color: "#fafafa", textTransform: "none", marginBottom: "0.5rem" }}>
|
||||||
|
Hele map herkoppelen
|
||||||
|
</h2>
|
||||||
|
<p style={{ color: "#a1a1aa", fontSize: "0.85rem", marginBottom: "1rem" }}>
|
||||||
|
Node: <strong style={{ color: "#e4e4e7" }}>{folderRelink.nodeName}</strong>
|
||||||
|
<br />
|
||||||
|
Map: <span className="mono">{folderRelink.folder}</span>
|
||||||
|
<br />
|
||||||
|
{folderRelink.fileCount} bestanden — kies de juiste serie (niet de verkeerde TMDB-hit).
|
||||||
|
Seizoen/aflevering blijft uit het pad.
|
||||||
|
</p>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
void searchFolderMeta();
|
||||||
|
}}
|
||||||
|
style={{ display: "flex", gap: "0.5rem", marginBottom: "0.75rem", flexWrap: "wrap" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
value={folderQuery}
|
||||||
|
onChange={(e) => setFolderQuery(e.target.value)}
|
||||||
|
placeholder="Titel of IMDb-id (tt…)"
|
||||||
|
style={{ flex: 1, minWidth: 200, marginBottom: 0 }}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<button type="submit" disabled={folderBusy}>
|
||||||
|
Zoek
|
||||||
|
</button>
|
||||||
|
<button type="button" disabled={folderBusy} onClick={() => void applyFolderImdbDirect()}>
|
||||||
|
Koppel IMDb-id
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{folderMsg && (
|
||||||
|
<p style={{ color: "#a1a1aa", fontSize: "0.85rem", marginBottom: "0.75rem" }}>{folderMsg}</p>
|
||||||
|
)}
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
|
||||||
|
{folderHits.map((hit) => (
|
||||||
|
<button
|
||||||
|
key={hit.tmdbId}
|
||||||
|
type="button"
|
||||||
|
disabled={folderBusy}
|
||||||
|
onClick={() => void applyFolderRelink(hit)}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "0.75rem",
|
||||||
|
textAlign: "left",
|
||||||
|
background: "#0f1117",
|
||||||
|
border: "1px solid #2a2d3a",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: "0.75rem",
|
||||||
|
cursor: "pointer",
|
||||||
|
color: "#fafafa",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hit.posterUrl ? (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img
|
||||||
|
src={hit.posterUrl}
|
||||||
|
alt=""
|
||||||
|
width={48}
|
||||||
|
height={72}
|
||||||
|
style={{ objectFit: "cover", borderRadius: 4, flexShrink: 0 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 48,
|
||||||
|
height: 72,
|
||||||
|
background: "#1f2937",
|
||||||
|
borderRadius: 4,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<strong>{hit.title}</strong>
|
||||||
|
{hit.year ? ` (${hit.year})` : ""}
|
||||||
|
</div>
|
||||||
|
<div style={{ color: "#71717a", fontSize: "0.8rem" }}>
|
||||||
|
TMDB {hit.tmdbId}
|
||||||
|
{hit.imdbId ? ` · ${hit.imdbId}` : ""}
|
||||||
|
</div>
|
||||||
|
{hit.overview && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: "#a1a1aa",
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
marginTop: 4,
|
||||||
|
display: "-webkit-box",
|
||||||
|
WebkitLineClamp: 2,
|
||||||
|
WebkitBoxOrient: "vertical",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hit.overview}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: "1rem", display: "flex", justifyContent: "flex-end" }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="danger"
|
||||||
|
disabled={folderBusy}
|
||||||
|
onClick={() => setFolderRelink(null)}
|
||||||
|
>
|
||||||
|
Annuleren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1056,6 +1056,37 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hele host-map op één node herkoppelen aan een TMDB/IMDb-serie.
|
||||||
|
* Body: { nodeId, folder, tmdbId? | imdbId? }
|
||||||
|
*/
|
||||||
|
app.post("/api/v1/admin/library/relink-folder", { preHandler: requireAdmin }, async (request) => {
|
||||||
|
const body = request.body as {
|
||||||
|
nodeId?: string;
|
||||||
|
folder?: string;
|
||||||
|
tmdbId?: number;
|
||||||
|
imdbId?: string;
|
||||||
|
};
|
||||||
|
if (!body.nodeId?.trim() || !body.folder?.trim()) {
|
||||||
|
throw new AppError("INVALID_REQUEST", "nodeId en folder verplicht", 400);
|
||||||
|
}
|
||||||
|
if (body.tmdbId == null && !body.imdbId?.trim()) {
|
||||||
|
throw new AppError("INVALID_REQUEST", "tmdbId of imdbId verplicht", 400);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await metadata.relinkFolderToSeries({
|
||||||
|
nodeId: body.nodeId.trim(),
|
||||||
|
folder: body.folder.trim(),
|
||||||
|
tmdbId: body.tmdbId,
|
||||||
|
imdbId: body.imdbId?.trim(),
|
||||||
|
});
|
||||||
|
return { ok: true, ...result };
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : "Herkoppelen mislukt";
|
||||||
|
throw new AppError("RELINK_FAILED", message, 400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/api/v1/admin/library/shelves", { preHandler: requireAdmin }, async () => {
|
app.get("/api/v1/admin/library/shelves", { preHandler: requireAdmin }, async () => {
|
||||||
const { ensureDefaultShelves } = await import("../library/shelves");
|
const { ensureDefaultShelves } = await import("../library/shelves");
|
||||||
await ensureDefaultShelves();
|
await ensureDefaultShelves();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,17 @@
|
||||||
import { prisma } from "../database/client";
|
import { prisma } from "../database/client";
|
||||||
import type { MediaFileInfo } from "@media-cluster/shared-types";
|
import type { MediaFileInfo } from "@media-cluster/shared-types";
|
||||||
|
import { parseSeriesFromPath } from "./filename-parser";
|
||||||
|
|
||||||
|
function normalizeFolderPrefix(folder: string): string {
|
||||||
|
return folder.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathUnderFolder(filePath: string | null | undefined, folder: string): boolean {
|
||||||
|
if (!filePath) return false;
|
||||||
|
const p = filePath.replace(/\\/g, "/");
|
||||||
|
const f = normalizeFolderPrefix(folder);
|
||||||
|
return p === f || p.startsWith(`${f}/`);
|
||||||
|
}
|
||||||
|
|
||||||
interface TmdbSearchResult {
|
interface TmdbSearchResult {
|
||||||
results: Array<{
|
results: Array<{
|
||||||
|
|
@ -1502,6 +1514,184 @@ export class MetadataService {
|
||||||
return { seriesId: target.id, merged };
|
return { seriesId: target.id, merged };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin: alle media-bestanden onder een host-map op één node herkoppelen
|
||||||
|
* aan een gekozen TMDB/IMDb-serie. S/E komt uit pad/bestandsnaam.
|
||||||
|
*/
|
||||||
|
async relinkFolderToSeries(opts: {
|
||||||
|
nodeId: string;
|
||||||
|
folder: string;
|
||||||
|
tmdbId?: number;
|
||||||
|
imdbId?: string;
|
||||||
|
}): Promise<{
|
||||||
|
seriesId: string;
|
||||||
|
seriesTitle: string;
|
||||||
|
moved: number;
|
||||||
|
failed: number;
|
||||||
|
total: number;
|
||||||
|
}> {
|
||||||
|
if (!this.tmdbApiKey) throw new Error("TMDB_API_KEY niet geconfigureerd");
|
||||||
|
const folder = normalizeFolderPrefix(opts.folder);
|
||||||
|
if (!folder) throw new Error("Map-pad is verplicht");
|
||||||
|
|
||||||
|
let tmdbId = opts.tmdbId;
|
||||||
|
if (!tmdbId && opts.imdbId) {
|
||||||
|
const hit = await this.findTmdbByImdb("series", opts.imdbId.trim());
|
||||||
|
tmdbId = hit?.tmdbId;
|
||||||
|
}
|
||||||
|
if (!tmdbId) throw new Error("Geef een TMDB-id of IMDb-id (tt…) op");
|
||||||
|
|
||||||
|
const meta = await this.getTmdbSeriesDetails(tmdbId);
|
||||||
|
if (!meta) throw new Error("TMDB-serie niet gevonden");
|
||||||
|
|
||||||
|
const extras = await this.getStremioMetaExtras(
|
||||||
|
"series",
|
||||||
|
meta.id,
|
||||||
|
meta.imdbId,
|
||||||
|
meta.logoUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
let series =
|
||||||
|
(await prisma.series.findFirst({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ tmdbId: meta.id },
|
||||||
|
...(meta.imdbId ? [{ imdbId: meta.imdbId }] : []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})) ??
|
||||||
|
(await prisma.series.create({
|
||||||
|
data: {
|
||||||
|
title: meta.title,
|
||||||
|
year: meta.year ?? null,
|
||||||
|
overview: meta.overview ?? null,
|
||||||
|
posterUrl: meta.posterUrl ?? null,
|
||||||
|
backdropUrl: meta.backdropUrl ?? null,
|
||||||
|
logoUrl: meta.logoUrl ?? null,
|
||||||
|
tmdbId: meta.id,
|
||||||
|
...(meta.imdbId ? { imdbId: meta.imdbId } : {}),
|
||||||
|
...(extras.genres?.length ? { genres: extras.genres } : {}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
series = await prisma.series.update({
|
||||||
|
where: { id: series.id },
|
||||||
|
data: {
|
||||||
|
title: meta.title,
|
||||||
|
year: meta.year ?? null,
|
||||||
|
overview: meta.overview ?? null,
|
||||||
|
posterUrl: meta.posterUrl ?? null,
|
||||||
|
backdropUrl: meta.backdropUrl ?? null,
|
||||||
|
logoUrl: meta.logoUrl ?? null,
|
||||||
|
tmdbId: meta.id,
|
||||||
|
...(meta.imdbId ? { imdbId: meta.imdbId } : {}),
|
||||||
|
...(extras.genres?.length ? { genres: extras.genres } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = await prisma.mediaFile.findMany({
|
||||||
|
where: {
|
||||||
|
nodeId: opts.nodeId,
|
||||||
|
path: { not: null },
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
path: true,
|
||||||
|
localFileId: true,
|
||||||
|
sizeBytes: true,
|
||||||
|
container: true,
|
||||||
|
resolution: true,
|
||||||
|
videoCodec: true,
|
||||||
|
audioCodec: true,
|
||||||
|
releaseName: true,
|
||||||
|
lastSeen: true,
|
||||||
|
shelfId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const underFolder = files.filter((f) => pathUnderFolder(f.path, folder));
|
||||||
|
let moved = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (const file of underFolder) {
|
||||||
|
const sourcePath = file.path!.trim();
|
||||||
|
const parsed = parseSeriesFromPath(sourcePath);
|
||||||
|
if (!parsed?.season || !parsed?.episode) {
|
||||||
|
failed += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let season = await prisma.season.findUnique({
|
||||||
|
where: {
|
||||||
|
seriesId_seasonNumber: {
|
||||||
|
seriesId: series.id,
|
||||||
|
seasonNumber: parsed.season,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!season) {
|
||||||
|
season = await prisma.season.create({
|
||||||
|
data: {
|
||||||
|
seriesId: series.id,
|
||||||
|
seasonNumber: parsed.season,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let episode = await prisma.episode.findUnique({
|
||||||
|
where: {
|
||||||
|
seriesId_seasonNumber_episodeNumber: {
|
||||||
|
seriesId: series.id,
|
||||||
|
seasonNumber: parsed.season,
|
||||||
|
episodeNumber: parsed.episode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!episode) {
|
||||||
|
episode = await prisma.episode.create({
|
||||||
|
data: {
|
||||||
|
seriesId: series.id,
|
||||||
|
seasonId: season.id,
|
||||||
|
seasonNumber: parsed.season,
|
||||||
|
episodeNumber: parsed.episode,
|
||||||
|
title: `Episode ${parsed.episode}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (series.tmdbId && this.tmdbApiKey) {
|
||||||
|
episode = await this.applyTmdbEpisodeMeta(series.tmdbId, episode);
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.mediaFile.update({
|
||||||
|
where: { id: file.id },
|
||||||
|
data: {
|
||||||
|
movieId: null,
|
||||||
|
episodeId: episode.id,
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
moved += 1;
|
||||||
|
} catch {
|
||||||
|
failed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refreshedSeriesIds.delete(series.id);
|
||||||
|
await this.refreshSeriesNl(series.id, meta.id).catch(() => undefined);
|
||||||
|
await this.ensureSeriesArtwork(series.id).catch(() => undefined);
|
||||||
|
await this.enrichSeriesEpisodes(series.id).catch(() => undefined);
|
||||||
|
|
||||||
|
return {
|
||||||
|
seriesId: series.id,
|
||||||
|
seriesTitle: series.title,
|
||||||
|
moved,
|
||||||
|
failed,
|
||||||
|
total: underFolder.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async findTmdbByImdb(
|
private async findTmdbByImdb(
|
||||||
type: "movie" | "series",
|
type: "movie" | "series",
|
||||||
imdbId: string
|
imdbId: string
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue