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.

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