mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-23 06:49:37 +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 django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from integrations.views import ImmichIntegrationView
|
from integrations.views import ImmichIntegrationView, IntegrationView
|
||||||
|
|
||||||
# Create the router and register the ViewSet
|
# Create the router and register the ViewSet
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'immich', ImmichIntegrationView, basename='immich')
|
router.register(r'immich', ImmichIntegrationView, basename='immich')
|
||||||
|
router.register(r'', IntegrationView, basename='integrations')
|
||||||
|
|
||||||
# Include the router URLs
|
# Include the router URLs
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
|
@ -7,6 +7,22 @@ from rest_framework.permissions import IsAuthenticated
|
||||||
import requests
|
import requests
|
||||||
from rest_framework.pagination import PageNumberPagination
|
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):
|
class StandardResultsSetPagination(PageNumberPagination):
|
||||||
page_size = 25
|
page_size = 25
|
||||||
page_size_query_param = 'page_size'
|
page_size_query_param = 'page_size'
|
||||||
|
|
|
@ -165,9 +165,6 @@ TEMPLATES = [
|
||||||
DISABLE_REGISTRATION = getenv('DISABLE_REGISTRATION', 'False') == 'True'
|
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.')
|
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'
|
AUTH_USER_MODEL = 'users.CustomUser'
|
||||||
|
|
||||||
ACCOUNT_ADAPTER = 'users.adapters.NoNewUsersAccountAdapter'
|
ACCOUNT_ADAPTER = 'users.adapters.NoNewUsersAccountAdapter'
|
||||||
|
@ -223,11 +220,6 @@ REST_FRAMEWORK = {
|
||||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
|
'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()]
|
CORS_ALLOWED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
>Documentation</a
|
>Documentation</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -60,6 +61,7 @@
|
||||||
>Source Code</a
|
>Source Code</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
<li><a href="/docs">API Docs</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<!--/.nav-collapse -->
|
<!--/.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 { addToast } from '$lib/toasts';
|
||||||
import { deserialize } from '$app/forms';
|
import { deserialize } from '$app/forms';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import ImmichLogo from '$lib/assets/immich.svg';
|
||||||
export let longitude: number | null = null;
|
export let longitude: number | null = null;
|
||||||
export let latitude: number | null = null;
|
export let latitude: number | null = null;
|
||||||
export let collection: Collection | null = null;
|
export let collection: Collection | null = null;
|
||||||
|
@ -180,6 +180,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchImage() {
|
async function fetchImage() {
|
||||||
|
try {
|
||||||
let res = await fetch(url);
|
let res = await fetch(url);
|
||||||
let data = await res.blob();
|
let data = await res.blob();
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
@ -190,19 +191,61 @@
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
formData.append('image', file);
|
formData.append('image', file);
|
||||||
formData.append('adventure', adventure.id);
|
formData.append('adventure', adventure.id);
|
||||||
|
|
||||||
let res2 = await fetch(`/adventures?/image`, {
|
let res2 = await fetch(`/adventures?/image`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
let data2 = await res2.json();
|
let data2 = await res2.json();
|
||||||
console.log(data2);
|
|
||||||
if (data2.type === 'success') {
|
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;
|
adventure.images = images;
|
||||||
|
|
||||||
addToast('success', $t('adventures.image_upload_success'));
|
addToast('success', $t('adventures.image_upload_success'));
|
||||||
} else {
|
} else {
|
||||||
addToast('error', $t('adventures.image_upload_error'));
|
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() {
|
async function fetchWikiImage() {
|
||||||
|
@ -337,6 +380,9 @@
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
let modal: HTMLDialogElement;
|
let modal: HTMLDialogElement;
|
||||||
|
|
||||||
|
let immichIntegration: boolean = false;
|
||||||
|
let immichImages: any[] = [];
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||||
modal.showModal();
|
modal.showModal();
|
||||||
|
@ -347,6 +393,16 @@
|
||||||
} else {
|
} else {
|
||||||
addToast('error', $t('adventures.category_fetch_error'));
|
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() {
|
function close() {
|
||||||
|
@ -915,10 +971,10 @@ it would also work to just use on:click on the MapLibre component itself. -->
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{: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">
|
<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')}
|
{$t('adventures.image')}
|
||||||
</label>
|
</label>
|
||||||
<form
|
<form
|
||||||
|
@ -944,7 +1000,7 @@ it would also work to just use on:click on the MapLibre component itself. -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<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')}
|
{$t('adventures.url')}
|
||||||
</label>
|
</label>
|
||||||
<div class="flex gap-2">
|
<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>
|
||||||
|
|
||||||
<div class="mb-4">
|
<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')}
|
{$t('adventures.wikipedia')}
|
||||||
</label>
|
</label>
|
||||||
<div class="flex gap-2">
|
<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>
|
||||||
</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>
|
<div class="divider"></div>
|
||||||
|
|
||||||
{#if images.length > 0}
|
{#if images.length > 0}
|
||||||
|
|
|
@ -500,5 +500,14 @@
|
||||||
"recent_adventures": "Recent Adventures",
|
"recent_adventures": "Recent Adventures",
|
||||||
"no_recent_adventures": "No 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."
|
"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