public
Last active

bulk-export run data from Garmin Connect

  • Download Gist
disconnect.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
#!/usr/bin/env ruby
 
## disconnect
 
# ./disconnect.rb -u yourusername
#
# This is a command-line utility for the bulk-downloading of run data from
# the connect.garmin.com web application, which has lackluster export
# capabilities.
#
# Using this code is a matter of your own relationship with Garmin Connect
# and their TOS. I can't imagine this being very destructive to their service,
# and it's just filling in a hole in their existing service.
#
# It's built against Garmin Connect as of July 22, 2011. It's a scraper:
# thus if Garmin changes, this **will break**.
#
# This script requires all of the utilities on the line below: install them
# with rubygems
%w{rubygems json fileutils mechanize choice highline/import}.map{|x| require x}
 
LOGIN_PAGE = "https://connect.garmin.com/signin"
ACTIVITIES_SEARCH = "http://connect.garmin.com/proxy/activity-search-service-1.0/json/activities?_dc=1220170621856&start=0&limit=1000"
GPX_EXPORT = "http://connect.garmin.com/proxy/activity-service-1.1/gpx/activity/%d?full=true"
KML_EXPORT = "http://connect.garmin.com/proxy/activity-service-1.0/kml/activity/%d?full=true"
TCX_EXPORT = "http://connect.garmin.com/proxy/activity-service-1.0/tcx/activity/%d?full=true"
 
Choice.options do
header ''
header 'Specific options:'
 
option :user, :required => true do
short '-u'
long '--user=USER'
desc 'connect.garmin.com username. Required'
end
 
option :dir do
short '-o'
long '--output-dir=OUTPUT'
desc 'the directory to save .tcx files'
default 'tcx'
end
end
 
password = ask("Enter your password: " ) { |q| q.echo = "*" }
 
def login(agent, user, password)
page = agent.get(LOGIN_PAGE)
login_form = page.form('login')
login_form['login:loginUsernameField'] = user
login_form['login:password'] = password
 
page = agent.submit(login_form, login_form.buttons.first)
raise "Login incorrect!" if page.title().match('Sign In')
page
end
 
def download_run(agent, id)
print "."
# This downloads TCX files: you can swap out the constant, or add
# more lines that download the different kinds of exports. I prefer TCX,
# because despite being a 'private standard,' it includes all data,
# including heart rate data.
agent.get(TCX_EXPORT % (id).to_i).save_as(File.join(Choice[:dir], "%d.tcx" % id))
end
 
def activities(agent)
j = agent.get(ACTIVITIES_SEARCH)
search = JSON.parse(j.content)
runs = search['results']['activities'].map {|r|
# Get each activity id to insert into the download URL
r['activity']['activityId']
}.map {|id|
# Download a run.
download_run(agent, id)
}
end
 
agent = Mechanize.new
 
# One needs to log in to get access to private runs. Mechanize will store
# the session data for the API call that cames next.
home_page = login(agent, Choice[:user], password)
 
FileUtils.mkdir_p(Choice[:dir]) if not File.directory?(Choice[:dir])
 
puts "Downloading runs..."
 
activities(agent)

If you get 400 => Net::HTTPBadRequest, it is possibly because Garmin's servers appear to be limiting activity queries to 100 requests at a time. Modify start and limit in ACTIVITIES_SEARCH and try again.

Works perfectly! Thanks a lot for this. just used it to export a couple of years of cycling data from Garmin and import it into Strava.

Works perfectly! Thanks a lot!

@pza --> thanks for your comment!

Thanks for great inspiration on writing Garmin Connect backup script.

yeah, thanks for this.

Ok. help me out here. Where and how do I copy and paste the above code? Thanks.

Thanks a lot for this, but.. I get Net::HTTPBadRequest even befgore I get one activity, and Garmin's "Activity Search Service 1.0" is deprecated: http://connect.garmin.com/proxy/activity-search-service-1.0/ Is it over..?

@cloveras: try @gljeremy's fork: https://gist.github.com/1232102

@dano11 you'd copy & paste it into a file called disconnect.rb, and you'll need to gem install the dependencies on the top and run it with Ruby in a terminal.

Yes! Works perfectly, thanks a lot for the quick reply!

I note the TCX file downloaded does not contain the name or description associated with and activity on the garmin connect website, even though the file format supports that. :-( Can this script be extend to down loads as well?

Yes, thanks a lot for sharing. I also experienced the 400 => Net::HTTPBadRequest problem. Instead of limiting ACTIVITIES_SEARCH to 100, I limited it to 1 and changed the activities function to look like the following:
def activities(agent)
$activityIdx = 0
$numActivities = 624
while $activityIdx <= $numActivities do
puts("Downloading activity = #$activityIdx")
as = ACTIVITIES_SEARCH % $activityIdx
print "\tas: ", as, "\n"
j = agent.get(as)
search = JSON.parse(j.content)
runs = search['results']['activities'].map {|r|
# Get each activity id to insert into the download URL
r['activity']['activityId']
}.map {|id|
# Download an activity.
download_run(agent, id)
}
$activityIdx += 1
end
end

In my case I wanted all of my history, 624.

Thanks a lot for sharing @tmcw!

Garmin Connect UI has changed and this script no longer works for me.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.