Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 1 commit into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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'),
),
]
23 changes: 21 additions & 2 deletions backend/server/adventures/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,16 +277,35 @@ class AdventureImage(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)

image = ResizedImageField(
force_format="WEBP",
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)
is_primary = models.BooleanField(default=False)

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):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
Expand Down
7 changes: 6 additions & 1 deletion backend/server/adventures/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
class AdventureImageSerializer(CustomModelSerializer):
class Meta:
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']

def to_representation(self, instance):
Expand All @@ -20,6 +20,11 @@ def to_representation(self, instance):
# remove any ' from the url
public_url = public_url.replace("'", "")
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

class AttachmentSerializer(CustomModelSerializer):
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
- ORIGIN=http://localhost:8015
- BODY_SIZE_LIMIT=Infinity
- VITE_IMMICH_UPLOAD_URLS_ONLY=false
ports:
- "8015:3000"
depends_on:
Expand Down
20 changes: 20 additions & 0 deletions documentation/docs/configuration/immich_integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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 change: 1 addition & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
PUBLIC_SERVER_URL=http://127.0.0.1:8000
BODY_SIZE_LIMIT=Infinity

VITE_IMMICH_UPLOAD_URLS_ONLY=false

# OPTIONAL VARIABLES FOR UMAMI ANALYTICS
PUBLIC_UMAMI_SRC=
Expand Down
35 changes: 34 additions & 1 deletion frontend/src/lib/components/AdventureModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';

const immichUploadURLSOnly = import.meta.env.VITE_IMMICH_UPLOAD_URLS_ONLY === 'true';

let query: string = '';
let places: OpenStreetMapPlace[] = [];
let images: { id: string; image: string; is_primary: boolean }[] = [];
Expand Down Expand Up @@ -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) {
let formData = new FormData();
formData.append('image', file);
Expand Down Expand Up @@ -1256,11 +1279,21 @@ it would also work to just use on:click on the MapLibre component itself. -->
</div>

{#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
adventure={adventure}
on:fetchImage={(e) => {
url = e.detail;
fetchImage();
if (immichUploadURLSOnly) {
uploadURLToImage(url);
} else {
fetchImage();
}
}}
/>
{/if}
Expand Down