Skip to content

Instantly share code, notes, and snippets.

@broadwaylamb
Created March 19, 2021 12:24
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save broadwaylamb/050651fae51d8dea8c6c6db232b7569c to your computer and use it in GitHub Desktop.
Save broadwaylamb/050651fae51d8dea8c6c6db232b7569c to your computer and use it in GitHub Desktop.
Automatic High-Res Strava Heatmap TMS link generator
#!/usr/bin/env python3
"""Automatic High-Res Strava Heatmap TMS link generator
This script signs in your Strava account using https://github.com/nnngrach/strava_auto_auth,
extracts the cookies and forms a TMS link that you can use in apps like OsmAnd or JOSM.
On macOS, leverages the keychain, so you don't have to type login and password every time.
Requires Docker.
"""
import sys
if sys.version_info.major < 3:
sys.exit(f"This script only supports Python 3. You have Python {sys.version_info.major}.{sys.version_info.minor}.")
import platform
import shutil
from getpass import getpass
import subprocess
import shlex
import re
import pathlib
import atexit
from time import sleep
from urllib.request import urlopen, HTTPError
from http.client import RemoteDisconnected
from urllib.parse import quote_plus
docker_exe = shutil.which("docker")
if not docker_exe:
sys.exit("This script requires Docker, which was not found. Make sure it is in your PATH.")
def colorized_stderr_log(string, color):
"""
Colorized terminal output to stderr
:param string: The string to print
:type string: str
:param color: The color of the output
:type color: str
"""
colors = {
'black': '\u001b[30m',
'red': '\u001b[31m',
'green': '\u001b[32m',
'yellow': '\u001b[33m',
'blue': '\u001b[34m',
'magenta': '\u001b[35m',
'cyan': '\u001b[36m',
'white': '\u001b[37m'
}
colors['warning'] = colors['yellow']
colors['error'] = colors['red']
sequence = colors[color.lower()]
if sequence:
print(sequence + string + '\u001b[0m', file=sys.stderr)
else:
print(string, file=sys.stderr)
def run_command(command, sensitive_data=None, check=False):
joined = shlex.join(command)
if sensitive_data:
joined = joined.replace(sensitive_data, "********")
colorized_stderr_log(joined, "cyan")
return subprocess.run(command,
universal_newlines=True,
capture_output=True,
check=check)
class KeychainProvider:
def get_password_from_keychain(self):
return None
def save_password_to_keychain(self, login, password):
pass
class MacOSKeychainProvider(KeychainProvider):
def __init__(self):
self.keychain_service_name = pathlib.Path(__file__).name
def get_password_from_keychain(self):
colorized_stderr_log("Checking for existing Strava login and password in Keychain...", "white")
r = run_command(["security",
"find-generic-password",
"-s", self.keychain_service_name,
"-g"])
if r.returncode == 0:
login = re.search(r"^ \"acct\"<blob>=\"(.+)\"$", r.stdout, flags=re.MULTILINE).group(1)
password = re.search(r"^password: \"(.+)\"$", r.stderr, flags=re.MULTILINE).group(1)
colorized_stderr_log("Found!", "white")
return (login, password)
else:
colorized_stderr_log("Keychain item not found.", "white")
return None
def save_password_to_keychain(self, login, password):
colorized_stderr_log("Saving login and password to Keychain...", "white")
run_command(["security",
"add-generic-password",
"-a", login,
"-s", self.keychain_service_name,
"-w", password],
sensitive_data=password,
check=True)
if platform.system() == "Darwin":
# Save the password to keychain on macOS
security_exe = shutil.which("security")
if security_exe:
keychain_provider = MacOSKeychainProvider()
else:
colorized_stderr_log("'security' command not found. Proceeding without Keychain", "warning")
keychain_provider = KeychainProvider()
else:
keychain_provider = KeychainProvider()
credentials = keychain_provider.get_password_from_keychain()
if credentials:
(login, password) = credentials
else:
login = input("Login: ")
password = getpass()
keychain_provider.save_password_to_keychain(login, password)
DOCKER_CONTAINER_NAME = "anygis_strava_auto_auth"
container_already_running = run_command([docker_exe, "container", "inspect", DOCKER_CONTAINER_NAME]).returncode == 0
if container_already_running:
colorized_stderr_log(f"The '{DOCKER_CONTAINER_NAME}' service is already running.", "white")
else:
colorized_stderr_log(f"Starting the '{DOCKER_CONTAINER_NAME}' service that will log in to your Strava account...", "white")
docker_cmd = [
docker_exe,
"run",
"--name",
DOCKER_CONTAINER_NAME,
"--rm",
"-p",
"5050:4000",
"--detach",
"nnngrach/anygis_strava_auto_auth",
]
run_command(docker_cmd, check=True)
sleep(1) # Otherwise the server may drop the connection
def stop_container():
run_command([docker_exe, "container", "stop", DOCKER_CONTAINER_NAME])
atexit.register(stop_container)
while True:
try:
colorized_stderr_log("Logging in Strava (this may take a while)...", "white")
response = urlopen(f"http://localhost:5050/StravaAuth/{quote_plus(login)}/{quote_plus(password)}")
except HTTPError as e:
if e.code == 429:
colorized_stderr_log("Got HTTP Error 429: Too Many Requests. Retrying...", "warning")
sleep(2)
continue
raise e
except RemoteDisconnected:
colorized_stderr_log("The service has dropped the connection. Retrying...", "warning")
sleep(2)
continue
break
query = response.read().decode()
colorized_stderr_log("Here is your TMS link:", "magenta")
print("tms[3,16]:https://heatmap-external-{switch:a,b,c}.strava.com/tiles-auth/ride/red/{zoom}/{x}/{y}.png?" + query)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment