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.

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