fix(metadata): drop EXIF:ModifyDate fallback, prefer SubSec, fall back to path

The taken_at extractor walked four EXIF fields in order: DateTimeOriginal,
CreateDate, MediaCreateDate, ModifyDate. The last one is set every time
a file is re-saved (Lightroom export, EXIF strip, batch resize), so any
photo whose original capture metadata was lost during editing ended up
labeled 'exif' with the *edit* date instead of the shoot date.

Changes:
  - SubSecDateTimeOriginal at the top of the list (sub-second precision,
    often carries OffsetTime).
  - QuickTime:CreateDate added next to MediaCreateDate.
  - ModifyDate dropped from the trusted list entirely.
  - When no trusted EXIF date is present, fall back to guess_date_from_path
    (already used for has_date_warning) and tag taken_at_source='path'.
    Better than filesystem mtime, which on Nextcloud-mounted libraries
    just reflects the upload time.
  - Skip the date-write block entirely if photo.taken_at_source == 'manual'
    so a rescan can't clobber a user correction.
  - parse_exif_datetime: handle the all-zero placeholder some cameras
    emit, accept tz-aware variants (%z), normalize to naive UTC.

Frontend: new 'PATH' badge in TakenAtEditor with a tooltip explaining
the date came from filename / folder rather than real EXIF.

Backfill: new backfill_taken_at celery task and
POST /api/v1/library/maintenance/backfill-taken-at endpoint that
re-enqueues extract_metadata for every non-manual photo. ~21k tasks
finish in ~15 min on the existing worker-light concurrency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claudio
2026-05-10 23:02:32 +02:00
parent 76551d898b
commit 7b153f0d28
3 changed files with 134 additions and 24 deletions

View File

@@ -36,7 +36,17 @@ export function TakenAtEditor({
? 'FILE'
: source === 'manual'
? 'MANUAL'
: null
: source === 'path'
? 'PATH'
: null
// Path-derived dates are guesses, so spell out where they came from
// in the tooltip. Other sources just need a short label.
const sourceTitle =
source === 'path'
? 'Date inferred from filename or folder name (no trusted EXIF capture date)'
: sourceLabel
? `Source: ${sourceLabel.toLowerCase()}`
: undefined
const guess = useMemo(
() => guessDateFromPath(photo.filepath),
@@ -89,7 +99,7 @@ export function TakenAtEditor({
{sourceLabel && (
<span
className="rounded-sm bg-black/60 px-1.5 py-0.5 text-[9px] font-semibold tracking-wider text-white"
title={`Source: ${sourceLabel.toLowerCase()}`}
title={sourceTitle}
>
{sourceLabel}
</span>