3 from abc import ABC, abstractmethod
9 from typing import Any, Callable, List, Optional, Set, Tuple
11 from pyutils.datetimes import datetime_utils
13 import kiosk_constants
17 logger = logging.getLogger(__file__)
21 """Base class of a thing that chooses pages"""
26 def get_page_list(self) -> List[str]:
28 valid_filename = re.compile("([^_]+)_(\d+)_([^\.]+)\.html")
32 for f in os.listdir(kiosk_constants.pages_dir)
33 if os.path.isfile(os.path.join(kiosk_constants.pages_dir, f))
36 result = re.match(valid_filename, page)
37 if result is not None:
38 if result.group(3) != "none":
39 freshness_requirement = int(result.group(3))
41 os.path.getmtime(os.path.join(kiosk_constants.pages_dir, page))
43 age = now - last_modified
44 if age > freshness_requirement:
45 logger.warning(f'chooser: "{page}" is too old.')
47 logger.info(f'chooser: candidate page: "{page}"')
48 filenames.append(page)
52 def choose_next_page(self) -> Any:
56 class weighted_random_chooser(chooser):
57 """Chooser that does it via weighted RNG."""
59 def __init__(self, filter_list: Optional[List[Callable[[str], bool]]]) -> None:
61 self.last_choice = None
62 self.valid_filename = re.compile("([^_]+)_(\d+)_([^\.]+)\.html")
63 self.pages: Optional[List[str]] = None
65 self.filter_list: List[Callable[[str], bool]] = []
66 if filter_list is not None:
67 self.filter_list.extend(filter_list)
69 def choose_next_page(self) -> Any:
70 if self.pages is None or self.count % 100 == 0:
71 logger.info("chooser: refreshing the candidate pages list.")
72 self.pages = self.get_page_list()
76 for page in self.pages:
77 result = re.match(self.valid_filename, page)
78 if result is not None:
79 weight = int(result.group(2))
80 weights.append(weight)
81 total_weight += weight
86 random_pick = random.randrange(0, total_weight - 1)
88 for x in range(0, len(weights)):
90 if so_far > random_pick:
92 choice = self.pages[x]
94 # Allow filters list to suppress pages.
95 choice_is_filtered = False
96 for f in self.filter_list:
98 choice_is_filtered = True
100 if choice_is_filtered:
108 class weighted_random_chooser_with_triggers(weighted_random_chooser):
109 """Same as WRC but has trigger events"""
113 trigger_list: Optional[List[trigger.trigger]],
114 filter_list: List[Callable[[str], bool]],
116 super().__init__(filter_list)
117 self.trigger_list: List[trigger.trigger] = []
118 if trigger_list is not None:
119 self.trigger_list.extend(trigger_list)
120 self.page_queue: Set[Tuple[str, int]] = set(())
122 def check_for_triggers(self) -> bool:
124 for t in self.trigger_list:
125 x = t.get_triggered_page_list()
126 if x is not None and len(x) > 0:
128 self.page_queue.add(y)
129 logger.info(f"chooser: noticed active trigger {y}")
133 def choose_next_page(self) -> Tuple[str, bool]:
134 if self.pages is None or self.count % 100 == 0:
135 logger.info("chooser: refreshing the candidates page list")
136 self.pages = self.get_page_list()
138 triggered = self.check_for_triggers()
140 # First try to satisfy from the page queue.
141 if len(self.page_queue) > 0:
142 logger.info("chooser: page queue has entries; pulling choice from there.")
145 for t in self.page_queue:
146 if priority is None or t[1] > priority:
149 assert page is not None
150 assert priority is not None
151 self.page_queue.remove((page, priority))
152 return (page, triggered)
154 # Always show the clock in the middle of the night.
155 now = datetime_utils.now_pacific()
157 for page in self.pages:
161 # Fall back on weighted random choice.
162 return (weighted_random_chooser.choose_next_page(self), False)
166 # def filter_news_during_dinnertime(page):
167 # now = datetime.datetime.now()
168 # is_dinnertime = now.hour >= 17 and now.hour <= 20
169 # return not is_dinnertime or not (
172 # or "mynorthwest" in page
173 # or "seattle" in page
174 # or "stranger" in page
175 # or "twitter" in page
178 # x = weighted_random_chooser_with_triggers([], [ filter_news_during_dinnertime ])
179 # print(x.choose_next_page())