diff --git a/garminexport/cli/backup.py b/garminexport/cli/backup.py index 87ab0619..8e7fdb76 100644 --- a/garminexport/cli/backup.py +++ b/garminexport/cli/backup.py @@ -58,6 +58,18 @@ def parse_args() -> argparse.Namespace: help=("The maximum number of retries to make on failed attempts to fetch an activity. " "Exponential backoff will be used, meaning that the delay between successive attempts " "will double with every retry, starting at one second. DEFAULT: {}").format(DEFAULT_MAX_RETRIES)) + parser.add_argument( + "--token", + default=None, + type=str, + help=("Authentication header token. Use with 'jwt_fgp' instead of username and password, for example " + "if login fails due to ReCaptcha.")) + parser.add_argument( + "--jwt_fgp", + default=None, + type=str, + help=("Authentication JWT_FGP Cookie. Use with 'token' instead of username and password, for example " + "if login fails due to ReCaptcha.")) return parser.parse_args() @@ -69,6 +81,8 @@ def main(): try: incremental_backup(username=args.username, password=args.password, + token=args.token, + jwt_fgp=args.jwt_fgp, backup_dir=args.backup_dir, export_formats=args.format, ignore_errors=args.ignore_errors, diff --git a/garminexport/cli/get_activity.py b/garminexport/cli/get_activity.py index c7ceff95..89fc8abd 100755 --- a/garminexport/cli/get_activity.py +++ b/garminexport/cli/get_activity.py @@ -43,6 +43,18 @@ def main(): "--log-level", metavar="LEVEL", type=str, help="Desired log output level (DEBUG, INFO, WARNING, ERROR). Default: INFO.", default="INFO") + parser.add_argument( + "--token", + default=None, + type=str, + help=("Authentication header token. Use with 'jwt_fgp' instead of username and password, for example " + "if login fails due to ReCaptcha.")) + parser.add_argument( + "--jwt_fgp", + default=None, + type=str, + help=("Authentication JWT_FGP Cookie. Use with 'token' instead of username and password, for example " + "if login fails due to ReCaptcha.")) args = parser.parse_args() @@ -60,10 +72,11 @@ def main(): if not os.path.isdir(args.destination): os.makedirs(args.destination) - if not args.password: + prompt_password = not args.password and (not args.token or not args.jwt_fgp) + if prompt_password: args.password = getpass.getpass("Enter password: ") - with GarminClient(args.username, args.password) as client: + with GarminClient(args.username, args.password, args.token, args.jwt_fgp) as client: log.info("fetching activity %s ...", args.activity) summary = client.get_activity_summary(args.activity) # set up a retryer that will handle retries of failed activity downloads diff --git a/garminexport/garminclient.py b/garminexport/garminclient.py index dddd6eed..b1d1cfbc 100755 --- a/garminexport/garminclient.py +++ b/garminexport/garminclient.py @@ -66,7 +66,7 @@ class GarminClient(object): """ - def __init__(self, username, password): + def __init__(self, username, password, token=None, jwt_fgp=None): """Initialize a :class:`GarminClient` instance. :param username: Garmin Connect user name or email address. @@ -76,6 +76,8 @@ def __init__(self, username, password): """ self.username = username self.password = password + self.token = token + self.jwt_fgp = jwt_fgp self.session = None @@ -89,6 +91,11 @@ def __exit__(self, exc_type, exc_value, traceback): def connect(self): self.session = new_http_session() + + if self.token is not None and self.jwt_fgp is not None: + self._authenticate_with_token() + return + self._authenticate() def disconnect(self): @@ -127,6 +134,17 @@ def _authenticate(self): 'Di-Backend': 'connectapi.garmin.com', }) + def _authenticate_with_token(self): + log.info("authenticating user with provided token ...") + token_type = "Bearer" + self.session.headers.update( + { + "Authorization": f"{token_type} {self.token}", + "Di-Backend": "connectapi.garmin.com", + "NK": "NT", + } + ) + self.session.cookies.update({"JWT_FGP": self.jwt_fgp}) def _get_oauth_token(self): """Retrieve an OAuth token to use for the session. diff --git a/garminexport/incremental_backup.py b/garminexport/incremental_backup.py index 213b3058..a53305af 100644 --- a/garminexport/incremental_backup.py +++ b/garminexport/incremental_backup.py @@ -15,6 +15,8 @@ def incremental_backup(username: str, password: str = None, + token: str = None, + jwt_fgp: str = None, backup_dir: str = os.path.join(".", "activities"), export_formats: List[str] = None, ignore_errors: bool = False, @@ -43,7 +45,8 @@ def incremental_backup(username: str, if not os.path.isdir(backup_dir): os.makedirs(backup_dir) - if not password: + prompt_password = not password and (not token or not jwt_fgp) + if prompt_password: password = getpass.getpass("Enter password: ") # set up a retryer that will handle retries of failed activity downloads @@ -51,7 +54,7 @@ def incremental_backup(username: str, delay_strategy=ExponentialBackoffDelayStrategy(initial_delay=timedelta(seconds=1)), stop_strategy=MaxRetriesStopStrategy(max_retries)) - with GarminClient(username, password) as client: + with GarminClient(username, password, token, jwt_fgp) as client: # get all activity ids and timestamps from Garmin account log.info("scanning activities for %s ...", username) activities = set(retryer.call(client.list_activities))