Strava sync: skip import when a FIT-file upload already covers the same start time

Before importing each Strava activity, build a set of existing timestamp
prefixes (YYYY-MM-DDTHHMMSSZ) from the activities directory. If the incoming
Strava activity matches an existing prefix, record its Strava ID as done and
skip — preventing duplicate entries when a FIT file and a Strava sync both
cover the same ride.

Also reports skipped-existing count in the summary line.
This commit is contained in:
Davide Scaini
2026-05-13 21:52:07 +02:00
parent fb033e3da2
commit 58a5d5b450
+27 -1
View File
@@ -343,9 +343,24 @@ def sync(
owner = index_data.get("owner", {}) owner = index_data.get("owner", {})
summaries: dict[str, dict] = {s["id"]: s for s in index_data.get("activities", [])} summaries: dict[str, dict] = {s["id"]: s for s in index_data.get("activities", [])}
# ── build timestamp-prefix index of existing activities ──────────────────
# Maps "YYYY-MM-DDTHHMMSSZ" → first matching activity filename (stem).
# Used to detect when a FIT-file upload already covers a Strava activity.
acts_dir = output_dir / "activities"
existing_ts: set[str] = set()
if acts_dir.is_dir():
for p in acts_dir.iterdir():
if p.suffix == ".json" and not p.name.endswith(".timeseries.json"):
stem = p.stem
# ID format: YYYY-MM-DDTHHMMSSZ[-optional-slug]
z_pos = stem.find("Z")
if z_pos != -1:
existing_ts.add(stem[: z_pos + 1])
# ── import loop ─────────────────────────────────────────────────────────── # ── import loop ───────────────────────────────────────────────────────────
errors: list[tuple[str, str]] = [] errors: list[tuple[str, str]] = []
imported = 0 imported = 0
skipped_existing = 0
with Progress( with Progress(
TextColumn("[progress.description]{task.description}"), TextColumn("[progress.description]{task.description}"),
@@ -362,11 +377,20 @@ def sync(
try: try:
streams = client.get_streams(act["id"]) streams = client.get_streams(act["id"])
parsed = _strava_to_parsed(act, streams) parsed = _strava_to_parsed(act, streams)
# Skip if any activity already exists for the same start time
ts_part = parsed.started_at.astimezone(timezone.utc).strftime("%Y-%m-%dT%H%M%SZ")
if ts_part in existing_ts:
imported_ids.add(strava_id)
skipped_existing += 1
continue
metrics = compute(parsed) metrics = compute(parsed)
metrics = _patch_from_summary(metrics, act) metrics = _patch_from_summary(metrics, act)
act_id = make_activity_id(parsed) act_id = make_activity_id(parsed)
write_activity(parsed, metrics, output_dir, privacy="public") write_activity(parsed, metrics, output_dir, privacy="public")
summaries[act_id] = build_summary(parsed, metrics, act_id, "public") summaries[act_id] = build_summary(parsed, metrics, act_id, "public")
existing_ts.add(ts_part)
imported_ids.add(strava_id) imported_ids.add(strava_id)
imported += 1 imported += 1
except Exception as exc: except Exception as exc:
@@ -384,9 +408,11 @@ def sync(
from bincio.render.merge import merge_all from bincio.render.merge import merge_all
merge_all(output_dir) merge_all(output_dir)
skipped_msg = f", skipped [bold]{skipped_existing}[/bold] already covered by local uploads" if skipped_existing else ""
console.print( console.print(
f"\n[green]Done.[/green] " f"\n[green]Done.[/green] "
f"Imported [bold]{imported}[/bold] activities, " f"Imported [bold]{imported}[/bold] activities"
f"{skipped_msg}, "
f"errors [bold]{len(errors)}[/bold]." f"errors [bold]{len(errors)}[/bold]."
) )
if errors: if errors: