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:
@@ -197,7 +197,7 @@ def _extract_effort(
|
|||||||
j: int,
|
j: int,
|
||||||
) -> SegmentEffort:
|
) -> SegmentEffort:
|
||||||
elapsed_s = track.times[j] - track.times[i]
|
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_speed = _avg_nonnull(track.speeds, i, j)
|
||||||
avg_hr_raw = _avg_nonnull(track.hrs, 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
|
avg_hr = int(round(avg_hr_raw)) if avg_hr_raw is not None else None
|
||||||
|
|||||||
@@ -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:
|
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 = load_efforts(data_dir, handle, segment_id)
|
||||||
efforts = [e for e in efforts if e.activity_id != effort.activity_id or
|
key = (effort.activity_id, _iso(effort.started_at))
|
||||||
e.started_at != effort.started_at]
|
efforts = [e for e in efforts if (e.activity_id, _iso(e.started_at)) != key]
|
||||||
efforts.append(effort)
|
efforts.append(effort)
|
||||||
efforts.sort(key=lambda e: e.started_at, reverse=True)
|
efforts.sort(key=lambda e: e.started_at, reverse=True)
|
||||||
save_efforts(data_dir, handle, segment_id, efforts)
|
save_efforts(data_dir, handle, segment_id, efforts)
|
||||||
|
|||||||
+27
-19
@@ -23,7 +23,7 @@ from typing import Any, Optional
|
|||||||
|
|
||||||
log = logging.getLogger("bincio.serve")
|
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.responses import FileResponse, RedirectResponse, StreamingResponse
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.middleware.gzip import GZipMiddleware
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
@@ -2459,6 +2459,7 @@ async def get_segment(segment_id: str) -> JSONResponse:
|
|||||||
@app.post("/api/segments")
|
@app.post("/api/segments")
|
||||||
async def create_segment(
|
async def create_segment(
|
||||||
body: CreateSegmentRequest,
|
body: CreateSegmentRequest,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
bincio_session: Optional[str] = Cookie(default=None),
|
bincio_session: Optional[str] = Cookie(default=None),
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
user = _require_user(bincio_session)
|
user = _require_user(bincio_session)
|
||||||
@@ -2483,7 +2484,9 @@ async def create_segment(
|
|||||||
created_by=user.handle,
|
created_by=user.handle,
|
||||||
created_at=datetime.now(_tz.utc),
|
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)
|
return JSONResponse({"id": seg_id}, status_code=201)
|
||||||
|
|
||||||
|
|
||||||
@@ -2529,30 +2532,22 @@ async def get_segment_efforts(
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/segments/{segment_id}/detect")
|
def _scan_segment_for_user(dd: Path, handle: str, segment_id: str) -> int:
|
||||||
async def trigger_detect(
|
"""Scan all of a user's activities against one segment. Returns effort count."""
|
||||||
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")
|
|
||||||
|
|
||||||
from datetime import datetime as _datetime
|
from datetime import datetime as _datetime
|
||||||
from bincio.segments.detect import track_from_timeseries_json, detect_one
|
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"
|
acts_dir = user_dir / "activities"
|
||||||
total = 0
|
total = 0
|
||||||
for detail_path in sorted(acts_dir.glob("*.json")):
|
for detail_path in sorted(acts_dir.glob("*.json")):
|
||||||
if ".timeseries." in detail_path.name:
|
if ".timeseries." in detail_path.name:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
detail = _json.loads(detail_path.read_text(encoding="utf-8"))
|
detail = json.loads(detail_path.read_text(encoding="utf-8"))
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
ts_url = detail.get("timeseries_url")
|
ts_url = detail.get("timeseries_url")
|
||||||
@@ -2562,7 +2557,7 @@ async def trigger_detect(
|
|||||||
if not ts_path.exists():
|
if not ts_path.exists():
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
ts = _json.loads(ts_path.read_text(encoding="utf-8"))
|
ts = json.loads(ts_path.read_text(encoding="utf-8"))
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
started_raw = detail.get("started_at")
|
started_raw = detail.get("started_at")
|
||||||
@@ -2578,9 +2573,22 @@ async def trigger_detect(
|
|||||||
continue
|
continue
|
||||||
efforts = detect_one(track, seg)
|
efforts = detect_one(track, seg)
|
||||||
for effort in efforts:
|
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)
|
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})
|
return JSONResponse({"ok": True, "efforts_found": total})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user