support customizing the 'User-Agent' value

This commit is contained in:
Peter Gardfjäll 2021-05-09 08:09:38 +02:00
parent cbe9e74704
commit 33b8354867
3 changed files with 36 additions and 4 deletions

View File

@ -18,6 +18,10 @@ log = logging.getLogger(__name__)
DEFAULT_MAX_RETRIES = 7
"""The default maximum number of retries to make when fetching a single activity."""
DEFAULT_USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36'
"""The default `User-Agent` to use for HTTP requests when none is supplied by
the user.
"""
def parse_args() -> argparse.Namespace:
"""Parse CLI arguments.
@ -59,6 +63,9 @@ 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(
"--user-agent", type=str, default=DEFAULT_USER_AGENT,
help="A value to use for the `User-Agent` request header. Use an authentic browser agent string to prevent being blocked by Garmin. A tool such as `user_agent` (`ua`) can be used to generate such values.")
return parser.parse_args()
@ -70,6 +77,7 @@ def main():
try:
incremental_backup(username=args.username,
password=args.password,
user_agent_fn=lambda:DEFAULT_USER_AGENT,
backup_dir=args.backup_dir,
export_formats=args.format,
ignore_errors=args.ignore_errors,

View File

@ -81,18 +81,27 @@ class GarminClient(object):
"""
def __init__(self, username, password):
def __init__(self, username, password, user_agent_fn=None):
"""Initialize a :class:`GarminClient` instance.
:param username: Garmin Connect user name or email address.
:type username: str
:param password: Garmin Connect account password.
:type password: str
:keyword user_agent_fn: A function that, when called, produces a
`User-Agent` string to be used as `User-Agent` for the remainder of the
session. If set to None, the default user agent of the http request
library is used.
:type user_agent_fn: Callable[[], str]
"""
self.username = username
self.password = password
self._user_agent_fn = user_agent_fn
self.session = None
def __enter__(self):
self.connect()
return self
@ -118,7 +127,15 @@ class GarminClient(object):
"embed": "false",
"_csrf": self._get_csrf_token(),
}
headers = {'origin': 'https://sso.garmin.com'}
headers = {
'origin': 'https://sso.garmin.com',
}
if self._user_agent_fn:
user_agent = self._user_agent_fn()
if not user_agent:
raise ValueError("user_agent_fn didn't produce a value")
headers['User-Agent'] = user_agent
auth_response = self.session.post(
SSO_SIGNIN_URL, headers=headers, params=self._auth_params(), data=form_data)
log.debug("got auth response: %s", auth_response.text)

View File

@ -3,7 +3,7 @@ import getpass
import logging
import os
from datetime import timedelta
from typing import List
from typing import Callable, List
import garminexport.backup
from garminexport.backup import supported_export_formats
@ -15,6 +15,7 @@ log = logging.getLogger(__name__)
def incremental_backup(username: str,
password: str = None,
user_agent_fn: Callable[[],str] = None,
backup_dir: str = os.path.join(".", "activities"),
export_formats: List[str] = None,
ignore_errors: bool = False,
@ -23,6 +24,11 @@ def incremental_backup(username: str,
:param username: Garmin Connect user name
:param password: Garmin Connect user password. Default: None. If not provided, would be asked interactively.
:keyword user_agent_fn: A function that, when called, produces a
`User-Agent` string to be used as `User-Agent` for the remainder of the
session. If set to None, the default user agent of the http request
library is used.
:type user_agent_fn: Callable[[], str]
:param backup_dir: Destination directory for downloaded activities. Default: ./activities/".
:param export_formats: List of desired output formats (json_summary, json_details, gpx, tcx, fit).
Default: `None` which means all supported formats will be backed up.
@ -34,6 +40,7 @@ def incremental_backup(username: str,
The activities are stored in a local directory on the user's computer.
The backups are incremental, meaning that only activities that aren't already
stored in the backup directory will be downloaded.
"""
# if no --format was specified, all formats are to be backed up
export_formats = export_formats if export_formats else supported_export_formats
@ -50,7 +57,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, user_agent_fn) 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))