1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-22 22:39: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): class ImmichIntegration(models.Model):
server_url = models.CharField(max_length=255) server_url = models.CharField(max_length=255)
api_key = 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): def __str__(self):
return self.user.username + ' - ' + self.server_url return self.user.username + ' - ' + self.server_url

View file

@ -1,10 +1,20 @@
import os
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import viewsets, status from rest_framework import viewsets, status
from .models import ImmichIntegration from .models import ImmichIntegration
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
import requests 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): class ImmichIntegrationView(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
pagination_class = StandardResultsSetPagination
def check_integration(self, request): def check_integration(self, request):
""" """
Checks if the user has an active Immich integration. Checks if the user has an active Immich integration.
@ -46,7 +56,8 @@ class ImmichIntegrationView(viewsets.ViewSet):
status=status.HTTP_400_BAD_REQUEST 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={ immich_fetch = requests.post(f'{integration.server_url}/search/smart', headers={
'x-api-key': integration.api_key 'x-api-key': integration.api_key
}, },
@ -55,9 +66,25 @@ class ImmichIntegrationView(viewsets.ViewSet):
} }
) )
res = immich_fetch.json() 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']: 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: else:
return Response( return Response(
{ {
@ -68,10 +95,40 @@ class ImmichIntegrationView(viewsets.ViewSet):
status=status.HTTP_404_NOT_FOUND status=status.HTTP_404_NOT_FOUND
) )
@action(detail=False, methods=['get'], url_path='get/(?P<imageid>[^/.]+)')
def get(self, request, imageid=None):
def get(self, request):
""" """
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> </form>
</div> </div>
{:else} {:else}
<p>{$t('adventures.upload_images_here')}</p> <p class="text-lg text-gray-600">{$t('adventures.upload_images_here')}</p>
<!-- <p>{adventureToEdit.id}</p> -->
<div class="mb-2"> <div class="mb-4">
<label for="image">{$t('adventures.image')} </label><br /> <label for="image" class="block font-medium text-gray-700 mb-2">
<div class="flex"> {$t('adventures.image')}
</label>
<form <form
method="POST" method="POST"
action="/adventures?/image" action="/adventures?/image"
use:enhance={imageSubmit} use:enhance={imageSubmit}
enctype="multipart/form-data" enctype="multipart/form-data"
class="flex flex-col items-start gap-2"
> >
<input <input
type="file" type="file"
name="image" 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} bind:this={fileInput}
accept="image/*" accept="image/*"
id="image" id="image"
/> />
<input type="hidden" name="adventure" value={adventure.id} id="adventure" /> <input type="hidden" name="adventure" value={adventure.id} id="adventure" />
<button class="btn btn-neutral mt-2 mb-2" type="submit" <button class="btn btn-neutral w-full max-w-sm" type="submit">
>{$t('adventures.upload_image')}</button {$t('adventures.upload_image')}
> </button>
</form> </form>
</div> </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 <input
type="text" type="text"
id="url" id="url"
name="url" name="url"
bind:value={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} <button class="btn btn-neutral" type="button" on:click={fetchImage}>
>{$t('adventures.fetch_image')}</button {$t('adventures.fetch_image')}
> </button>
</div> </div>
<div class="mt-2"> </div>
<label for="name">{$t('adventures.wikipedia')}</label><br />
<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 <input
type="text" type="text"
id="name" id="name"
name="name" name="name"
bind:value={imageSearch} 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} <button class="btn btn-neutral" type="button" on:click={fetchWikiImage}>
>{$t('adventures.fetch_image')}</button {$t('adventures.fetch_image')}
> </button>
</div> </div>
</div>
<div class="divider"></div> <div class="divider"></div>
{#if images.length > 0} {#if images.length > 0}
<h1 class="font-semibold text-xl">{$t('adventures.my_images')}</h1> <h1 class="font-semibold text-xl mb-4">{$t('adventures.my_images')}</h1>
{:else} <div class="flex flex-wrap gap-4">
<h1 class="font-semibold text-xl">{$t('adventures.no_images')}</h1>
{/if}
<div class="flex flex-wrap gap-2 mt-2">
{#each images as image} {#each images as image}
<div class="relative h-32 w-32"> <div class="relative h-32 w-32">
<button <button
type="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)} on:click={() => removeImage(image.id)}
> >
X
</button> </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> </div>
{/each} {/each}
</div> </div>
</div> {:else}
<div class="mt-4"> <h1 class="font-semibold text-xl text-gray-500">{$t('adventures.no_images')}</h1>
<button type="button" class="btn btn-primary" on:click={saveAndClose} {/if}
>{$t('about.close')}</button
> <div class="mt-6">
<button type="button" class="btn btn-primary w-full max-w-sm" on:click={saveAndClose}>
{$t('about.close')}
</button>
</div> </div>
{/if} {/if}
{#if adventure.is_public && adventure.id} {#if adventure.is_public && adventure.id}
<div class="bg-neutral p-4 mt-2 rounded-md shadow-sm"> <div class="bg-neutral p-4 mt-2 rounded-md shadow-sm">
<p class=" font-semibold">{$t('adventures.share_adventure')}</p> <p class=" font-semibold">{$t('adventures.share_adventure')}</p>