Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added an option to export more than ~500 workouts and added a feature to format the output to be compatible with Garmin Connect. #10

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 26 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,42 @@
Endomondo Export
================
# Endomondo Export

Export a user's most recent Endomondo workouts as TCX files.
This is an extended fork of https://github.com/yannickcarer/endomondo-export. Since Endomondo is shutting down it's service with the end of 2020 (what a year), i was searching for a way to export all my workouts (>1000) to Garmin Connect. yannickcarers script works fine, but you can not export more than ~500 workouts since the server will timeout roughly around 500 exports. I added a feature that downloads the last 20 years as chunks of 500 trainings and also added the option to format the output for Garmin Connect, which needs an extra <Track> Wrapper around trackpoints and only allows "Running", "Biking" and "Other" as activity names.

## Usage

Usage
-----
The script export.py may be used to backup the complete workout history. You will be asked for your Endomondo email and password, followed by the number of workouts you want to export (leave empty to export the last 20 years) and if you want to format the export to be compatible with Garmin Connect.

The script export.py may be used to backup the complete workout history:
**Important: If you have done more than 500 workouts in a year (congratulations!) you will run into problems using my script!**

python export.py
```shell
python export.py
Endomondo: Export the most recent n workouts (or ALL) as TCX files.
Email: [email protected]
Password:
Maximum number of workouts n (press Enter download ALL OF THEM): 5
Format for Garmin Connect? (press Enter to ignore): Y
Files will be formatted to be compatible with Garmin Connects import function.
fetched latest 5 workouts
writing export/20201113_1648626623.tcx, Other, 1 trackpoints
writing export/20201112_1647888548.tcx, Biking, 693 trackpoints
writing export/20201110_1647453368.tcx, Running, 1319 trackpoints
...
```

You will be asked for your Endomondo email and password, and then the script will do the rest.
## Requirements


Requirements
------------

- Python 2.6+
- lxml
- requests


Installing
----------

To set up the requirements for this project, you can install the dependencies by pip.

First, it's highly recommended to set up a virtualenv:

virtualenv venv --distribute
source ./venv/bin/activate

Then install the requirements:

pip install -r requirements

And you're set!
## Installing

```shell
pip install -r requirements
```

Authors
-------
## Credit

