From 69dc7febe2b036db5b7191cb2147a1065ec9c9f3 Mon Sep 17 00:00:00 2001 From: Rephl3x Date: Fri, 23 Jan 2026 18:05:17 +1300 Subject: [PATCH] Split search actions and improve download options --- backend/app/routers/requests.py | 82 ++++++++++++++++------------- backend/app/services/snapshot.py | 11 +++- frontend/app/requests/[id]/page.tsx | 12 +++-- 3 files changed, 63 insertions(+), 42 deletions(-) diff --git a/backend/app/routers/requests.py b/backend/app/routers/requests.py index 7c7926f..b4d8353 100644 --- a/backend/app/routers/requests.py +++ b/backend/app/routers/requests.py @@ -1297,6 +1297,37 @@ async def ai_triage(request_id: str, user: Dict[str, str] = Depends(get_current_ @router.post("/{request_id}/actions/search") async def action_search(request_id: str, user: Dict[str, str] = Depends(get_current_user)) -> dict: + runtime = get_runtime_settings() + client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) + if client.configured(): + await _ensure_request_access(client, int(request_id), user) + snapshot = await build_snapshot(request_id) + prowlarr_results: List[Dict[str, Any]] = [] + prowlarr = ProwlarrClient(runtime.prowlarr_base_url, runtime.prowlarr_api_key) + if not prowlarr.configured(): + raise HTTPException(status_code=400, detail="Prowlarr not configured") + query = snapshot.title + if snapshot.year: + query = f"{query} {snapshot.year}" + try: + results = await prowlarr.search(query=query) + prowlarr_results = _filter_prowlarr_results(results, snapshot.request_type) + except httpx.HTTPStatusError: + prowlarr_results = [] + + await asyncio.to_thread( + save_action, + request_id, + "search_releases", + "Search and choose a download", + "ok", + f"Found {len(prowlarr_results)} releases.", + ) + return {"status": "ok", "releases": prowlarr_results} + + +@router.post("/{request_id}/actions/search_auto") +async def action_search_auto(request_id: str, user: Dict[str, str] = Depends(get_current_user)) -> dict: runtime = get_runtime_settings() client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) if client.configured(): @@ -1306,18 +1337,6 @@ async def action_search(request_id: str, user: Dict[str, str] = Depends(get_curr if not isinstance(arr_item, dict): raise HTTPException(status_code=404, detail="Item not found in Sonarr/Radarr") - prowlarr_results: List[Dict[str, Any]] = [] - prowlarr = ProwlarrClient(runtime.prowlarr_base_url, runtime.prowlarr_api_key) - if prowlarr.configured(): - query = snapshot.title - if snapshot.year: - query = f"{query} {snapshot.year}" - try: - results = await prowlarr.search(query=query) - prowlarr_results = _filter_prowlarr_results(results, snapshot.request_type) - except httpx.HTTPStatusError: - prowlarr_results = [] - if snapshot.request_type.value == "tv": client = SonarrClient(runtime.sonarr_base_url, runtime.sonarr_api_key) if not client.configured(): @@ -1325,12 +1344,11 @@ async def action_search(request_id: str, user: Dict[str, str] = Depends(get_curr episodes = await client.get_episodes(int(arr_item["id"])) missing_by_season = _missing_episode_ids_by_season(episodes) if not missing_by_season: - return { - "status": "ok", - "message": "No missing monitored episodes found", - "searched": [], - "releases": prowlarr_results, - } + message = "No missing monitored episodes found." + await asyncio.to_thread( + save_action, request_id, "search_auto", "Search and auto-download", "ok", message + ) + return {"status": "ok", "message": message, "searched": []} responses = [] for season_number in sorted(missing_by_season.keys()): episode_ids = missing_by_season[season_number] @@ -1339,33 +1357,23 @@ async def action_search(request_id: str, user: Dict[str, str] = Depends(get_curr responses.append( {"season": season_number, "episodeCount": len(episode_ids), "response": response} ) - result = {"status": "ok", "searched": responses, "releases": prowlarr_results} + message = "Search sent to Sonarr." await asyncio.to_thread( - save_action, - request_id, - "search", - "Re-run search in Sonarr/Radarr", - "ok", - f"Found {len(prowlarr_results)} releases.", + save_action, request_id, "search_auto", "Search and auto-download", "ok", message ) - return result - elif snapshot.request_type.value == "movie": + return {"status": "ok", "message": message, "searched": responses} + if snapshot.request_type.value == "movie": client = RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key) if not client.configured(): raise HTTPException(status_code=400, detail="Radarr not configured") response = await client.search(int(arr_item["id"])) - result = {"status": "ok", "response": response, "releases": prowlarr_results} + message = "Search sent to Radarr." await asyncio.to_thread( - save_action, - request_id, - "search", - "Re-run search in Sonarr/Radarr", - "ok", - f"Found {len(prowlarr_results)} releases.", + save_action, request_id, "search_auto", "Search and auto-download", "ok", message ) - return result - else: - raise HTTPException(status_code=400, detail="Unknown request type") + return {"status": "ok", "message": message, "response": response} + + raise HTTPException(status_code=400, detail="Unknown request type") @router.post("/{request_id}/actions/qbit/resume") diff --git a/backend/app/services/snapshot.py b/backend/app/services/snapshot.py index 50f9ae2..55c89b4 100644 --- a/backend/app/services/snapshot.py +++ b/backend/app/services/snapshot.py @@ -550,8 +550,15 @@ async def build_snapshot(request_id: str) -> Snapshot: elif arr_item and arr_state != "available": actions.append( ActionOption( - id="search", - label="Search again for releases", + id="search_auto", + label="Search and auto-download", + risk="low", + ) + ) + actions.append( + ActionOption( + id="search_releases", + label="Search and choose a download", risk="low", ) ) diff --git a/frontend/app/requests/[id]/page.tsx b/frontend/app/requests/[id]/page.tsx index 3966108..5a67da6 100644 --- a/frontend/app/requests/[id]/page.tsx +++ b/frontend/app/requests/[id]/page.tsx @@ -484,7 +484,8 @@ export default function RequestTimelinePage({ params }: { params: { id: string } } const baseUrl = getApiBase() const actionMap: Record = { - search: 'actions/search', + search_releases: 'actions/search', + search_auto: 'actions/search_auto', resume_torrent: 'actions/qbit/resume', readd_to_arr: 'actions/readd', } @@ -493,7 +494,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string } setActionMessage('This action is not wired yet.') return } - if (action.id === 'search') { + if (action.id === 'search_releases') { setActionMessage(null) setReleaseOptions([]) setSearchRan(false) @@ -513,7 +514,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string } throw new Error(text || `Request failed: ${response.status}`) } const data = await response.json() - if (action.id === 'search') { + if (action.id === 'search_releases') { if (Array.isArray(data.releases)) { setReleaseOptions(data.releases) } @@ -526,6 +527,10 @@ export default function RequestTimelinePage({ params }: { params: { id: string } setModalMessage('Search complete. Pick an option below if you want to download.') } setActionMessage(`${action.label} started.`) + } else if (action.id === 'search_auto') { + const message = data?.message ?? 'Search sent to Sonarr/Radarr.' + setActionMessage(message) + setModalMessage(message) } else { const message = data?.message ?? `${action.label} started.` setActionMessage(message) @@ -565,6 +570,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string } {release.seeders ?? 0} seeders ยท {formatBytes(release.size)}