Files
bincio-activity/bincio/segments/cli.py
T

121 lines
3.9 KiB
Python

"""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)")
@click.option("--fresh", is_flag=True, default=False, help="Clear existing efforts before detecting")
def detect_cmd(data_dir: str, handle: str, activity_id: str | None, segment_id: str | None, fresh: bool) -> 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)
if fresh:
for seg in segments:
_store.save_efforts(dd, handle, seg.id, [])
click.echo(f"Cleared existing efforts for {len(segments)} segment(s).")
# 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).")