You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

256 lines
7.5 KiB

7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
  1. #!/usr/bin/python3
  2. from dateutil.parser import parse as date_parse
  3. from dateutil import tz
  4. from disk_cache import disk_cache, make_key
  5. import argparse
  6. import ctabus
  7. import datetime
  8. import os
  9. import re
  10. import socket
  11. import time
  12. import urllib
  13. import subprocess
  14. # for logging
  15. import os.path as osp
  16. import sys
  17. import shutil
  18. HAS_TOAST = shutil.which('termux-toast') is not None
  19. CHICAGO_TZ = tz.gettz("America/Chicago")
  20. DATETIME_FORMAT = "%A, %B %e, %Y %H:%M:%S"
  21. # https://stackoverflow.com/a/5967539
  22. parser = argparse.ArgumentParser(prog='ctabus')
  23. parser.add_argument('-l', '--lucky', action='store_true',
  24. help='picks first result')
  25. parser.add_argument('-p', '--periodic', metavar='SEC',
  26. type=int, help='checks periodically')
  27. parser.add_argument('-r', '--route', default=None)
  28. parser.add_argument('-d', '--direction', default=None)
  29. parser.add_argument('-t', '--disable_toast', action='store_false')
  30. parser.add_argument('-k', '--kill-cache', action="store_true")
  31. parser.add_argument('arg', nargs='+', metavar='(stop-id | cross streets)')
  32. def toast(text):
  33. read, write = os.pipe()
  34. os.write(write, text.encode())
  35. os.close(write)
  36. subprocess.Popen(["termux-toast", "-s", "-g", "top", "-c", "white", "-b", "black"],
  37. stdin=read)
  38. def atoi(text):
  39. return int(text) if text.isdigit() else text
  40. def numb_sort(text):
  41. '''
  42. alist.sort(key=natural_keys) sorts in human order
  43. http://nedbatchelder.com/blog/200712/human_sorting.html
  44. (See Toothy's implementation in the comments)
  45. '''
  46. return [atoi(c) for c in re.split(r'(\d+)', text)]
  47. def clearscr():
  48. os.system('cls' if os.name == 'nt' else 'clear')
  49. def pprint_delta(delta):
  50. delta = str(delta)
  51. days = None
  52. s1 = delta.split(', ')
  53. if len(s1) > 1:
  54. days, time = s1
  55. else:
  56. time = s1[0]
  57. time = time.split('.')[0]
  58. hour, minute, second = map(int, time.split(':'))
  59. time = ''
  60. if hour:
  61. time += '{hour} hour'.format(hour=hour) + ('s' if hour != 1 else '')
  62. if minute:
  63. if time and not time.endswith(', '):
  64. time += ', '
  65. time += '{minute} minute'.format(minute=minute) + \
  66. ('s' if minute != 1 else '')
  67. if second:
  68. if time and not time.endswith(', '):
  69. time += ', '
  70. time += '{second} second'.format(second=second) + \
  71. ('s' if second != 1 else '')
  72. ret = ''
  73. if days:
  74. ret = days + ', ' if time else ''
  75. ret += time
  76. return ret
  77. def gen_list(objs, data, *displays, key=None, sort=0, num_pic=True):
  78. from print2d import create_table, render_table
  79. # sort based on column number
  80. k = displays[sort]
  81. display_data = {obj[k]: obj[data] for obj in objs}
  82. srt_keys = sorted(display_data.keys(), key=key)
  83. display = sorted(
  84. [
  85. [obj[d] for d in displays] for obj in objs
  86. ],
  87. key=lambda row: key(row[sort]) if key else row[sort]
  88. )
  89. if num_pic:
  90. display = [[i] + data for i, data in enumerate(display)]
  91. table = create_table(display, DATETIME_FORMAT)
  92. render_table(table)
  93. if num_pic:
  94. which = None
  95. while not which:
  96. try:
  97. which = input('Which one?: ')
  98. except KeyboardInterrupt:
  99. quit()
  100. try:
  101. which = srt_keys[int(which)]
  102. except (ValueError, IndexError):
  103. which = None
  104. return display_data[which]
  105. else:
  106. ret = None
  107. while not ret:
  108. try:
  109. ret = display_data[input('Which one?: ')]
  110. except KeyError:
  111. pass
  112. return ret
  113. config = '''\
  114. {route} - {end} ({direction})
  115. {nm}, stop {stop_id}
  116. {delta} ({t})\
  117. '''
  118. def show(data, rt_filter=None, _clear=False, enable_toast=False):
  119. times = data['prd']
  120. now = datetime.datetime.now(CHICAGO_TZ)
  121. arrivals = sorted(times, key=lambda t: t['prdtm'])
  122. if rt_filter is not None:
  123. arrivals = filter(lambda arrival: arrival['rt'] == rt_filter, arrivals)
  124. if _clear:
  125. clearscr()
  126. do_toast = True
  127. for bustime in arrivals:
  128. before = date_parse(bustime['prdtm'])
  129. arrival = before.replace(tzinfo=CHICAGO_TZ)
  130. if arrival > now:
  131. stop_id = bustime['stpid']
  132. delta = pprint_delta(arrival-now)
  133. t = arrival.strftime('%H:%M:%S')
  134. route = bustime['rt']
  135. direction = bustime['rtdir']
  136. end = bustime['des']
  137. nm = bustime['stpnm'].rstrip()
  138. if do_toast and enable_toast:
  139. toast(config.format(**locals()) + '\n'*2+"\n")
  140. do_toast = False
  141. print(
  142. config.format(**locals()), end='\n'*2
  143. )
  144. print("="*36)
  145. def _picker(args):
  146. # save on import time slightly
  147. from search import Search, StopSearch
  148. # routes
  149. if not args.route:
  150. data = ctabus.get_routes()['routes']
  151. route = gen_list(data, 'rt', 'rt', 'rtnm',
  152. num_pic=False, key=numb_sort)
  153. else:
  154. route = args.route
  155. data = ctabus.get_directions(route)['directions']
  156. # direction
  157. if not args.direction:
  158. for direction_obj in data:
  159. friendly_name = ctabus.get_name_from_direction(
  160. route, direction_obj['dir'])
  161. direction_obj['friendly_name'] = friendly_name
  162. direction = gen_list(data, 'dir', 'dir', 'friendly_name')
  163. else:
  164. s = Search(args.direction)
  165. direction = sorted((obj['dir'] for obj in data), key=s)[0]
  166. # direction
  167. stops = ctabus.get_stops(route, direction)['stops']
  168. s = StopSearch(args.arg)
  169. if args.lucky:
  170. stop_id = sorted(stops, key=lambda stop: s(stop['stpnm']))[
  171. 0]['stpid']
  172. else:
  173. stop_id = gen_list(stops, 'stpid', 'stpnm', key=s)
  174. return stop_id
  175. def _picker_recent(args):
  176. pass
  177. def _main_periodic(args, stop_id, init_data):
  178. _done = False
  179. data = init_data
  180. while not _done:
  181. try:
  182. show(data, args.route, True, args.disable_toast and HAS_TOAST)
  183. s = time.perf_counter()
  184. timeout = 1
  185. if args.periodic > timeout:
  186. timeout = args.periodic
  187. data = ctabus.get_times(stop_id, timeout=timeout)
  188. e = time.perf_counter() - s
  189. except KeyboardInterrupt:
  190. _done = True
  191. except (urllib.error.URLError, socket.timeout):
  192. e = time.perf_counter() - s
  193. print("Error fetching times")
  194. if e < args.periodic:
  195. time.sleep(args.periodic-e)
  196. def main(args):
  197. sys.stderr = open(osp.join(osp.dirname(__file__), 'stderr.log'), 'w')
  198. if args.kill_cache:
  199. for cache_obj in disk_cache.caches:
  200. cache_obj.delete_cache()
  201. args.arg = ' '.join(args.arg)
  202. if args.arg.isdecimal():
  203. stop_id = args.arg
  204. else:
  205. if args.arg == ':recent:':
  206. pass
  207. else:
  208. stop_id = _picker(args)
  209. data = ctabus.get_times(stop_id)
  210. info = data['prd'][0]
  211. key = make_key(info['rt'], info['rtdir'], ctabus.api, None)
  212. if key not in ctabus.get_name_from_direction.cache.keys():
  213. ctabus.get_name_from_direction.cache[key] = info['des']
  214. ctabus.get_name_from_direction.fresh = True
  215. if args.periodic is not None:
  216. _main_periodic(args, stop_id, data)
  217. else:
  218. show(data, args.route)
  219. for cache_obj in disk_cache.caches:
  220. if cache_obj.fresh:
  221. cache_obj.save_cache()
  222. if __name__ == '__main__':
  223. args = parser.parse_args()
  224. main(args)