"""bincio segments — segment management CLI commands.""" from __future__ import annotations import json import sys from datetime import datetime, timezone from pathlib import Path import click def _dt(s: str) -> datetime: return datetime.fromisoformat(s.replace("Z", "+00:00")) @click.group("segments") def segments_group() -> None: """Manage segments and detect efforts.""" @segments_group.command("detect") @click.option("--data-dir", required=True, type=click.Path(), help="BAS data directory (e.g. /var/bincio)") @click.option("--handle", required=True, help="User handle to run detection for") @click.option("--activity-id", default=None, help="Limit to a single activity ID (optional)") @click.option("--segment-id", default=None, help="Limit to a single segment ID (optional)") def detect_cmd(data_dir: str, handle: str, activity_id: str | None, segment_id: str | None) -> None: """Retroactively detect segment efforts for stored activities. Walks every activity with GPS data, runs the detection algorithm against all (or a single) segment, and persists any new efforts found. """ from bincio.segments.detect import track_from_timeseries_json, detect_one, detect_all from bincio.segments import store as _store dd = Path(data_dir).expanduser().resolve() user_dir = dd / handle acts_dir = user_dir / "activities" if not acts_dir.exists(): click.echo(f"No activities directory at {acts_dir}", err=True) sys.exit(1) # Choose which segments to check. if segment_id: seg = _store.load_segment(dd, segment_id) if seg is None: click.echo(f"Segment not found: {segment_id}", err=True) sys.exit(1) segments = [seg] else: segments = _store.list_segments(dd) if not segments: click.echo("No segments defined.", err=True) sys.exit(0) # Choose which activities to process. if activity_id: detail_files = [acts_dir / f"{activity_id}.json"] else: detail_files = sorted(acts_dir.glob("*.json")) # Exclude timeseries files. detail_files = [f for f in detail_files if ".timeseries." not in f.name] total_efforts = 0 processed = 0 for detail_path in detail_files: try: detail = json.loads(detail_path.read_text(encoding="utf-8")) except Exception: continue ts_url = detail.get("timeseries_url") if not ts_url: continue act_id = detail.get("id", detail_path.stem) sport = detail.get("sport", "other") started = detail.get("started_at") if not started: continue try: started_at = _dt(started) except Exception: continue ts_path = user_dir / ts_url if not ts_path.exists(): continue try: ts = json.loads(ts_path.read_text(encoding="utf-8")) except Exception: continue track = track_from_timeseries_json(ts, act_id, sport, started_at) if track is None: continue processed += 1 for seg in segments: from bincio.segments.detect import detect_one efforts = detect_one(track, seg) for effort in efforts: _store.add_effort(dd, handle, seg.id, effort) if efforts: click.echo( f" {act_id}: {len(efforts)} effort(s) on '{seg.name}' " f"({', '.join(str(e.elapsed_s) + 's' for e in efforts)})" ) total_efforts += len(efforts) click.echo(f"\nProcessed {processed} activities, found {total_efforts} effort(s).")