diff --git a/apps/admin-ui/src/app/movies/page.tsx b/apps/admin-ui/src/app/movies/page.tsx index 9696ccd..070f82a 100644 --- a/apps/admin-ui/src/app/movies/page.tsx +++ b/apps/admin-ui/src/app/movies/page.tsx @@ -29,6 +29,15 @@ interface Movie { files: MovieFile[]; } +interface MetaHit { + tmdbId: number; + title: string; + year: number | null; + overview: string | null; + posterUrl: string | null; + imdbId: string | null; +} + function formatBytes(bytes: string | number | null | undefined): string { const valNum = Number(bytes ?? 0); if (!Number.isFinite(valNum) || valNum < 0) return "0 B"; @@ -47,6 +56,11 @@ export default function MoviesPage() { const [movies, setMovies] = useState([]); const [search, setSearch] = useState(""); const [expanded, setExpanded] = useState(null); + const [linkMovieId, setLinkMovieId] = useState(null); + const [linkQuery, setLinkQuery] = useState(""); + const [linkHits, setLinkHits] = useState([]); + const [linkBusy, setLinkBusy] = useState(false); + const [linkMsg, setLinkMsg] = useState(""); function load(q = search) { const qs = q ? `?search=${encodeURIComponent(q)}` : ""; @@ -78,6 +92,100 @@ export default function MoviesPage() { load(); } + function openLink(m: Movie) { + setLinkMovieId(m.id); + setLinkQuery(m.year ? `${m.title} ${m.year}` : m.title); + setLinkHits([]); + setLinkMsg(""); + void searchMeta(m.year ? `${m.title} ${m.year}` : m.title); + } + + async function searchMeta(q = linkQuery) { + const query = q.trim(); + if (!query) return; + setLinkBusy(true); + setLinkMsg(""); + try { + const res = await fetch( + `/api/v1/admin/metadata/search?type=movie&q=${encodeURIComponent(query)}`, + { credentials: "include" } + ); + const data = await res.json(); + if (!res.ok) { + setLinkMsg(data.error?.message ?? "Zoeken mislukt"); + setLinkHits([]); + return; + } + setLinkHits(data.results ?? []); + if (!(data.results ?? []).length) setLinkMsg("Geen resultaten"); + } finally { + setLinkBusy(false); + } + } + + async function applyLink(hit: MetaHit) { + if (!linkMovieId) return; + setLinkBusy(true); + setLinkMsg("Koppelen…"); + try { + const res = await fetch(`/api/v1/admin/movies/${linkMovieId}/link`, { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + tmdbId: hit.tmdbId, + ...(hit.imdbId ? { imdbId: hit.imdbId } : {}), + }), + }); + const data = await res.json(); + if (!res.ok) { + setLinkMsg(data.error?.message ?? "Opslaan mislukt"); + return; + } + setLinkMsg( + data.merged + ? "Gekoppeld (samengevoegd met bestaande film). Metadata vernieuwd." + : "Gekoppeld. Metadata vernieuwd." + ); + setLinkMovieId(null); + setExpanded(data.movieId ?? linkMovieId); + load(); + } finally { + setLinkBusy(false); + } + } + + async function applyImdbDirect() { + const imdb = linkQuery.trim(); + if (!/^tt\d+$/i.test(imdb) || !linkMovieId) { + setLinkMsg("Vul een IMDb-id in (bijv. tt1375666) of zoek eerst op titel."); + return; + } + setLinkBusy(true); + setLinkMsg("Koppelen via IMDb…"); + try { + const res = await fetch(`/api/v1/admin/movies/${linkMovieId}/link`, { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ imdbId: imdb }), + }); + const data = await res.json(); + if (!res.ok) { + setLinkMsg(data.error?.message ?? "Opslaan mislukt"); + return; + } + setLinkMsg("Gekoppeld via IMDb. Metadata vernieuwd."); + setLinkMovieId(null); + setExpanded(data.movieId ?? linkMovieId); + load(); + } finally { + setLinkBusy(false); + } + } + + const activeMovie = movies.find((m) => m.id === linkMovieId); + return ( <>