Implement pagination and visibility settings in Immich integration. Updated tests to validate new behavior and ensure correct handling of multiple pages in album metadata retrieval.

This commit is contained in:
Sean Morley
2026-07-22 20:29:50 -04:00
parent aec4ac576e
commit e6bcd0016b
3 changed files with 102 additions and 22 deletions

View File

@@ -43,6 +43,9 @@ class ImmichViewTests(TestCase):
_, kwargs = mock_post.call_args
self.assertEqual(kwargs['json']['takenAfter'], '2024-06-15T00:00:00.000Z')
self.assertEqual(kwargs['json']['takenBefore'], '2024-06-16T00:00:00.000Z')
self.assertEqual(kwargs['json']['visibility'], 'timeline')
self.assertEqual(kwargs['json']['page'], 1)
self.assertEqual(kwargs['json']['size'], 1000)
@patch('integrations.views.immich_view.requests.get')
@patch('integrations.views.immich_view.requests.post')
@@ -72,8 +75,53 @@ class ImmichViewTests(TestCase):
mock_post.assert_called_once()
_, kwargs = mock_post.call_args
self.assertEqual(kwargs['json']['albumIds'], ['album-1'])
self.assertEqual(kwargs['json']['visibility'], 'timeline')
self.assertEqual(len(response.data['results']), 2)
@patch('integrations.views.immich_view.requests.get')
@patch('integrations.views.immich_view.requests.post')
def test_album_metadata_search_paginates_past_first_page(self, mock_post, mock_get):
mock_get_response = MagicMock()
mock_get_response.json.return_value = {
'id': 'album-1',
'albumName': 'Large Album',
}
mock_get.return_value = mock_get_response
first_page_response = MagicMock()
first_page_response.ok = True
first_page_response.json.return_value = {
'assets': {
'items': [{'id': f'asset-{index}'} for index in range(1000)],
'nextPage': '2',
}
}
second_page_response = MagicMock()
second_page_response.ok = True
second_page_response.json.return_value = {
'assets': {
'items': [{'id': 'asset-1000'}, {'id': 'asset-1001'}],
'nextPage': None,
}
}
mock_post.side_effect = [first_page_response, second_page_response]
request = self.factory.get('/api/integrations/immich/albums/album-1/')
force_authenticate(request, user=self.user)
response = self.album_view(request, albumid='album-1')
self.assertEqual(response.status_code, 200)
self.assertEqual(mock_post.call_count, 2)
first_call_kwargs = mock_post.call_args_list[0].kwargs
second_call_kwargs = mock_post.call_args_list[1].kwargs
self.assertEqual(first_call_kwargs['json']['page'], 1)
self.assertEqual(second_call_kwargs['json']['page'], 2)
self.assertEqual(len(response.data['results']), 25)
self.assertEqual(response.data['count'], 1002)
@patch('integrations.views.immich_view.requests.get')
def test_album_uses_inline_assets_when_available(self, mock_get):
mock_get_response = MagicMock()

View File

@@ -17,6 +17,7 @@ import logging
logger = logging.getLogger(__name__)
IMMICH_SEARCH_SIZE = 1000
IMMICH_SEARCH_VISIBILITY = 'timeline'
class ImmichIntegrationView(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
@@ -98,29 +99,60 @@ class ImmichIntegrationView(viewsets.ViewSet):
search_type='metadata',
not_found_code='immich.no_items_found',
):
try:
url = f'{integration.server_url}/search/{search_type}'
immich_fetch = requests.post(
url,
headers={'x-api-key': integration.api_key},
json={'size': IMMICH_SEARCH_SIZE, **arguments},
)
res = immich_fetch.json()
except requests.exceptions.ConnectionError:
return self._immich_connection_error()
url = f'{integration.server_url}/search/{search_type}'
items = []
page = 1
if not immich_fetch.ok:
logger.warning(
'Immich search request failed: status=%s response=%s',
immich_fetch.status_code,
res,
)
return self._immich_no_items_error(code=not_found_code)
while True:
try:
immich_fetch = requests.post(
url,
headers={'x-api-key': integration.api_key},
json={
'size': IMMICH_SEARCH_SIZE,
'page': page,
'visibility': IMMICH_SEARCH_VISIBILITY,
**arguments,
},
)
res = immich_fetch.json()
except requests.exceptions.ConnectionError:
return self._immich_connection_error()
if 'assets' in res and 'items' in res['assets']:
items = res['assets']['items']
if items:
return self._paginate_immich_assets(integration, request, items)
if not immich_fetch.ok:
logger.warning(
'Immich search request failed: status=%s response=%s',
immich_fetch.status_code,
res,
)
return self._immich_no_items_error(code=not_found_code)
assets = res.get('assets')
if not isinstance(assets, dict):
break
page_items = assets.get('items') or []
if page_items:
items.extend(page_items)
next_page = assets.get('nextPage')
if next_page:
try:
page = int(next_page)
continue
except (TypeError, ValueError):
logger.warning(
'Immich search returned invalid nextPage value: %s',
next_page,
)
if len(page_items) < IMMICH_SEARCH_SIZE:
break
page += 1
if items:
return self._paginate_immich_assets(integration, request, items)
return self._immich_no_items_error(code=not_found_code)

View File

@@ -1,4 +1,4 @@
export let appVersion = 'v0.13.0-beta-072126';
export let appVersion = 'v0.13.0-beta-072226';
export let appCodename = 'Acadia';
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.12.1';
export let appTitle = 'AdventureLog';