Fix segment effort duplicates; auto-scan on segment creation

- detect.py: truncate started_at to seconds so dedup key survives JSON round-trip
- store.py: dedup by (activity_id, iso-started_at) string key, not object equality
- server.py: extract _scan_segment_for_user helper; trigger background scan
  for the creating user's activities when a new segment is saved
This commit is contained in:
Davide Scaini
2026-05-13 15:58:57 +02:00
parent cb3c9b6e41
commit 2395a6e566
3 changed files with 31 additions and 23 deletions
+1 -1
View File
@@ -197,7 +197,7 @@ def _extract_effort(
j: int,
) -> SegmentEffort:
elapsed_s = track.times[j] - track.times[i]
started_at = track.started_at + timedelta(seconds=track.times[i])
started_at = (track.started_at + timedelta(seconds=track.times[i])).replace(microsecond=0)
avg_speed = _avg_nonnull(track.speeds, i, j)
avg_hr_raw = _avg_nonnull(track.hrs, i, j)
avg_hr = int(round(avg_hr_raw)) if avg_hr_raw is not None else None
+3 -3
View File
@@ -172,10 +172,10 @@ def save_efforts(data_dir: Path, handle: str, segment_id: str, efforts: list[Seg
def add_effort(data_dir: Path, handle: str, segment_id: str, effort: SegmentEffort) -> None:
"""Append an effort, replacing any existing effort for the same activity."""
"""Append an effort, replacing any existing effort with the same activity + start time."""
efforts = load_efforts(data_dir, handle, segment_id)
efforts = [e for e in efforts if e.activity_id != effort.activity_id or
e.started_at != effort.started_at]
key = (effort.activity_id, _iso(effort.started_at))
efforts = [e for e in efforts if (e.activity_id, _iso(e.started_at)) != key]
efforts.append(effort)
efforts.sort(key=lambda e: e.started_at, reverse=True)
save_efforts(data_dir, handle, segment_id, efforts)
+27 -19
View File
@@ -23,7 +23,7 @@ from typing import Any, Optional
log = logging.getLogger("bincio.serve")
from fastapi import Cookie, Depends, FastAPI, File, Form, HTTPException, Request, Response, UploadFile
from fastapi import BackgroundTasks, Cookie, Depends, FastAPI, File, Form, HTTPException, Request, Response, UploadFile
from fastapi.responses import FileResponse, RedirectResponse, StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
@@ -2459,6 +2459,7 @@ async def get_segment(segment_id: str) -> JSONResponse:
@app.post("/api/segments")
async def create_segment(
body: CreateSegmentRequest,
background_tasks: BackgroundTasks,
bincio_session: Optional[str] = Cookie(default=None),
) -> JSONResponse:
user = _require_user(bincio_session)
@@ -2483,7 +2484,9 @@ async def create_segment(
created_by=user.handle,
created_at=datetime.now(_tz.utc),
)
_seg_store.save_segment(_get_data_dir(), seg)
dd = _get_data_dir()
_seg_store.save_segment(dd, seg)
background_tasks.add_task(_scan_segment_for_user, dd, user.handle, seg_id)
return JSONResponse({"id": seg_id}, status_code=201)
@@ -2529,30 +2532,22 @@ async def get_segment_efforts(
])
@app.post("/api/segments/{segment_id}/detect")
async def trigger_detect(
segment_id: str,
bincio_session: Optional[str] = Cookie(default=None),
) -> JSONResponse:
"""Retroactively detect efforts on a segment for the logged-in user."""
user = _require_user(bincio_session)
dd = _get_data_dir()
seg = _seg_store.load_segment(dd, segment_id)
if seg is None:
raise HTTPException(404, "Segment not found")
def _scan_segment_for_user(dd: Path, handle: str, segment_id: str) -> int:
"""Scan all of a user's activities against one segment. Returns effort count."""
from datetime import datetime as _datetime
from bincio.segments.detect import track_from_timeseries_json, detect_one
import json as _json
user_dir = dd / user.handle
seg = _seg_store.load_segment(dd, segment_id)
if seg is None:
return 0
user_dir = dd / handle
acts_dir = user_dir / "activities"
total = 0
for detail_path in sorted(acts_dir.glob("*.json")):
if ".timeseries." in detail_path.name:
continue
try:
detail = _json.loads(detail_path.read_text(encoding="utf-8"))
detail = json.loads(detail_path.read_text(encoding="utf-8"))
except Exception:
continue
ts_url = detail.get("timeseries_url")
@@ -2562,7 +2557,7 @@ async def trigger_detect(
if not ts_path.exists():
continue
try:
ts = _json.loads(ts_path.read_text(encoding="utf-8"))
ts = json.loads(ts_path.read_text(encoding="utf-8"))
except Exception:
continue
started_raw = detail.get("started_at")
@@ -2578,9 +2573,22 @@ async def trigger_detect(
continue
efforts = detect_one(track, seg)
for effort in efforts:
_seg_store.add_effort(dd, user.handle, segment_id, effort)
_seg_store.add_effort(dd, handle, segment_id, effort)
total += len(efforts)
return total
@app.post("/api/segments/{segment_id}/detect")
async def trigger_detect(
segment_id: str,
bincio_session: Optional[str] = Cookie(default=None),
) -> JSONResponse:
"""Retroactively detect efforts on a segment for the logged-in user."""
user = _require_user(bincio_session)
dd = _get_data_dir()
if _seg_store.load_segment(dd, segment_id) is None:
raise HTTPException(404, "Segment not found")
total = _scan_segment_for_user(dd, user.handle, segment_id)
return JSONResponse({"ok": True, "efforts_found": total})