Skip to content

Commit

Permalink
Fix deletion of hashtag albums after rescanning images
Browse files Browse the repository at this point in the history
- also improve performance by adding a new field
  • Loading branch information
derneuere committed Mar 30, 2024
1 parent 837caeb commit 602bb1e
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 113 deletions.
3 changes: 1 addition & 2 deletions api/directory_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from django.db.models import Q, QuerySet
from django_q.tasks import AsyncTask

import api.models.album_thing
import api.util as util
from api.batch_jobs import create_batch_job
from api.face_classify import cluster_all_faces
Expand Down Expand Up @@ -377,7 +376,7 @@ def scan_photos(user, full_scan, job_id, scan_directory="", scan_files=[]):

place365_instance.unload()
util.logger.info("Scanned {} files in : {}".format(files_found, scan_directory))
api.models.album_thing.update()

util.logger.info("Finished updating album things")
exisisting_photos = Photo.objects.filter(owner=user.id).order_by("image_hash")
paginator = Paginator(exisisting_photos, 5000)
Expand Down
19 changes: 19 additions & 0 deletions api/migrations/0062_albumthing_cover_photos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.11 on 2024-03-29 17:33

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("api", "0061_alter_person_name"),
]

operations = [
migrations.AddField(
model_name="albumthing",
name="cover_photos",
field=models.ManyToManyField(
related_name="album_thing_cover_photos", to="api.photo"
),
),
]
22 changes: 22 additions & 0 deletions api/migrations/0063_apply_default_album_things_cover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.db import migrations


class Migration(migrations.Migration):
def apply_default(apps, schema_editor):
AlbumThing = apps.get_model("api", "AlbumThing")

for thing in AlbumThing.objects.all():
if not thing.cover_photos and thing.photos.count() > 0:
thing.cover_photos.add(*thing.photos.all()[:4])
thing.save()

def remove_default(apps, schema_editor):
AlbumThing = apps.get_model("api", "AlbumThing")
for thing in AlbumThing.objects.all():
thing.cover_photos = None

dependencies = [
("api", "0062_albumthing_cover_photos"),
]

operations = [migrations.RunPython(apply_default, remove_default)]
117 changes: 19 additions & 98 deletions api/models/album_thing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from django.db import connection, models
from django.db import models
from django.db.models.signals import m2m_changed
from django.dispatch import receiver

from api.models.photo import Photo
from api.models.user import User, get_deleted_user
Expand All @@ -14,6 +16,9 @@ class AlbumThing(models.Model):
)

shared_to = models.ManyToManyField(User, related_name="album_thing_shared_to")
cover_photos = models.ManyToManyField(
Photo, related_name="album_thing_cover_photos"
)

class Meta:
constraints = [
Expand All @@ -22,106 +27,22 @@ class Meta:
)
]

def _set_default_cover_photo(self):
if self.cover_photos.count() < 4:
self.cover_photos.add(*self.photos.all()[:4])

def __str__(self):
return "%d: %s" % (self.id, self.title)


def get_album_thing(title, owner):
return AlbumThing.objects.get_or_create(title=title, owner=owner)[0]


# List all existing (thing / thing type / user)
view_api_album_thing_sql = """
api_albumthing_sql as (
select title, 'places365_attribute' thing_type, false favorited, owner_id
from (select owner_id, jsonb_array_elements_text(jsonb_extract_path(captions_json, 'places365', 'attributes')) title from api_photo ) photo_attribut
group by title, thing_type, favorited, owner_id
union all
select title, 'places365_category' thing_type, false favorited, owner_id
from (select owner_id, jsonb_array_elements_text(jsonb_extract_path(captions_json, 'places365', 'categories')) title from api_photo ) photo_attribut
group by title, thing_type, favorited, owner_id
)"""

# List all photos per albumThing
view_api_album_thing_photos_sql = """
api_albumthing_photos_sql as (
select api_albumthing.id albumthing_id, photo_id
from (select owner_id, jsonb_array_elements_text(jsonb_extract_path(captions_json, 'places365', 'attributes')) title, image_hash photo_id, 'places365_attribute' thing_type from api_photo ) photo_attribut
join api_albumthing using (title,thing_type, owner_id )
group by api_albumthing.id, photo_id
union all
select api_albumthing.id albumthing_id, photo_id
from (select owner_id, jsonb_array_elements_text(jsonb_extract_path(captions_json, 'places365', 'categories')) title, image_hash photo_id, 'places365_category' thing_type from api_photo ) photo_attribut
join api_albumthing using (title,thing_type, owner_id )
group by api_albumthing.id, photo_id
)
"""


def create_new_album_thing(cursor):
"""This function create albums from all detected thing on photos"""
SQL = """
with {}
insert into api_albumthing (title, thing_type,favorited, owner_id)
select api_albumthing_sql.*
from api_albumthing_sql
left join api_albumthing using (title, thing_type, owner_id)
where api_albumthing is null;
""".replace(
"{}", view_api_album_thing_sql
)
cursor.execute(SQL)


def create_new_album_thing_photo(cursor):
"""This function create link between albums thing and photo from all detected thing on photos"""
SQL = """
with {}
insert into api_albumthing_photos (albumthing_id, photo_id)
select api_albumthing_photos_sql.*
from api_albumthing_photos_sql
left join api_albumthing_photos using (albumthing_id, photo_id)
where api_albumthing_photos is null;
""".replace(
"{}", view_api_album_thing_photos_sql
)
cursor.execute(SQL)


def delete_album_thing_photo(cursor):
"""This function delete photos form albums thing where thing disappears"""
SQL = """
with {}
delete
from api_albumthing_photos as p
where not exists (
select 1
from api_albumthing_photos_sql
where albumthing_id = p.albumthing_id
and photo_id = p.photo_id
limit 1
)
""".replace(
"{}", view_api_album_thing_photos_sql
)
cursor.execute(SQL)


def delete_album_thing(cursor):
"""This function delete albums thing without photos"""
SQL = """
with {}
delete from api_albumthing
where (title, thing_type, owner_id) not in ( select title, thing_type, owner_id from api_albumthing_sql );
""".replace(
"{}", view_api_album_thing_sql
)
cursor.execute(SQL)
@receiver(m2m_changed, sender=AlbumThing.photos.through)
def update_default_cover_photo(sender, instance, action, **kwargs):
if action == "post_add":
instance._set_default_cover_photo()
instance.save()


def update():
with connection.cursor() as cursor:
create_new_album_thing(cursor)
create_new_album_thing_photo(cursor)
delete_album_thing_photo(cursor)
delete_album_thing(cursor)
def get_album_thing(title, owner, thing_type=None):
return AlbumThing.objects.get_or_create(
title=title, owner=owner, thing_type=thing_type
)[0]
33 changes: 31 additions & 2 deletions api/models/photo.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,11 +244,10 @@ def _save_captions(self, commit=True, caption=None):

for hashtag in hashtags:
album_thing = api.models.album_thing.get_album_thing(
title=hashtag, owner=self.owner
title=hashtag, owner=self.owner, thing_type="hashtag_attribute"
)
if album_thing.photos.filter(image_hash=self.image_hash).count() == 0:
album_thing.photos.add(self)
album_thing.thing_type = "hashtag_attribute"
album_thing.save()

for album_thing in api.models.album_thing.AlbumThing.objects.filter(
Expand Down Expand Up @@ -286,6 +285,36 @@ def _generate_captions(self, commit):
self.search_captions = " , ".join(
res_places365["categories"] + [res_places365["environment"]]
)

# Add to album things, when photo is not yet in any album thing with type places365_attribute or places365_category
if api.models.album_thing.AlbumThing.objects.filter(
Q(photos__in=[self.image_hash])
& (
Q(thing_type="places365_attribute")
or Q(thing_type="places365_category")
)
& Q(owner=self.owner)
).exists():
return

for attribute in res_places365["attributes"]:
album_thing = api.models.album_thing.get_album_thing(
title=attribute,
owner=self.owner,
thing_type="places365_attribute",
)
album_thing.photos.add(self)
album_thing.save()

for category in res_places365["categories"]:
album_thing = api.models.album_thing.get_album_thing(
title=category,
owner=self.owner,
thing_type="places365_category",
)
album_thing.photos.add(self)
album_thing.save()

if commit:
self.save()
util.logger.info(
Expand Down
9 changes: 8 additions & 1 deletion api/serializers/album_thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,14 @@ class AlbumThingListSerializer(serializers.ModelSerializer):

class Meta:
model = AlbumThing
fields = ("id", "cover_photos", "title", "photo_count", "thing_type")
fields = (
"id",
"cover_photos",
"title",
"photo_count",
"thing_type",
"cover_photos",
)

def get_photo_count(self, obj) -> int:
return obj.photo_count
11 changes: 1 addition & 10 deletions api/views/albums.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ def list(self, *args, **kwargs):
return Response({"results": serializer.data})


# To-Do: Make album_cover an actual database field to improve performance
# To-Do: Could be literally the list command in AlbumThingViewSet
class AlbumThingListViewSet(ListViewSet):
serializer_class = AlbumThingListSerializer
Expand All @@ -183,17 +182,9 @@ def get_queryset(self):
if self.request.user.is_anonymous:
return AlbumThing.objects.none()

cover_photos_query = Photo.objects.filter(hidden=False).only(
"image_hash", "video"
)

queryset = (
AlbumThing.objects.filter(owner=self.request.user)
.prefetch_related(
Prefetch(
"photos", queryset=cover_photos_query[:4], to_attr="cover_photos"
)
)
.prefetch_related("cover_photos")
.annotate(
photo_count=Count(
Case(
Expand Down

0 comments on commit 602bb1e

Please sign in to comment.