Last active
December 4, 2024 18:49
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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