diff --git a/backend/app/services/request_language.py b/backend/app/services/request_language.py index 36d2da0..534a83c 100644 --- a/backend/app/services/request_language.py +++ b/backend/app/services/request_language.py @@ -100,10 +100,11 @@ async def movie_search_outcome(client, movie_id, command, attempts=12, delay=2): movie = await client.get_movie(movie_id) if (movie or {}).get('hasFile'): return {'status': 'complete', 'message': 'Radarr already has the movie file. Recheck the request for Jellyfin availability.'} - return {'status': 'attention', 'message': 'Search finished, but no download appeared in Radarr. Use Search and choose a download to see matching releases and rejection reasons. For a foreign-language title, review the audio choice above.'} + # Command completion precedes download-client queue refresh. Keep polling. + pass if attempt + 1 < attempts: await asyncio.sleep(delay) - return {'status': 'searching', 'message': 'Radarr is still searching. No download is confirmed yet; the pipeline will keep checking. You can close this window.'} + return {'status': 'pending', 'message': 'The search was submitted, but a download is not confirmed yet. The download queue may still be updating. Close this window and recheck the request shortly.'} async def series_search_outcome(client, series_id, commands, attempts=12, delay=2): @@ -122,8 +123,7 @@ async def series_search_outcome(client, series_id, commands, attempts=12, delay= statuses = {str((state or {}).get('status', '')).lower() for state in states} if statuses & {'failed', 'aborted', 'cancelled'}: return {'status': 'attention', 'message': 'A Sonarr search failed. Check the service or try Search and choose a download.'} - if statuses == {'completed'}: - return {'status': 'attention', 'message': 'Sonarr finished searching, but no download is visible yet. Use Search and choose a download to review available releases and rejection reasons.'} + # Even completed commands can precede Sonarr's download queue refresh. if attempt + 1 < attempts: await asyncio.sleep(delay) - return {'status': 'searching', 'message': 'Sonarr is still searching. No download is confirmed yet; you can close this window and follow the pipeline.'} + return {'status': 'pending', 'message': 'The search was submitted, but a download is not confirmed yet. The download queue may still be updating. Close this window and recheck the request shortly.'} diff --git a/backend/tests/test_request_language.py b/backend/tests/test_request_language.py index 3a7fd94..7bc1dd4 100644 --- a/backend/tests/test_request_language.py +++ b/backend/tests/test_request_language.py @@ -85,8 +85,8 @@ class RequestLanguageTests(unittest.IsolatedAsyncioTestCase): await apply_original_to_movie(client, 613) async def test_search_reports_real_outcomes(self): - for command_status, queue, expected in [('completed', [], 'attention'), ('failed', [], 'attention'), - ('started', [], 'searching'), ('completed', [{'movieId': 6940}], 'downloading')]: + for command_status, queue, expected in [('completed', [], 'pending'), ('failed', [], 'attention'), + ('started', [], 'pending'), ('completed', [{'movieId': 6940}], 'downloading')]: client = SimpleNamespace(get=AsyncMock(return_value={'status': command_status}), get_queue=AsyncMock(return_value={'records': queue}), get_movie=AsyncMock(return_value={'hasFile': False})) result = await movie_search_outcome(client, 6940, {'id': 1}, attempts=1, delay=0) @@ -114,6 +114,23 @@ class RequestLanguageTests(unittest.IsolatedAsyncioTestCase): async def test_tv_search_distinguishes_no_download_and_queue(self): from backend.app.services.request_language import series_search_outcome client = SimpleNamespace(get=AsyncMock(return_value={'status': 'completed'}), get_queue=AsyncMock(return_value={'records': []})) - self.assertEqual((await series_search_outcome(client, 50, [{'id': 1}], attempts=1))['status'], 'attention') + self.assertEqual((await series_search_outcome(client, 50, [{'id': 1}], attempts=1))['status'], 'pending') client.get_queue.return_value = {'records': [{'seriesId': 50}]} self.assertEqual((await series_search_outcome(client, 50, [{'id': 1}], attempts=1))['status'], 'downloading') + + +class SearchHandoffTests(unittest.IsolatedAsyncioTestCase): + async def test_radarr_completed_before_queue_refresh(self): + client = SimpleNamespace(get=AsyncMock(return_value={'status':'completed'}), + get_queue=AsyncMock(side_effect=[{'records':[]}, {'records':[]}, {'records':[{'movieId':2206}]}]), + get_movie=AsyncMock(return_value={'hasFile':False})) + result = await movie_search_outcome(client, 2206, {'id':1}, attempts=3, delay=0) + self.assertEqual(result['status'], 'downloading') + self.assertEqual(client.get_queue.await_count, 3) + + async def test_sonarr_completed_before_queue_refresh(self): + from backend.app.services.request_language import series_search_outcome + client = SimpleNamespace(get=AsyncMock(return_value={'status':'completed'}), + get_queue=AsyncMock(side_effect=[{'records':[{'seriesId':999}]}, {'records':[{'seriesId':50}]}])) + result = await series_search_outcome(client, 50, [{'id':1}], attempts=2, delay=0) + self.assertEqual(result['status'], 'downloading') diff --git a/frontend/app/requests/[id]/page.tsx b/frontend/app/requests/[id]/page.tsx index 92c83b2..12e284b 100644 --- a/frontend/app/requests/[id]/page.tsx +++ b/frontend/app/requests/[id]/page.tsx @@ -615,6 +615,10 @@ export default function RequestTimelinePage() { : canChoose ? { title: available ? 'Downloads found' : 'Other versions are available', message: available ? 'Choose the version you want to download.' : 'These versions are outside your usual download settings.', next: available ? 'Your download starts after you choose a version.' : 'You can review them and confirm a download outside your profile.', action: 'Choose a version' } : { title: items.length ? 'No suitable downloads' : 'Nothing available yet', message: items.length ? 'The versions found cannot be downloaded with your current settings.' : 'No downloads were found in this search.', next: result?.nextOffset != null ? 'You can check the next group of missing episodes.' : 'You can try again later.', action: result?.nextOffset != null ? 'View search results' : undefined } + : response.ok && result?.status === 'pending' + ? { title: 'Waiting for download confirmation', message: 'The search was sent. The download queue may still be updating.', next: 'Close this window and recheck the request shortly. You do not need to start another search yet.' } + : response.ok && result?.status === 'downloading' + ? { title: 'Download queued', message: 'The download service has confirmed a download for this title.', next: 'Close this window to follow its progress.' } : response.ok && /\/actions\/grab$/.test(url) ? { title: 'Waiting to start', message: 'Your download has been sent.', next: 'Close this box to follow its progress. It may take a moment to start.' } : undefined