f2788d9e1d
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
94 lines
2.9 KiB
Python
94 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Download GPX files for your Garmin Connect activities.
|
|
|
|
Setup:
|
|
pip install garminconnect
|
|
|
|
Usage:
|
|
python garmin_gpx_export.py
|
|
|
|
You'll be prompted for your Garmin Connect email and password
|
|
(the same ones you use to log into connect.garmin.com).
|
|
Credentials are only used locally to authenticate with Garmin's
|
|
servers and are never stored or sent anywhere else.
|
|
"""
|
|
|
|
import getpass
|
|
import os
|
|
from datetime import datetime
|
|
|
|
from garminconnect import Garmin, GarminConnectAuthenticationError
|
|
|
|
|
|
def get_mfa_code():
|
|
"""Called automatically by garminconnect if Garmin requests an MFA code."""
|
|
return input("Enter the MFA code sent to you by Garmin: ").strip()
|
|
|
|
|
|
def main():
|
|
# --- Login ---
|
|
email = input("Garmin Connect email: ").strip()
|
|
password = getpass.getpass("Garmin Connect password: ")
|
|
|
|
try:
|
|
client = Garmin(email=email, password=password, prompt_mfa=get_mfa_code)
|
|
client.login()
|
|
except GarminConnectAuthenticationError:
|
|
print("Login failed. Check your email/password/MFA code.")
|
|
return
|
|
|
|
print(f"Logged in as {client.get_full_name()}")
|
|
|
|
# --- Config ---
|
|
output_dir = "garmin_gpx_exports"
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# How many recent activities to check (increase if you want more history)
|
|
limit = 50
|
|
|
|
# Only export activities matching this type, or set to None for all types.
|
|
# Common values: "cycling", "running", "walking", "hiking", "swimming"
|
|
activity_type_filter = "cycling"
|
|
|
|
# --- Fetch activity list ---
|
|
activities = client.get_activities(0, limit)
|
|
print(f"Found {len(activities)} recent activities (checking up to {limit}).")
|
|
|
|
exported = 0
|
|
for act in activities:
|
|
act_type = act.get("activityType", {}).get("typeKey", "")
|
|
if activity_type_filter and activity_type_filter not in act_type:
|
|
continue
|
|
|
|
activity_id = act["activityId"]
|
|
start_time = act.get("startTimeLocal", "unknown_time")
|
|
name = act.get("activityName", "activity")
|
|
|
|
# Sanitize filename
|
|
safe_time = start_time.replace(":", "-").replace(" ", "_")
|
|
safe_name = "".join(c for c in name if c.isalnum() or c in (" ", "_", "-")).strip()
|
|
filename = f"{safe_time}_{safe_name}_{activity_id}.gpx"
|
|
filepath = os.path.join(output_dir, filename)
|
|
|
|
if os.path.exists(filepath):
|
|
print(f" Skipping (already downloaded): {filename}")
|
|
continue
|
|
|
|
try:
|
|
gpx_data = client.download_activity(
|
|
activity_id, dl_fmt=client.ActivityDownloadFormat.GPX
|
|
)
|
|
with open(filepath, "wb") as f:
|
|
f.write(gpx_data)
|
|
print(f" Saved: {filename}")
|
|
exported += 1
|
|
except Exception as e:
|
|
print(f" Failed to export activity {activity_id}: {e}")
|
|
|
|
print(f"\nDone. Exported {exported} new GPX file(s) to ./{output_dir}/")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|