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