1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-19 12:59:36 +02:00

feat: add Immich integration view and API documentation, enhance error handling, and include SVG asset

This commit is contained in:
Sean Morley 2025-01-01 16:24:44 -05:00
parent 15fd15ba40
commit 67f6af8ca3
8 changed files with 209 additions and 35 deletions

View file

@ -1,10 +1,11 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from integrations.views import ImmichIntegrationView
from integrations.views import ImmichIntegrationView, IntegrationView
# Create the router and register the ViewSet
router = DefaultRouter()
router.register(r'immich', ImmichIntegrationView, basename='immich')
router.register(r'', IntegrationView, basename='integrations')
# Include the router URLs
urlpatterns = [

View file

@ -7,6 +7,22 @@ from rest_framework.permissions import IsAuthenticated
import requests
from rest_framework.pagination import PageNumberPagination
class IntegrationView(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
def list(self, request):
"""
RESTful GET method for listing all integrations.
"""
immich_integrations = ImmichIntegration.objects.filter(user=request.user)
return Response(
{
'immich': immich_integrations.exists()
},
status=status.HTTP_200_OK
)
class StandardResultsSetPagination(PageNumberPagination):
page_size = 25
page_size_query_param = 'page_size'

View file

@ -165,9 +165,6 @@ TEMPLATES = [
DISABLE_REGISTRATION = getenv('DISABLE_REGISTRATION', 'False') == 'True'
DISABLE_REGISTRATION_MESSAGE = getenv('DISABLE_REGISTRATION_MESSAGE', 'Registration is disabled. Please contact the administrator if you need an account.')
ALLAUTH_UI_THEME = "dark"
SILENCED_SYSTEM_CHECKS = ["slippers.E001"]
AUTH_USER_MODEL = 'users.CustomUser'
ACCOUNT_ADAPTER = 'users.adapters.NoNewUsersAccountAdapter'
@ -223,11 +220,6 @@ REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
}
SWAGGER_SETTINGS = {
'LOGIN_URL': 'login',
'LOGOUT_URL': 'logout',
}
CORS_ALLOWED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()]

View file

@ -53,6 +53,7 @@
>Documentation</a
>
</li>
<li>
<a
target="_blank"
@ -60,6 +61,7 @@
>Source Code</a
>
</li>
<li><a href="/docs">API Docs</a></li>
</ul>
</div>
<!--/.nav-collapse -->

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#fa2921" d="M22.384 14.575c3.143 2.782 5.675 5.764 7.305 8.574 2.8-5.007 4.67-10.957 4.694-14.746V8.33c0-5.607-5.593-7.79-10.411-7.79S13.56 2.723 13.56 8.33v.303c2.686 1.194 5.87 3.327 8.824 5.943"/><path fill="#ed79b5" d="M5.24 29.865c1.965-2.185 4.978-4.554 8.379-6.556 3.618-2.13 7.236-3.617 10.412-4.298-3.897-4.21-8.977-7.827-12.574-9.02l-.07-.023C6.054 8.236 2.25 12.88.762 17.463S-.38 28.039 4.953 29.77z"/><path fill="#ffb400" d="M47.238 17.385c-1.488-4.582-5.292-9.227-10.625-7.494l-.288.093c-.305 2.922-1.35 6.61-2.925 10.229-1.674 3.848-3.728 7.179-5.897 9.597 5.627 1.116 11.863 1.055 15.474-.093l.07-.023c5.333-1.733 5.68-7.727 4.191-12.309"/><path fill="#1e83f7" d="M19.217 34.345c-.907-4.099-1.204-8-.87-11.23-5.208 2.404-10.218 6.118-12.465 9.17l-.043.06c-3.296 4.537-.054 9.59 3.844 12.42 3.898 2.833 9.706 4.355 13.002-.181l.178-.245c-1.471-2.543-2.794-6.142-3.646-9.994"/><path fill="#18c249" d="M42.074 32.052c-2.874.613-6.704.759-10.632.379-4.178-.403-7.98-1.327-10.95-2.643.678 5.695 2.662 11.608 4.87 14.688l.044.06c3.295 4.536 9.103 3.014 13.001.182s7.14-7.885 3.845-12.421z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -13,7 +13,7 @@
import { addToast } from '$lib/toasts';
import { deserialize } from '$app/forms';
import { t } from 'svelte-i18n';
import ImmichLogo from '$lib/assets/immich.svg';
export let longitude: number | null = null;
export let latitude: number | null = null;
export let collection: Collection | null = null;
@ -180,6 +180,7 @@
}
async function fetchImage() {
try {
let res = await fetch(url);
let data = await res.blob();
if (!data) {
@ -190,19 +191,61 @@
let formData = new FormData();
formData.append('image', file);
formData.append('adventure', adventure.id);
let res2 = await fetch(`/adventures?/image`, {
method: 'POST',
body: formData
});
let data2 = await res2.json();
console.log(data2);
if (data2.type === 'success') {
images = [...images, data2];
console.log('Response Data:', data2);
// Deserialize the nested data
let rawData = JSON.parse(data2.data); // Parse the data field
console.log('Deserialized Data:', rawData);
// Assuming the first object in the array is the new image
let newImage = {
id: rawData[0].id,
image: rawData[2] // This is the URL for the image
};
console.log('New Image:', newImage);
// Update images and adventure
images = [...images, newImage];
adventure.images = images;
addToast('success', $t('adventures.image_upload_success'));
} else {
addToast('error', $t('adventures.image_upload_error'));
}
} catch (error) {
console.error('Error in fetchImage:', error);
addToast('error', $t('adventures.image_upload_error'));
}
}
let immichSearchValue: string = '';
let immichError: string = '';
async function searchImmich() {
let res = await fetch(`/api/integrations/immich/search/?query=${immichSearchValue}`);
if (!res.ok) {
let data = await res.json();
let errorMessage = data.message;
console.log(errorMessage);
immichError = $t(data.code);
} else {
let data = await res.json();
console.log(data);
immichError = '';
if (data.results && data.results.length > 0) {
immichImages = data.results;
} else {
immichError = $t('immich.no_items_found');
}
}
}
async function fetchWikiImage() {
@ -337,6 +380,9 @@
const dispatch = createEventDispatcher();
let modal: HTMLDialogElement;
let immichIntegration: boolean = false;
let immichImages: any[] = [];
onMount(async () => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
modal.showModal();
@ -347,6 +393,16 @@
} else {
addToast('error', $t('adventures.category_fetch_error'));
}
// Check for Immich Integration
let res = await fetch('/api/integrations');
if (!res.ok) {
addToast('error', $t('immich.integration_fetch_error'));
} else {
let data = await res.json();
if (data.immich) {
immichIntegration = true;
}
}
});
function close() {
@ -915,10 +971,10 @@ it would also work to just use on:click on the MapLibre component itself. -->
</form>
</div>
{:else}
<p class="text-lg text-gray-600">{$t('adventures.upload_images_here')}</p>
<p class="text-lg">{$t('adventures.upload_images_here')}</p>
<div class="mb-4">
<label for="image" class="block font-medium text-gray-700 mb-2">
<label for="image" class="block font-medium mb-2">
{$t('adventures.image')}
</label>
<form
@ -944,7 +1000,7 @@ it would also work to just use on:click on the MapLibre component itself. -->
</div>
<div class="mb-4">
<label for="url" class="block font-medium text-gray-700 mb-2">
<label for="url" class="block font-medium mb-2">
{$t('adventures.url')}
</label>
<div class="flex gap-2">
@ -963,7 +1019,7 @@ it would also work to just use on:click on the MapLibre component itself. -->
</div>
<div class="mb-4">
<label for="name" class="block font-medium text-gray-700 mb-2">
<label for="name" class="block font-medium mb-2">
{$t('adventures.wikipedia')}
</label>
<div class="flex gap-2">
@ -981,6 +1037,49 @@ it would also work to just use on:click on the MapLibre component itself. -->
</div>
</div>
{#if immichIntegration}
<div class="mb-4">
<label for="immich" class="block font-medium mb-2">
{$t('immich.immich')}
<img src={ImmichLogo} alt="Immich Logo" class="h-6 w-6 inline-block -mt-1" />
</label>
<!-- search bar -->
<div>
<input
type="text"
placeholder="Type here"
bind:value={immichSearchValue}
class="input input-bordered w-full max-w-xs"
/>
<button on:click={searchImmich} class="btn btn-neutral mt-2">Search</button>
</div>
<p class="text-red-500">{immichError}</p>
<div class="flex flex-wrap gap-4 mr-4 mt-2">
{#each immichImages as image}
<div class="flex flex-col items-center gap-2">
<img
src={`/immich/${image.id}`}
alt="Image from Immich"
class="h-24 w-24 object-cover rounded-md"
/>
<button
type="button"
class="btn btn-sm btn-primary"
on:click={() => {
let currentDomain = window.location.origin;
let fullUrl = `${currentDomain}/immich/${image.id}`;
url = fullUrl;
fetchImage();
}}
>
{$t('adventures.upload_image')}
</button>
</div>
{/each}
</div>
</div>
{/if}
<div class="divider"></div>
{#if images.length > 0}

View file

@ -500,5 +500,14 @@
"recent_adventures": "Recent Adventures",
"no_recent_adventures": "No recent adventures?",
"add_some": "Why not start planning your next adventure? You can add a new adventure by clicking the button below."
},
"immich": {
"immich": "Immich",
"integration_fetch_error": "Error fetching data from the Immich integration",
"integration_missing": "The Immich integration is missing from the backend",
"query_required": "Query is required",
"server_down": "The Immich server is currently down or unreachable",
"no_items_found": "No items found",
"imageid_required": "Image ID is required"
}
}

View file

@ -0,0 +1,54 @@
import type { RequestHandler } from './$types';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const GET: RequestHandler = async (event) => {
try {
const key = event.params.key;
// Forward the session ID from cookies
const sessionid = event.cookies.get('sessionid');
if (!sessionid) {
return new Response(JSON.stringify({ error: 'Session ID is missing' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
// Proxy the request to the backend
const res = await fetch(`${endpoint}/api/integrations/immich/get/${key}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Cookie: `sessionid=${sessionid}`
}
});
if (!res.ok) {
// Return an error response if the backend request fails
const errorData = await res.json();
return new Response(JSON.stringify(errorData), {
status: res.status,
headers: { 'Content-Type': 'application/json' }
});
}
// Get the image as a Blob
const image = await res.blob();
// Create a Response to pass the image back
return new Response(image, {
status: res.status,
headers: {
'Content-Type': res.headers.get('Content-Type') || 'image/jpeg'
}
});
} catch (error) {
console.error('Error proxying request:', error);
return new Response(JSON.stringify({ error: 'Failed to fetch image' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
};