From 0460ea7c1f0ad9c004e2c9544ab6bf34dea04963 Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Thu, 26 Mar 2026 06:24:00 +0100 Subject: [PATCH] feat: add qBittorrent download button to download detection report Show a download button on each available release in the detection report when qBittorrent is configured, matching the Prowlarr search modal behavior. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/backoffice/app/(app)/jobs/[id]/page.tsx | 8 +- .../components/QbittorrentDownloadButton.tsx | 82 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 apps/backoffice/app/components/QbittorrentDownloadButton.tsx diff --git a/apps/backoffice/app/(app)/jobs/[id]/page.tsx b/apps/backoffice/app/(app)/jobs/[id]/page.tsx index c4eacae..61b3343 100644 --- a/apps/backoffice/app/(app)/jobs/[id]/page.tsx +++ b/apps/backoffice/app/(app)/jobs/[id]/page.tsx @@ -8,6 +8,7 @@ import { StatusBadge, JobTypeBadge, StatBox, ProgressBar } from "@/app/components/ui"; import { JobDetailLive } from "@/app/components/JobDetailLive"; +import { QbittorrentProvider, QbittorrentDownloadButton } from "@/app/components/QbittorrentDownloadButton"; import { getServerTranslations } from "@/lib/i18n/server"; interface JobDetailPageProps { @@ -984,6 +985,7 @@ export default async function JobDetailPage({ params }: JobDetailPageProps) { {/* Download detection — available releases per series */} {isDownloadDetection && downloadDetectionResults.length > 0 && ( + {t("jobDetail.downloadAvailableReleases")} @@ -1010,7 +1012,7 @@ export default async function JobDetailPage({ params }: JobDetailPageProps) { {r.available_releases && r.available_releases.length > 0 && (
{r.available_releases.map((release, idx) => ( -
+

{release.title}

@@ -1032,6 +1034,9 @@ export default async function JobDetailPage({ params }: JobDetailPageProps) {
+ {release.download_url && ( + + )}
))}
@@ -1040,6 +1045,7 @@ export default async function JobDetailPage({ params }: JobDetailPageProps) { ))}
+
)} {/* Metadata batch results */} diff --git a/apps/backoffice/app/components/QbittorrentDownloadButton.tsx b/apps/backoffice/app/components/QbittorrentDownloadButton.tsx new file mode 100644 index 0000000..7a64a75 --- /dev/null +++ b/apps/backoffice/app/components/QbittorrentDownloadButton.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useState, useEffect, createContext, useContext, type ReactNode } from "react"; +import { Icon } from "./ui"; +import { useTranslation } from "@/lib/i18n/context"; + +const QbConfigContext = createContext(false); + +export function QbittorrentProvider({ children }: { children: ReactNode }) { + const [configured, setConfigured] = useState(false); + + useEffect(() => { + fetch("/api/settings/qbittorrent") + .then((r) => (r.ok ? r.json() : null)) + .then((data) => { + setConfigured(!!(data && data.url && data.url.trim() && data.username && data.username.trim())); + }) + .catch(() => setConfigured(false)); + }, []); + + return {children}; +} + +export function QbittorrentDownloadButton({ downloadUrl, releaseId }: { downloadUrl: string; releaseId: string }) { + const { t } = useTranslation(); + const configured = useContext(QbConfigContext); + const [sending, setSending] = useState(false); + const [sent, setSent] = useState(false); + const [error, setError] = useState(null); + + if (!configured) return null; + + async function handleSend() { + setSending(true); + setError(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) { + setError(data.error); + } else if (data.success) { + setSent(true); + } else { + setError(data.message || t("prowlarr.sentError")); + } + } catch { + setError(t("prowlarr.sentError")); + } finally { + setSending(false); + } + } + + return ( + + ); +}