""" Skriv og query passeringslogg i SQLite. Deduplisering: Bilder av samme utøver (bib) ved samme stasjon innen DEDUP_WINDOW_SECONDS grupperes under én passering. Alle bilder bevares i passage_images. Passeringstidsstempel = siste bilde i burst-sekvensen (nyeste timestamp). """ import uuid from datetime import datetime, timedelta from typing import Optional import aiosqlite DEDUP_WINDOW_SECONDS = 2 async def _find_active_passage( db: aiosqlite.Connection, bib_number: str, station: str, timestamp_utc: datetime, ) -> Optional[dict]: """ Finn eksisterende passering med samme bib og stasjon der nytt bilde faller innen DEDUP_WINDOW_SECONDS etter siste registrerte bilde i passeringen. """ async with db.execute( """ SELECT p.passage_id, MAX(pi.timestamp_utc) AS last_image_ts FROM passages p JOIN passage_images pi ON pi.passage_id = p.passage_id WHERE p.bib_number = ? AND p.station = ? GROUP BY p.passage_id HAVING last_image_ts >= ? ORDER BY last_image_ts DESC LIMIT 1 """, ( bib_number, station, (timestamp_utc - timedelta(seconds=DEDUP_WINDOW_SECONDS)).isoformat(), ), ) as cur: row = await cur.fetchone() return dict(row) if row else None async def log_passage( db: aiosqlite.Connection, *, race_id: Optional[str] = None, profile_id: Optional[str], bib_number: Optional[str], station: str, timestamp_utc: datetime, gps_lat: float, gps_lon: float, gps_alt: Optional[float], confidence: float, proximity_score: float = 0.0, id_method: str, source_image: str, needs_review: bool = False, review_note: Optional[str] = None, ) -> str: """ Logg ett bilde som en passering. - Er bildet del av en pågående burst (bib+stasjon innen vinduet)? → Legg til i passage_images, oppdater passeringstidsstempel til dette bildet (siste i tid = offisiell passeringstid). - Nytt bib/stasjon eller utenfor vinduet? → Opprett ny passering. Returnerer passage_id. """ image_id = str(uuid.uuid4()) if bib_number: existing = await _find_active_passage(db, bib_number, station, timestamp_utc) if existing: passage_id = existing["passage_id"] # Legg til bilde i sekvensen await db.execute( """ INSERT INTO passage_images (image_id, passage_id, image_path, timestamp_utc, proximity_score) VALUES (?, ?, ?, ?, ?) """, (image_id, passage_id, source_image, timestamp_utc.isoformat(), proximity_score), ) # Oppdater passeringen med siste bilde som offisiell tid og bildesti await db.execute( """ UPDATE passages SET timestamp_utc = ?, source_image = ?, confidence = ?, id_method = ? WHERE passage_id = ? """, ( timestamp_utc.isoformat(), source_image, confidence, id_method, passage_id, ), ) await db.commit() return passage_id # Ny passering passage_id = str(uuid.uuid4()) await db.execute( """ INSERT INTO passages ( passage_id, race_id, profile_id, bib_number, station, timestamp_utc, gps_lat, gps_lon, gps_alt, confidence, id_method, source_image, needs_review, review_note ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( passage_id, race_id, profile_id, bib_number, station, timestamp_utc.isoformat(), gps_lat, gps_lon, gps_alt, confidence, id_method, source_image, int(needs_review), review_note, ), ) await db.execute( """ INSERT INTO passage_images (image_id, passage_id, image_path, timestamp_utc, proximity_score) VALUES (?, ?, ?, ?, ?) """, (image_id, passage_id, source_image, timestamp_utc.isoformat(), proximity_score), ) await db.commit() return passage_id async def get_passage_images(db: aiosqlite.Connection, passage_id: str) -> list[dict]: """Hent alle bilder for en passering, kronologisk sortert.""" async with db.execute( """ SELECT image_id, image_path, timestamp_utc, proximity_score FROM passage_images WHERE passage_id = ? ORDER BY timestamp_utc """, (passage_id,), ) as cur: rows = await cur.fetchall() return [dict(r) for r in rows] async def get_passages( db: aiosqlite.Connection, race_id: Optional[str] = None, profile_id: Optional[str] = None, station: Optional[str] = None, needs_review: Optional[bool] = None, ) -> list[dict]: clauses = [] params = [] if race_id is not None: clauses.append("p.race_id = ?") params.append(race_id) if profile_id is not None: clauses.append("p.profile_id = ?") params.append(profile_id) if station is not None: clauses.append("p.station = ?") params.append(station) if needs_review is not None: clauses.append("p.needs_review = ?") params.append(int(needs_review)) where = ("WHERE " + " AND ".join(clauses)) if clauses else "" query = f""" SELECT p.*, a.name, a.club FROM passages p LEFT JOIN athletes a ON a.profile_id = p.profile_id {where} ORDER BY p.timestamp_utc """ async with db.execute(query, params) as cur: rows = await cur.fetchall() return [dict(r) for r in rows] async def resolve_passage( db: aiosqlite.Connection, passage_id: str, profile_id: Optional[str], bib_number: Optional[str], review_note: Optional[str] = None, ) -> bool: cur = await db.execute( """ UPDATE passages SET profile_id = ?, bib_number = ?, needs_review = 0, review_note = ?, id_method = 'manual' WHERE passage_id = ? """, (profile_id, bib_number, review_note, passage_id), ) await db.commit() return cur.rowcount > 0 async def delete_passage(db: aiosqlite.Connection, passage_id: str) -> bool: cur = await db.execute("DELETE FROM passages WHERE passage_id = ?", (passage_id,)) await db.commit() return cur.rowcount > 0