feat: map view with GPS extraction fix
Adds a new Map sidebar entry that plots photos by their EXIF GPS coordinates on a clustered Leaflet map. While wiring this up, the metadata extractor was reading unprefixed GPS keys that never exist in `exiftool -G -j` output AND assumed coordinates were already floats — every photo silently lost its GPS. The new extract_gps helper handles Composite/EXIF group prefixes and parses DMS strings, and lat/lon are stored as first-class indexed columns so the map can query them without parsing exif_json on every request. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,14 @@ async def get_library_stats(db: AsyncSession = Depends(get_db)):
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
with_gps_count = (
|
||||
await db.execute(
|
||||
select(func.count(Photo.id)).where(
|
||||
not_discarded, Photo.latitude.is_not(None)
|
||||
)
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
duplicates_count = (
|
||||
await db.execute(
|
||||
select(func.count(Photo.id)).where(
|
||||
@@ -96,6 +104,7 @@ async def get_library_stats(db: AsyncSession = Depends(get_db)):
|
||||
"all_photos": all_photos_count,
|
||||
"rated": rated_count,
|
||||
"colored": colored_count,
|
||||
"with_gps": with_gps_count,
|
||||
"duplicates": duplicates_count,
|
||||
"discarded": discarded_count,
|
||||
"total_photos": photo_count,
|
||||
@@ -108,11 +117,23 @@ async def get_library_stats(db: AsyncSession = Depends(get_db)):
|
||||
async def trigger_scan():
|
||||
"""Trigger full library re-scan"""
|
||||
from app.tasks.scan import scan_all_source_roots
|
||||
|
||||
|
||||
scan_all_source_roots.delay()
|
||||
|
||||
|
||||
return {"status": "success", "message": "Library scan started"}
|
||||
|
||||
|
||||
@router.post("/backfill-gps")
|
||||
async def trigger_backfill_gps():
|
||||
"""Re-run EXIF metadata extraction on every photo that's still missing
|
||||
GPS coordinates. Useful after fixing the EXIF parser, or any time the
|
||||
Map view looks emptier than expected. Returns immediately — work runs
|
||||
on the Celery worker."""
|
||||
from app.tasks.scan import backfill_gps
|
||||
|
||||
backfill_gps.delay()
|
||||
return {"status": "success", "message": "GPS backfill queued"}
|
||||
|
||||
@router.get("/scan/status")
|
||||
async def get_scan_status(db: AsyncSession = Depends(get_db)):
|
||||
"""Get current scan status"""
|
||||
|
||||
Reference in New Issue
Block a user