diff --git a/bincio/segments/detect.py b/bincio/segments/detect.py index f7cbb96..af1626c 100644 --- a/bincio/segments/detect.py +++ b/bincio/segments/detect.py @@ -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 diff --git a/bincio/segments/store.py b/bincio/segments/store.py index e9d89fe..833ec66 100644 --- a/bincio/segments/store.py +++ b/bincio/segments/store.py @@ -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) diff --git a/bincio/serve/server.py b/bincio/serve/server.py index 6af0551..267e309 100644 --- a/bincio/serve/server.py +++ b/bincio/serve/server.py @@ -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})