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:
parent
dc89743569
commit
15fd15ba40
4 changed files with 167 additions and 68 deletions
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue