1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-09 23:25:18 +02:00

immich: Allow users to upload the URL's to their images, instead of the entire file

This saves duplicate storage, and stores all of your images in one place, meaning edits will appear in both places!
This commit is contained in:
Thies 2025-02-02 16:48:37 +01:00
parent 659c56f02d
commit 720bd4d142
No known key found for this signature in database
7 changed files with 114 additions and 4 deletions

View file

@ -0,0 +1,31 @@
# Generated by Django 5.0.11 on 2025-02-02 14:59
import adventures.models
import django_resized.forms
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0021_alter_attachment_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='adventureimage',
name='external_url',
field=models.URLField(null=True),
),
migrations.AlterField(
model_name='adventureimage',
name='image',
field=django_resized.forms.ResizedImageField(blank=True, crop=None, force_format='WEBP', keep_meta=True, quality=75, scale=None, size=[1920, 1080], upload_to=adventures.models.PathAndRename('images/')),
),
migrations.AddConstraint(
model_name='adventureimage',
constraint=models.CheckConstraint(check=models.Q(('image__exact', ''), ('external_url__isnull', True), _connector='XOR'), name='image_xor_external_url'),
),
]

View file

@ -277,16 +277,35 @@ class AdventureImage(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey( user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id) User, on_delete=models.CASCADE, default=default_user_id)
image = ResizedImageField( image = ResizedImageField(
force_format="WEBP", force_format="WEBP",
quality=75, quality=75,
upload_to=PathAndRename('images/') # Use the callable class here upload_to=PathAndRename('images/'), # Use the callable class here
blank=True
) )
external_url = models.URLField(null=True)
class Meta:
# Require image, or external_url, but not both -> XOR(^)
# Image is a string(Path to a file), so we can check if it is empty
constraints = [
models.CheckConstraint(
check=models.Q(image__exact='') ^ models.Q(external_url__isnull=True),
name="image_xor_external_url"
)
]
adventure = models.ForeignKey(Adventure, related_name='images', on_delete=models.CASCADE) adventure = models.ForeignKey(Adventure, related_name='images', on_delete=models.CASCADE)
is_primary = models.BooleanField(default=False) is_primary = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return self.image.url if self.external_url is not None:
return self.external_url
else:
return self.image.url
class Attachment(models.Model): class Attachment(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)

View file

@ -9,7 +9,7 @@ from users.serializers import CustomUserDetailsSerializer
class AdventureImageSerializer(CustomModelSerializer): class AdventureImageSerializer(CustomModelSerializer):
class Meta: class Meta:
model = AdventureImage model = AdventureImage
fields = ['id', 'image', 'adventure', 'is_primary', 'user_id'] fields = ['id', 'image', 'adventure', 'is_primary', 'user_id', 'external_url']
read_only_fields = ['id', 'user_id'] read_only_fields = ['id', 'user_id']
def to_representation(self, instance): def to_representation(self, instance):
@ -20,6 +20,11 @@ class AdventureImageSerializer(CustomModelSerializer):
# remove any ' from the url # remove any ' from the url
public_url = public_url.replace("'", "") public_url = public_url.replace("'", "")
representation['image'] = f"{public_url}/media/{instance.image.name}" representation['image'] = f"{public_url}/media/{instance.image.name}"
representation['external_url'] = representation['image']
elif instance.external_url:
representation['image'] = instance.external_url
representation['external_url'] = instance.external_url
return representation return representation
class AttachmentSerializer(CustomModelSerializer): class AttachmentSerializer(CustomModelSerializer):

View file

@ -8,6 +8,7 @@ services:
- PUBLIC_SERVER_URL=http://server:8000 # Should be the service name of the backend with port 8000, even if you change the port in the backend service - PUBLIC_SERVER_URL=http://server:8000 # Should be the service name of the backend with port 8000, even if you change the port in the backend service
- ORIGIN=http://localhost:8015 - ORIGIN=http://localhost:8015
- BODY_SIZE_LIMIT=Infinity - BODY_SIZE_LIMIT=Infinity
- VITE_IMMICH_UPLOAD_URLS_ONLY=false
ports: ports:
- "8015:3000" - "8015:3000"
depends_on: depends_on:

View file

