00
🧪
GOOGLE ACCOUNT
Needed to open and run Google Colab notebooks in your browser.
GARMIN CONNECT
An active Garmin Connect account with your activity history. Email + password required.
🏃
STRAVA ACCOUNT
Required for the Uploader and Visibility Changer. A free account works fine.
🔑
STRAVA API APP
Free to create at strava.com/settings/api. Takes about 2 minutes. See script instructions for details.
📁
SCRIPT FILES
Download all three .py files and upload them to your Colab session.
⏱️
TIME
Downloading and uploading hundreds of activities can take 30–90 minutes. Colab has a 90 min idle timeout.
01
1
OPEN GOOGLE COLAB

Go to colab.research.google.com and create a new notebook, or open an existing one. Sign in with your Google account if prompted.

2
UPLOAD THE SCRIPT

In the left sidebar, click the Files icon (📁), then the upload icon at the top. Upload garmin_download.py to your Colab session.

3
INSTALL DEPENDENCIES

In a new Colab cell, run the install command. This only needs to be done once per session.

colab cell
!pip install garminconnect garth --quiet
4
RUN THE SCRIPT

In another cell, use %run to execute the script. A styled login screen will appear in the cell output.

colab cell
%run garmin_download.py
5
LOG IN TO GARMIN CONNECT

A login card will appear. Scroll down past it — you'll see text input fields for your email and password. Enter your Garmin Connect credentials. The password field is hidden as you type.

💾 After the first login, session tokens are cached to /content/garmin_tokens. On repeat runs, the login screen is skipped automatically — no password needed again.
6
ENTER MFA CODE (IF PROMPTED)

Garmin may send a verification code to your email or phone. If prompted, check your inbox and enter the code in the field that appears below the ▼▼▼ markers.

7
WAIT FOR DOWNLOADS TO COMPLETE

The script fetches your full activity list then downloads FIT, GPX, TCX files and metadata JSON for each run. Progress is shown for every activity. When done, all files are saved to /content/GarminActivities/.

⚠️ Google Colab has a 90 minute idle timeout. For very large libraries (500+ runs), keep the tab active. If it disconnects, re-run — already downloaded activities are safely preserved.
garmin_download.py
# ─── CELL 1: Install ───────────────────────────────────
# !pip install garminconnect garth --quiet

# ─── CELL 2: Imports & Config ──────────────────────────
import json, time, zipfile
from datetime import datetime
from getpass import getpass
from pathlib import Path
from IPython.display import display, HTML
"""
╔══════════════════════════════════════════════════════════════════╗
║         GARMIN CONNECT → RUNNING ACTIVITIES DOWNLOADER          ║
║                    Google Colab Edition  (v3 — login screen)    ║
╚══════════════════════════════════════════════════════════════════╝

SETUP:
  1. In a Colab cell, run:  !pip install garminconnect garth --quiet
  2. Then run this script — a login screen will appear.
     Already logged in this session? Tokens are cached and the
     login screen is skipped automatically.
"""

# ─── CELL 1: Install ─────────────────────────────────────────────────────────
# !pip install garminconnect garth --quiet

# ─── CELL 2: Imports & Config ────────────────────────────────────────────────
import json, time, zipfile
from datetime import datetime
from getpass import getpass
from pathlib import Path
from IPython.display import display, HTML

try:
    import garminconnect
except ImportError:
    raise ImportError("Run: !pip install garminconnect garth --quiet")

OUTPUT_DIR = Path("/content/GarminActivities")
TOKENSTORE = Path("/content/garmin_tokens")
BATCH_SIZE    = 100
DELAY_BETWEEN = 0.6

RUNNING_TYPE_KEYS = {
    "running", "trail_running", "treadmill_running", "indoor_running",
    "virtual_run", "obstacle_run", "street_running", "track_running",
    "ultra_run", "run",
}

# ─── CELL 3: Authenticate ────────────────────────────────────────────────────
def _prompt_mfa():
    print("\n" + "▼"*56)
    print("  GARMIN SENT A VERIFICATION CODE TO YOUR EMAIL / PHONE")
    print("▼"*56)
    return input("  Enter the MFA code here: ").strip()

def show_login_screen():
    display(HTML("""
    
GARMINSTATS // CONNECT DOWNLOADER
EMAIL
Enter your Garmin Connect email below ▼
PASSWORD
Enter your Garmin Connect password below ▼
(hidden as you type via getpass)
🔒 Credentials are used only to authenticate. Never stored — session tokens cached instead.
""")) print("\n" + "▼"*56) print(" GARMIN CONNECT LOGIN — ENTER CREDENTIALS BELOW") print("▼"*56) email = input(" Email → ").strip() password = getpass(" Password → ") print("▼"*56 + "\n") return email, password def authenticate(): if TOKENSTORE.exists(): try: print("🔑 Found cached tokens — attempting fast login …") client = garminconnect.Garmin() client.login(str(TOKENSTORE)) print(f"✅ Logged in as: {client.get_full_name()} (cached)") return client except Exception as e: print(f"⚠️ Cache expired ({e}) — showing login screen …\n") email, password = show_login_screen() if not email or not password: raise ValueError("❌ Email and password are required.") print("🔐 Connecting to Garmin Connect …") client = garminconnect.Garmin(email=email, password=password, is_cn=False, prompt_mfa=_prompt_mfa) try: client.login() except garminconnect.GarminConnectAuthenticationError as exc: print(f"\n⚠️ Garmin requires verification: {exc}") mfa = input(" MFA code → ").strip() if mfa: client.resume_login(mfa) else: raise RuntimeError("Login failed — no MFA code entered.") from exc try: TOKENSTORE.mkdir(parents=True, exist_ok=True) client.garth.dump(str(TOKENSTORE)) print(f"💾 Tokens cached → {TOKENSTORE}") except Exception as e: print(f"⚠️ Could not cache tokens: {e}") print(f"✅ Logged in as: {client.get_full_name()}") return client # ─── CELL 4: Fetch + Download ──────────────────────────────────────────────── def fetch_all_running_activities(client): print("\n📋 Fetching running activity list …") all_activities, start = [], 0 while True: batch = client.get_activities(start, BATCH_SIZE) if not batch: break running = [a for a in batch if a.get("activityType", {}).get("typeKey", "").lower() in RUNNING_TYPE_KEYS] all_activities.extend(running) print(f" scanned {start + len(batch)} total | {len(all_activities)} running", end="\r") if len(batch) < BATCH_SIZE: break start += BATCH_SIZE time.sleep(DELAY_BETWEEN) print(f"\n✅ Found {len(all_activities)} running activities\n") return all_activities def download_activity(client, activity, base_dir): import zipfile aid = activity["activityId"] name = activity.get("activityName", "Unnamed Run") start = activity.get("startTimeLocal", "unknown") safe = "".join(c if c.isalnum() or c in " _-" else "_" for c in name)[:50] folder = base_dir / f"{start[:10]}_{aid}_{safe}" folder.mkdir(parents=True, exist_ok=True) result = {"activity_id": aid, "name": name, "start_time": start, "folder": str(folder), "files": {}, "errors": []} def save_json(key, fn): try: path = folder / f"{key}.json" with open(path, "w") as f: json.dump(fn(), f, indent=2) result["files"][key] = str(path) except Exception as e: result["errors"].append(f"{key}: {e}") def save_binary(key, ext, fn): try: path = folder / f"{aid}.{ext}" with open(path, "wb") as f: f.write(fn()) result["files"][key] = str(path) except Exception as e: result["errors"].append(f"{key}: {e}") save_json("metadata", lambda: activity) save_json("details", lambda: client.get_activity_details(aid)) save_json("hr_zones", lambda: client.get_activity_hr_in_timezones(aid)) try: raw = client.download_activity(aid, dl_fmt=client.ActivityDownloadFormat.ORIGINAL) zip_path = folder / f"{aid}.zip" zip_path.write_bytes(raw) with zipfile.ZipFile(zip_path) as zf: for member in zf.namelist(): if member.lower().endswith(".fit"): fit_path = folder / f"{aid}.fit" fit_path.write_bytes(zf.read(member)) result["files"]["fit"] = str(fit_path) break zip_path.unlink(missing_ok=True) except Exception as e: result["errors"].append(f"fit: {e}") save_binary("gpx", "gpx", lambda: client.download_activity(aid, dl_fmt=client.ActivityDownloadFormat.GPX)) save_binary("tcx", "tcx", lambda: client.download_activity(aid, dl_fmt=client.ActivityDownloadFormat.TCX)) return result def main(): OUTPUT_DIR.mkdir(parents=True, exist_ok=True) client = authenticate() activities = fetch_all_running_activities(client) if not activities: print("⚠️ No running activities found.") return print(f"⬇️ Downloading {len(activities)} activities → {OUTPUT_DIR}\n{'─'*70}") results, ok, err = [], 0, 0 for i, activity in enumerate(activities, 1): dist = round(activity.get("distance", 0) / 1000, 2) print(f"[{i:>4}/{len(activities)}] {activity.get('startTimeLocal','?')[:10]} | " f"{activity.get('activityName','Run')[:35]:<35} | {dist:>6.2f} km", end=" … ") try: r = download_activity(client, activity, OUTPUT_DIR) results.append(r) if r["errors"]: print(f"⚠️ ({r['errors'][0]})"); err += 1 else: print("✅"); ok += 1 except Exception as e: print(f"❌ {e}"); err += 1 time.sleep(DELAY_BETWEEN) index_path = OUTPUT_DIR / "download_index.json" index_path.write_text(json.dumps({ "downloaded_at": datetime.now().isoformat(), "total": len(activities), "success": ok, "errors": err, "activities": results, }, indent=2)) print(f"\n{'─'*70}") print(f"🏁 Done! ✅ {ok} clean | ⚠️ {err} with partial errors") print(f"📁 {OUTPUT_DIR}") if __name__ == "__main__": main()
02
1
CREATE A STRAVA API APP

