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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user