Queue unmatched series folders for manual linking instead of auto-creating.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ce278bbb70
commit
3b9b95469b
4 changed files with 478 additions and 19 deletions
318
apps/admin-ui/src/app/pending/page.tsx
Normal file
318
apps/admin-ui/src/app/pending/page.tsx
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Nav, useAuth } from "@/components/Nav";
|
||||
|
||||
interface PendingFolder {
|
||||
key: string;
|
||||
nodeId: string;
|
||||
nodeName: string;
|
||||
folder: string;
|
||||
parsedTitle: string | null;
|
||||
fileCount: number;
|
||||
seasons: number[];
|
||||
samplePaths: string[];
|
||||
}
|
||||
|
||||
interface MetaHit {
|
||||
tmdbId: number;
|
||||
title: string;
|
||||
year: number | null;
|
||||
overview: string | null;
|
||||
posterUrl: string | null;
|
||||
imdbId: string | null;
|
||||
}
|
||||
|
||||
export default function PendingSeriesPage() {
|
||||
useAuth();
|
||||
const [pending, setPending] = useState<PendingFolder[]>([]);
|
||||
const [totalFiles, setTotalFiles] = useState(0);
|
||||
const [msg, setMsg] = useState("");
|
||||
const [link, setLink] = useState<PendingFolder | null>(null);
|
||||
const [query, setQuery] = useState("");
|
||||
const [hits, setHits] = useState<MetaHit[]>([]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [linkMsg, setLinkMsg] = useState("");
|
||||
|
||||
function load() {
|
||||
fetch("/api/v1/admin/library/pending-series", { credentials: "include" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
setPending(d.pending ?? []);
|
||||
setTotalFiles(d.totalFiles ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, []);
|
||||
|
||||
function openLink(row: PendingFolder) {
|
||||
setLink(row);
|
||||
setQuery(row.parsedTitle || row.folder.split("/").filter(Boolean).pop() || "");
|
||||
setHits([]);
|
||||
setLinkMsg("");
|
||||
void searchMeta(row.parsedTitle || row.folder.split("/").filter(Boolean).pop() || "");
|
||||
}
|
||||
|
||||
async function searchMeta(q = query) {
|
||||
const term = q.trim();
|
||||
if (!term) return;
|
||||
setBusy(true);
|
||||
setLinkMsg("");
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/v1/admin/metadata/search?type=series&q=${encodeURIComponent(term)}`,
|
||||
{ credentials: "include" }
|
||||
);
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
setLinkMsg(data.error?.message ?? "Zoeken mislukt");
|
||||
setHits([]);
|
||||
return;
|
||||
}
|
||||
setHits(data.results ?? []);
|
||||
if (!(data.results ?? []).length) setLinkMsg("Geen resultaten");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function applyHit(hit: MetaHit) {
|
||||
if (!link) return;
|
||||
if (
|
||||
!confirm(
|
||||
`${link.fileCount} bestanden in\n${link.folder}\n(${link.nodeName})\nkoppelen aan “${hit.title}${hit.year ? ` (${hit.year})` : ""}”?`
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setLinkMsg("Koppelen…");
|
||||
try {
|
||||
const res = await fetch("/api/v1/admin/library/relink-folder", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
nodeId: link.nodeId,
|
||||
folder: link.folder,
|
||||
tmdbId: hit.tmdbId,
|
||||
...(hit.imdbId ? { imdbId: hit.imdbId } : {}),
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
setLinkMsg(data.error?.message ?? "Koppelen mislukt");
|
||||
return;
|
||||
}
|
||||
setMsg(
|
||||
`Gekoppeld → ${data.seriesTitle}: ${data.moved}/${data.total}` +
|
||||
(data.failed ? ` (${data.failed} mislukt)` : "")
|
||||
);
|
||||
setLink(null);
|
||||
load();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function applyImdb() {
|
||||
const imdb = query.trim();
|
||||
if (!/^tt\d+$/i.test(imdb) || !link) {
|
||||
setLinkMsg("Vul een IMDb-id in (tt…) of zoek op titel.");
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setLinkMsg("Koppelen 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: link.nodeId,
|
||||
folder: link.folder,
|
||||
imdbId: imdb,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
setLinkMsg(data.error?.message ?? "Koppelen mislukt");
|
||||
return;
|
||||
}
|
||||
setMsg(`Gekoppeld → ${data.seriesTitle}: ${data.moved}/${data.total}`);
|
||||
setLink(null);
|
||||
load();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<div className="container">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<div className="page-kicker">Library</div>
|
||||
<h1>Nog te koppelen</h1>
|
||||
</div>
|
||||
<button type="button" onClick={() => load()}>
|
||||
Vernieuwen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="muted page-lead">
|
||||
Series-mappen zonder betrouwbare TMDB-match. Ze staan niet in de catalogus tot jij ze
|
||||
handmatig koppelt. {pending.length} mappen · {totalFiles} bestanden.
|
||||
</p>
|
||||
|
||||
{msg && (
|
||||
<p className="muted" style={{ marginBottom: "1rem" }}>
|
||||
{msg}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{pending.map((row) => (
|
||||
<div key={row.key} className="card">
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: "1rem", flexWrap: "wrap" }}>
|
||||
<div>
|
||||
<strong>{row.parsedTitle || "Onbekende serie"}</strong>
|
||||
<div className="muted" style={{ fontSize: "0.875rem", marginTop: 4 }}>
|
||||
{row.nodeName} · {row.fileCount} bestanden
|
||||
{row.seasons.length ? ` · seizoen ${row.seasons.join(", ")}` : ""}
|
||||
</div>
|
||||
<div className="mono tiny" style={{ marginTop: 8, color: "#a1a1aa" }}>
|
||||
{row.folder}
|
||||
</div>
|
||||
{row.samplePaths.length > 0 && (
|
||||
<div className="muted tiny" style={{ marginTop: 6 }}>
|
||||
{row.samplePaths.map((p) => (
|
||||
<div key={p}>{p}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button type="button" onClick={() => openLink(row)}>
|
||||
Koppelen…
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{pending.length === 0 && (
|
||||
<div className="card" style={{ textAlign: "center", color: "#71717a" }}>
|
||||
Geen openstaande series-mappen. Alles is gekoppeld of nog niet gescand.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{link && (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
background: "rgba(0,0,0,0.65)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 50,
|
||||
padding: "1rem",
|
||||
}}
|
||||
onClick={() => !busy && setLink(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" }}>
|
||||
Serie koppelen
|
||||
</h2>
|
||||
<p style={{ color: "#a1a1aa", fontSize: "0.85rem", marginBottom: "1rem" }}>
|
||||
{link.nodeName}
|
||||
<br />
|
||||
<span className="mono">{link.folder}</span>
|
||||
<br />
|
||||
{link.fileCount} bestanden
|
||||
</p>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void searchMeta();
|
||||
}}
|
||||
style={{ display: "flex", gap: "0.5rem", marginBottom: "0.75rem", flexWrap: "wrap" }}
|
||||
>
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Titel of IMDb-id (tt…)"
|
||||
style={{ flex: 1, minWidth: 200, marginBottom: 0 }}
|
||||
autoFocus
|
||||
/>
|
||||
<button type="submit" disabled={busy}>
|
||||
Zoek
|
||||
</button>
|
||||
<button type="button" disabled={busy} onClick={() => void applyImdb()}>
|
||||
Koppel IMDb-id
|
||||
</button>
|
||||
</form>
|
||||
{linkMsg && (
|
||||
<p style={{ color: "#a1a1aa", fontSize: "0.85rem", marginBottom: "0.75rem" }}>{linkMsg}</p>
|
||||
)}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
|
||||
{hits.map((hit) => (
|
||||
<button
|
||||
key={hit.tmdbId}
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => void applyHit(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 }} />
|
||||
)}
|
||||
<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>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: "1rem", display: "flex", justifyContent: "flex-end" }}>
|
||||
<button type="button" className="danger" disabled={busy} onClick={() => setLink(null)}>
|
||||
Annuleren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ const desktopLinks = [
|
|||
{ href: "/nodes", label: "Nodes" },
|
||||
{ href: "/movies", label: "Movies" },
|
||||
{ href: "/series", label: "Series" },
|
||||
{ href: "/pending", label: "Te koppelen" },
|
||||
{ href: "/downloads", label: "Downloads" },
|
||||
{ href: "/streams", label: "Streams" },
|
||||
{ href: "/viewers", label: "Gebruikers" },
|
||||
|
|
|
|||
|
|
@ -1140,6 +1140,92 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Series-achtige bestanden zonder match (geen episode/movie), gegroepeerd per node+map.
|
||||
*/
|
||||
app.get("/api/v1/admin/library/pending-series", { preHandler: requireAdmin }, async (request) => {
|
||||
const query = request.query as { nodeId?: string };
|
||||
const nodeId = query.nodeId?.trim() || undefined;
|
||||
|
||||
const files = await prisma.mediaFile.findMany({
|
||||
where: {
|
||||
movieId: null,
|
||||
episodeId: null,
|
||||
available: true,
|
||||
...(nodeId ? { nodeId } : {}),
|
||||
OR: [
|
||||
{ path: { not: null } },
|
||||
{ shelf: { kind: "SERIES" } },
|
||||
],
|
||||
},
|
||||
orderBy: [{ path: "asc" }, { releaseName: "asc" }],
|
||||
take: 5000,
|
||||
include: {
|
||||
node: { select: { id: true, name: true, status: true } },
|
||||
shelf: { select: { id: true, name: true, kind: true } },
|
||||
},
|
||||
});
|
||||
|
||||
type Group = {
|
||||
key: string;
|
||||
nodeId: string;
|
||||
nodeName: string;
|
||||
folder: string;
|
||||
parsedTitle: string | null;
|
||||
fileCount: number;
|
||||
samplePaths: string[];
|
||||
seasonsHint: Set<number>;
|
||||
};
|
||||
const groups = new Map<string, Group>();
|
||||
|
||||
for (const f of files) {
|
||||
const parsed = f.path ? parseSeriesFromPath(f.path) : null;
|
||||
const isSeriesShelf = f.shelf?.kind === "SERIES";
|
||||
if (!parsed && !isSeriesShelf) continue;
|
||||
|
||||
const folder =
|
||||
(f.path ? seriesFolderFromPath(f.path) : null) ||
|
||||
(f.path ? f.path.replace(/\\/g, "/").split("/").slice(0, -1).join("/") : null) ||
|
||||
`(geen pad) ${f.releaseName}`;
|
||||
const key = `${f.nodeId}::${folder}`;
|
||||
let g = groups.get(key);
|
||||
if (!g) {
|
||||
g = {
|
||||
key,
|
||||
nodeId: f.nodeId,
|
||||
nodeName: f.node.name,
|
||||
folder,
|
||||
parsedTitle: parsed?.title ?? null,
|
||||
fileCount: 0,
|
||||
samplePaths: [],
|
||||
seasonsHint: new Set(),
|
||||
};
|
||||
groups.set(key, g);
|
||||
}
|
||||
g.fileCount += 1;
|
||||
if (parsed?.season) g.seasonsHint.add(parsed.season);
|
||||
if (g.samplePaths.length < 3) {
|
||||
g.samplePaths.push(f.path || f.releaseName);
|
||||
}
|
||||
if (!g.parsedTitle && parsed?.title) g.parsedTitle = parsed.title;
|
||||
}
|
||||
|
||||
const pending = [...groups.values()]
|
||||
.map((g) => ({
|
||||
key: g.key,
|
||||
nodeId: g.nodeId,
|
||||
nodeName: g.nodeName,
|
||||
folder: g.folder,
|
||||
parsedTitle: g.parsedTitle,
|
||||
fileCount: g.fileCount,
|
||||
seasons: [...g.seasonsHint].sort((a, b) => a - b),
|
||||
samplePaths: g.samplePaths,
|
||||
}))
|
||||
.sort((a, b) => a.nodeName.localeCompare(b.nodeName) || a.folder.localeCompare(b.folder));
|
||||
|
||||
return { pending, totalFolders: pending.length, totalFiles: pending.reduce((n, p) => n + p.fileCount, 0) };
|
||||
});
|
||||
|
||||
app.get("/api/v1/admin/library/shelves", { preHandler: requireAdmin }, async () => {
|
||||
const { ensureDefaultShelves } = await import("../library/shelves");
|
||||
await ensureDefaultShelves();
|
||||
|
|
|
|||
|
|
@ -38,6 +38,30 @@ function sleep(ms: number): Promise<void> {
|
|||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/** Normaliseer titel voor confident match (geen fuzzy first-hit). */
|
||||
export function normalizeMatchTitle(s: string): string {
|
||||
return s
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/\p{M}/gu, "")
|
||||
.replace(/&/g, " and ")
|
||||
.replace(/[^a-z0-9]+/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** Alleen auto-matchen als lokale mapnaam en TMDB-titel echt overeenkomen. */
|
||||
export function seriesTitlesMatchConfident(local: string, remote: string): boolean {
|
||||
const a = normalizeMatchTitle(local);
|
||||
const b = normalizeMatchTitle(remote);
|
||||
if (!a || !b) return false;
|
||||
if (a === b) return true;
|
||||
// optioneel: "The X" ↔ "X"
|
||||
if (a.startsWith("the ") && a.slice(4) === b) return true;
|
||||
if (b.startsWith("the ") && b.slice(4) === a) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
async function fetchTmdbJson(url: string, retries = 4): Promise<Response | null> {
|
||||
for (let attempt = 0; attempt < retries; attempt++) {
|
||||
const res = await fetch(url);
|
||||
|
|
@ -562,9 +586,11 @@ export class MetadataService {
|
|||
private async matchEpisode(nodeId: string, file: MediaFileInfo): Promise<string | null> {
|
||||
const parsed = file.parsedEpisode!;
|
||||
const title = parsed.title.trim();
|
||||
let series: SeriesRow | null = await this.findExistingSeries(title);
|
||||
|
||||
if (this.tmdbApiKey && (!series || series.tmdbId == null)) {
|
||||
// Alleen koppelen aan een al-geïdentificeerde serie (TMDB/IMDb), nooit aan orphan-titels.
|
||||
let series: SeriesRow | null = await this.findExistingCanonicalSeries(title);
|
||||
|
||||
if (this.tmdbApiKey && !series) {
|
||||
const tmdb = await this.searchTmdbSeries(title);
|
||||
if (tmdb) {
|
||||
series = await prisma.series.upsert({
|
||||
|
|
@ -601,10 +627,6 @@ export class MetadataService {
|
|||
}
|
||||
}
|
||||
|
||||
if (!series) {
|
||||
series = await this.findExistingSeries(title);
|
||||
}
|
||||
|
||||
if (
|
||||
series?.tmdbId &&
|
||||
this.tmdbApiKey &&
|
||||
|
|
@ -614,10 +636,10 @@ export class MetadataService {
|
|||
this.refreshedSeriesIds.add(series.id);
|
||||
}
|
||||
|
||||
// Geen betrouwbare match → bestand indexeren zonder serie (Admin: Nog te koppelen).
|
||||
if (!series) {
|
||||
series = await prisma.series.create({
|
||||
data: { title },
|
||||
});
|
||||
await this.upsertMediaFile(nodeId, file, {});
|
||||
return null;
|
||||
}
|
||||
|
||||
let season = await prisma.season.findUnique({
|
||||
|
|
@ -664,7 +686,7 @@ export class MetadataService {
|
|||
episode = await this.applyTmdbEpisodeMeta(series.tmdbId, episode);
|
||||
}
|
||||
|
||||
await this.upsertMediaFile(nodeId, file, { episodeId: episode.id });
|
||||
await this.upsertMediaFile(nodeId, file, { episodeId: episode.id, movieId: null });
|
||||
return episode.id;
|
||||
}
|
||||
|
||||
|
|
@ -1995,7 +2017,7 @@ export class MetadataService {
|
|||
private async upsertMediaFile(
|
||||
nodeId: string,
|
||||
file: MediaFileInfo,
|
||||
refs: { movieId?: string; episodeId?: string }
|
||||
refs: { movieId?: string | null; episodeId?: string | null }
|
||||
): Promise<void> {
|
||||
const mtime = new Date(file.modifiedAt);
|
||||
const sizeBytes = BigInt(file.sizeBytes);
|
||||
|
|
@ -2012,8 +2034,8 @@ export class MetadataService {
|
|||
create: {
|
||||
nodeId,
|
||||
localFileId: file.localFileId,
|
||||
movieId: refs.movieId,
|
||||
episodeId: refs.episodeId,
|
||||
movieId: refs.movieId ?? null,
|
||||
episodeId: refs.episodeId ?? null,
|
||||
shelfId: file.shelfId || null,
|
||||
sizeBytes,
|
||||
container: file.container,
|
||||
|
|
@ -2028,8 +2050,8 @@ export class MetadataService {
|
|||
discoveredAt: new Date(),
|
||||
},
|
||||
update: {
|
||||
movieId: refs.movieId,
|
||||
episodeId: refs.episodeId,
|
||||
...("movieId" in refs ? { movieId: refs.movieId ?? null } : {}),
|
||||
...("episodeId" in refs ? { episodeId: refs.episodeId ?? null } : {}),
|
||||
...(file.shelfId !== undefined ? { shelfId: file.shelfId || null } : {}),
|
||||
sizeBytes,
|
||||
container: file.container,
|
||||
|
|
@ -2082,6 +2104,21 @@ export class MetadataService {
|
|||
});
|
||||
}
|
||||
|
||||
/** Alleen series met TMDB of IMDb — orphan titel-records tellen niet als match. */
|
||||
private async findExistingCanonicalSeries(title: string) {
|
||||
return prisma.series.findFirst({
|
||||
where: {
|
||||
title: { equals: title, mode: "insensitive" },
|
||||
OR: [{ tmdbId: { not: null } }, { imdbId: { not: null } }],
|
||||
},
|
||||
orderBy: [
|
||||
{ imdbId: { sort: "desc", nulls: "last" } },
|
||||
{ tmdbId: { sort: "desc", nulls: "last" } },
|
||||
{ createdAt: "asc" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async searchTmdbMovie(title: string, year?: number) {
|
||||
if (!this.tmdbApiKey) return null;
|
||||
const cacheKey = `${title.toLowerCase()}::${year ?? ""}`;
|
||||
|
|
@ -2128,13 +2165,30 @@ export class MetadataService {
|
|||
this.seriesSearchCache.set(cacheKey, null);
|
||||
return null;
|
||||
}
|
||||
const data = (await res.json()) as TmdbSearchResult;
|
||||
const item = data.results[0];
|
||||
if (!item) {
|
||||
const data = (await res.json()) as {
|
||||
results?: Array<{
|
||||
id: number;
|
||||
name?: string;
|
||||
original_name?: string;
|
||||
first_air_date?: string;
|
||||
overview?: string;
|
||||
poster_path?: string;
|
||||
backdrop_path?: string;
|
||||
}>;
|
||||
};
|
||||
const hit = (data.results ?? []).find((item) => {
|
||||
const name = item.name || "";
|
||||
const original = item.original_name || "";
|
||||
return (
|
||||
seriesTitlesMatchConfident(title, name) ||
|
||||
(original ? seriesTitlesMatchConfident(title, original) : false)
|
||||
);
|
||||
});
|
||||
if (!hit) {
|
||||
this.seriesSearchCache.set(cacheKey, null);
|
||||
return null;
|
||||
}
|
||||
const meta = await this.getTmdbSeriesDetails(item.id);
|
||||
const meta = await this.getTmdbSeriesDetails(hit.id);
|
||||
this.seriesSearchCache.set(cacheKey, meta);
|
||||
return meta;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue