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:
parent
15fd15ba40
commit
67f6af8ca3
8 changed files with 209 additions and 35 deletions
|
@ -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 = [
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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()]
|
||||
|
||||
|
||||
|
|
|
@ -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 -->
|
||||
|
|
1
frontend/src/lib/assets/immich.svg
Normal file
1
frontend/src/lib/assets/immich.svg
Normal 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 |
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
54
frontend/src/routes/immich/[key]/+server.ts
Normal file
54
frontend/src/routes/immich/[key]/+server.ts
Normal 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' }
|
||||
});
|
||||
}
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue