"use client"; import { useState, useEffect, useCallback } from "react"; import { createPortal } from "react-dom"; import { Icon } from "./ui"; import type { ProwlarrRelease, ProwlarrSearchResponse } from "../../lib/api"; import { useTranslation } from "../../lib/i18n/context"; interface MissingBookItem { title: string | null; volume_number: number | null; external_book_id: string | null; } interface ProwlarrSearchModalProps { seriesName: string; missingBooks: MissingBookItem[] | null; } function formatSize(bytes: number): string { if (bytes >= 1073741824) return (bytes / 1073741824).toFixed(1) + " GB"; if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + " MB"; if (bytes >= 1024) return (bytes / 1024).toFixed(0) + " KB"; return bytes + " B"; } export function ProwlarrSearchModal({ seriesName, missingBooks }: ProwlarrSearchModalProps) { const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); const [isConfigured, setIsConfigured] = useState(null); const [isSearching, setIsSearching] = useState(false); const [results, setResults] = useState([]); const [query, setQuery] = useState(""); const [error, setError] = useState(null); // qBittorrent state const [isQbConfigured, setIsQbConfigured] = useState(false); const [sendingGuid, setSendingGuid] = useState(null); const [sentGuids, setSentGuids] = useState>(new Set()); const [sendError, setSendError] = useState(null); // Check if Prowlarr and qBittorrent are configured on mount useEffect(() => { fetch("/api/settings/prowlarr") .then((r) => (r.ok ? r.json() : null)) .then((data) => { setIsConfigured(!!(data && data.api_key && data.api_key.trim())); }) .catch(() => setIsConfigured(false)); fetch("/api/settings/qbittorrent") .then((r) => (r.ok ? r.json() : null)) .then((data) => { setIsQbConfigured(!!(data && data.url && data.url.trim() && data.username && data.username.trim())); }) .catch(() => setIsQbConfigured(false)); }, []); const doSearch = useCallback(async (searchSeriesName: string, volumeNumber?: number) => { setIsSearching(true); setError(null); setResults([]); try { const body: { series_name: string; volume_number?: number } = { series_name: searchSeriesName }; if (volumeNumber !== undefined) body.volume_number = volumeNumber; const resp = await fetch("/api/prowlarr/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = await resp.json(); if (data.error) { setError(data.error); } else { const searchResp = data as ProwlarrSearchResponse; setResults(searchResp.results); setQuery(searchResp.query); } } catch { setError(t("prowlarr.searchError")); } finally { setIsSearching(false); } }, [t]); function handleOpen() { setIsOpen(true); setResults([]); setError(null); setQuery(""); // Auto-search the series on open doSearch(seriesName); } function handleClose() { setIsOpen(false); } async function handleSendToQbittorrent(downloadUrl: string, guid: string) { setSendingGuid(guid); setSendError(null); try { const resp = await fetch("/api/qbittorrent/add", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: downloadUrl }), }); const data = await resp.json(); if (data.error) { setSendError(data.error); } else if (data.success) { setSentGuids((prev) => new Set(prev).add(guid)); } else { setSendError(data.message || t("prowlarr.sentError")); } } catch { setSendError(t("prowlarr.sentError")); } finally { setSendingGuid(null); } } // Don't render button if not configured if (isConfigured === false) return null; if (isConfigured === null) return null; const modal = isOpen ? createPortal( <>
{/* Header */}

{t("prowlarr.modalTitle")}

{/* Missing volumes + re-search */}
{missingBooks && missingBooks.length > 0 && missingBooks.map((book, i) => ( ))}
{/* Error */} {error && (
{error}
)} {/* Searching indicator */} {isSearching && (
{t("prowlarr.searching")}
)} {/* Results */} {!isSearching && results.length > 0 && (

{t("prowlarr.resultCount", { count: results.length, plural: results.length !== 1 ? "s" : "" })} {query && ({query})}

{results.map((release, i) => ( ))}
{t("prowlarr.columnTitle")} {t("prowlarr.columnIndexer")} {t("prowlarr.columnSize")} {t("prowlarr.columnSeeders")} {t("prowlarr.columnLeechers")} {t("prowlarr.columnProtocol")}
{release.title} {release.indexer || "—"} {release.size > 0 ? formatSize(release.size) : "—"} {release.seeders != null ? ( 0 ? "text-green-500 font-medium" : "text-muted-foreground"}> {release.seeders} ) : "—"} {release.leechers != null ? release.leechers : "—"} {release.protocol && ( {release.protocol} )}
{isQbConfigured && release.downloadUrl && ( )} {release.downloadUrl && ( )} {release.infoUrl && ( )}
)} {/* qBittorrent send error */} {sendError && (
{sendError}
)} {/* No results */} {!isSearching && !error && query && results.length === 0 && (

{t("prowlarr.noResults")}

)}
, document.body, ) : null; return ( <> {modal} ); }