This script was created [@yannickcarer](https://github.com/yannickcarer), with some updates by [@mikedory](https://github.com/mikedory).
This script was created [@yannickcarer](https://github.com/yannickcarer), with some updates by [@mikedory](https://github.com/mikedory) and some minor improvements of [@countablyinfinite](https://github.com/countablyinfinite)
31 changes: 24 additions & 7 deletions export.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import re
import getpass
import sys
import os
import os, datetime


# create a somewhat useful filename for the specified workout
Expand Down Expand Up @@ -47,14 +47,31 @@ def create_tcx_file(workout):

def main():
try:
print "Endomondo: export most recent workouts as TCX files"

email = raw_input("Email: ")
print "Endomondo: export most recent n workouts (or ALL) as TCX files."
mail = raw_input("Email: ")
password = getpass.getpass()
maximum_workouts = raw_input("Maximum number of workouts (press Enter to ignore)")
endomondo = Endomondo(email, password)
maximum_workouts = raw_input("Maximum number of workouts n (press Enter to download ALL OF THEM): ")
garmin = raw_input("Format for Garmin Connect? (press Enter to ignore): ")
endomondo = Endomondo(mail, password, garmin)
if garmin:
print "Files will be formatted to be compatible with Garmin Connects import function."

if not maximum_workouts:
days_per_year = 365.24
maximum_workouts = 500
print "Downloading workouts from the last 15 years in chunks. (If you logged more than 500 workouts per year, this won't work)"
for years in range(0,20):
before=(datetime.datetime.now()-datetime.timedelta(days=(days_per_year*years)))
after=before-datetime.timedelta(days=(days_per_year*(1)))
print "Chunk before:" +str(before)
print "Chunk after:" +str(after)
workouts = endomondo.get_workouts(maximum_workouts, before, after)
for workout in workouts:
create_tcx_file(workout)
print "done."
return 0

workouts = endomondo.get_workouts(maximum_workouts)
workouts = endomondo.get_workouts(maximum_workouts, before=None, after=None)
print "fetched latest", len(workouts), "workouts"
for workout in workouts:
create_tcx_file(workout)
Expand Down
110 changes: 94 additions & 16 deletions lib/endomondo.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import uuid
import socket
import datetime

import pytz
import sys

def to_datetime(v):
return datetime.datetime.strptime(v, "%Y-%m-%d %H:%M:%S %Z")
Expand All @@ -29,6 +30,59 @@ def to_meters(v):
v *= 1000
return v

SPORTS_GARMIN = {
0: 'Running',
1: 'Biking',
2: 'Biking',
3: 'Biking',
4: 'Other',
5: 'Other',
6: 'Other',
7: 'Other',
8: 'Other',
9: 'Other',
10: 'Other',
11: 'Other',
12: 'Other',
13: 'Other',
14: 'Other',
15: 'Other',
17: 'Other',
18: 'Other',
19: 'Other',
20: 'Other',
21: 'Other',
22: 'Other',
23: 'Other',
24: 'Other',
25: 'Other',
26: 'Other',
27: 'Other',
28: 'Other',
29: 'Other',
30: 'Other',
31: 'Other',
32: 'Other',
33: 'Other',
34: 'Other',
35: 'Other',
36: 'Other',
37: 'Other',
38: 'Other',
39: 'Other',
40: 'Other',
41: 'Other',
42: 'Other',
43: 'Other',
44: 'Other',
45: 'Other',
46: 'Other',
47: 'Other',
48: 'Other',
49: 'Other',
50: 'Other'
}

SPORTS = {
0: 'Running',
1: 'Cycling, transport',
Expand Down Expand Up @@ -89,19 +143,20 @@ class Endomondo:
os_version = "2.2"
model = "M"

def __init__(self, email=None, password=None):
def __init__(self, email=None, password=None, garmin=None):
self.auth_token = None
self.request = requests.session()
self.request.headers['User-Agent'] = self.get_user_agent()
if email and password:
self.auth_token = self.request_auth_token(email, password)
self.garmin = garmin

def get_user_agent(self):
"""HTTP User-Agent"""
return "Dalvik/1.4.0 (Linux; U; %s %s; %s Build/GRI54)" % (self.os, self.os_version, self.model)

def get_device_id(self):
return str(uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname()))
return "ebb1083f-7ead-5e11-9847-86fc1d77348e"#str(uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname()))

def request_auth_token(self, email, password):

Expand Down Expand Up @@ -173,30 +228,43 @@ def call(self, url, format, params={}):
return self.parse_json(r)
return r

def get_workouts(self, max_results=40):
"""Get the most recent workouts"""
if not max_results:
max_results = 100000000
json = self.call('api/workout/list', 'json',
{'maxResults': max_results})
return [EndomondoWorkout(self, w) for w in json]
def get_workouts(self, max_results, before, after):
"""Get workouts"""
params = {'maxResults': max_results}
if before:
params.update({'after': self.to_endomondo_time(after)})
params.update({'before': self.to_endomondo_time(before)})

json = self.call('api/workout/list', 'json', params)
return [EndomondoWorkout(self, w, self.garmin) for w in json]

def to_endomondo_time(self,time):
return time.strftime("%Y-%m-%d %H:%M:%S UTC")


def to_python_time(self, endomondo_time):
return datetime.datetime.strptime(endomondo_time, "%Y-%m-%d %H:%M:%S UTC").replace(tzinfo=pytz.utc)


class EndomondoWorkout:
"""Endomondo Workout wrapper"""

def __init__(self, parent, properties):
def __init__(self, parent, properties, garmin):
self.parent = parent
self.properties = properties
self.activity = None
self.garmin = garmin

# dict wrapper
def __getattr__(self, name):
value = None
if name in self.properties:
value = self.properties[name]
if name == 'sport':
value = SPORTS.get(value, 'Other')
if self.garmin:
value = SPORTS_GARMIN.get(value, 'Other')
else:
value = SPORTS.get(value, 'Other')
elif name == 'start_time':
value = to_datetime(value)
return value
Expand All @@ -212,9 +280,16 @@ def get_activity(self):

# the 1st line is activity details
data = lines[0].split(";")
start_time = to_datetime(data[6])
try:
start_time = to_datetime(data[6])
except Exception as e:
print "start time"
sys.exit()
self.activity = tcx.Activity()
self.activity.sport = SPORTS.get(int(data[5]), "Other")
if self.garmin:
self.activity.sport = SPORTS_GARMIN.get(int(data[5]), "Other")
else:
self.activity.sport = SPORTS.get(int(data[5]), "Other")
self.activity.start_time = start_time
self.activity.notes = self.notes

Expand All @@ -235,8 +310,11 @@ def get_activity(self):
for line in lines[1:]:
data = line.split(";")
if len(data) >= 9:
w = tcx.Trackpoint()
w.timestamp = to_datetime(data[0])
w = tcx.Trackpoint()
try:
w.timestamp = to_datetime(data[0])
except Exception as e:
w.timestamp=start_time
w.latitude = to_float(data[2])
w.longitude = to_float(data[3])
w.altitude_meters = to_float(data[6])
Expand Down
6 changes: 3 additions & 3 deletions lib/tcx.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def add_heart_rate(self, element, name, value):

def add_lap(self, element, activity, lap):
elem = self.create_element(element, "Lap")

elem.set("StartTime", self.time_to_s(lap.start_time))

self.add_property(elem, "TotalTimeSeconds", lap.total_time_seconds)
Expand All @@ -102,10 +103,9 @@ def add_lap(self, element, activity, lap):
self.add_property(elem, "Intensity", lap.intensity)
self.add_property(elem, "Cadence", lap.cadence)
self.add_property(elem, "TriggerMethod", lap.trigger_method)

# Add trackpoints
track = self.create_element(elem, "Track")
for w in activity.trackpoints:
self.add_trackpoint(elem, w)
self.add_trackpoint(track, w)

def add_activity(self, element, activity):
sport = activity.sport
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
distribute==0.6.34
lxml==3.2.1
lxml
requests==1.2.3
wsgiref==0.1.2