@ -26,3 +26,23 @@ To integrate Immich with AdventureLog, you need to have an Immich server running
3. Now, when you are adding images to an adventure, you will see an option to search for images in Immich or upload from an album. 3. Now, when you are adding images to an adventure, you will see an option to search for images in Immich or upload from an album.
Enjoy the privacy and control of managing your travel media with Immich and AdventureLog! 🎉 Enjoy the privacy and control of managing your travel media with Immich and AdventureLog! 🎉
### How to use the pictures from Immich, but not save them in AdventureLog?
This is possible with the environment variable `VITE_IMMICH_UPLOAD_URLS_ONLY` on the frontend/web container. When set to `true`, AdventureLog will only use the pictures from Immich and not save them in AdventureLog. This can be useful if you want to save storage space on your AdventureLog server but still want to use the pictures from Immich in your adventures.
1. Go to the AdventureLog server and open the `docker-compose` file.
2. Add the following environment variable to the `web` service:
```yaml
environment:
- VITE_IMMICH_UPLOAD_URLS_ONLY=true
```
3. Save the file and restart the AdventureLog server with `docker-compose up -d`.
This saves the URL's in the format of: `https://<frontend-url>/immich/b8a8b977-37b6-48fe-b4a0-1739ed7997dc`. Changing the URL where immich is hosted will not break this, but changing the frontend URL will break the links. To fix this, run the following migration inside your postgres database:
```sql
UPDATE adventures_adventureimage SET external_url = REPLACE(external_url, 'http://127.0.0.1:5173/', 'https://https://adventurelog.app/');
```

View file

@ -1,6 +1,7 @@
PUBLIC_SERVER_URL=http://127.0.0.1:8000 PUBLIC_SERVER_URL=http://127.0.0.1:8000
BODY_SIZE_LIMIT=Infinity BODY_SIZE_LIMIT=Infinity
VITE_IMMICH_UPLOAD_URLS_ONLY=false
# OPTIONAL VARIABLES FOR UMAMI ANALYTICS # OPTIONAL VARIABLES FOR UMAMI ANALYTICS
PUBLIC_UMAMI_SRC= PUBLIC_UMAMI_SRC=

View file

@ -20,6 +20,8 @@
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre'; import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
const immichUploadURLSOnly = import.meta.env.VITE_IMMICH_UPLOAD_URLS_ONLY === 'true';
let query: string = ''; let query: string = '';
let places: OpenStreetMapPlace[] = []; let places: OpenStreetMapPlace[] = [];
let images: { id: string; image: string; is_primary: boolean }[] = []; let images: { id: string; image: string; is_primary: boolean }[] = [];
@ -331,6 +333,27 @@
} }
} }
async function uploadURLToImage(url: string) {
let formData = new FormData();
formData.append('external_url', url);
formData.append('adventure', adventure.id);
let res = await fetch(`/adventures?/image`, {
method: 'POST',
body: formData
});
if (res.ok) {
let newData = deserialize(await res.text()) as { data: { id: string; image: string } };
let newImage = { id: newData.data.id, image: newData.data.image, is_primary: false };
images = [...images, newImage];
adventure.images = images;
addToast('success', $t('adventures.image_upload_success'));
} else {
addToast('error', $t('adventures.image_upload_error'));
}
}
async function uploadImage(file: File) { async function uploadImage(file: File) {
let formData = new FormData(); let formData = new FormData();
formData.append('image', file); formData.append('image', file);
@ -1256,11 +1279,21 @@ it would also work to just use on:click on the MapLibre component itself. -->
</div> </div>
{#if immichIntegration} {#if immichIntegration}
{#if immichUploadURLSOnly}
<i>
We are only uploading the URL of your immich picture, not the picture itself. This is to save space on our servers. If you want to upload the picture, please disable this feature in the frontend environment.
</i>
<br/>
{/if}
<ImmichSelect <ImmichSelect
adventure={adventure} adventure={adventure}
on:fetchImage={(e) => { on:fetchImage={(e) => {
url = e.detail; url = e.detail;
fetchImage(); if (immichUploadURLSOnly) {
uploadURLToImage(url);
} else {
fetchImage();
}
}} }}
/> />
{/if} {/if}