Make admin movies/series search-first instead of loading the full library.
Avoids heavy mount queries so lookup stays fast and server load stays low.
This commit is contained in:
parent
14556c82f3
commit
23c81f2b83
3 changed files with 159 additions and 29 deletions
|
|
@ -81,6 +81,9 @@ export default function MoviesPage() {
|
|||
const [nodeId, setNodeId] = useState("");
|
||||
const [search, setSearch] = useState("");
|
||||
const [pathSearch, setPathSearch] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searched, setSearched] = useState(false);
|
||||
const [limitHit, setLimitHit] = useState(false);
|
||||
const [expanded, setExpanded] = useState<string | null>(null);
|
||||
const [msg, setMsg] = useState("");
|
||||
const [busyFileId, setBusyFileId] = useState<string | null>(null);
|
||||
|
|
@ -100,15 +103,31 @@ export default function MoviesPage() {
|
|||
const [folderBusy, setFolderBusy] = useState(false);
|
||||
const [folderMsg, setFolderMsg] = useState("");
|
||||
|
||||
function hasSearchQuery(q = search, pathQ = pathSearch) {
|
||||
return q.trim().length >= 2 || pathQ.trim().length >= 2;
|
||||
}
|
||||
|
||||
function load(q = search, filterNodeId = nodeId, pathQ = pathSearch) {
|
||||
if (!hasSearchQuery(q, pathQ)) {
|
||||
setMovies([]);
|
||||
setSearched(false);
|
||||
setLimitHit(false);
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
if (q.trim()) params.set("search", q.trim());
|
||||
if (filterNodeId) params.set("nodeId", filterNodeId);
|
||||
if (pathQ.trim()) params.set("path", pathQ.trim());
|
||||
const qs = params.toString() ? `?${params}` : "";
|
||||
fetch(`/api/v1/admin/movies${qs}`, { credentials: "include" })
|
||||
setLoading(true);
|
||||
setSearched(true);
|
||||
fetch(`/api/v1/admin/movies?${params}`, { credentials: "include" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => setMovies(d.movies ?? []));
|
||||
.then((d) => {
|
||||
const list = d.movies ?? [];
|
||||
setMovies(list);
|
||||
setLimitHit(Boolean(d.limit && list.length >= d.limit));
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -118,10 +137,10 @@ export default function MoviesPage() {
|
|||
const list = (d.nodes ?? []) as NodeOption[];
|
||||
setNodes(list.filter((n) => n.id).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
});
|
||||
load();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searched) return;
|
||||
load(search, nodeId, pathSearch);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nodeId]);
|
||||
|
|
@ -384,6 +403,11 @@ export default function MoviesPage() {
|
|||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (!hasSearchQuery()) {
|
||||
setMsg("Typ minstens 2 tekens in titel of pad.");
|
||||
return;
|
||||
}
|
||||
setMsg("");
|
||||
load();
|
||||
}}
|
||||
style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap", alignItems: "center" }}
|
||||
|
|
@ -404,8 +428,9 @@ export default function MoviesPage() {
|
|||
<input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Zoek titel..."
|
||||
placeholder="Zoek titel / IMDb…"
|
||||
style={{ width: 180, marginBottom: 0 }}
|
||||
autoFocus
|
||||
/>
|
||||
<input
|
||||
value={pathSearch}
|
||||
|
|
@ -413,12 +438,15 @@ export default function MoviesPage() {
|
|||
placeholder="Zoek map/pad…"
|
||||
style={{ width: 220, marginBottom: 0 }}
|
||||
/>
|
||||
<button type="submit">Zoek</button>
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? "Zoeken…" : "Zoek"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p className="muted page-lead">
|
||||
Serie per ongeluk als film? Open de titel → bij de map{" "}
|
||||
Zoek op titel of pad (min. 2 tekens) — de hele bibliotheek wordt niet meer geladen. Serie per
|
||||
ongeluk als film? Open de titel → bij de map{" "}
|
||||
<strong style={{ color: "#a1a1aa", fontWeight: 600 }}>Als serie herkoppelen</strong>. Vereist
|
||||
node 1.3.7+ met paden (anders eerst upgraden + rescan).
|
||||
</p>
|
||||
|
|
@ -604,9 +632,18 @@ export default function MoviesPage() {
|
|||
|
||||
{movies.length === 0 && (
|
||||
<div className="card" style={{ textAlign: "center", color: "#71717a" }}>
|
||||
Geen movies gevonden
|
||||
{loading
|
||||
? "Zoeken…"
|
||||
: searched
|
||||
? "Geen movies gevonden"
|
||||
: "Typ een titel of pad en klik Zoek"}
|
||||
</div>
|
||||
)}
|
||||
{limitHit && (
|
||||
<p className="muted" style={{ marginTop: "0.75rem", textAlign: "center" }}>
|
||||
Max. 50 resultaten — verfijn je zoekopdracht als je titel er niet tussen staat.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{linkMovieId && (
|
||||
|
|
|
|||
|
|
@ -87,6 +87,9 @@ export default function SeriesPage() {
|
|||
const [nodeId, setNodeId] = useState("");
|
||||
const [search, setSearch] = useState("");
|
||||
const [pathSearch, setPathSearch] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searched, setSearched] = useState(false);
|
||||
const [limitHit, setLimitHit] = useState(false);
|
||||
const [expanded, setExpanded] = useState<string | null>(null);
|
||||
const [busyFileId, setBusyFileId] = useState<string | null>(null);
|
||||
const [msg, setMsg] = useState("");
|
||||
|
|
@ -106,15 +109,31 @@ export default function SeriesPage() {
|
|||
const [folderBusy, setFolderBusy] = useState(false);
|
||||
const [folderMsg, setFolderMsg] = useState("");
|
||||
|
||||
function hasSearchQuery(q = search, pathQ = pathSearch) {
|
||||
return q.trim().length >= 2 || pathQ.trim().length >= 2;
|
||||
}
|
||||
|
||||
function loadSeries(q = search, filterNodeId = nodeId, pathQ = pathSearch) {
|
||||
if (!hasSearchQuery(q, pathQ)) {
|
||||
setSeries([]);
|
||||
setSearched(false);
|
||||
setLimitHit(false);
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
if (q.trim()) params.set("search", q.trim());
|
||||
if (filterNodeId) params.set("nodeId", filterNodeId);
|
||||
if (pathQ.trim()) params.set("path", pathQ.trim());
|
||||
const qs = params.toString() ? `?${params}` : "";
|
||||
fetch(`/api/v1/admin/series${qs}`, { credentials: "include" })
|
||||
setLoading(true);
|
||||
setSearched(true);
|
||||
fetch(`/api/v1/admin/series?${params}`, { credentials: "include" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => setSeries(d.series ?? []));
|
||||
.then((d) => {
|
||||
const list = d.series ?? [];
|
||||
setSeries(list);
|
||||
setLimitHit(Boolean(d.limit && list.length >= d.limit));
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
|
||||
function loadPathFiles(pathQ = pathSearch, filterNodeId = nodeId) {
|
||||
|
|
@ -122,6 +141,10 @@ export default function SeriesPage() {
|
|||
setPathFiles(null);
|
||||
return;
|
||||
}
|
||||
if (!pathQ.trim()) {
|
||||
setPathFiles(null);
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
if (pathQ.trim()) params.set("path", pathQ.trim());
|
||||
if (filterNodeId) params.set("nodeId", filterNodeId);
|
||||
|
|
@ -143,10 +166,10 @@ export default function SeriesPage() {
|
|||
const list = (d.nodes ?? []) as NodeOption[];
|
||||
setNodes(list.filter((n) => n.id).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
});
|
||||
load();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searched) return;
|
||||
load(search, nodeId, pathSearch);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nodeId]);
|
||||
|
|
@ -401,6 +424,11 @@ export default function SeriesPage() {
|
|||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (!hasSearchQuery()) {
|
||||
setMsg("Typ minstens 2 tekens in titel of pad.");
|
||||
return;
|
||||
}
|
||||
setMsg("");
|
||||
load();
|
||||
}}
|
||||
style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap", alignItems: "center" }}
|
||||
|
|
@ -422,8 +450,9 @@ export default function SeriesPage() {
|
|||
<input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Zoek titel..."
|
||||
placeholder="Zoek titel / IMDb…"
|
||||
style={{ width: 180, marginBottom: 0 }}
|
||||
autoFocus
|
||||
/>
|
||||
<input
|
||||
value={pathSearch}
|
||||
|
|
@ -431,17 +460,21 @@ export default function SeriesPage() {
|
|||
placeholder="Zoek map/pad… (bv. Breaking Bad)"
|
||||
style={{ width: 260, marginBottom: 0 }}
|
||||
/>
|
||||
<button type="submit">Zoek</button>
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? "Zoeken…" : "Zoek"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p className="muted page-lead">
|
||||
Per node zie je de show-map. Padzoeken toont ook losse bestanden om foutmatches
|
||||
opnieuw te koppelen.{" "}
|
||||
{nodeId
|
||||
? `Filter: ${selectedNodeName ?? "node"} · ${series.length} series.`
|
||||
: `${series.length} series.`}
|
||||
{pathSearch.trim() ? ` Padfilter: “${pathSearch.trim()}”.` : ""}
|
||||
Zoek op titel of pad (min. 2 tekens) — de hele bibliotheek wordt niet meer geladen. Per node
|
||||
zie je de show-map. Padzoeken toont ook losse bestanden om foutmatches opnieuw te koppelen.
|
||||
{searched
|
||||
? nodeId
|
||||
? ` Filter: ${selectedNodeName ?? "node"} · ${series.length} series.`
|
||||
: ` ${series.length} series.`
|
||||
: ""}
|
||||
{pathSearch.trim() && searched ? ` Padfilter: “${pathSearch.trim()}”.` : ""}
|
||||
</p>
|
||||
|
||||
{msg && (
|
||||
|
|
@ -660,11 +693,20 @@ export default function SeriesPage() {
|
|||
|
||||
{series.length === 0 && (
|
||||
<div className="card" style={{ textAlign: "center", color: "#71717a" }}>
|
||||
{nodeId || pathSearch
|
||||
? "Geen series voor dit filter. Check scan-roots / pad / rescan."
|
||||
: "Geen series gevonden"}
|
||||
{loading
|
||||
? "Zoeken…"
|
||||
: searched
|
||||
? nodeId || pathSearch
|
||||
? "Geen series voor dit filter. Check scan-roots / pad / rescan."
|
||||
: "Geen series gevonden"
|
||||
: "Typ een titel of pad en klik Zoek"}
|
||||
</div>
|
||||
)}
|
||||
{limitHit && (
|
||||
<p className="muted" style={{ marginTop: "0.75rem", textAlign: "center" }}>
|
||||
Max. 50 resultaten — verfijn je zoekopdracht als je titel er niet tussen staat.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{linkSeriesId && (
|
||||
|
|
|
|||
|
|
@ -136,15 +136,38 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
});
|
||||
|
||||
app.get("/api/v1/admin/movies", { preHandler: requireAdmin }, async (request) => {
|
||||
const query = request.query as { search?: string; nodeId?: string; path?: string };
|
||||
const query = request.query as {
|
||||
search?: string;
|
||||
nodeId?: string;
|
||||
path?: string;
|
||||
limit?: string;
|
||||
};
|
||||
const nodeId = query.nodeId?.trim() || undefined;
|
||||
const pathQ = query.path?.trim() || undefined;
|
||||
const searchQ = query.search?.trim() || undefined;
|
||||
const hasQuery =
|
||||
(searchQ != null && searchQ.length >= 2) || (pathQ != null && pathQ.length >= 2);
|
||||
if (!hasQuery) {
|
||||
return { movies: [], total: 0, requiresQuery: true };
|
||||
}
|
||||
|
||||
const limitRaw = Number.parseInt(query.limit ?? "50", 10);
|
||||
const take = Number.isFinite(limitRaw)
|
||||
? Math.min(Math.max(limitRaw, 1), 100)
|
||||
: 50;
|
||||
|
||||
const movies = await prisma.movie.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
searchQ ? { title: { contains: searchQ, mode: "insensitive" } } : {},
|
||||
searchQ
|
||||
? {
|
||||
OR: [
|
||||
{ title: { contains: searchQ, mode: "insensitive" } },
|
||||
{ originalTitle: { contains: searchQ, mode: "insensitive" } },
|
||||
{ imdbId: { contains: searchQ, mode: "insensitive" } },
|
||||
],
|
||||
}
|
||||
: {},
|
||||
nodeId || pathQ
|
||||
? {
|
||||
mediaFiles: {
|
||||
|
|
@ -163,7 +186,7 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
include: { node: { select: { id: true, name: true, status: true } } },
|
||||
},
|
||||
},
|
||||
take: 5000,
|
||||
take,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
@ -228,6 +251,9 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
})),
|
||||
};
|
||||
}),
|
||||
total: movies.length,
|
||||
limit: take,
|
||||
requiresQuery: false,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -244,15 +270,37 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
});
|
||||
|
||||
app.get("/api/v1/admin/series", { preHandler: requireAdmin }, async (request) => {
|
||||
const query = request.query as { search?: string; nodeId?: string; path?: string };
|
||||
const query = request.query as {
|
||||
search?: string;
|
||||
nodeId?: string;
|
||||
path?: string;
|
||||
limit?: string;
|
||||
};
|
||||
const nodeId = query.nodeId?.trim() || undefined;
|
||||
const pathQ = query.path?.trim() || undefined;
|
||||
const searchQ = query.search?.trim() || undefined;
|
||||
const hasQuery =
|
||||
(searchQ != null && searchQ.length >= 2) || (pathQ != null && pathQ.length >= 2);
|
||||
if (!hasQuery) {
|
||||
return { series: [], total: 0, requiresQuery: true };
|
||||
}
|
||||
|
||||
const limitRaw = Number.parseInt(query.limit ?? "50", 10);
|
||||
const take = Number.isFinite(limitRaw)
|
||||
? Math.min(Math.max(limitRaw, 1), 100)
|
||||
: 50;
|
||||
|
||||
const series = await prisma.series.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
searchQ ? { title: { contains: searchQ, mode: "insensitive" } } : {},
|
||||
searchQ
|
||||
? {
|
||||
OR: [
|
||||
{ title: { contains: searchQ, mode: "insensitive" } },
|
||||
{ imdbId: { contains: searchQ, mode: "insensitive" } },
|
||||
],
|
||||
}
|
||||
: {},
|
||||
nodeId || pathQ
|
||||
? {
|
||||
seasons: {
|
||||
|
|
@ -293,7 +341,7 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
orderBy: { seasonNumber: "asc" },
|
||||
},
|
||||
},
|
||||
take: 2000,
|
||||
take,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
@ -397,6 +445,9 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
})).filter((season) => (!nodeId && !pathQ) || season.episodes.length > 0),
|
||||
};
|
||||
}),
|
||||
total: series.length,
|
||||
limit: take,
|
||||
requiresQuery: false,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue