Split search actions and improve download options

This commit is contained in:
2026-01-23 18:05:17 +13:00
parent 7b8fc1d99b
commit 69dc7febe2
3 changed files with 63 additions and 42 deletions

View File

@@ -1302,13 +1302,10 @@ async def action_search(request_id: str, user: Dict[str, str] = Depends(get_curr
if client.configured(): if client.configured():
await _ensure_request_access(client, int(request_id), user) await _ensure_request_access(client, int(request_id), user)
snapshot = await build_snapshot(request_id) snapshot = await build_snapshot(request_id)
arr_item = snapshot.raw.get("arr", {}).get("item")
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_results: List[Dict[str, Any]] = []
prowlarr = ProwlarrClient(runtime.prowlarr_base_url, runtime.prowlarr_api_key) prowlarr = ProwlarrClient(runtime.prowlarr_base_url, runtime.prowlarr_api_key)
if prowlarr.configured(): if not prowlarr.configured():
raise HTTPException(status_code=400, detail="Prowlarr not configured")
query = snapshot.title query = snapshot.title
if snapshot.year: if snapshot.year:
query = f"{query} {snapshot.year}" query = f"{query} {snapshot.year}"
@@ -1318,6 +1315,28 @@ async def action_search(request_id: str, user: Dict[str, str] = Depends(get_curr
except httpx.HTTPStatusError: except httpx.HTTPStatusError:
prowlarr_results = [] 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():
await _ensure_request_access(client, int(request_id), user)
snapshot = await build_snapshot(request_id)
arr_item = snapshot.raw.get("arr", {}).get("item")
if not isinstance(arr_item, dict):
raise HTTPException(status_code=404, detail="Item not found in Sonarr/Radarr")
if snapshot.request_type.value == "tv": if snapshot.request_type.value == "tv":
client = SonarrClient(runtime.sonarr_base_url, runtime.sonarr_api_key) client = SonarrClient(runtime.sonarr_base_url, runtime.sonarr_api_key)
if not client.configured(): 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"])) episodes = await client.get_episodes(int(arr_item["id"]))
missing_by_season = _missing_episode_ids_by_season(episodes) missing_by_season = _missing_episode_ids_by_season(episodes)
if not missing_by_season: if not missing_by_season:
return { message = "No missing monitored episodes found."
"status": "ok", await asyncio.to_thread(
"message": "No missing monitored episodes found", save_action, request_id, "search_auto", "Search and auto-download", "ok", message
"searched": [], )
"releases": prowlarr_results, return {"status": "ok", "message": message, "searched": []}
}
responses = [] responses = []
for season_number in sorted(missing_by_season.keys()): for season_number in sorted(missing_by_season.keys()):
episode_ids = missing_by_season[season_number] episode_ids = missing_by_season[season_number]
@@ -1339,32 +1357,22 @@ async def action_search(request_id: str, user: Dict[str, str] = Depends(get_curr
responses.append( responses.append(
{"season": season_number, "episodeCount": len(episode_ids), "response": response} {"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( await asyncio.to_thread(
save_action, save_action, request_id, "search_auto", "Search and auto-download", "ok", message
request_id,
"search",
"Re-run search in Sonarr/Radarr",
"ok",
f"Found {len(prowlarr_results)} releases.",
) )
return result return {"status": "ok", "message": message, "searched": responses}
elif snapshot.request_type.value == "movie": if snapshot.request_type.value == "movie":
client = RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key) client = RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key)
if not client.configured(): if not client.configured():
raise HTTPException(status_code=400, detail="Radarr not configured") raise HTTPException(status_code=400, detail="Radarr not configured")
response = await client.search(int(arr_item["id"])) 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( await asyncio.to_thread(
save_action, save_action, request_id, "search_auto", "Search and auto-download", "ok", message
request_id,
"search",
"Re-run search in Sonarr/Radarr",
"ok",
f"Found {len(prowlarr_results)} releases.",
) )
return result return {"status": "ok", "message": message, "response": response}
else:
raise HTTPException(status_code=400, detail="Unknown request type") raise HTTPException(status_code=400, detail="Unknown request type")

View File

@@ -550,8 +550,15 @@ async def build_snapshot(request_id: str) -> Snapshot:
elif arr_item and arr_state != "available": elif arr_item and arr_state != "available":
actions.append( actions.append(
ActionOption( ActionOption(
id="search", id="search_auto",
label="Search again for releases", label="Search and auto-download",
risk="low",
)
)
actions.append(
ActionOption(
id="search_releases",
label="Search and choose a download",
risk="low", risk="low",
) )
) )

View File

@@ -484,7 +484,8 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
} }
const baseUrl = getApiBase() const baseUrl = getApiBase()
const actionMap: Record<string, string> = { const actionMap: Record<string, string> = {
search: 'actions/search', search_releases: 'actions/search',
search_auto: 'actions/search_auto',
resume_torrent: 'actions/qbit/resume', resume_torrent: 'actions/qbit/resume',
readd_to_arr: 'actions/readd', readd_to_arr: 'actions/readd',
} }
@@ -493,7 +494,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
setActionMessage('This action is not wired yet.') setActionMessage('This action is not wired yet.')
return return
} }
if (action.id === 'search') { if (action.id === 'search_releases') {
setActionMessage(null) setActionMessage(null)
setReleaseOptions([]) setReleaseOptions([])
setSearchRan(false) setSearchRan(false)
@@ -513,7 +514,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
throw new Error(text || `Request failed: ${response.status}`) throw new Error(text || `Request failed: ${response.status}`)
} }
const data = await response.json() const data = await response.json()
if (action.id === 'search') { if (action.id === 'search_releases') {
if (Array.isArray(data.releases)) { if (Array.isArray(data.releases)) {
setReleaseOptions(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.') setModalMessage('Search complete. Pick an option below if you want to download.')
} }
setActionMessage(`${action.label} started.`) setActionMessage(`${action.label} started.`)
} else if (action.id === 'search_auto') {
const message = data?.message ?? 'Search sent to Sonarr/Radarr.'
setActionMessage(message)
setModalMessage(message)
} else { } else {
const message = data?.message ?? `${action.label} started.` const message = data?.message ?? `${action.label} started.`
setActionMessage(message) setActionMessage(message)
@@ -565,6 +570,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
<span>{release.seeders ?? 0} seeders · {formatBytes(release.size)}</span> <span>{release.seeders ?? 0} seeders · {formatBytes(release.size)}</span>
<button <button
type="button" type="button"
disabled={!release.guid || !release.indexerId}
onClick={async () => { onClick={async () => {
if (!snapshot || !release.guid || !release.indexerId) { if (!snapshot || !release.guid || !release.indexerId) {
setActionMessage('Missing details to start the download.') setActionMessage('Missing details to start the download.')