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:
parent
659c56f02d
commit
720bd4d142
7 changed files with 114 additions and 4 deletions
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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/');
|
||||||
|
```
|
|
@ -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=
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue