Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

strava-utils

Small scripts for poking at Strava data. Standard library only (no pip install).

Auth

1. Create the API app

Go to https://www.strava.com/settings/api:

Field What to put Why
Application Name anything, e.g. strutils cosmetic, can't include "strava"
Category anything, e.g. Data Importer cosmetic
Club leave empty cosmetic
Website anything valid, e.g. your GitHub URL cosmetic, but must parse as a URL
Application Description leave empty cosmetic
Authorization Callback Domain localhost must be exactly this: no http://, no port, no path, or the redirect fails

Strava then demands an app icon before showing the credentials. Upload any small square image. Client ID and Client Secret (click "Show") appear after that.

2. Run the token helper

python3 get_token.py

Prompts for Client ID and Secret, opens the consent page, catches the redirect on http://localhost:8080, and writes .env (chmod 600, gitignored). The other scripts read it automatically.

Keep both boxes checked on the consent screen: "View data about your private activities" (activity:read_all, without it private activities are silently missing) and "Upload your activities" (activity:write, without it every import_gpx.py upload fails with Authorization Error). The helper warns if either was not granted.

Refresh tokens do not expire, so this is one-time. To skip the refresh flow instead, put STRAVA_ACCESS_TOKEN=... (valid ~6h) in .env or the environment.

find_duplicate_runs.py

Every day with more than one run, flagging pairs that look like the same activity imported twice.

python3 find_duplicate_runs.py --after 2024-01-01
2024-05-12  (2 activities)
  07:02   10.03 km   52 min  Morning Run                              https://www.strava.com/activities/111
  07:02   10.05 km   52 min  Morning Run (imported)                   https://www.strava.com/activities/222
  !! likely duplicate: 111 <-> 222

1 multi-activity days, 1 likely duplicate pairs.

Flagged when distance and moving time both match within 2%. Genuine doubles are printed but not flagged. Nothing is ever deleted.

Flag Meaning
--type Run sport_type filter. Pass --type '' for all types. Default Run.
--after YYYY-MM-DD Only activities after this date.
--json Dump the grouped days as JSON instead of the report.
--no-verify Skip the deleted-activity recheck (fewer requests, stale results).
--demo Run the built-in self-check.

Strava's activity list keeps returning activities for a while after you delete them, so each candidate day is re-checked one activity at a time and anything already gone is dropped.

import_gpx.py

Bulk-import GPX files, one upload at a time.

python3 import_gpx.py ./tracks --sport-type Run
312 file(s) to upload, 0 already done.
[1/312] 2019-03-04-run.gpx: ok https://www.strava.com/activities/111
[2/312] 2019-03-06-run.gpx: duplicate of activity 222
rate limited, sleeping 11.4 min until 14:15 CEST
[3/312] 2019-03-09-run.gpx: ok https://www.strava.com/activities/333

Directories are searched recursively, so a tree like tracks/2019/…/track.gpx works as-is; only .gpx and .gpx.gz are picked up. Each settled file (imported, or rejected as a duplicate) has its full path appended to .imported and is skipped on the next run, so an interrupted import resumes where it stopped and same-named files in different subfolders never shadow each other. Delete .imported to start over.

Flag Meaning
--sport-type Run Run, Ride, Hike, ... Default: whatever Strava reads from the file.
--description ... Applied to every upload.
--no-wait Queue and move on: halves the requests, but duplicates/errors go unseen.
--state PATH Use a different done-list file.
--dry-run Print the files that would be uploaded.
--demo Offline self-check of the multipart encoder and rate-limit math.

The GPX itself supplies the name, dates and track.

strava.py

Not a script. Shared plumbing the others import: .env loading, access_token() (refresh flow), request_json(), rate_limit_reset(), fetch_activities(), and the API / TOKEN_URL / ENV_FILE constants.

Limits

Default limits are 100 requests / 15 min and 1,000 / day. On a 429, request_json() reads X-RateLimit-Usage and sleeps until the 15-minute window resets (aligned to :00 :15 :30 :45), up to 10 waits. If the daily limit is what ran out, it exits and prints the local datetime to retry after — re-running import_gpx.py then picks up from .imported.

An import costs 2 requests per file (upload + one status poll, more if Strava is slow to process), so roughly 45 files per 15-minute window and ~450 per day. 300+ files means a few sleeps but one run; --no-wait doubles the throughput at the cost of not seeing failures.

Strava subscribers (premium) can raise their app's limits to 400 requests / 15 min and 4,000 / day — request the increase from the app's page at https://www.strava.com/settings/api (or via https://developers.strava.com/docs/rate-limits/). That takes the same import to ~200 files per window, i.e. one pass with a single sleep. Nothing in these scripts needs changing: the backoff reads the actual limits off the response headers.

Reports only, never deleted.

About

A small collection of utilities built around the Strava API for managing and maintaining activity data.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages