Created
March 19, 2021 12:24
-
-
Save broadwaylamb/050651fae51d8dea8c6c6db232b7569c to your computer and use it in GitHub Desktop.
Automatic High-Res Strava Heatmap TMS link generator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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