1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-22 14:29:36 +02:00

feat: update Immich integration to use OneToOneField for user and enhance image retrieval functionality

This commit is contained in:
Sean Morley 2025-01-01 11:00:11 -05:00
parent dc89743569
commit 15fd15ba40
4 changed files with 167 additions and 68 deletions

View file

@ -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),
),
]

View file

@ -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

View file

@ -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,7 +56,8 @@ 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
},
@ -55,9 +66,25 @@ class ImmichIntegrationView(viewsets.ViewSet):
}
)
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']:
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<imageid>[^/.]+)')
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
)

View file

@ -915,84 +915,105 @@ it would also work to just use on:click on the MapLibre component itself. -->
</form>
</div>
{:else}
<p>{$t('adventures.upload_images_here')}</p>
<!-- <p>{adventureToEdit.id}</p> -->
<div class="mb-2">
<label for="image">{$t('adventures.image')} </label><br />
<div class="flex">
<p class="text-lg text-gray-600">{$t('adventures.upload_images_here')}</p>
<div class="mb-4">
<label for="image" class="block font-medium text-gray-700 mb-2">
{$t('adventures.image')}
</label>
<form
method="POST"
action="/adventures?/image"
use:enhance={imageSubmit}
enctype="multipart/form-data"
class="flex flex-col items-start gap-2"
>
<input
type="file"
name="image"
class="file-input file-input-bordered w-full max-w-xs"
class="file-input file-input-bordered w-full max-w-sm"
bind:this={fileInput}
accept="image/*"
id="image"
/>
<input type="hidden" name="adventure" value={adventure.id} id="adventure" />
<button class="btn btn-neutral mt-2 mb-2" type="submit"
>{$t('adventures.upload_image')}</button
>
<button class="btn btn-neutral w-full max-w-sm" type="submit">
{$t('adventures.upload_image')}
</button>
</form>
</div>
<div class="mt-2">
<label for="url">{$t('adventures.url')}</label><br />
<div class="mb-4">
<label for="url" class="block font-medium text-gray-700 mb-2">
{$t('adventures.url')}
</label>
<div class="flex gap-2">
<input
type="text"
id="url"
name="url"
bind:value={url}
class="input input-bordered w-full"
class="input input-bordered flex-1"
placeholder="Enter image URL"
/>
<button class="btn btn-neutral mt-2" type="button" on:click={fetchImage}
>{$t('adventures.fetch_image')}</button
>
<button class="btn btn-neutral" type="button" on:click={fetchImage}>
{$t('adventures.fetch_image')}
</button>
</div>
<div class="mt-2">
<label for="name">{$t('adventures.wikipedia')}</label><br />
</div>
<div class="mb-4">
<label for="name" class="block font-medium text-gray-700 mb-2">
{$t('adventures.wikipedia')}
</label>
<div class="flex gap-2">
<input
type="text"
id="name"
name="name"
bind:value={imageSearch}
class="input input-bordered w-full"
class="input input-bordered flex-1"
placeholder="Search Wikipedia for images"
/>
<button class="btn btn-neutral mt-2" type="button" on:click={fetchWikiImage}
>{$t('adventures.fetch_image')}</button
>
<button class="btn btn-neutral" type="button" on:click={fetchWikiImage}>
{$t('adventures.fetch_image')}
</button>
</div>
</div>
<div class="divider"></div>
{#if images.length > 0}
<h1 class="font-semibold text-xl">{$t('adventures.my_images')}</h1>
{:else}
<h1 class="font-semibold text-xl">{$t('adventures.no_images')}</h1>
{/if}
<div class="flex flex-wrap gap-2 mt-2">
<h1 class="font-semibold text-xl mb-4">{$t('adventures.my_images')}</h1>
<div class="flex flex-wrap gap-4">
{#each images as image}
<div class="relative h-32 w-32">
<button
type="button"
class="absolute top-0 left-0 btn btn-error btn-sm z-10"
class="absolute top-1 right-1 btn btn-error btn-xs z-10"
on:click={() => removeImage(image.id)}
>
X
</button>
<img src={image.image} alt={image.id} class="w-full h-full object-cover" />
<img
src={image.image}
alt={image.id}
class="w-full h-full object-cover rounded-md shadow-md"
/>
</div>
{/each}
</div>
</div>
<div class="mt-4">
<button type="button" class="btn btn-primary" on:click={saveAndClose}
>{$t('about.close')}</button
>
{:else}
<h1 class="font-semibold text-xl text-gray-500">{$t('adventures.no_images')}</h1>
{/if}
<div class="mt-6">
<button type="button" class="btn btn-primary w-full max-w-sm" on:click={saveAndClose}>
{$t('about.close')}
</button>
</div>
{/if}
{#if adventure.is_public && adventure.id}
<div class="bg-neutral p-4 mt-2 rounded-md shadow-sm">
<p class=" font-semibold">{$t('adventures.share_adventure')}</p>