From 15fd15ba40cf20adde92a6c2f1f98a7ed369bc56 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 1 Jan 2025 11:00:11 -0500 Subject: [PATCH] feat: update Immich integration to use OneToOneField for user and enhance image retrieval functionality --- .../0002_alter_immichintegration_user.py | 21 +++ backend/server/integrations/models.py | 2 +- backend/server/integrations/views.py | 87 +++++++++--- .../src/lib/components/AdventureModal.svelte | 125 ++++++++++-------- 4 files changed, 167 insertions(+), 68 deletions(-) create mode 100644 backend/server/integrations/migrations/0002_alter_immichintegration_user.py diff --git a/backend/server/integrations/migrations/0002_alter_immichintegration_user.py b/backend/server/integrations/migrations/0002_alter_immichintegration_user.py new file mode 100644 index 0000000..6f6912b --- /dev/null +++ b/backend/server/integrations/migrations/0002_alter_immichintegration_user.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.8 on 2024-12-31 18:29 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('integrations', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='immichintegration', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/backend/server/integrations/models.py b/backend/server/integrations/models.py index d3394cc..0d7a0a4 100644 --- a/backend/server/integrations/models.py +++ b/backend/server/integrations/models.py @@ -6,7 +6,7 @@ User = get_user_model() class ImmichIntegration(models.Model): server_url = models.CharField(max_length=255) api_key = models.CharField(max_length=255) - user = models.ForeignKey(User, on_delete=models.CASCADE) + user = models.OneToOneField(User, on_delete=models.CASCADE) def __str__(self): return self.user.username + ' - ' + self.server_url \ No newline at end of file diff --git a/backend/server/integrations/views.py b/backend/server/integrations/views.py index 2622df9..72c9c4a 100644 --- a/backend/server/integrations/views.py +++ b/backend/server/integrations/views.py @@ -1,10 +1,20 @@ +import os from rest_framework.response import Response from rest_framework import viewsets, status from .models import ImmichIntegration from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated import requests +from rest_framework.pagination import PageNumberPagination + +class StandardResultsSetPagination(PageNumberPagination): + page_size = 25 + page_size_query_param = 'page_size' + max_page_size = 1000 class ImmichIntegrationView(viewsets.ViewSet): + permission_classes = [IsAuthenticated] + pagination_class = StandardResultsSetPagination def check_integration(self, request): """ Checks if the user has an active Immich integration. @@ -46,18 +56,35 @@ 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: + immich_fetch = requests.post(f'{integration.server_url}/search/smart', headers={ + 'x-api-key': integration.api_key + }, + json = { + 'query': query + } + ) + 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 + ) - immich_fetch = requests.post(f'{integration.server_url}/search/smart', headers={ - 'x-api-key': integration.api_key - }, - json = { - 'query': query - } - ) - res = immich_fetch.json() - if 'assets' in res and 'items' in res['assets']: - return Response(res['assets']['items'], status=status.HTTP_200_OK) + 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/get/{item["id"]}' + result_page = paginator.paginate_queryset(res['assets']['items'], request) + return paginator.get_paginated_response(result_page) else: return Response( { @@ -68,10 +95,40 @@ class ImmichIntegrationView(viewsets.ViewSet): status=status.HTTP_404_NOT_FOUND ) - - - def get(self, request): + @action(detail=False, methods=['get'], url_path='get/(?P[^/.]+)') + def get(self, request, imageid=None): """ - RESTful GET method for searching Immich images. + RESTful GET method for retrieving a specific Immich image by ID. """ - return self.search(request) + # Check for integration before proceeding + integration = self.check_integration(request) + if isinstance(integration, Response): + return integration + + if not imageid: + return Response( + { + 'message': 'Image ID is required.', + 'error': True, + 'code': 'immich.imageid_required' + }, + 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: + immich_fetch = requests.get(f'{integration.server_url}/assets/{imageid}/thumbnail?size=preview', headers={ + 'x-api-key': integration.api_key + }) + # should return the image file + from django.http import HttpResponse + return HttpResponse(immich_fetch.content, content_type='image/jpeg', status=status.HTTP_200_OK) + 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 + ) diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index ff9770d..cc081a0 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -915,84 +915,105 @@ it would also work to just use on:click on the MapLibre component itself. --> {:else} -

{$t('adventures.upload_images_here')}

- -
-
-
-
- - - -
-
-
-
+

{$t('adventures.upload_images_here')}

+ +
+ +
+ + + +
+
+ +
+ +
- +
-
-
+
+ +
+ +
- +
-
- {#if images.length > 0} -

{$t('adventures.my_images')}

- {:else} -

{$t('adventures.no_images')}

- {/if} -
+
+ +
+ + {#if images.length > 0} +

{$t('adventures.my_images')}

+
{#each images as image}
- {image.id} + {image.id}
{/each}
-
-
- + {:else} +

{$t('adventures.no_images')}

+ {/if} + +
+
{/if} + {#if adventure.is_public && adventure.id}

{$t('adventures.share_adventure')}