Skip to content

Instantly share code, notes, and snippets.

@alxbl
Last active December 4, 2024 18:49
Show Gist options
  • Save alxbl/52fa037f9d3fe837a18da110309f7679 to your computer and use it in GitHub Desktop.
Save alxbl/52fa037f9d3fe837a18da110309f7679 to your computer and use it in GitHub Desktop.
Zwift to Tacx App For Windows activity type converted and Garmin Connect auto-uploader.
#!/bin/env python3
#
# Converts a Zwift activity (.fit) using fitfiletools.com to a "Tacx App for Windows" activity
# so that Garmin Connect accrues distance and activity count for badge tallies.
# Because apparently Garmin doesn't like Zwift ;)
# Author: alxbl
#
# Assumptions:
# - You are logged in to Garmin Connect using firefox (for automatic upload)
# - You recently visited connect.garmin.com to make sure your authentication token is valid
#
# Future Work:
# - Add CLI parsing for --upload and --delete.
#
#
# Usage: `pipenv shell; pipenv install; python fft.py /path/to/*.fit`
#
import requests
import sys
import json
import os
cookies = {}
try:
import browsercookie
cookies = browsercookie.firefox()
except:
print('Not able to load firefox cookies. This script will not work!')
pass # No cookies available.
GARMIN_UPLOAD_URL = 'https://connect.garmin.com/upload-service/upload/.fit'
GARMIN_TOKEN_URL = 'https://connect.garmin.com/modern/di-oauth/exchange'
FFT_URL = 'https://www.fitfiletools.com/tools/devicechanger?devicetype=20533&mfgr=1'
S = requests.Session()
S.cookies.update(cookies)
_token_cache = None
def garmin_upload(dpath, sess):
global _token_cache
if _token_cache is None:
_token_cache = sess.post(GARMIN_TOKEN_URL)
if not _token_cache.ok:
print(' ERROR: Garmin Connect authentication failed')
resp = json.loads(_token_cache.text)
token = resp["access_token"]
userfile = open(dpath, 'rb')
upload = sess.post(GARMIN_UPLOAD_URL, files=[('userfile', (os.path.basename(dpath), userfile, 'application/fits'))],
headers={'Authorization': f'Bearer {token}',
'Referer': 'https://connect.garmin.com/modern/import-data',
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0',
'Origin': 'https://connect.garmin.com',
'DI-Backend': 'connectapi.garmin.com',
})
return upload
def convert(file, upload=False):
fpath = os.path.abspath(file)
print(f'Converting "{fpath}"', end='', flush=True)
parts = os.path.split(fpath)
fname = parts[-1]
# Skip non-.fit files and already converted files
if '.fit' not in fname or 'converted' in fname:
print(f'... skip')
return
dpath = os.path.join(parts[0], fname.replace('.fit', '_converted.fit'))
# Do not convert twice.
converted = os.path.exists(dpath)
if converted and not upload:
print(f'... already converted')
return
size = os.path.getsize(fpath)
if size < 2048:
print(f'... activity too short')
return # No data in activity.
# convert using fitfiletools
if not converted:
try:
with open(fpath, 'rb') as f:
resp = S.post(FFT_URL, files={'file': f})
data = json.loads(resp.text)
converted_url = data['file']
with S.get(converted_url, stream=True) as r:
r.raise_for_status()
with open(dpath, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
except:
print(f'... conversion failed')
return
if upload:
res = garmin_upload(dpath, S)
if res.status_code == 409:
print(f'... already uploaded')
elif res.ok:
print(f'... uploaded')
else:
print(f'... upload failed (converted)')
else:
print(f'... converted')
for f in sys.argv[1:]:
convert(f, upload=True)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment