mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2026-07-30 15:28:30 -04:00
Update Immich integration for Immich v3.0
This commit is contained in:
95
backend/server/integrations/tests/test_immich_view.py
Normal file
95
backend/server/integrations/tests/test_immich_view.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIRequestFactory, force_authenticate
|
||||
|
||||
from integrations.models import ImmichIntegration
|
||||
from integrations.views.immich_view import ImmichIntegrationView
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class ImmichViewTests(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = APIRequestFactory()
|
||||
self.user = User.objects.create_user(username='immich-user', password='test-pass')
|
||||
self.integration = ImmichIntegration.objects.create(
|
||||
user=self.user,
|
||||
server_url='http://immich.example.com/api',
|
||||
api_key='test-api-key',
|
||||
)
|
||||
self.view = ImmichIntegrationView.as_view({'get': 'search'})
|
||||
self.album_view = ImmichIntegrationView.as_view({'get': 'album'})
|
||||
|
||||
@patch('integrations.views.immich_view.requests.post')
|
||||
def test_search_by_date_uses_iso8601_datetime_range(self, mock_post):
|
||||
mock_response = MagicMock()
|
||||
mock_response.ok = True
|
||||
mock_response.json.return_value = {
|
||||
'assets': {
|
||||
'items': [{'id': 'asset-1'}],
|
||||
}
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
request = self.factory.get('/api/integrations/immich/search/', {'date': '2024-06-15'})
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = self.view(request)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
mock_post.assert_called_once()
|
||||
_, 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')
|
||||
|
||||
@patch('integrations.views.immich_view.requests.get')
|
||||
@patch('integrations.views.immich_view.requests.post')
|
||||
def test_album_falls_back_to_metadata_search_without_inline_assets(self, mock_post, mock_get):
|
||||
mock_get_response = MagicMock()
|
||||
mock_get_response.json.return_value = {
|
||||
'id': 'album-1',
|
||||
'albumName': 'Trip Photos',
|
||||
}
|
||||
mock_get.return_value = mock_get_response
|
||||
|
||||
mock_post_response = MagicMock()
|
||||
mock_post_response.ok = True
|
||||
mock_post_response.json.return_value = {
|
||||
'assets': {
|
||||
'items': [{'id': 'asset-1'}, {'id': 'asset-2'}],
|
||||
}
|
||||
}
|
||||
mock_post.return_value = mock_post_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)
|
||||
mock_post.assert_called_once()
|
||||
_, kwargs = mock_post.call_args
|
||||
self.assertEqual(kwargs['json']['albumIds'], ['album-1'])
|
||||
self.assertEqual(len(response.data['results']), 2)
|
||||
|
||||
@patch('integrations.views.immich_view.requests.get')
|
||||
def test_album_uses_inline_assets_when_available(self, mock_get):
|
||||
mock_get_response = MagicMock()
|
||||
mock_get_response.json.return_value = {
|
||||
'id': 'album-1',
|
||||
'albumName': 'Trip Photos',
|
||||
'assets': [{'id': 'asset-legacy-1'}],
|
||||
}
|
||||
mock_get.return_value = mock_get_response
|
||||
|
||||
request = self.factory.get('/api/integrations/immich/albums/album-1/')
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
with patch('integrations.views.immich_view.requests.post') as mock_post:
|
||||
response = self.album_view(request, albumid='album-1')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
mock_post.assert_not_called()
|
||||
self.assertEqual(len(response.data['results']), 1)
|
||||
@@ -1,4 +1,6 @@
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import viewsets, status
|
||||
from integrations.serializers import ImmichIntegrationSerializer
|
||||
@@ -14,6 +16,8 @@ import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
IMMICH_SEARCH_SIZE = 1000
|
||||
|
||||
class ImmichIntegrationView(viewsets.ViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
pagination_class = StandardResultsSetPagination
|
||||
@@ -48,6 +52,78 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
||||
|
||||
return user_integrations.first()
|
||||
|
||||
def _immich_connection_error(self):
|
||||
return Response(
|
||||
{
|
||||
'message': 'The Immich server is currently down or unreachable.',
|
||||
'error': True,
|
||||
'code': 'immich.server_down'
|
||||
},
|
||||
status=status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
)
|
||||
|
||||
def _immich_no_items_error(self, code='immich.no_items_found', message='No items found.'):
|
||||
return Response(
|
||||
{
|
||||
'message': message,
|
||||
'error': True,
|
||||
'code': code
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
def _build_date_range_arguments(self, date):
|
||||
selected_date = datetime.strptime(date, '%Y-%m-%d')
|
||||
next_day = selected_date + timedelta(days=1)
|
||||
return {
|
||||
'takenAfter': f'{selected_date.strftime("%Y-%m-%d")}T00:00:00.000Z',
|
||||
'takenBefore': f'{next_day.strftime("%Y-%m-%d")}T00:00:00.000Z',
|
||||
}
|
||||
|
||||
def _paginate_immich_assets(self, integration, request, items):
|
||||
public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')
|
||||
public_url = public_url.replace("'", "")
|
||||
for item in items:
|
||||
item['image_url'] = f'{public_url}/api/integrations/immich/{integration.id}/get/{item["id"]}'
|
||||
|
||||
paginator = self.pagination_class()
|
||||
result_page = paginator.paginate_queryset(items, request)
|
||||
return paginator.get_paginated_response(result_page)
|
||||
|
||||
def _fetch_immich_search_assets(
|
||||
self,
|
||||
integration,
|
||||
request,
|
||||
arguments,
|
||||
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()
|
||||
|
||||
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)
|
||||
|
||||
if 'assets' in res and 'items' in res['assets']:
|
||||
items = res['assets']['items']
|
||||
if items:
|
||||
return self._paginate_immich_assets(integration, request, items)
|
||||
|
||||
return self._immich_no_items_error(code=not_found_code)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='search')
|
||||
def search(self, request):
|
||||
"""
|
||||
@@ -75,16 +151,8 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
||||
if query:
|
||||
arguments['query'] = query
|
||||
if date:
|
||||
# Create date range for the entire selected day
|
||||
from datetime import datetime, timedelta
|
||||
try:
|
||||
# Parse the date and create start/end of day
|
||||
selected_date = datetime.strptime(date, '%Y-%m-%d')
|
||||
start_of_day = selected_date.strftime('%Y-%m-%d')
|
||||
end_of_day = (selected_date + timedelta(days=1)).strftime('%Y-%m-%d')
|
||||
|
||||
arguments['takenAfter'] = start_of_day
|
||||
arguments['takenBefore'] = end_of_day
|
||||
arguments.update(self._build_date_range_arguments(date))
|
||||
except ValueError:
|
||||
return Response(
|
||||
{
|
||||
@@ -95,43 +163,12 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# check so if the server is down, it does not tweak out like a madman and crash the server with a 500 error code
|
||||
try:
|
||||
url = f'{integration.server_url}/search/{"smart" if query else "metadata"}'
|
||||
immich_fetch = requests.post(url, headers={
|
||||
'x-api-key': integration.api_key
|
||||
},
|
||||
json = arguments
|
||||
)
|
||||
res = immich_fetch.json()
|
||||
except requests.exceptions.ConnectionError:
|
||||
return Response(
|
||||
{
|
||||
'message': 'The Immich server is currently down or unreachable.',
|
||||
'error': True,
|
||||
'code': 'immich.server_down'
|
||||
},
|
||||
status=status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
)
|
||||
|
||||
if 'assets' in res and 'items' in res['assets']:
|
||||
paginator = self.pagination_class()
|
||||
# for each item in the items, we need to add the image url to the item so we can display it in the frontend
|
||||
public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')
|
||||
public_url = public_url.replace("'", "")
|
||||
for item in res['assets']['items']:
|
||||
item['image_url'] = f'{public_url}/api/integrations/immich/{integration.id}/get/{item["id"]}'
|
||||
result_page = paginator.paginate_queryset(res['assets']['items'], request)
|
||||
return paginator.get_paginated_response(result_page)
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
'message': 'No items found.',
|
||||
'error': True,
|
||||
'code': 'immich.no_items_found'
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return self._fetch_immich_search_assets(
|
||||
integration,
|
||||
request,
|
||||
arguments,
|
||||
search_type='smart' if query else 'metadata',
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def albums(self, request):
|
||||
@@ -171,7 +208,6 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
||||
"""
|
||||
# Check for integration before proceeding
|
||||
integration = self.check_integration(request)
|
||||
print(integration.user)
|
||||
if isinstance(integration, Response):
|
||||
return integration
|
||||
|
||||
@@ -201,24 +237,17 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
||||
status=status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
)
|
||||
|
||||
if 'assets' in res:
|
||||
paginator = self.pagination_class()
|
||||
# for each item in the items, we need to add the image url to the item so we can display it in the frontend
|
||||
public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')
|
||||
public_url = public_url.replace("'", "")
|
||||
for item in res['assets']:
|
||||
item['image_url'] = f'{public_url}/api/integrations/immich/{integration.id}/get/{item["id"]}'
|
||||
result_page = paginator.paginate_queryset(res['assets'], request)
|
||||
return paginator.get_paginated_response(result_page)
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
'message': 'No assets found in this album.',
|
||||
'error': True,
|
||||
'code': 'immich.no_assets_found'
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
album_assets = res.get('assets')
|
||||
if isinstance(album_assets, list) and album_assets:
|
||||
return self._paginate_immich_assets(integration, request, album_assets)
|
||||
|
||||
return self._fetch_immich_search_assets(
|
||||
integration,
|
||||
request,
|
||||
{'albumIds': [albumid]},
|
||||
search_type='metadata',
|
||||
not_found_code='immich.no_assets_found',
|
||||
)
|
||||
|
||||
@action(
|
||||
detail=False,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export let appVersion = 'v0.13.0-beta-072026';
|
||||
export let appVersion = 'v0.13.0-beta-072126';
|
||||
export let appCodename = 'Acadia';
|
||||
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.12.1';
|
||||
export let appTitle = 'AdventureLog';
|
||||
|
||||
Reference in New Issue
Block a user