mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-21 22:09:36 +02:00
feat: add Immich album retrieval functionality and implement album selection component
This commit is contained in:
parent
81b60d6021
commit
386014db92
4 changed files with 278 additions and 111 deletions
|
@ -151,6 +151,88 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
||||||
status=status.HTTP_503_SERVICE_UNAVAILABLE
|
status=status.HTTP_503_SERVICE_UNAVAILABLE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
def albums(self, request):
|
||||||
|
"""
|
||||||
|
RESTful GET method for retrieving all Immich albums.
|
||||||
|
"""
|
||||||
|
# Check for integration before proceeding
|
||||||
|
integration = self.check_integration(request)
|
||||||
|
if isinstance(integration, Response):
|
||||||
|
return integration
|
||||||
|
|
||||||
|
# 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}/albums', headers={
|
||||||
|
'x-api-key': integration.api_key
|
||||||
|
})
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
res,
|
||||||
|
status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'], url_path='albums/(?P<albumid>[^/.]+)')
|
||||||
|
def album(self, request, albumid=None):
|
||||||
|
"""
|
||||||
|
RESTful GET method for retrieving a specific Immich album by ID.
|
||||||
|
"""
|
||||||
|
# Check for integration before proceeding
|
||||||
|
integration = self.check_integration(request)
|
||||||
|
if isinstance(integration, Response):
|
||||||
|
return integration
|
||||||
|
|
||||||
|
if not albumid:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
'message': 'Album ID is required.',
|
||||||
|
'error': True,
|
||||||
|
'code': 'immich.albumid_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}/albums/{albumid}', headers={
|
||||||
|
'x-api-key': integration.api_key
|
||||||
|
})
|
||||||
|
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:
|
||||||
|
return Response(
|
||||||
|
res['assets'],
|
||||||
|
status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
'message': 'No assets found in this album.',
|
||||||
|
'error': True,
|
||||||
|
'code': 'immich.no_assets_found'
|
||||||
|
},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
class ImmichIntegrationViewSet(viewsets.ModelViewSet):
|
class ImmichIntegrationViewSet(viewsets.ModelViewSet):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
serializer_class = ImmichIntegrationSerializer
|
serializer_class = ImmichIntegrationSerializer
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
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;
|
||||||
|
@ -33,6 +32,7 @@
|
||||||
import CategoryDropdown from './CategoryDropdown.svelte';
|
import CategoryDropdown from './CategoryDropdown.svelte';
|
||||||
import { findFirstValue } from '$lib';
|
import { findFirstValue } from '$lib';
|
||||||
import MarkdownEditor from './MarkdownEditor.svelte';
|
import MarkdownEditor from './MarkdownEditor.svelte';
|
||||||
|
import ImmichSelect from './ImmichSelect.svelte';
|
||||||
|
|
||||||
let wikiError: string = '';
|
let wikiError: string = '';
|
||||||
|
|
||||||
|
@ -207,7 +207,7 @@
|
||||||
|
|
||||||
// Assuming the first object in the array is the new image
|
// Assuming the first object in the array is the new image
|
||||||
let newImage = {
|
let newImage = {
|
||||||
id: rawData[0].id,
|
id: rawData[1],
|
||||||
image: rawData[2] // This is the URL for the image
|
image: rawData[2] // This is the URL for the image
|
||||||
};
|
};
|
||||||
console.log('New Image:', newImage);
|
console.log('New Image:', newImage);
|
||||||
|
@ -217,6 +217,7 @@
|
||||||
adventure.images = images;
|
adventure.images = images;
|
||||||
|
|
||||||
addToast('success', $t('adventures.image_upload_success'));
|
addToast('success', $t('adventures.image_upload_success'));
|
||||||
|
url = '';
|
||||||
} else {
|
} else {
|
||||||
addToast('error', $t('adventures.image_upload_error'));
|
addToast('error', $t('adventures.image_upload_error'));
|
||||||
}
|
}
|
||||||
|
@ -226,68 +227,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let immichSearchValue: string = '';
|
|
||||||
let immichError: string = '';
|
|
||||||
let immichNext: string = '';
|
|
||||||
let immichPage: number = 1;
|
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
if (data.next) {
|
|
||||||
immichNext =
|
|
||||||
'/api/integrations/immich/search?query=' +
|
|
||||||
immichSearchValue +
|
|
||||||
'&page=' +
|
|
||||||
(immichPage + 1);
|
|
||||||
} else {
|
|
||||||
immichNext = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadMoreImmich() {
|
|
||||||
let res = await fetch(immichNext);
|
|
||||||
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 = [...immichImages, ...data.results];
|
|
||||||
} else {
|
|
||||||
immichError = $t('immich.no_items_found');
|
|
||||||
}
|
|
||||||
if (data.next) {
|
|
||||||
immichNext =
|
|
||||||
'/api/integrations/immich/search?query=' +
|
|
||||||
immichSearchValue +
|
|
||||||
'&page=' +
|
|
||||||
(immichPage + 1);
|
|
||||||
immichPage++;
|
|
||||||
} else {
|
|
||||||
immichNext = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchWikiImage() {
|
async function fetchWikiImage() {
|
||||||
let res = await fetch(`/api/generate/img/?name=${imageSearch}`);
|
let res = await fetch(`/api/generate/img/?name=${imageSearch}`);
|
||||||
let data = await res.json();
|
let data = await res.json();
|
||||||
|
@ -421,7 +360,6 @@
|
||||||
let modal: HTMLDialogElement;
|
let modal: HTMLDialogElement;
|
||||||
|
|
||||||
let immichIntegration: boolean = false;
|
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;
|
||||||
|
@ -1078,52 +1016,12 @@ it would also work to just use on:click on the MapLibre component itself. -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if immichIntegration}
|
{#if immichIntegration}
|
||||||
<div class="mb-4">
|
<ImmichSelect
|
||||||
<label for="immich" class="block font-medium mb-2">
|
on:fetchImage={(e) => {
|
||||||
{$t('immich.immich')}
|
url = e.detail;
|
||||||
<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">
|
|
||||||
<!-- svelte-ignore a11y-img-redundant-alt -->
|
|
||||||
<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();
|
fetchImage();
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{$t('adventures.upload_image')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{#if immichNext}
|
|
||||||
<button class="btn btn-neutral" on:click={loadMoreImmich}
|
|
||||||
>{$t('immich.load_more')}</button
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
159
frontend/src/lib/components/ImmichSelect.svelte
Normal file
159
frontend/src/lib/components/ImmichSelect.svelte
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let immichSearchValue: string = '';
|
||||||
|
let searchOrSelect: string = 'search';
|
||||||
|
let immichError: string = '';
|
||||||
|
let immichNext: string = '';
|
||||||
|
let immichPage: number = 1;
|
||||||
|
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let albums: ImmichAlbum[] = [];
|
||||||
|
let currentAlbum: string = '';
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (currentAlbum) {
|
||||||
|
fetchAlbumAssets(currentAlbum);
|
||||||
|
} else {
|
||||||
|
immichImages = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAlbumAssets(album_id: string) {
|
||||||
|
let res = await fetch(`/api/integrations/immich/albums/${album_id}`);
|
||||||
|
if (res.ok) {
|
||||||
|
let data = await res.json();
|
||||||
|
immichNext = '';
|
||||||
|
immichImages = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
let res = await fetch('/api/integrations/immich/albums');
|
||||||
|
if (res.ok) {
|
||||||
|
let data = await res.json();
|
||||||
|
albums = data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let immichImages: any[] = [];
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import ImmichLogo from '$lib/assets/immich.svg';
|
||||||
|
import type { ImmichAlbum } from '$lib/types';
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
if (data.next) {
|
||||||
|
immichNext =
|
||||||
|
'/api/integrations/immich/search?query=' +
|
||||||
|
immichSearchValue +
|
||||||
|
'&page=' +
|
||||||
|
(immichPage + 1);
|
||||||
|
} else {
|
||||||
|
immichNext = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMoreImmich() {
|
||||||
|
let res = await fetch(immichNext);
|
||||||
|
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 = [...immichImages, ...data.results];
|
||||||
|
} else {
|
||||||
|
immichError = $t('immich.no_items_found');
|
||||||
|
}
|
||||||
|
if (data.next) {
|
||||||
|
immichNext =
|
||||||
|
'/api/integrations/immich/search?query=' +
|
||||||
|
immichSearchValue +
|
||||||
|
'&page=' +
|
||||||
|
(immichPage + 1);
|
||||||
|
immichPage++;
|
||||||
|
} else {
|
||||||
|
immichNext = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={searchOrSelect} value="search" /> Search
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={searchOrSelect} value="select" /> Select Album
|
||||||
|
</label>
|
||||||
|
{#if searchOrSelect === 'search'}
|
||||||
|
<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>
|
||||||
|
{:else}
|
||||||
|
<select class="select select-bordered w-full max-w-xs" bind:value={currentAlbum}>
|
||||||
|
<option value="" disabled selected>Select an Album</option>
|
||||||
|
{#each albums as album}
|
||||||
|
<option value={album.id}>{album.albumName}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{/if}
|
||||||
|
</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">
|
||||||
|
<!-- svelte-ignore a11y-img-redundant-alt -->
|
||||||
|
<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}`;
|
||||||
|
dispatch('fetchImage', fullUrl);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$t('adventures.upload_image')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{#if immichNext}
|
||||||
|
<button class="btn btn-neutral" on:click={loadMoreImmich}>{$t('immich.load_more')}</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -202,3 +202,31 @@ export type ImmichIntegration = {
|
||||||
server_url: string;
|
server_url: string;
|
||||||
api_key: string;
|
api_key: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ImmichAlbum = {
|
||||||
|
albumName: string;
|
||||||
|
description: string;
|
||||||
|
albumThumbnailAssetId: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
id: string;
|
||||||
|
ownerId: string;
|
||||||
|
owner: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
profileImagePath: string;
|
||||||
|
avatarColor: string;
|
||||||
|
profileChangedAt: string;
|
||||||
|
};
|
||||||
|
albumUsers: any[];
|
||||||
|
shared: boolean;
|
||||||
|
hasSharedLink: boolean;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
assets: any[];
|
||||||
|
assetCount: number;
|
||||||
|
isActivityEnabled: boolean;
|
||||||
|
order: string;
|
||||||
|
lastModifiedAssetTimestamp: string;
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue