Sort gallery media by capture date instead of file name
All checks were successful
Docker / build (release) Successful in 9s
All checks were successful
Docker / build (release) Successful in 9s
This commit is contained in:
parent
2f0c426dc1
commit
f7415fb5f6
11 changed files with 314 additions and 33 deletions
|
|
@ -5,7 +5,12 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
from datetime import datetime, timezone as dt_timezone
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils import timezone
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
import av
|
||||
|
|
@ -61,3 +66,89 @@ def generate_video_thumbnail(video):
|
|||
video.thumbnail.save(thumb_name, ContentFile(buffer.getvalue()), save=True)
|
||||
except Exception:
|
||||
logger.error("Failed to save video thumbnail", exc_info=True)
|
||||
|
||||
|
||||
def capture_date_from_exif(exif_value):
|
||||
"""Turn an EXIF "YYYY:MM:DD HH:MM:SS" value into an aware datetime.
|
||||
|
||||
EXIF dates carry no time zone: they are local time as read by the camera, so
|
||||
they are interpreted in the site time zone.
|
||||
"""
|
||||
try:
|
||||
day, hour = str(exif_value).split()
|
||||
year, month, dayofmonth = (int(part) for part in day.split(":"))
|
||||
hours, minutes, seconds = (int(part) for part in hour.split(":"))
|
||||
naive = datetime(year, month, dayofmonth, hours, minutes, seconds)
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
logger.error("Unreadable EXIF date %r", exif_value)
|
||||
return None
|
||||
return timezone.make_aware(naive) if settings.USE_TZ else naive
|
||||
|
||||
|
||||
def capture_date_from_video(path):
|
||||
"""Read the capture date from a video container, or None."""
|
||||
try:
|
||||
container = av.open(path)
|
||||
raw = container.metadata.get("creation_time")
|
||||
container.close()
|
||||
except Exception:
|
||||
logger.error("Failed to read video metadata from %s", path, exc_info=True)
|
||||
return None
|
||||
if not raw:
|
||||
return None
|
||||
parsed = parse_datetime(raw)
|
||||
if parsed is None:
|
||||
return None
|
||||
if settings.USE_TZ and timezone.is_naive(parsed):
|
||||
# Containers usually store UTC; a naive value is the camera's local time.
|
||||
parsed = timezone.make_aware(parsed)
|
||||
return parsed
|
||||
|
||||
|
||||
def capture_date_from_upload(last_modified_ms):
|
||||
"""Turn a browser File.lastModified value (ms since epoch) into a datetime."""
|
||||
try:
|
||||
stamp = int(last_modified_ms) / 1000
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if not stamp:
|
||||
return None
|
||||
return datetime.fromtimestamp(stamp, tz=dt_timezone.utc)
|
||||
|
||||
|
||||
def order_media_by_capture(items):
|
||||
"""Sort media in capture order, and fill in the dates that are missing.
|
||||
|
||||
A file name is a reliable chronological clue inside one photographer's batch
|
||||
(most cameras number their files in order) and meaningless between two of
|
||||
them. So media without a capture date borrow the date of the closest dated
|
||||
file of the same owner, which puts them back in the middle of the sequence
|
||||
instead of exiling them at the end of the gallery. Only media whose owner has
|
||||
no dated file at all end up at the end, in file name order.
|
||||
|
||||
Every item is annotated with `capture_date` and `capture_date_is_guessed`.
|
||||
"""
|
||||
by_owner = {}
|
||||
for item in items:
|
||||
item.capture_date = item.date_taken
|
||||
item.capture_date_is_guessed = False
|
||||
by_owner.setdefault(item.owner_id, []).append(item)
|
||||
|
||||
dated, undated = [], []
|
||||
for batch in by_owner.values():
|
||||
batch.sort(key=lambda item: (item.title, item.pk))
|
||||
# Forwards, then backwards for the files that come before the first date.
|
||||
for sequence in (batch, batch[::-1]):
|
||||
known = None
|
||||
for item in sequence:
|
||||
if item.capture_date is not None:
|
||||
known = item.capture_date
|
||||
elif known is not None:
|
||||
item.capture_date = known
|
||||
item.capture_date_is_guessed = True
|
||||
for item in batch:
|
||||
(dated if item.capture_date is not None else undated).append(item)
|
||||
|
||||
dated.sort(key=lambda item: (item.capture_date, item.owner_id, item.title, item.pk))
|
||||
undated.sort(key=lambda item: (item.owner_id, item.title, item.pk))
|
||||
return dated + undated
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue