From 49e9ee771f844a36d66026d0a315b368b01f4067 Mon Sep 17 00:00:00 2001 From: Rephl3x Date: Fri, 23 Jan 2026 18:15:36 +1300 Subject: [PATCH] Fallback manual grab to qBittorrent --- backend/app/clients/qbittorrent.py | 6 ++++ backend/app/routers/requests.py | 45 +++++++++++++++++++++++++++++ frontend/app/requests/[id]/page.tsx | 2 ++ 3 files changed, 53 insertions(+) diff --git a/backend/app/clients/qbittorrent.py b/backend/app/clients/qbittorrent.py index 621d6e4..7204bf0 100644 --- a/backend/app/clients/qbittorrent.py +++ b/backend/app/clients/qbittorrent.py @@ -67,3 +67,9 @@ class QBittorrentClient(ApiClient): await self._post_form("/api/v2/torrents/start", data={"hashes": hashes}) return raise + + async def add_torrent_url(self, url: str, category: Optional[str] = None) -> None: + data: Dict[str, Any] = {"urls": url} + if category: + data["category"] = category + await self._post_form("/api/v2/torrents/add", data=data) diff --git a/backend/app/routers/requests.py b/backend/app/routers/requests.py index b4d8353..82774a3 100644 --- a/backend/app/routers/requests.py +++ b/backend/app/routers/requests.py @@ -1569,6 +1569,7 @@ async def action_grab( snapshot = await build_snapshot(request_id) guid = payload.get("guid") indexer_id = payload.get("indexerId") + download_url = payload.get("downloadUrl") if not guid or not indexer_id: raise HTTPException(status_code=400, detail="Missing guid or indexerId") @@ -1580,6 +1581,28 @@ async def action_grab( try: response = await client.grab_release(str(guid), int(indexer_id)) except httpx.HTTPStatusError as exc: + status_code = exc.response.status_code if exc.response is not None else 502 + if status_code == 404 and download_url: + qbit = QBittorrentClient( + runtime.qbittorrent_base_url, + runtime.qbittorrent_username, + runtime.qbittorrent_password, + ) + if not qbit.configured(): + raise HTTPException(status_code=400, detail="qBittorrent not configured") + try: + await qbit.add_torrent_url(str(download_url)) + except httpx.HTTPStatusError as qbit_exc: + raise HTTPException(status_code=502, detail=str(qbit_exc)) from qbit_exc + await asyncio.to_thread( + save_action, + request_id, + "grab", + "Grab release", + "ok", + "Sent to qBittorrent via Prowlarr.", + ) + return {"status": "ok", "message": "Sent to qBittorrent.", "via": "qbittorrent"} raise HTTPException(status_code=502, detail=str(exc)) from exc await asyncio.to_thread( save_action, request_id, "grab", "Grab release", "ok", "Grab sent to Sonarr." @@ -1592,6 +1615,28 @@ async def action_grab( try: response = await client.grab_release(str(guid), int(indexer_id)) except httpx.HTTPStatusError as exc: + status_code = exc.response.status_code if exc.response is not None else 502 + if status_code == 404 and download_url: + qbit = QBittorrentClient( + runtime.qbittorrent_base_url, + runtime.qbittorrent_username, + runtime.qbittorrent_password, + ) + if not qbit.configured(): + raise HTTPException(status_code=400, detail="qBittorrent not configured") + try: + await qbit.add_torrent_url(str(download_url)) + except httpx.HTTPStatusError as qbit_exc: + raise HTTPException(status_code=502, detail=str(qbit_exc)) from qbit_exc + await asyncio.to_thread( + save_action, + request_id, + "grab", + "Grab release", + "ok", + "Sent to qBittorrent via Prowlarr.", + ) + return {"status": "ok", "message": "Sent to qBittorrent.", "via": "qbittorrent"} raise HTTPException(status_code=502, detail=str(exc)) from exc await asyncio.to_thread( save_action, request_id, "grab", "Grab release", "ok", "Grab sent to Radarr." diff --git a/frontend/app/requests/[id]/page.tsx b/frontend/app/requests/[id]/page.tsx index 5a67da6..73cc0a1 100644 --- a/frontend/app/requests/[id]/page.tsx +++ b/frontend/app/requests/[id]/page.tsx @@ -34,6 +34,7 @@ type ReleaseOption = { leechers?: number protocol?: string infoUrl?: string + downloadUrl?: string } type SnapshotHistory = { @@ -589,6 +590,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string } body: JSON.stringify({ guid: release.guid, indexerId: release.indexerId, + downloadUrl: release.downloadUrl, }), } )