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

Bug fixes and enhancements #34

Open
wants to merge 4 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
63 changes: 49 additions & 14 deletions garmin_uploader/api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import requests
import cloudscraper

import re
from garmin_uploader import logger
Expand Down Expand Up @@ -39,12 +39,11 @@ def authenticate(self, username, password):
on Garmin Connect as closely as possible
Outputs a Requests session, loaded with precious cookies
"""
# Use a valid Browser user agent
# TODO: use several UA picked randomly
session = requests.Session()
session.headers.update({
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:48.0) Gecko/20100101 Firefox/50.0', # noqa
})

# Use Cloudscraper to avoid cloudflare spam detection
# session = cloudscraper.create_scraper( browser={ 'browser': 'firefox', 'platform': 'windows', 'mobile': False } )
session = cloudscraper.create_scraper()
logger.info('Using cloud scraper lib')

# Request sso hostname
sso_hostname = None
Expand All @@ -64,7 +63,7 @@ def authenticate(self, username, password):
('redirectAfterAccountLoginUrl', 'https://connect.garmin.com/modern/'), # noqa
('redirectAfterAccountCreationUrl', 'https://connect.garmin.com/modern/'), # noqa
('gauthHost', sso_hostname),
('locale', 'fr_FR'),
('locale', 'en_US'),
('id', 'gauth-widget'),
('cssUrl', 'https://connect.garmin.com/gauth-custom-v3.2-min.css'),
('privacyStatementUrl', 'https://www.garmin.com/fr-FR/privacy/connect/'), # noqa
Expand Down Expand Up @@ -215,20 +214,22 @@ def load_activity_types(self):
Fetch valid activity types from Garmin Connect
"""
# Only fetch once
if self.activity_types:
return self.activity_types
if GarminAPI.activity_types:
return GarminAPI.activity_types

logger.debug('Fetching activity types')
resp = requests.get(URL_ACTIVITY_TYPES, headers=self.common_headers)
# Use Cloudscraper to avoid cloudflare spam detection
session = cloudscraper.create_scraper()
resp = session.get(URL_ACTIVITY_TYPES, headers=self.common_headers)
if not resp.ok:
raise GarminAPIException('Failed to retrieve activity types')

# Store as a clean dict, mapping keys and lower case common name
types = resp.json()
self.activity_types = {t['typeKey']: t for t in types}
GarminAPI.activity_types = {t['typeKey']: t for t in types}

logger.debug('Fetched {} activity types'.format(len(self.activity_types))) # noqa
return self.activity_types
logger.debug('Fetched {} activity types'.format(len(GarminAPI.activity_types))) # noqa
return GarminAPI.activity_types

def set_activity_type(self, session, activity):
"""
Expand All @@ -254,3 +255,37 @@ def set_activity_type(self, session, activity):
res = session.post(url, json=data, headers=headers)
if not res.ok:
raise GarminAPIException('Activity type not set: {}'.format(res.content)) # noqa

def set_activity_info(self, session, activity):
"""
Update activity fields
"""
assert activity.id is not None

data = {
'activityId': activity.id
}

if activity.type:
# Load the corresponding type key on Garmin Connect
types = self.load_activity_types()
type_key = types.get(activity.type)
if type_key is None:
logger.error("Activity type '{}' not valid".format(activity.type))
return False
else:
data['activityTypeDTO'] = type_key

if activity.name:
data['activityName'] = activity.name
if activity.notes:
data['description'] = activity.notes

assert len(data) > 1

url = '{}/{}'.format(URL_ACTIVITY_BASE, activity.id)
headers = dict(self.common_headers) # clone
headers['X-HTTP-Method-Override'] = 'PUT' # weird. again.
res = session.post(url, json=data, headers=headers)
if not res.ok:
raise GarminAPIException('Activity info not set: {}'.format(res.content)) # noqa
34 changes: 15 additions & 19 deletions garmin_uploader/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ class Activity(object):
"""
Garmin Connect Activity model
"""
def __init__(self, path, name=None, type=None):
def __init__(self, path, name=None, type=None, notes=None):
self.id = None # provided on upload
self.path = path
self.name = name
self.type = type
self.notes = notes

def __repr__(self):
if self.id is None:
Expand Down Expand Up @@ -86,19 +87,12 @@ def upload(self, user):
if uploaded:
logger.info('Uploaded activity {}'.format(self))

# Set activity name if specified
if self.name:
# Set activity info, if specified
if self.name or self.type or self.notes:
try:
api.set_activity_name(user.session, self)
api.set_activity_info(user.session, self)
except GarminAPIException as e:
logger.warning('Activity name update failed: {}'.format(e))

# Set activity type if specified
if self.type:
try:
api.set_activity_type(user.session, self)
except GarminAPIException as e:
logger.warning('Activity type update failed: {}'.format(e))
logger.warning('Activity info update failed: {}'.format(e))

else:
logger.info('Activity already uploaded {}'.format(self))
Expand Down Expand Up @@ -164,10 +158,10 @@ def is_activity(filename):

# Valid file extensions are .tcx, .fit, and .gpx
if extension in VALID_GARMIN_FILE_EXTENSIONS:
logger.debug("File '{}' extension '{}' is valid.".format(filename, extension)) # noqa
logger.debug("File '{}' extension '{}' is valid track file. ".format(filename, extension)) # noqa
return True
else:
logger.warning("File '{}' extension '{}' is not valid. Skipping file...".format(filename, extension)) # noqa
logger.warning("File '{}' extension '{}' is not valid track file. Skipping file...".format(filename, extension)) # noqa
return False

valid_paths, csv_files = [], []
Expand Down Expand Up @@ -208,13 +202,16 @@ def is_activity(filename):
with open(csv_file, 'r') as csvfile:
reader = csv.DictReader(csvfile)
activities += [
Activity(row['filename'], row['name'], row['type'])
Activity(row['filename'], row['name'], row['type'], row['notes'])
for row in reader
if is_activity(row['filename'])
]

if len(activities) == 0:
raise Exception('No valid files.')
else:
logger.info("{} activities will be processed...".format(len(activities)))


return activities

Expand All @@ -238,9 +235,8 @@ def rate_limit(self):
self.last_request = 0.0

wait_time = max(0, min_period - (time.time() - self.last_request))
if wait_time <= 0:
return
time.sleep(wait_time)
if wait_time > 0:
logger.info("Rate limited for %f" % wait_time)
time.sleep(wait_time)

self.last_request = time.time()
logger.info("Rate limited for %f" % wait_time)