""" Skriv og query passeringslogg i SQLite. """ import uuid from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Optional import aiosqlite # Tidsvindu for deduplisering: bilder av samme utøver ved samme stasjon DEDUP_WINDOW_SECONDS = 2 async def _find_duplicate( db: aiosqlite.Connection, bib_number: str, station: str, timestamp_utc: datetime, ) -> Optional[dict]: """ Finn eksisterende passering med samme bib og stasjon innen DEDUP_WINDOW_SECONDS. Returnerer raden, eller None. """ window_start = (timestamp_utc - timedelta(seconds=DEDUP_WINDOW_SECONDS)).isoformat() window_end = (timestamp_utc + timedelta(seconds=DEDUP_WINDOW_SECONDS)).isoformat() async with db.execute( """ SELECT passage_id, proximity_score, source_image FROM passages WHERE bib_number = ? AND station = ? AND timestamp_utc BETWEEN ? AND ? ORDER BY proximity_score DESC LIMIT 1 """, (bib_number, station, window_start, window_end), ) as cur: row = await cur.fetchone() return dict(row) if row else None async def log_passage( db: aiosqlite.Connection, *, 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 én passering med deduplisering. Hvis et bilde av samme utøver ved samme stasjon allerede er logget innen DEDUP_WINDOW_SECONDS, beholder vi bildet nærmest kamera (høyest proximity_score) ettersom det gir det mest nøyaktige tidsstempelet. Returnerer passage_id (enten ny eller eksisterende). """ if bib_number: duplicate = await _find_duplicate(db, bib_number, station, timestamp_utc) if duplicate: if proximity_score > duplicate["proximity_score"]: # Nytt bilde er nærmere kamera — oppdater tidsstempel og bildesti old_image = duplicate["source_image"] await db.execute( """ UPDATE passages SET timestamp_utc = ?, proximity_score = ?, source_image = ?, confidence = ?, id_method = ? WHERE passage_id = ? """, ( timestamp_utc.isoformat(), proximity_score, source_image, confidence, id_method, duplicate["passage_id"], ), ) await db.commit() # Slett det gamle, dårligere bildet _delete_image_file(old_image) return duplicate["passage_id"] else: # Eksisterende bilde er nærmere kamera — forkast nytt bilde _delete_image_file(source_image) return duplicate["passage_id"] passage_id = str(uuid.uuid4()) await db.execute( """ INSERT INTO passages ( passage_id, profile_id, bib_number, station, timestamp_utc, gps_lat, gps_lon, gps_alt, confidence, proximity_score, id_method, source_image, needs_review, review_note ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( passage_id, profile_id, bib_number, station, timestamp_utc.isoformat(), gps_lat, gps_lon, gps_alt, confidence, proximity_score, id_method, source_image, int(needs_review), review_note, ), ) await db.commit() return passage_id def _delete_image_file(path: str) -> None: """Slett bildefil stille — logg advarsel ved feil.""" import logging try: Path(path).unlink(missing_ok=True) except Exception as e: logging.getLogger(__name__).warning("Kunne ikke slette duplikatbilde %s: %s", path, e) async def get_passages( db: aiosqlite.Connection, profile_id: Optional[str] = None, station: Optional[str] = None, needs_review: Optional[bool] = None, ) -> list[dict]: """Hent passeringer med valgfrie filtre.""" clauses = [] params = [] 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: """Manuell oppdatering av en passering etter gjennomgang.""" 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