d9f607e3447b08e7616cfe66ef94e6793ce78d55
[kiosk.git] / kiosk.py
1 #!/usr/bin/env python3
2
3 import sys
4 import traceback
5 import os
6 from threading import Thread
7 import time
8 from datetime import datetime
9 import constants
10 import renderer
11 import renderer
12 import renderer_catalog
13 import chooser
14 import logging
15 import trigger_catalog
16 import utils
17
18 def filter_news_during_dinnertime(page):
19     now = datetime.now()
20     is_dinnertime = now.hour >= 17 and now.hour <= 20
21     return (not is_dinnertime or
22             not ("cnn" in page or
23                  "news" in page or
24                  "mynorthwest" in page or
25                  "seattle" in page or
26                  "stranger" in page or
27                  "twitter" in page or
28                  "wsj" in page))
29
30 def thread_change_current():
31     page_chooser = chooser.weighted_random_chooser_with_triggers(
32         trigger_catalog.get_triggers(),
33         [ filter_news_during_dinnertime ])
34     swap_page_target = 0
35     last_page = ""
36     while True:
37         now = time.time()
38         (page, triggered) = page_chooser.choose_next_page()
39
40         if triggered:
41             print('chooser[%s] - WE ARE TRIGGERED.' % utils.timestamp())
42             if page != last_page:
43                 print('chooser[%s] - EMERGENCY PAGE %s LOAD NEEDED' % (
44                     utils.timestamp(), page))
45                 try:
46                     f = open(os.path.join(constants.pages_dir,
47                                           'current.shtml'), 'w')
48                     emit_wrapped(f, page)
49                     f.close()
50                 except:
51                     print('chooser[%s] - page does not exist?!' % (
52                         utils.timestamp()))
53                     continue
54                 last_page = page
55                 swap_page_target = now + constants.refresh_period_sec
56
57                 # Also notify XMLHTTP clients that they need to refresh now.
58                 path = os.path.join(constants.pages_dir,
59                                     'reload_immediately.html')
60                 f = open(path, 'w')
61                 f.write('Reload, suckers!')
62                 f.close()
63
64                 # Fix this hack... maybe read the webserver logs and see if it
65                 # actually was picked up?
66                 time.sleep(0.750)
67                 os.remove(path)
68
69         elif now >= swap_page_target:
70             if (page == last_page):
71                 print(('chooser[%s] - nominal choice got the same page...' % (
72                     utils.timestamp())))
73                 continue
74             print('chooser[%s] - nominal choice of %s' % (utils.timestamp(), page))
75             try:
76                 f = open(os.path.join(constants.pages_dir,
77                                       'current.shtml'), 'w')
78                 emit_wrapped(f, page)
79                 f.close()
80             except:
81                 print('chooser[%s] - page does not exist?!' % (utils.timestamp()))
82                 continue
83             last_page = page
84             swap_page_target = now + constants.refresh_period_sec
85         time.sleep(1)
86
87 def pick_background_color():
88     now = datetime.now()
89     if now.hour <= 6 or now.hour >= 21:
90         return "E6B8B8"
91     elif now.hour == 7 or now.hour == 20:
92         return "EECDCD"
93     else:
94         return "FFFFFF"
95
96 def emit_wrapped(f, filename):
97     age = utils.describe_age_of_file_briefly("pages/%s" % filename)
98     bgcolor = pick_background_color()
99     f.write("""
100 <HEAD>
101   <TITLE>Kitchen Kiosk</TITLE>
102   <LINK rel="stylesheet" type="text/css" href="style.css">
103   <SCRIPT TYPE="text/javascript">
104
105   // Zoom the 'contents' div to fit without scrollbars and then make
106   // it visible.
107   function zoomScreen() {
108     z = 285;
109     do {
110       document.getElementById("content").style.zoom = z+"%%";
111       var body = document.body;
112       var html = document.documentElement;
113       var height = Math.max(body.scrollHeight,
114                             body.offsetHeight,
115                             html.clientHeight,
116                             html.scrollHeight,
117                             html.offsetHeight);
118       var windowHeight = window.innerHeight;
119       var width = Math.max(body.scrollWidth,
120                            body.offsetWidth,
121                            html.clientWidth,
122                            html.scrollWidth,
123                            html.offsetWidth);
124       var windowWidth = window.innerWidth;
125       var heightRatio = height / windowHeight;
126       var widthRatio = width / windowWidth;
127
128       if (heightRatio <= 1.0 && widthRatio <= 1.0) {
129         break;
130       }
131       z -= 4;
132     } while(z >= 70);
133     document.getElementById("content").style.visibility = "visible";
134   }
135
136   // Load IMG tags with DATA-SRC attributes late.
137   function lateLoadImages() {
138     var image = document.getElementsByTagName('img');
139     for (var i = 0; i < image.length; i++) {
140       if (image[i].getAttribute('DATA-SRC')) {
141         image[i].setAttribute('SRC', image[i].getAttribute('DATA-SRC'));
142       }
143     }
144   }
145
146   // Operate the clock at the top of the page.
147   function runClock() {
148     var today = new Date();
149     var h = today.getHours();
150     var ampm = h >= 12 ? 'pm' : 'am';
151     h = h %% 12;
152     h = h ? h : 12; // the hour '0' should be '12'
153     var m = maybeAddZero(today.getMinutes());
154     var colon = ":";
155     if (today.getSeconds() %% 2 == 0) {
156       colon = "<FONT STYLE='color: #%s; font-size: 4vmin; font-weight: bold'>:</FONT>";
157     }
158     document.getElementById("time").innerHTML = h + colon + m + ampm;
159     document.getElementById("date").innerHTML = today.toDateString();
160     var t = setTimeout(function(){runClock()}, 1000);
161   }
162
163   // Helper method for running the clock.
164   function maybeAddZero(x) {
165     return (x < 10) ? "0" + x : x;
166   }
167
168   // Do something on page load.
169   function addLoadEvent(func) {
170     var oldonload = window.onload;
171     if (typeof window.onload != 'function') {
172       window.onload = func;
173     } else {
174       window.onload = function() {
175         if (oldonload) {
176           oldonload();
177         }
178         func();
179       }
180     }
181   }
182
183   // Sleep thread helper.
184   const sleep = (milliseconds) => {
185     return new Promise(resolve => setTimeout(resolve, milliseconds))
186   }
187
188   var loadedDate = new Date();
189
190   addLoadEvent(zoomScreen);
191   addLoadEvent(runClock);
192   addLoadEvent(lateLoadImages);
193
194   // Runs the countdown line at the bottom and is responsible for
195   // normal page reloads caused by the expiration of a timer.
196   (function countdown() {
197     setTimeout(
198       function() {
199         var now = new Date();
200         var deltaMs = now.getTime() - loadedDate.getTime();
201         var totalMs = %d;
202         var remainingMs = (totalMs - deltaMs);
203
204         if (remainingMs > 0) {
205           var hr = document.getElementById("countdown");
206           var width = (remainingMs / (totalMs - 5000)) * 100.0;
207           if (width <= 100) {
208             hr.style.visibility = "visible";
209             hr.style.width = " ".concat(width, "%%");
210             hr.style.backgroundColor = "maroon";
211           }
212         } else {
213           // Reload unconditionally after 22 sec.
214           window.location.reload();
215         }
216
217         // Brief sleep before doing it all over again.
218         sleep(50).then(() => {
219           countdown();
220         });
221       }, 50)
222   })();
223
224   // Periodically checks for emergency reload events.
225   (function poll() {
226     setTimeout(
227       function() {
228         var xhr = new XMLHttpRequest();
229         xhr.open('GET',
230                  'http://wannabe.house/kiosk/pages/reload_immediately.html');
231         xhr.onload =
232           function() {
233             if (xhr.status === 200) {
234               window.location.reload();
235             } else {
236               sleep(500).then(() => {
237                 poll();
238               });
239             }
240           };
241         xhr.send();
242       }, 500);
243   })();
244 </SCRIPT>
245 </HEAD>
246 <BODY BGCOLOR="#%s">
247     <TABLE style="height:100%%; width:100%%" BORDER=0>
248     <TR HEIGHT=30>
249         <TD ALIGN="left">
250             <DIV id="date">&nbsp;</DIV>
251         </TD>
252         <TD ALIGN="center"><FONT COLOR=#bbbbbb>
253             <DIV id="info"></DIV></FONT>
254         </TD>
255         <TD ALIGN="right">
256             <DIV id="time">&nbsp;</DIV>
257         </TD>
258     </TR>
259     <TR STYLE="vertical-align:top">
260         <TD COLSPAN=3>
261             <DIV ID="content" STYLE="zoom: 1; visibility: hidden;">
262               <!-- BEGIN main page contents. -->
263 <!--#include virtual=\"%s\"-->
264               <!-- END main page contents. -->
265             </DIV>
266             <BR>
267             <DIV STYLE="position: absolute; top:1030px; width:99%%">
268             <P ALIGN="right">
269               <FONT SIZE=2 COLOR=#bbbbbb>%s @ %s ago.</FONT>
270             </P>
271             <HR id="countdown" STYLE="width:0px;
272                                       text-align:left;
273                                       margin:0;
274                                       border:none;
275                                       border-width:0;
276                                       height:5px;
277                                       visibility:hidden;
278                                       background-color:#ffffff;">
279             </DIV>
280         </TD>
281     </TR>
282     </TABLE>
283 </BODY>""" % (bgcolor,
284               constants.refresh_period_sec * 1000,
285               bgcolor,
286               filename,
287               filename,
288               age))
289
290 def thread_invoke_renderers():
291     while True:
292         print("renderer[%s]: invoking all renderers in catalog..." % (
293             utils.timestamp()))
294         for r in renderer_catalog.get_renderers():
295             now = time.time()
296             try:
297                 r.render()
298             except Exception as e:
299                 traceback.print_exc()
300                 print("renderer[%s] unknown exception in %s, swallowing it." % (
301                     utils.timestamp(), r.get_name()))
302             except Error as e:
303                 traceback.print_exc()
304                 print("renderer[%s] unknown error in %s, swallowing it." % (
305                     utils.timestamp(), r.get_name()))
306             delta = time.time() - now
307             if (delta > 1.0):
308                 print("renderer[%s]: Warning: %s's rendering took %5.2fs." % (
309                     utils.timestamp(), r.get_name(), delta))
310         print("renderer[%s]: thread having a little break for %ds..." % (
311             utils.timestamp(), constants.render_period_sec))
312         time.sleep(constants.render_period_sec)
313
314 if __name__ == "__main__":
315     logging.basicConfig()
316     changer_thread = None
317     renderer_thread = None
318     while True:
319         if (changer_thread == None or
320             not changer_thread.is_alive()):
321             print("MAIN[%s] - (Re?)initializing chooser thread..." % (
322                 utils.timestamp()))
323             changer_thread = Thread(target = thread_change_current, args=())
324             changer_thread.start()
325         if (renderer_thread == None or
326             not renderer_thread.is_alive()):
327             print("MAIN[%s] - (Re?)initializing render thread..." % (
328                 utils.timestamp()))
329             renderer_thread = Thread(target = thread_invoke_renderers, args=())
330             renderer_thread.start()
331         time.sleep(60)
332     print("Should never get here.")