mypy clean
[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://wannabe.house/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             bgcolor,
297             filename,
298             filename,
299             age,
300         )
301     )
302
303
304 def thread_invoke_renderers() -> None:
305     while True:
306         print(f"renderer[{utils.timestamp()}]: invoking all renderers in catalog...")
307         for r in renderer_catalog.get_renderers():
308             now = time.time()
309             try:
310                 r.render()
311             except Exception as e:
312                 traceback.print_exc()
313                 print(
314                     f"renderer[{utils.timestamp()}] unknown exception in {r.get_name()}, swallowing it."
315                 )
316             delta = time.time() - now
317             if delta > 1.0:
318                 print(
319                     f"renderer[{utils.timestamp()}]: Warning: {r.get_name()}'s rendering took {delta:%5.2f}s."
320                 )
321         print(
322             f"renderer[{utils.timestamp()}]: thread having a little break for {constants.render_period_sec}s..."
323         )
324         time.sleep(constants.render_period_sec)
325
326
327 if __name__ == "__main__":
328     logging.basicConfig()
329     changer_thread: Optional[Thread] = None
330     renderer_thread: Optional[Thread] = None
331     while True:
332         if changer_thread is None or not changer_thread.is_alive():
333             print(
334                 f"MAIN[{utils.timestamp()}] - (Re?)initializing chooser thread... (wtf?!)"
335             )
336             changer_thread = Thread(target=thread_change_current, args=())
337             changer_thread.start()
338         if renderer_thread is None or not renderer_thread.is_alive():
339             print(
340                 f"MAIN[{utils.timestamp()}] - (Re?)initializing render thread... (wtf?!)"
341             )
342             renderer_thread = Thread(target=thread_invoke_renderers, args=())
343             renderer_thread.start()
344         time.sleep(60)