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 DEFAULT_MAX_RETRIES = 7
"""The default maximum number of retries to make when fetching a single activity.""" """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: def parse_args() -> argparse.Namespace:
"""Parse CLI arguments. """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. " 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 " "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)) "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() return parser.parse_args()
@ -70,6 +77,7 @@ def main():
try: try:
incremental_backup(username=args.username, incremental_backup(username=args.username,
password=args.password, password=args.password,
user_agent_fn=lambda:DEFAULT_USER_AGENT,
backup_dir=args.backup_dir, backup_dir=args.backup_dir,
export_formats=args.format, export_formats=args.format,
ignore_errors=args.ignore_errors, 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. """Initialize a :class:`GarminClient` instance.
:param username: Garmin Connect user name or email address. :param username: Garmin Connect user name or email address.
:type username: str :type username: str
:param password: Garmin Connect account password. :param password: Garmin Connect account password.
:type password: str :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.username = username
self.password = password self.password = password
self._user_agent_fn = user_agent_fn
self.session = None self.session = None
def __enter__(self): def __enter__(self):
self.connect() self.connect()
return self return self
@ -118,7 +127,15 @@ class GarminClient(object):
"embed": "false", "embed": "false",
"_csrf": self._get_csrf_token(), "_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( auth_response = self.session.post(
SSO_SIGNIN_URL, headers=headers, params=self._auth_params(), data=form_data) SSO_SIGNIN_URL, headers=headers, params=self._auth_params(), data=form_data)
log.debug("got auth response: %s", auth_response.text) log.debug("got auth response: %s", auth_response.text)

View File

@ -3,7 +3,7 @@ import getpass
import logging import logging
import os import os
from datetime import timedelta from datetime import timedelta
from typing import List from typing import Callable, List
import garminexport.backup import garminexport.backup
from garminexport.backup import supported_export_formats from garminexport.backup import supported_export_formats
@ -15,6 +15,7 @@ log = logging.getLogger(__name__)
def incremental_backup(username: str, def incremental_backup(username: str,
password: str = None, password: str = None,
user_agent_fn: Callable[[],str] = None,
backup_dir: str = os.path.join(".", "activities"), backup_dir: str = os.path.join(".", "activities"),
export_formats: List[str] = None, export_formats: List[str] = None,
ignore_errors: bool = False, ignore_errors: bool = False,
@ -23,6 +24,11 @@ def incremental_backup(username: str,
:param username: Garmin Connect user name :param username: Garmin Connect user name
:param password: Garmin Connect user password. Default: None. If not provided, would be asked interactively. :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 backup_dir: Destination directory for downloaded activities. Default: ./activities/".
:param export_formats: List of desired output formats (json_summary, json_details, gpx, tcx, fit). :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. 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 activities are stored in a local directory on the user's computer.
The backups are incremental, meaning that only activities that aren't already The backups are incremental, meaning that only activities that aren't already
stored in the backup directory will be downloaded. stored in the backup directory will be downloaded.
""" """
# if no --format was specified, all formats are to be backed up # if no --format was specified, all formats are to be backed up
export_formats = export_formats if export_formats else supported_export_formats 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)), delay_strategy=ExponentialBackoffDelayStrategy(initial_delay=timedelta(seconds=1)),
stop_strategy=MaxRetriesStopStrategy(max_retries)) 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 # get all activity ids and timestamps from Garmin account
log.info("scanning activities for %s ...", username) log.info("scanning activities for %s ...", username)
activities = set(retryer.call(client.list_activities)) activities = set(retryer.call(client.list_activities))