Go to strava.com/settings/api and create a new app. Fill in:

App Name: anything (e.g. GarminImport)
Website: https://localhost
Authorization Callback Domain: localhost

Copy your Client ID and Client Secret — you'll need them next.

2
ADD YOUR CREDENTIALS TO THE SCRIPT

Open strava_upload.py and find these two lines near the top. Replace the placeholder values with your actual Client ID and Secret.

strava_upload.py — credentials section
# ── Your Strava app credentials ──────────────
CLIENT_ID     = "YOUR_CLIENT_ID_HERE"
CLIENT_SECRET = "YOUR_CLIENT_SECRET_HERE"
3
RUN THE GARMIN DOWNLOADER FIRST

The upload script reads the download_index.json file created by the Garmin downloader. Make sure you've run garmin_download.py and the downloads are complete before proceeding.

4
UPLOAD THE SCRIPT AND RUN IT

Upload strava_upload.py to your Colab session and run it in a new cell.

colab cell
%run strava_upload.py
5
AUTHORIZE STRAVA (ONE-TIME)

On the first run, the script prints an authorization URL. Open it in your browser, click Authorize, then copy the full redirect URL from the address bar (it starts with http://localhost/...) and paste it back into the cell input.

💡 The page will show a browser error — that's normal. localhost isn't a real server. You just need the URL from the address bar before closing the tab.
6
CHOOSE UPLOAD MODE

The script shows a menu with 5 options:

[1] Upload all remaining (skips already uploaded)
[2] Start from a specific number
[3] Upload specific activity numbers
[4] Show full list first, then choose
[5] Quit

♻️ Progress is saved after every upload. If the session disconnects, re-run and choose option [2] to resume from where you left off. Duplicates are detected and skipped automatically.
strava_upload.py
# ─── GARMIN ACTIVITIES → STRAVA AUTO-UPLOADER ───────────
# Run with: %run strava_upload.py

import json, time
from pathlib import Path
from datetime import datetime
from urllib.parse import urlencode, urlparse, parse_qs
import requests
"""
╔══════════════════════════════════════════════════════════════════╗
║            GARMIN ACTIVITIES → STRAVA AUTO-UPLOADER             ║
║                    Google Colab Edition  (v3)                   ║
╚══════════════════════════════════════════════════════════════════╝
Run with:  %run strava_upload.py
"""

# !pip install requests --quiet

import json, time
from pathlib import Path
from datetime import datetime
from urllib.parse import urlencode, urlparse, parse_qs
import requests

GARMIN_DIR = Path("/content/GarminActivities")
TOKEN_FILE = Path("/content/strava_token.json")

# ── ADD YOUR STRAVA APP CREDENTIALS HERE ──────────────────────────
CLIENT_ID     = "YOUR_CLIENT_ID_HERE"
CLIENT_SECRET = "YOUR_CLIENT_SECRET_HERE"

STRAVA_AUTH_URL   = "https://www.strava.com/oauth/authorize"
STRAVA_TOKEN_URL  = "https://www.strava.com/oauth/token"
STRAVA_UPLOAD_URL = "https://www.strava.com/api/v3/uploads"

DELAY_BETWEEN = 3
POLL_INTERVAL = 4
POLL_MAX      = 15

def save_token(token):
    with open(TOKEN_FILE, "w") as f:
        json.dump(token, f, indent=2)

def do_refresh(refresh_tok):
    resp = requests.post(STRAVA_TOKEN_URL, data={
        "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET,
        "refresh_token": refresh_tok, "grant_type": "refresh_token",
    })
    resp.raise_for_status()
    token = resp.json()
    save_token(token)
    return token

def get_valid_token():
    if TOKEN_FILE.exists():
        with open(TOKEN_FILE) as f:
            token = json.load(f)
        if "activity:write" in token.get("scope", ""):
            if token.get("expires_at", 0) > time.time() + 60:
                print("✅ Using cached Strava token")
                return token
            print("🔄 Refreshing token …")
            token = do_refresh(token["refresh_token"])
            return token
        print("⚠️  Token missing activity:write scope — re-authorizing …")
    params = {
        "client_id": CLIENT_ID, "response_type": "code",
        "redirect_uri": "http://localhost/exchange_token",
        "approval_prompt": "force",
        "scope": "activity:write,activity:read_all",
    }
    auth_url = f"{STRAVA_AUTH_URL}?{urlencode(params)}"
    print("\n" + "═"*65)
    print("  STRAVA AUTHORIZATION — one-time setup")
    print("═"*65)
    print(f"\n1️⃣  Open this URL:\n\n   {auth_url}")
    print("\n2️⃣  Click Authorize.")
    print("\n3️⃣  Copy the full redirect URL (starts with http://localhost/...)")
    print("═"*65)
    redirect = input("\nPaste the full redirect URL: ").strip()
    code = parse_qs(urlparse(redirect).query).get("code", [None])[0]
    if not code:
        raise ValueError("❌ No code found — copy the full URL.")
    resp = requests.post(STRAVA_TOKEN_URL, data={
        "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET,
        "code": code, "grant_type": "authorization_code",
    })
    resp.raise_for_status()
    token = resp.json()
    save_token(token)
    a = token.get("athlete", {})
    print(f"\n✅ Authorized as: {a.get('firstname','')} {a.get('lastname','')}")
    return token

class RateLimitError(Exception): pass

def upload_fit(access_token, fit_path, name):
    headers = {"Authorization": f"Bearer {access_token}"}
    with open(fit_path, "rb") as f:
        resp = requests.post(STRAVA_UPLOAD_URL, headers=headers,
            files={"file": (fit_path.name, f, "application/octet-stream")},
            data={"data_type": "fit", "name": name, "sport_type": "Run"})
    if resp.status_code == 429: raise RateLimitError("Rate limit hit")
    resp.raise_for_status()
    upload_id = resp.json()["id"]
    for _ in range(POLL_MAX):
        time.sleep(POLL_INTERVAL)
        status = requests.get(f"{STRAVA_UPLOAD_URL}/{upload_id}", headers=headers).json()
        if status.get("error"):
            return {"status": "error", "error": status["error"], "upload_id": upload_id}
        if status.get("activity_id"):
            return {"status": "success", "strava_activity_id": status["activity_id"], "upload_id": upload_id}
    return {"status": "timeout", "upload_id": upload_id}

def discover_activities():
    index_path = GARMIN_DIR / "download_index.json"
    if not index_path.exists():
        raise FileNotFoundError(f"No download_index.json — run garmin_download.py first.")
    with open(index_path) as f: index = json.load(f)
    activities = []
    for e in index.get("activities", []):
        fit = e.get("files", {}).get("fit")
        if fit and Path(fit).exists():
            activities.append({"activity_id": str(e["activity_id"]), "name": e.get("name","Run"),
                                "start_time": e.get("start_time","?"), "fit_path": Path(fit)})
    return activities

def prompt_upload_mode(activities, upload_log):
    already_done = sum(1 for a in activities
                       if upload_log.get(a["activity_id"], {}).get("status") == "success")
    print(f"\n📊 {len(activities)} activities  |  {already_done} already uploaded\n")
    print("  [1] Upload ALL remaining")
    print("  [2] Start from a specific number")
    print("  [3] Upload specific numbers")
    print("  [4] Show full list first")
    print("  [5] Quit\n")
    choice = input("Enter choice [1-5]: ").strip()
    if choice == "1":
        return [a for a in activities if upload_log.get(a["activity_id"],{}).get("status") != "success"]
    elif choice == "2":
        n = int(input(f"Start from number (1–{len(activities)}): ").strip())
        return [a for a in activities[n-1:] if upload_log.get(a["activity_id"],{}).get("status") != "success"]
    elif choice == "3":
        nums = [int(x.strip()) for x in input("Numbers (comma-separated): ").split(",")]
        return [activities[n-1] for n in nums]
    elif choice == "4":
        for i, a in enumerate(activities, 1):
            st = upload_log.get(a["activity_id"], {}).get("status","—")
            print(f"  {i:>4}  {a['start_time'][:10]}  {a['name'][:40]}  {st}")
        return prompt_upload_mode(activities, upload_log)
    else:
        return []

def run_uploads(to_upload, upload_log, log_path, token):
    access_token = token["access_token"]
    ok = dupes = err = 0
    print(f"⬆️  Uploading {len(to_upload)} activities …\n{'─'*65}")
    for i, act in enumerate(to_upload, 1):
        aid, name = act["activity_id"], act["name"]
        print(f"[{i:>4}/{len(to_upload)}] {act['start_time'][:10]} | {name[:40]:<40}", end="  … ")
        try:
            result = upload_fit(access_token, act["fit_path"], name)
            upload_log[aid] = {**result, "uploaded_at": datetime.now().isoformat(), "name": name}
            if result["status"] == "success":
                print(f"✅  strava.com/activities/{result['strava_activity_id']}"); ok += 1
            elif result["status"] == "error":
                e = result.get("error","")
                if "duplicate" in e.lower(): print("⏭️  duplicate"); dupes += 1
                else: print(f"❌  {e}"); err += 1
            else: print("⏳  timeout"); err += 1
        except RateLimitError:
            print("🚫 Rate limit — pausing 15 minutes …")
            log_path.write_text(json.dumps(upload_log, indent=2))
            time.sleep(900)
            continue
        except Exception as e:
            print(f"❌  {e}"); err += 1
        log_path.write_text(json.dumps(upload_log, indent=2))
        time.sleep(DELAY_BETWEEN)
    print(f"\n{'─'*65}")
    print(f"🏁 Done!  ✅ {ok} uploaded  |  ⏭️  {dupes} duplicates  |  ❌ {err} errors")

def main():
    log_path   = GARMIN_DIR / "strava_upload_log.json"
    upload_log = json.loads(log_path.read_text()) if log_path.exists() else {}
    token      = get_valid_token()
    activities = discover_activities()
    if not activities:
        print("⚠️  No .fit files found — run garmin_download.py first.")
        return
    to_upload = prompt_upload_mode(activities, upload_log)
    if to_upload:
        run_uploads(to_upload, upload_log, log_path, token)

if __name__ == "__main__":
    main()
03
1
ADD YOUR STRAVA CREDENTIALS

Open strava_visibility.py and update the credentials at the top of the script. Use the same Strava API app you created for the uploader.

strava_visibility.py — credentials
CLIENT_ID     = "YOUR_CLIENT_ID_HERE"
CLIENT_SECRET = "YOUR_CLIENT_SECRET_HERE"
2
UPLOAD AND RUN

Upload strava_visibility.py to Colab and run it. No extra pip install needed — it only uses the built-in requests library.

colab cell
%run strava_visibility.py
3
AUTHORIZE STRAVA (ONE-TIME)

A styled authorization card appears with a button. Click Authorize on Strava, approve the app, then copy the full http://localhost/... URL from your browser and paste it into the input field below the card.

💾 The token is saved to /content/strava_visibility_token.json. On future runs it refreshes automatically — no need to authorize again.
4
SELECT ACTIVITY TYPE

A numbered menu appears with 16 activity types. Pick the type you want to change, or choose [0] ALL to affect every activity on your account.

[1] Run   [2] TrailRun   [3] VirtualRun   [4] Walk   [5] Hike
[6] Ride   [7] VirtualRide   [8] Swim   [9] Workout   [10] WeightTraining
[11] Yoga   [12] Rowing   ...   [0] ALL

5
SELECT TARGET VISIBILITY

Choose what visibility to set:

[1] 🌍 everyone — fully public
[2] 👥 followers_only — only your followers
[3] 🔒 only_me — private

6
CONFIRM AND WATCH IT RUN

A preview table shows the first 10 matching activities and their current visibility. Activities already at the target visibility are counted and skipped. Type YES to confirm, then watch the progress bar as each activity is updated.

The script auto-pauses for 15 minutes every 90 requests to stay under Strava's rate limit. For large accounts this may take a while — keep the Colab tab active.
strava_visibility.py
# ─── STRAVA BULK VISIBILITY CHANGER ────────────────────
import requests, time, json
from IPython.display import display, HTML

CLIENT_ID     = "YOUR_CLIENT_ID_HERE"
CLIENT_SECRET = "YOUR_CLIENT_SECRET_HERE"
"""
╔══════════════════════════════════════════════════════════════════╗
║         STRAVA BULK VISIBILITY CHANGER  ·  Google Colab         ║
║  Changes the visibility of all activities of a chosen type      ║
╚══════════════════════════════════════════════════════════════════╝
"""

import requests, time, json
from IPython.display import display, HTML

# ── ADD YOUR STRAVA APP CREDENTIALS HERE ──────────────────────────
CLIENT_ID     = "YOUR_CLIENT_ID_HERE"
CLIENT_SECRET = "YOUR_CLIENT_SECRET_HERE"

TOKEN_FILE = "/content/strava_visibility_token.json"
AUTH_URL   = "https://www.strava.com/oauth/authorize"
TOKEN_URL  = "https://www.strava.com/oauth/token"
API_BASE   = "https://www.strava.com/api/v3"

REQUESTS_PER_15MIN = 90
DELAY_BETWEEN      = 0.7
RATE_PAUSE         = 900

ACTIVITY_TYPES = {
    "1":  ("Run","🏃 Running"),  "2": ("TrailRun","🌲 Trail Running"),
    "3":  ("VirtualRun","🖥️ Virtual Run"), "4": ("Walk","🚶 Walking"),
    "5":  ("Hike","⛰️ Hiking"),  "6": ("Ride","🚴 Cycling"),
    "7":  ("VirtualRide","🖥️ Virtual Ride"), "8": ("Swim","🏊 Swimming"),
    "9":  ("Workout","💪 Workout"), "10": ("WeightTraining","🏋️ Weight Training"),
    "11": ("Yoga","🧘 Yoga"), "12": ("Rowing","🚣 Rowing"),
    "13": ("Kayaking","🛶 Kayaking"), "14": ("Soccer","⚽ Soccer"),
    "15": ("Tennis","🎾 Tennis"), "16": ("Golf","⛳ Golf"),
    "0":  ("ALL","🌐 ALL activity types"),
}

VISIBILITY_OPTIONS = {
    "1": ("everyone",       "🌍 Public — visible to everyone"),
    "2": ("followers_only", "👥 Followers only"),
    "3": ("only_me",        "🔒 Private — only you"),
}

def load_token():
    try:
        with open(TOKEN_FILE) as f: tok = json.load(f)
        if tok.get("expires_at", 0) < time.time() + 60:
            tok = refresh_token(tok["refresh_token"])
        return tok
    except: return None

def refresh_token(refresh_tok):
    resp = requests.post(TOKEN_URL, data={
        "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET,
        "grant_type": "refresh_token", "refresh_token": refresh_tok,
    })
    resp.raise_for_status()
    tok = resp.json()
    with open(TOKEN_FILE, "w") as f: json.dump(tok, f)
    print("✅ Token refreshed")
    return tok

def authorize():
    redirect_uri = "http://localhost"
    auth_link = (f"{AUTH_URL}?client_id={CLIENT_ID}&response_type=code"
                 f"&redirect_uri={redirect_uri}&approval_prompt=auto"
                 f"&scope=activity:read_all,activity:write")
    display(HTML(f"""
    
🔗 STRAVA AUTHORIZATION — 3 STEPS
STEP 1 — CLICK THIS BUTTON
AUTHORIZE ON STRAVA ↗
STEP 2 — COPY THE REDIRECT URL
After clicking Authorize, copy the full URL from your browser address bar.
It looks like: http://localhost/?state=&code=abc123...&scope=...
STEP 3 — PASTE BELOW ↓
Scroll down — there is a text input field waiting for you.
""")) print("\n" + "▼"*60) print(" PASTE THE REDIRECT URL INTO THE BOX BELOW AND PRESS ENTER") print("▼"*60) redirect_resp = input(" Redirect URL → ").strip() from urllib.parse import urlparse, parse_qs code = parse_qs(urlparse(redirect_resp).query).get("code", [None])[0] if not code: raise ValueError("❌ No code found in URL.") resp = requests.post(TOKEN_URL, data={ "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "grant_type": "authorization_code", "code": code, "redirect_uri": redirect_uri, }) resp.raise_for_status() tok = resp.json() with open(TOKEN_FILE, "w") as f: json.dump(tok, f) athlete = tok.get("athlete", {}) name = f"{athlete.get('firstname','')} {athlete.get('lastname','')}".strip() print(f"\n✅ Authorized as: {name}") return tok # ── AUTH ────────────────────────────────────────────────────────── print("🔐 Checking Strava authorization...") token = load_token() if token: athlete = requests.get(f"{API_BASE}/athlete", headers={"Authorization": f"Bearer {token['access_token']}"}).json() print(f"✅ Already authorized as: {athlete.get('firstname','')} {athlete.get('lastname','')}") else: token = authorize() ACCESS_TOKEN = token["access_token"] HEADERS = {"Authorization": f"Bearer {ACCESS_TOKEN}"} # ── SELECT ACTIVITY TYPE ────────────────────────────────────────── print("\n" + "═"*56 + "\n ACTIVITY TYPE\n" + "═"*56) for k, (_, label) in ACTIVITY_TYPES.items(): print(f" [{k:>2}] {label}") while True: type_choice = input("\nEnter number: ").strip() if type_choice in ACTIVITY_TYPES: break print(" ⚠ Invalid choice") chosen_type, chosen_type_label = ACTIVITY_TYPES[type_choice] # ── SELECT VISIBILITY ───────────────────────────────────────────── print("\n" + "═"*56 + "\n SET VISIBILITY TO\n" + "═"*56) for k, (_, label) in VISIBILITY_OPTIONS.items(): print(f" [{k}] {label}") while True: vis_choice = input("\nEnter number: ").strip() if vis_choice in VISIBILITY_OPTIONS: break print(" ⚠ Invalid choice") chosen_visibility, chosen_vis_label = VISIBILITY_OPTIONS[vis_choice] # ── CONFIRM ─────────────────────────────────────────────────────── print(f"\n Activity type : {chosen_type_label}") print(f" New visibility: {chosen_vis_label}") if input("\n Type YES to proceed: ").strip().upper() != "YES": print(" ⛔ Cancelled."); raise SystemExit # ── FETCH ACTIVITIES ────────────────────────────────────────────── print("\n📡 Fetching your Strava activities...") all_acts, page = [], 1 while True: resp = requests.get(f"{API_BASE}/athlete/activities", headers=HEADERS, params={"per_page": 200, "page": page}) if resp.status_code == 429: time.sleep(RATE_PAUSE); continue resp.raise_for_status() batch = resp.json() if not batch: break for act in batch: atype = act.get("sport_type") or act.get("type") or "" if chosen_type == "ALL" or atype == chosen_type: all_acts.append(act) print(f" Scanned {(page-1)*200+len(batch)} activities, matched {len(all_acts)} so far...", end="\r") if len(batch) < 200: break page += 1 time.sleep(DELAY_BETWEEN) print(f"\n✅ Found {len(all_acts)} matching activities") needs_update = [a for a in all_acts if a.get("visibility") != chosen_visibility] already_correct = len(all_acts) - len(needs_update) print(f"\n Already correct: {already_correct}") print(f" Need updating : {len(needs_update)}") if not needs_update: print(f"\n✅ All activities already set to '{chosen_visibility}'."); raise SystemExit if input(f"\n🚀 Update {len(needs_update)} activities? Press ENTER (or type SKIP): ").strip().upper() == "SKIP": raise SystemExit # ── UPDATE ──────────────────────────────────────────────────────── success_count = fail_count = request_count = 0 for i, act in enumerate(needs_update): act_id = act["id"] title = act.get("name","Untitled")[:40] if request_count > 0 and request_count % REQUESTS_PER_15MIN == 0: print(f"\n⏳ Rate limit guard — pausing 15 min...") time.sleep(RATE_PAUSE) resp = requests.put(f"{API_BASE}/activities/{act_id}", headers=HEADERS, json={"visibility": chosen_visibility}) request_count += 1 if resp.status_code == 429: time.sleep(RATE_PAUSE) resp = requests.put(f"{API_BASE}/activities/{act_id}", headers=HEADERS, json={"visibility": chosen_visibility}) if resp.ok: success_count += 1 pct = int((i+1)/len(needs_update)*30) bar = "█"*pct + "░"*(30-pct) print(f" ✅ [{bar}] {i+1}/{len(needs_update)} {title[:28]}", end="\r") else: fail_count += 1 print(f"\n ❌ FAILED [{i+1}] HTTP {resp.status_code} {title}") time.sleep(DELAY_BETWEEN) print(f"\n\n{'═'*56}\n 🏁 DONE\n{'═'*56}") print(f" Updated: {success_count} | Skipped: {already_correct} | Failed: {fail_count}") vis_icon = {"everyone":"🌍","followers_only":"👥","only_me":"🔒"}.get(chosen_visibility,"") print(f"\n {vis_icon} All {chosen_type_label} activities are now: {chosen_vis_label}\n")
04
?
COLAB DISCONNECTS MID-DOWNLOAD

Already downloaded activities are preserved in /content/GarminActivities/. Just re-run garmin_download.py — it picks up from where it left off. For uploads, use option [2] to start from a specific activity number.

?
GARMIN MFA CODE NOT ARRIVING

Check your spam folder. The code is sent to the email address associated with your Garmin account. You have about 5 minutes to enter it before it expires. If it expires, just re-run the script to get a new one.

?
STRAVA "NO CODE FOUND" ERROR

Make sure you copied the full URL from your browser, including the ?state=&code=... part. The browser may truncate it if you single-click — triple-click the address bar to select all, then copy.

?
STRAVA RATE LIMIT (429 ERROR)

Strava allows 100 API requests per 15 minutes. All three scripts handle this automatically by pausing and resuming. If you're running multiple scripts simultaneously, pause one while the other is active.