330ba7a93d
- Backend: FastAPI, EXIF-parser, EasyOCR, SQLite - Frontend: React admin (startliste, passeringer, gjennomgang, resultater) - Docker: docker-compose med depot/processed/data-volumer Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
126 lines
3.8 KiB
Python
126 lines
3.8 KiB
Python
"""
|
|
Beregn split-tider og totalresultat fra passeringsloggen.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
import aiosqlite
|
|
|
|
|
|
async def get_results(db: aiosqlite.Connection) -> list[dict]:
|
|
"""
|
|
Hent totalresultat for alle utøvere som har passert start og mål.
|
|
Returnerer sortert liste med split-tider.
|
|
"""
|
|
# Hent alle bekreftede passeringer gruppert per utøver
|
|
async with db.execute("""
|
|
SELECT p.profile_id, p.bib_number, a.name, a.club,
|
|
p.station, p.timestamp_utc
|
|
FROM passages p
|
|
LEFT JOIN athletes a ON a.profile_id = p.profile_id
|
|
WHERE p.needs_review = 0 AND p.profile_id IS NOT NULL
|
|
ORDER BY p.profile_id, p.timestamp_utc
|
|
""") as cur:
|
|
rows = await cur.fetchall()
|
|
|
|
# Grupper per utøver
|
|
athletes: dict[str, dict] = {}
|
|
for row in rows:
|
|
pid = row["profile_id"]
|
|
if pid not in athletes:
|
|
athletes[pid] = {
|
|
"profile_id": pid,
|
|
"bib_number": row["bib_number"],
|
|
"name": row["name"],
|
|
"club": row["club"],
|
|
"passages": [],
|
|
}
|
|
athletes[pid]["passages"].append({
|
|
"station": row["station"],
|
|
"timestamp_utc": row["timestamp_utc"],
|
|
})
|
|
|
|
results = []
|
|
for pid, data in athletes.items():
|
|
passages = data["passages"]
|
|
if not passages:
|
|
continue
|
|
|
|
# Finn start og mål
|
|
start_p = next((p for p in passages if p["station"] == "start"), None)
|
|
finish_p = next((p for p in passages if p["station"] == "finish"), None)
|
|
|
|
start_time = _parse_ts(start_p["timestamp_utc"]) if start_p else None
|
|
finish_time = _parse_ts(finish_p["timestamp_utc"]) if finish_p else None
|
|
|
|
total_seconds = None
|
|
if start_time and finish_time:
|
|
total_seconds = (finish_time - start_time).total_seconds()
|
|
|
|
# Split-tider mellom alle stasjoner
|
|
splits = []
|
|
prev_ts = start_time
|
|
prev_station = "start"
|
|
for p in passages:
|
|
if p["station"] == "start":
|
|
continue
|
|
ts = _parse_ts(p["timestamp_utc"])
|
|
split_s = (ts - prev_ts).total_seconds() if prev_ts else None
|
|
splits.append({
|
|
"from": prev_station,
|
|
"to": p["station"],
|
|
"split_seconds": split_s,
|
|
"split_formatted": _fmt_seconds(split_s),
|
|
"timestamp_utc": p["timestamp_utc"],
|
|
})
|
|
prev_ts = ts
|
|
prev_station = p["station"]
|
|
|
|
results.append({
|
|
"profile_id": pid,
|
|
"bib_number": data["bib_number"],
|
|
"name": data["name"] or f"Ukjent ({data['bib_number']})",
|
|
"club": data["club"],
|
|
"start_time": start_p["timestamp_utc"] if start_p else None,
|
|
"finish_time": finish_p["timestamp_utc"] if finish_p else None,
|
|
"total_seconds": total_seconds,
|
|
"total_formatted": _fmt_seconds(total_seconds),
|
|
"splits": splits,
|
|
"dnf": finish_time is None and start_time is not None,
|
|
"dns": start_time is None,
|
|
})
|
|
|
|
# Sorter: fullført (etter tid), DNF, DNS
|
|
results.sort(key=_sort_key)
|
|
return results
|
|
|
|
|
|
def _parse_ts(ts_str: str) -> Optional[datetime]:
|
|
if not ts_str:
|
|
return None
|
|
try:
|
|
return datetime.fromisoformat(ts_str)
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def _fmt_seconds(seconds: Optional[float]) -> Optional[str]:
|
|
if seconds is None:
|
|
return None
|
|
seconds = int(seconds)
|
|
h = seconds // 3600
|
|
m = (seconds % 3600) // 60
|
|
s = seconds % 60
|
|
if h:
|
|
return f"{h}:{m:02d}:{s:02d}"
|
|
return f"{m}:{s:02d}"
|
|
|
|
|
|
def _sort_key(r: dict):
|
|
if r["dns"]:
|
|
return (3, 0)
|
|
if r["dnf"]:
|
|
return (2, 0)
|
|
return (1, r["total_seconds"] or float("inf"))
|