feat: gear registry — manage bikes/shoes per athlete, set per activity
- New /api/gear CRUD endpoints (gear.json per user) - Gear tab in AthleteView (owner-only): add, edit, retire items - EditDrawer gear field becomes a dropdown when registry has items - Strava API sync now resolves gear_id → name, adds to registry automatically - Strava ZIP import reads Gear column from activities.csv - POST /api/strava/import-gear for one-time backfill from stored originals
This commit is contained in:
@@ -100,18 +100,22 @@ def strava_sync_iter(
|
||||
- ``"done"`` — final summary; keys: imported, skipped, error_count, errors
|
||||
- ``"error"`` — fatal error before processing started; key: message
|
||||
"""
|
||||
import contextlib
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from bincio.extract.strava_api import (
|
||||
StravaError,
|
||||
ensure_fresh,
|
||||
fetch_activities,
|
||||
fetch_gear,
|
||||
fetch_streams,
|
||||
save_token,
|
||||
strava_meta_to_partial,
|
||||
strava_to_parsed,
|
||||
)
|
||||
from bincio.extract.writer import make_activity_id
|
||||
from bincio.serve.routers.gear import _load as _gear_load, _save as _gear_save
|
||||
|
||||
if not client_id or not client_secret:
|
||||
yield {"type": "error", "message": "Strava not configured"}
|
||||
@@ -137,6 +141,35 @@ def strava_sync_iter(
|
||||
skipped = 0
|
||||
errors: list[str] = []
|
||||
|
||||
# Cache: strava gear_id → gear name (avoid duplicate API calls within one sync)
|
||||
_gear_name_cache: dict[str, str] = {}
|
||||
|
||||
def _resolve_gear(gear_id: str) -> str:
|
||||
"""Return gear name for a Strava gear_id, adding to registry if new."""
|
||||
if gear_id in _gear_name_cache:
|
||||
return _gear_name_cache[gear_id]
|
||||
# Check registry first
|
||||
registry = _gear_load(data_dir)
|
||||
existing = next((g for g in registry if g.get("strava_id") == gear_id), None)
|
||||
if existing:
|
||||
name = existing["name"]
|
||||
_gear_name_cache[gear_id] = name
|
||||
return name
|
||||
# Fetch from Strava
|
||||
details = fetch_gear(token["access_token"], gear_id)
|
||||
name = details.get("name") or ""
|
||||
if not name:
|
||||
_gear_name_cache[gear_id] = ""
|
||||
return ""
|
||||
# Determine type from Strava: primary_type "A" = bike, "B" = shoe
|
||||
gear_type = "shoes" if details.get("primary_type") == "B" else "bike"
|
||||
# Add to registry
|
||||
new_item: dict = {"id": str(uuid.uuid4()), "name": name, "type": gear_type, "retired": False, "strava_id": gear_id}
|
||||
registry.append(new_item)
|
||||
_gear_save(data_dir, registry)
|
||||
_gear_name_cache[gear_id] = name
|
||||
return name
|
||||
|
||||
for n, meta in enumerate(activities, 1):
|
||||
name = meta.get("name", "Untitled")
|
||||
try:
|
||||
@@ -146,6 +179,11 @@ def strava_sync_iter(
|
||||
yield {"type": "progress", "n": n, "total": total, "name": name, "status": "skipped"}
|
||||
continue
|
||||
streams = fetch_streams(token["access_token"], meta["id"])
|
||||
# Resolve gear name before converting
|
||||
gear_id = meta.get("gear_id") or ""
|
||||
if gear_id:
|
||||
with contextlib.suppress(Exception):
|
||||
meta["_gear_name"] = _resolve_gear(gear_id)
|
||||
if originals_dir is not None:
|
||||
orig_path = originals_dir / f"{activity_id}.json"
|
||||
orig_path.write_text(
|
||||
|
||||
Reference in New Issue
Block a user