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