Restore monitoring on recheck and prevent release-search proxy timeouts
This commit is contained in:
@@ -2578,6 +2578,54 @@ async def get_snapshot(request_id: str, user: Dict[str, str] = Depends(get_curre
|
||||
return _filter_snapshot_for_user(snapshot, user)
|
||||
|
||||
|
||||
async def _restore_request_monitoring(snapshot: Snapshot, request: dict) -> bool:
|
||||
# Pending or declined requests must not gain collection access via Recheck.
|
||||
if request.get('status') != 2:
|
||||
return False
|
||||
item = (snapshot.raw.get('arr') or {}).get('item')
|
||||
if not isinstance(item, dict) or not isinstance(item.get('id'), int):
|
||||
return False
|
||||
runtime = get_runtime_settings()
|
||||
if snapshot.request_type == RequestType.movie:
|
||||
client = RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key)
|
||||
fresh = await client.get_movie(item['id'])
|
||||
if not isinstance(fresh, dict):
|
||||
raise ValueError('Radarr did not return the requested movie')
|
||||
if fresh.get('monitored') is True:
|
||||
return False
|
||||
await client.update_movie({**fresh, 'monitored': True})
|
||||
verified = await client.get_movie(item['id'])
|
||||
if not isinstance(verified, dict) or verified.get('monitored') is not True:
|
||||
raise ValueError('Radarr did not enable monitoring')
|
||||
return True
|
||||
client = SonarrClient(runtime.sonarr_base_url, runtime.sonarr_api_key)
|
||||
fresh = await client.get_series(item['id'])
|
||||
if not isinstance(fresh, dict):
|
||||
raise ValueError('Sonarr did not return the requested series')
|
||||
requested = {season['seasonNumber'] for season in (request.get('seasons') or [])
|
||||
if isinstance(season, dict) and isinstance(season.get('seasonNumber'), int)}
|
||||
seasons = [{**season, 'monitored': True} if season.get('seasonNumber') in requested else season
|
||||
for season in (fresh.get('seasons') or [])]
|
||||
changed = fresh.get('monitored') is not True or seasons != fresh.get('seasons', [])
|
||||
if changed:
|
||||
await client.update_series({**fresh, 'monitored': True, 'seasons': seasons})
|
||||
episodes = await client.get_episodes(item['id']) if requested else []
|
||||
ids = [episode['id'] for episode in episodes if episode.get('seasonNumber') in requested
|
||||
and episode.get('monitored') is not True and isinstance(episode.get('id'), int)]
|
||||
if ids:
|
||||
await client.monitor_episodes(ids, True)
|
||||
verified_episodes = await client.get_episodes(item['id'])
|
||||
if any(e.get('id') in ids and e.get('monitored') is not True for e in verified_episodes):
|
||||
raise ValueError('Sonarr did not enable episode monitoring')
|
||||
if changed:
|
||||
verified = await client.get_series(item['id'])
|
||||
if not isinstance(verified, dict) or verified.get('monitored') is not True or any(
|
||||
season.get('seasonNumber') in requested and season.get('monitored') is not True
|
||||
for season in verified.get('seasons', [])):
|
||||
raise ValueError('Sonarr did not enable monitoring')
|
||||
return changed or bool(ids)
|
||||
|
||||
|
||||
@router.post("/{request_id}/actions/recheck")
|
||||
async def action_recheck(request_id: str, user: Dict[str, str] = Depends(get_current_user)) -> dict:
|
||||
if not request_id.isdigit():
|
||||
@@ -2634,9 +2682,17 @@ async def action_recheck(request_id: str, user: Dict[str, str] = Depends(get_cur
|
||||
_cache_set(f"request:{request_id}", fresh_request)
|
||||
_refresh_recent_cache_from_db()
|
||||
|
||||
snapshot = _filter_snapshot_for_user(await build_snapshot(request_id), user)
|
||||
snapshot = await build_snapshot(request_id)
|
||||
try:
|
||||
restored = await _restore_request_monitoring(snapshot, fresh_request)
|
||||
except Exception as exc:
|
||||
logger.warning('Recheck monitoring failed request_id=%s: %s', request_id, exc)
|
||||
raise HTTPException(502, 'Could not restore monitoring in Sonarr/Radarr. Please try Recheck again.') from exc
|
||||
if restored:
|
||||
snapshot = await build_snapshot(request_id)
|
||||
snapshot = _filter_snapshot_for_user(snapshot, user)
|
||||
status_label = str((snapshot.presentation.get("status") or {}).get("label") or "Status updated")
|
||||
message = f"Recheck complete. {status_label}."
|
||||
message = ("Monitoring restored. " if restored else "") + f"Recheck complete. {status_label}."
|
||||
await asyncio.to_thread(
|
||||
save_action,
|
||||
request_id,
|
||||
@@ -3340,7 +3396,7 @@ async def action_search(request_id: str, user: Dict[str, str] = Depends(get_curr
|
||||
return {"status": "ok", "message": message, "collector": collector, "releases": []}
|
||||
missing_ids = [identity for season in season_numbers for identity in missing_by_season[season]]
|
||||
total_missing = len(missing_ids)
|
||||
batch = missing_ids[offset:offset + 20]
|
||||
batch = missing_ids[offset:offset + 3]
|
||||
next_offset = offset + len(batch) if offset + len(batch) < total_missing else None
|
||||
semaphore = asyncio.Semaphore(3)
|
||||
async def search_episode(identity):
|
||||
@@ -3406,7 +3462,7 @@ async def action_search(request_id: str, user: Dict[str, str] = Depends(get_curr
|
||||
if len(results) > len(releases):
|
||||
message += ' Duplicate results are combined; up to 200 ranked releases are shown.'
|
||||
if total_missing:
|
||||
message += f' Searched {min(20, max(0, total_missing - offset))} of {total_missing} missing monitored episodes.'
|
||||
message += f' Searched {min(3, max(0, total_missing - offset))} of {total_missing} missing monitored episodes.'
|
||||
await asyncio.to_thread(save_action, request_id, 'search_releases', 'Search and choose a download', 'ok', message)
|
||||
return {'status': 'ok', 'collector': collector, 'qualityFiltered': False, 'message': message,
|
||||
'outcome': 'matches' if approved else 'attention', 'rejectionReasons': rejection_reasons,
|
||||
|
||||
@@ -75,9 +75,9 @@ class ManualEpisodeSearchTests(unittest.IsolatedAsyncioTestCase):
|
||||
snapshot = Snapshot(request_id='42',request_type=RequestType.tv,title='Example',raw={'arr':{'item':{'id':55,'qualityProfileId':9}}})
|
||||
with patch.object(requests, 'get_runtime_settings', return_value=runtime), patch.object(requests, 'build_snapshot', new=AsyncMock(return_value=snapshot)), patch.object(requests,'SonarrClient',return_value=sonarr), patch.object(requests,'save_action'):
|
||||
first = await requests.action_search('42', {'username':'viewer','role':'user'})
|
||||
second = await requests.action_search('42', {'username':'viewer','role':'user'}, offset=20)
|
||||
self.assertEqual(first['nextOffset'],20); self.assertIsNone(second['nextOffset'])
|
||||
self.assertEqual(sonarr.search_episode_releases.await_count,25)
|
||||
second = await requests.action_search('42', {'username':'viewer','role':'user'}, offset=24)
|
||||
self.assertEqual(first['nextOffset'],3); self.assertIsNone(second['nextOffset'])
|
||||
self.assertEqual(sonarr.search_episode_releases.await_count,4)
|
||||
self.assertLessEqual(peak,3)
|
||||
self.assertEqual(first['totalMissingEpisodes'],25)
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from backend.app.routers import requests
|
||||
from backend.app.models import Snapshot, RequestType
|
||||
|
||||
class RecheckMonitoringTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_series_restores_only_requested_seasons_and_episodes(self):
|
||||
original={'id':10,'monitored':False,'qualityProfileId':7,'seasons':[{'seasonNumber':1,'monitored':False},{'seasonNumber':2,'monitored':False}]}
|
||||
restored={**original,'monitored':True,'seasons':[{'seasonNumber':1,'monitored':True},{'seasonNumber':2,'monitored':False}]}
|
||||
client=SimpleNamespace(get_series=AsyncMock(side_effect=[original,restored]),update_series=AsyncMock(),get_episodes=AsyncMock(side_effect=[[{'id':1,'seasonNumber':1,'monitored':False},{'id':2,'seasonNumber':2,'monitored':False}],[{'id':1,'seasonNumber':1,'monitored':True},{'id':2,'seasonNumber':2,'monitored':False}]]),monitor_episodes=AsyncMock())
|
||||
snapshot=Snapshot(request_id='42',title='Test',request_type=RequestType.tv,raw={'arr':{'item':{'id':10}}})
|
||||
runtime=SimpleNamespace(sonarr_base_url='http://sonarr',sonarr_api_key='test')
|
||||
with patch.object(requests,'get_runtime_settings',return_value=runtime),patch.object(requests,'SonarrClient',return_value=client):
|
||||
self.assertTrue(await requests._restore_request_monitoring(snapshot,{'status':2,'seasons':[{'seasonNumber':1}]}))
|
||||
client.update_series.assert_awaited_once_with(restored)
|
||||
client.monitor_episodes.assert_awaited_once_with([1],True)
|
||||
|
||||
async def test_movie_monitoring_preserves_profile_and_pending_is_noop(self):
|
||||
movie={'id':10,'monitored':False,'qualityProfileId':7}
|
||||
client=SimpleNamespace(get_movie=AsyncMock(side_effect=[movie,{**movie,'monitored':True}]),update_movie=AsyncMock())
|
||||
snapshot=Snapshot(request_id='42',title='Test',request_type=RequestType.movie,raw={'arr':{'item':{'id':10}}})
|
||||
runtime=SimpleNamespace(radarr_base_url='http://radarr',radarr_api_key='test')
|
||||
with patch.object(requests,'get_runtime_settings',return_value=runtime),patch.object(requests,'RadarrClient',return_value=client):
|
||||
self.assertFalse(await requests._restore_request_monitoring(snapshot,{'status':1}))
|
||||
self.assertTrue(await requests._restore_request_monitoring(snapshot,{'status':2}))
|
||||
client.update_movie.assert_awaited_once_with({**movie,'monitored':True})
|
||||
Reference in New Issue
Block a user