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:
@@ -187,6 +187,19 @@ async def trigger_backfill_gps(current_user: User = Depends(get_current_user)):
|
||||
backfill_gps.delay()
|
||||
return {"status": "success", "message": "GPS backfill queued"}
|
||||
|
||||
|
||||
@router.post("/maintenance/backfill-taken-at")
|
||||
async def trigger_backfill_taken_at(current_user: User = Depends(get_current_user)):
|
||||
"""Re-run extract_metadata on every non-manual photo to recompute
|
||||
taken_at with the current EXIF-priority list and path-based fallback.
|
||||
Useful after the date-extraction logic changes (e.g. dropping the
|
||||
ModifyDate fallback). Manual edits are preserved."""
|
||||
from app.services.metadata import backfill_taken_at
|
||||
|
||||
backfill_taken_at.delay()
|
||||
return {"status": "success", "message": "taken_at backfill queued"}
|
||||
|
||||
|
||||
@router.get("/scan/status")
|
||||
async def get_scan_status(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""Get current scan status"""
|
||||
|
||||
Reference in New Issue
Block a user