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:
2026-04-09 23:44:29 +02:00
parent 9c9f5bd899
commit 7cf546af7a
17 changed files with 529 additions and 46 deletions

View File

@@ -91,11 +91,18 @@ async def init_db():
"duplicate_group_id",
"ALTER TABLE photos ADD COLUMN duplicate_group_id VARCHAR",
),
("latitude", "ALTER TABLE photos ADD COLUMN latitude REAL"),
("longitude", "ALTER TABLE photos ADD COLUMN longitude REAL"),
]
# Track whether the GPS columns were just added so we can kick
# off a one-shot backfill of existing photos at the end of init.
gps_columns_added = False
for col_name, alter_sql in pending_alters:
if col_name not in existing_cols:
logger.info(f"Adding photos.{col_name} column")
await conn.execute(text(alter_sql))
if col_name in ("latitude", "longitude"):
gps_columns_added = True
# Indexes for the new duplicate-detection columns. CREATE INDEX
# IF NOT EXISTS is supported on SQLite so this is safe to run
# every startup.
@@ -108,9 +115,27 @@ async def init_db():
"ON photos(duplicate_group_id)"
)
)
await conn.execute(
text(
"CREATE INDEX IF NOT EXISTS ix_photos_lat_lon "
"ON photos(latitude, longitude)"
)
)
logger.info("Database initialized successfully")
# If we just introduced the GPS columns on an existing install, kick
# off a one-shot backfill so the Map view is populated without a
# manual full re-scan. Imported lazily to avoid pulling Celery into
# the import graph for non-worker processes that don't need it.
if "sqlite" in settings.database_url and gps_columns_added:
try:
from app.tasks.scan import backfill_gps
backfill_gps.delay()
logger.info("Queued one-shot backfill_gps task after column add")
except Exception as e:
logger.warning(f"Could not queue backfill_gps task: {e}")
async def create_fts_table():
"""Create Full-Text Search table for SQLite"""
if "sqlite" in settings.database_url: