From d529398acc15b0d394c18598c2bd8397dc94878e Mon Sep 17 00:00:00 2001 From: Henrik Lissner Date: Thu, 20 Apr 2017 21:37:22 -0400 Subject: [PATCH] Add time-in script --- bin/time-in | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100755 bin/time-in diff --git a/bin/time-in b/bin/time-in new file mode 100755 index 00000000..f968ce92 --- /dev/null +++ b/bin/time-in @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 + +# Retrieves the time (and time difference) at a location (can be an address, +# postal code, country name, etc). Uses Google Maps APIs to map locations to +# coordinates, and coordinates to timezones. +# +# Requires: pytz. tzlocal +# Usage: time-in Copenhagen "Toronto, ON" Vancouver + +import sys +import os +import json +from base64 import b64encode +from datetime import datetime +from urllib import request + +from pytz import timezone +from tzlocal import get_localzone + +URL_BASE = "https://maps.googleapis.com/maps/api/" +URL_TZ = URL_BASE + "timezone/json?location={},{}×tamp={}" +URL_GEOCODE = URL_BASE + "geocode/json?address={}" + +CACHE_FILE = "/tmp/time-in" +CACHE = {} + + +# +def _generate_url(url, *args): + return url.format(*map(request.quote, map(str, args))) + +def get_timezone(lat, lng, timestamp): + try: + url = _generate_url(URL_TZ, lat, lng, timestamp) + resp = request.urlopen(url).read() + result = json.loads(resp.decode('utf-8')) + if result['status'] == 'OK': + return result['timeZoneId'] + elif result['status'] != 'UNKNOWN_ERROR': + raise Exception(result['error_message']) + except IOError: + print("IOError") + +def get_coords(desc): + try: + url = _generate_url(URL_GEOCODE, desc) + resp = request.urlopen(url).read() + result = json.loads(resp.decode('utf-8')) + if result['status'] == 'OK': + if result['results']: + return { + "lat": result['results'][0]['geometry']['location']['lat'], + "lng": result['results'][0]['geometry']['location']['lng'], + "name": result['results'][0]['formatted_address'] + } + else: + return None + elif result['status'] != 'UNKNOWN_ERROR': + raise Exception(result['error_message']) + except IOError: + print("IOError") + +def cache(key, fn, ttl): + if key in CACHE and CACHE[key]['time'] < CACHE[key]['time'] + ttl: + return CACHE[key]['value'] + value = fn() + if value is not None: + CACHE[key] = { + 'time': datetime.now().timestamp(), + 'value': fn() + } + return value + +def diff(a, b): + a = a.replace(tzinfo=None) + b = b.replace(tzinfo=None) + return max(a, b) - min(a, b) + +def encode(s): + return str(b64encode(s.encode('ascii'))) + + +# +if __name__ == '__main__': + if not sys.argv[1:]: + print("Usage: {} [LOCATION]".format(sys.argv[0])) + sys.exit(1) + + if os.path.isfile(CACHE_FILE): + with open(CACHE_FILE, 'r') as f: + CACHE = json.load(f) + + here = get_localzone().localize(datetime.now()) + for location in sys.argv[1:]: + d_coords = cache("coords-" + encode(location), lambda: get_coords(location), 86400) + if d_coords: + d_tzname = cache( + "tz-" + encode(location), + lambda: get_timezone(d_coords['lat'], d_coords['lng'], here.timestamp()), + 1800 + ) + + then = here.astimezone(timezone(d_tzname)) + diff = diff(here, then) + + print("{:30}{} ({} {})".format( + d_coords['name'], + then.isoformat(), + diff, + "ago" if then < here else "ahead" + )) + else: + print("Couldn't find {}".format(location)) + + with open(CACHE_FILE, 'w') as f: + json.dump(CACHE, f) + + sys.exit(0)