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