diff --git a/garminbackup.py b/garminbackup.py index 014c8b8..576faa7 100755 --- a/garminbackup.py +++ b/garminbackup.py @@ -59,7 +59,7 @@ if __name__ == "__main__": parser.add_argument( "-E", "--ignore-errors", action='store_true', help="Ignore errors and keep going. Default: FALSE") - + args = parser.parse_args() if not args.log_level in LOG_LEVELS: raise ValueError("Illegal log-level: {}".format(args.log_level)) @@ -67,32 +67,31 @@ if __name__ == "__main__": # if no --format was specified, all formats are to be backed up args.format = args.format if args.format else export_formats log.info("backing up formats: %s", ", ".join(args.format)) - + logging.root.setLevel(LOG_LEVELS[args.log_level]) try: if not os.path.isdir(args.backup_dir): os.makedirs(args.backup_dir) - + if not args.password: args.password = getpass.getpass("Enter password: ") - + with GarminClient(args.username, args.password) as client: # get all activity ids and timestamps from Garmin account - log.info("retrieving activities for {} ...".format(args.username)) + log.info("scanning activities for %s ...", args.username) activities = set(client.list_activities()) - log.info("account has a total of {} activities.".format( - len(activities))) - + log.info("account has a total of %d activities", len(activities)) + missing_activities = garminexport.backup.need_backup( activities, args.backup_dir, args.format) backed_up = activities - missing_activities - log.info("{} contains {} backed up activities.".format( - args.backup_dir, len(backed_up))) + log.info("%s contains %d backed up activities", + args.backup_dir, len(backed_up)) + + log.info("activities that aren't backed up: %d", + len(missing_activities)) - log.info("activities that aren't backed up: {}".format( - len(missing_activities))) - for index, activity in enumerate(missing_activities): id, start = activity log.info("backing up activity %d from %s (%d out of %d) ..." % @@ -106,5 +105,4 @@ if __name__ == "__main__": raise except Exception as e: exc_type, exc_value, exc_traceback = sys.exc_info() - log.error(u"failed with exception: %s", e) - raise + log.error(u"failed with exception: %s", str(e)) diff --git a/garminexport/garminclient.py b/garminexport/garminclient.py index b3cde19..b4310cb 100755 --- a/garminexport/garminclient.py +++ b/garminexport/garminclient.py @@ -49,10 +49,10 @@ def require_session(client_function): client_object = args[0] if not client_object.session: raise Exception("Attempt to use GarminClient without being connected. Call connect() before first use.'") - return client_function(*args, **kwargs) + return client_function(*args, **kwargs) return check_session - + class GarminClient(object): """A client class used to authenticate with Garmin Connect and extract data from the user account. @@ -62,13 +62,13 @@ class GarminClient(object): automatically take care of logging in to Garmin Connect before any further interactions and logging out after the block completes or a failure occurs. - + Example of use: :: with GarminClient("my.sample@sample.com", "secretpassword") as client: ids = client.list_activity_ids() for activity_id in ids: gpx = client.get_activity_gpx(activity_id) - + """ def __init__(self, username, password): @@ -86,119 +86,70 @@ class GarminClient(object): def __enter__(self): self.connect() return self - + def __exit__(self, exc_type, exc_value, traceback): self.disconnect() - + def connect(self): self.session = requests.Session() self._authenticate() - + def disconnect(self): if self.session: self.session.close() self.session = None - + def _authenticate(self): log.info("authenticating user ...") - params = { - "service": "http://connect.garmin.com/post-auth/login", - "clientId": "GarminConnect", - "consumeServiceTicket": "false", - "gauthHost": "https://sso.garmin.com/sso" - } - flow_execution_key = self._get_flow_execution_key(params) - log.debug("flow execution key: '{}'".format(flow_execution_key)) - validation_url = self._get_auth_ticket(flow_execution_key, params) - # recently, the validation url has started to escape slash characters - # (with a backslash). remove any such occurences. - validation_url = validation_url.replace("\/", "/") - log.debug("auth ticket validation url: {}".format(validation_url)) - self._validate_auth_ticket(validation_url) + form_data = { + "username": self.username, + "password": self.password, + "embed": "false" + } + request_params = { + "service": "https://connect.garmin.com/modern" + } + auth_response = self.session.post( + SSO_LOGIN_URL, params=request_params, data=form_data) + log.debug("got auth response: %s", auth_response.text) + if auth_response.status_code != 200: + raise ValueError( + "authentication failure: did you enter valid credentials?") + auth_ticket_url = self._extract_auth_ticket_url( + auth_response.text) + log.debug("auth ticket url: '%s'", auth_ticket_url) - # Referer seems to be a header that is required by the REST API - self.session.headers.update({'Referer': "https://some.random.site"}) - - - - def _get_flow_execution_key(self, request_params): - # The flowExecutionKey is embedded in the - # https://sso.garmin.com/sso/login response page. For example: - # - log.debug("get flow execution key ...") - response = self.session.get(SSO_LOGIN_URL, params=request_params) + log.info("claiming auth ticket ...") + response = self.session.get(auth_ticket_url) if response.status_code != 200: raise RuntimeError( - "auth failure: %s: code %d: %s" % - (SSO_LOGIN_URL, response.status_code, response.text)) - # extract flowExecutionKey - match = re.search(r'