Support analog photo dates and correct gallery years

This commit is contained in:
2026-09-20 01:39:42 +03:30
parent dad62df66f
commit cf46d62ede
55 changed files with 183 additions and 75 deletions
+29 -7
View File
@@ -6,6 +6,7 @@ import html
import io
import json
import os
from collections import Counter
from pathlib import Path
import re
import shutil
@@ -79,14 +80,30 @@ def submitted_photo_dates(raw, count):
return dates
def submitted_batch_date(raw):
"""Parse one optional date override for an entire analog-photo batch."""
value = raw.strip()
if not value:
return None
try:
if re.fullmatch(r"\d{4}", value):
return datetime(int(value), 1, 1)
return datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as error:
raise ValueError("date_taken must be a year, ISO date, or ISO timestamp") from error
def distinct_photo_dates(dates):
"""Keep exact capture times, using earlier seconds only to break true ties."""
"""Keep exact capture times, using later seconds only to break true ties."""
result = []
used = set()
remaining = Counter(taken.isoformat() for taken in dates)
for taken in dates:
candidate = taken
key = taken.isoformat()
candidate = taken + timedelta(seconds=remaining[key] - 1)
remaining[key] -= 1
while candidate.isoformat() in used:
candidate -= timedelta(seconds=1)
candidate += timedelta(seconds=1)
result.append(candidate)
used.add(candidate.isoformat())
return result
@@ -321,6 +338,8 @@ def create_app(data_dir=None, token=None):
try:
supplied_dates = (submitted_photo_dates(request.form.get("photo_dates", ""), len(photos))
if kind == "photo" else None)
batch_date = (submitted_batch_date(request.form.get("date_taken", ""))
if kind == "photo" else None)
except ValueError as error:
abort(400, str(error))
# Ordered byte hashes + caption make retries idempotent, even across restarts.
@@ -339,7 +358,8 @@ def create_app(data_dir=None, token=None):
try:
embedded_date = web_image(raw, temporary / "images" / f"{index:02}.jpg")
if kind == "photo":
capture_dates.append(supplied_dates[index] if supplied_dates else embedded_date)
capture_dates.append(batch_date or
(supplied_dates[index] if supplied_dates else embedded_date))
except (UnidentifiedImageError, OSError, ValueError, SyntaxError,
Image.DecompressionBombError, Image.DecompressionBombWarning,
ImageCms.PyCMSError):
@@ -348,14 +368,16 @@ def create_app(data_dir=None, token=None):
if highlight:
digest.update(b"\0highlight\0" + highlight.encode())
undated_job = digest.hexdigest()
if supplied_dates:
if batch_date:
digest.update(b"\0date_taken\0" + batch_date.isoformat().encode())
elif supplied_dates:
normalized_dates = "\n".join(taken.isoformat() for taken in supplied_dates)
digest.update(b"\0photo_dates\0" + normalized_dates.encode())
job = digest.hexdigest()
date = datetime.now(timezone.utc).isoformat()
manifest = {"id": job, "date": date, "caption": caption,
"highlight": highlight, "count": len(photos)}
if (supplied_dates and undated_job != job and
if ((batch_date or supplied_dates) and undated_job != job and
(data / "submissions" / undated_job / "manifest.json").is_file()):
manifest["replaces"] = undated_job
if kind == "snippet":
@@ -363,7 +385,7 @@ def create_app(data_dir=None, token=None):
atomic_json(temporary / "manifest.json", manifest)
if kind == "photo":
if any(taken is None for taken in capture_dates):
abort(400, "Photo capture date is missing; send photo_dates from the Photos library")
abort(400, "Photo capture date is missing; send date_taken or photo_dates")
capture_dates = distinct_photo_dates(capture_dates)
manifest["capture_dates"] = [taken.isoformat() for taken in capture_dates]
atomic_json(temporary / "manifest.json", manifest)