Sort gallery media by capture date instead of file name
All checks were successful
Docker / build (release) Successful in 9s

This commit is contained in:
krek0 2026-09-24 12:52:50 +02:00
parent 2f0c426dc1
commit f7415fb5f6
11 changed files with 314 additions and 33 deletions

View file

@ -4,7 +4,6 @@
import logging
import os
import unicodedata
from datetime import datetime
from functools import partial
from importlib import import_module
from inspect import isclass
@ -39,6 +38,19 @@ LATEST_LIMIT = getattr(settings, "PHOTOLOGUE_GALLERY_LATEST_LIMIT", None)
# max_length setting for the ImageModel ImageField
IMAGE_FIELD_MAX_LENGTH = getattr(settings, "PHOTOLOGUE_IMAGE_FIELD_MAX_LENGTH", 100)
# Where a capture date comes from, from the most to the least trustworthy. An
# empty value means the date was never determined.
DATE_FROM_EXIF = "exif"
DATE_FROM_VIDEO = "video"
DATE_FROM_FILE = "file"
DATE_MANUAL = "manual"
DATE_SOURCE_CHOICES = (
(DATE_FROM_EXIF, _("EXIF metadata")),
(DATE_FROM_VIDEO, _("video metadata")),
(DATE_FROM_FILE, _("file date at upload")),
(DATE_MANUAL, _("set by hand")),
)
# Modify image file buffer size.
ImageFile.MAXBLOCK = getattr(settings, "PHOTOLOGUE_MAXBLOCK", 256 * 2**10)
@ -265,6 +277,20 @@ class MediaModel(models.Model):
)
thumbnail_width = models.PositiveIntegerField(editable=False, null=True)
thumbnail_height = models.PositiveIntegerField(editable=False, null=True)
date_taken = models.DateTimeField(
_("date taken"),
null=True,
blank=True,
help_text=_("Date the media was captured; read from its metadata."),
)
date_source = models.CharField(
_("date source"),
max_length=10,
blank=True,
editable=False,
choices=DATE_SOURCE_CHOICES,
help_text=_("Where the capture date comes from."),
)
is_video = False
@ -288,9 +314,32 @@ class MediaModel(models.Model):
def get_download_url(self):
raise NotImplementedError
@property
def date_is_approximate(self):
"""True when the date was deduced rather than read from the capture itself."""
return self.date_source == DATE_FROM_FILE
def set_capture_date(self, value, source):
"""Record a capture date read from the file, and where it comes from."""
self.date_taken = value
self.date_source = source
self._date_read_from_file = True
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
if self.pk and not getattr(self, "_date_read_from_file", False):
# A date changed by anything but our own readers was set by a human,
# and must survive a later re-read of the metadata.
stored = (
type(self)
.objects.filter(pk=self.pk)
.values_list("date_taken", flat=True)
.first()
)
if stored != self.date_taken:
self.date_source = DATE_MANUAL
self._date_read_from_file = False
super().save(*args, **kwargs)
@ -302,12 +351,6 @@ class Photo(MediaModel):
width_field="thumbnail_width",
height_field="thumbnail_height",
)
date_taken = models.DateTimeField(
_("date taken"),
null=True,
blank=True,
help_text=_("Date image was taken; is obtained from the image EXIF data."),
)
view_count = models.PositiveIntegerField(_("view count"), default=0, editable=False)
crop_from = models.CharField(
_("crop from"),
@ -577,21 +620,15 @@ class Photo(MediaModel):
if (self.date_taken is None or image_has_changed) and self.image:
# Attempt to get the date the photo was taken from the EXIF data.
try:
from .utils import capture_date_from_exif
exif_date = self.exif(self.image.file).get(
"EXIF DateTimeOriginal", None
)
if exif_date is not None:
d, t = exif_date.values.split()
year, month, day = d.split(":")
hour, minute, second = t.split(":")
self.date_taken = datetime(
int(year),
int(month),
int(day),
int(hour),
int(minute),
int(second),
)
taken = capture_date_from_exif(exif_date.values)
if taken is not None:
self.set_capture_date(taken, DATE_FROM_EXIF)
except Exception:
logger.error("Failed to read EXIF DateTimeOriginal", exc_info=True)
# If crop_from property has been changed on existing image,