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.

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