diff --git a/bincio/extract/parsers/fit.py b/bincio/extract/parsers/fit.py index 5353107..d5e165b 100644 --- a/bincio/extract/parsers/fit.py +++ b/bincio/extract/parsers/fit.py @@ -147,11 +147,13 @@ def _normalise_sub_sport(value: Any) -> str | None: mapping = { "generic": None, # FIT default — unspecified "virtual_activity": "indoor", + "virtual": "indoor", "road": "road", "mountain": "mountain", "gravel_cycling": "gravel", "cyclocross": "gravel", "indoor_cycling": "indoor", + "treadmill": "indoor", "trail": "trail", "track": "track", "cross_country_skiing": "nordic", diff --git a/bincio/extract/writer.py b/bincio/extract/writer.py index d425c78..1eb3d30 100644 --- a/bincio/extract/writer.py +++ b/bincio/extract/writer.py @@ -277,8 +277,10 @@ def write_athlete_json(summaries: list[dict], output_dir: Path, athlete_config: best[d] = w return [[d, w] for d, w in sorted(best.items())] + _INDOOR_SUB_SPORTS = {"indoor", "treadmill", "virtual"} + def _is_outdoor(s: dict) -> bool: - return s.get("sub_sport") != "indoor" + return s.get("sub_sport") not in _INDOOR_SUB_SPORTS all_mmps = [s["mmp"] for s in summaries if s.get("mmp") and _is_outdoor(s)] mmps_365 = [s["mmp"] for s in summaries if s.get("mmp") and _is_outdoor(s) and s["started_at"] >= cutoff_365] diff --git a/bincio/render/cli.py b/bincio/render/cli.py index b2efaa4..489233e 100644 --- a/bincio/render/cli.py +++ b/bincio/render/cli.py @@ -105,6 +105,35 @@ def _bake_tracks(data: Path, handle: str | None = None) -> None: console.print(f" [yellow]{user_dir.name}[/yellow]: bake_tracks failed: {exc}") +def _rebuild_athlete_json(data: Path, handle: str | None = None) -> None: + """Rebuild athlete.json for one user or all users from their current index.json.""" + import json + from bincio.extract.writer import write_athlete_json + + targets = [data / handle] if handle else _user_dirs(data) + _COMPUTED = {"bas_version", "generated_at", "power_curve", "records", "best_climbs"} + for user_dir in targets: + index_path = user_dir / "index.json" + if not index_path.exists(): + continue + try: + index_data = json.loads(index_path.read_text(encoding="utf-8")) + summaries = index_data.get("activities", []) + if not summaries: + continue + athlete_config: dict = {} + athlete_path = user_dir / "athlete.json" + if athlete_path.exists(): + try: + existing = json.loads(athlete_path.read_text(encoding="utf-8")) + athlete_config = {k: v for k, v in existing.items() if k not in _COMPUTED} + except Exception: + pass + write_athlete_json(summaries, user_dir, athlete_config) + except Exception as exc: + console.print(f" [yellow]{user_dir.name}[/yellow]: rebuild_athlete failed: {exc}") + + def _write_root_manifest(data: Path) -> None: """Rewrite the root index.json shard manifest from current user dirs.""" import json @@ -207,6 +236,7 @@ def render( console.print(f"Data: [cyan]{data}[/cyan]") _merge_edits(data, handle=handle) + _rebuild_athlete_json(data, handle=handle) _bake_tracks(data, handle=handle) _write_root_manifest(data)