From 19c407f804a3f8c1c4ff5e12a649273476051ea0 Mon Sep 17 00:00:00 2001
From: David Trattnig <david.trattnig@o94.at>
Date: Mon, 1 Jun 2020 09:43:01 +0200
Subject: [PATCH] Improved timer behaviour and schedule updates.

---
 modules/scheduling/scheduler.py | 125 ++++++++++++++++++--------------
 1 file changed, 71 insertions(+), 54 deletions(-)

diff --git a/modules/scheduling/scheduler.py b/modules/scheduling/scheduler.py
index 98d075f2..879d2572 100644
--- a/modules/scheduling/scheduler.py
+++ b/modules/scheduling/scheduler.py
@@ -138,24 +138,27 @@ class AuraScheduler(threading.Thread):
             3. Queues all playlists of the programm, if the soundssystem is ready to accept commands.
         """
         while not self.exit_event.is_set():
-            seconds_to_wait = int(self.config.get("fetching_frequency"))
-            next_time = datetime.datetime.now() + datetime.timedelta(seconds=seconds_to_wait)
-            self.logger.info("Fetch new programmes every %ss. Next fetching in %ss." % (str(seconds_to_wait), str(next_time)))
-            self.fetch_new_programme()
-
-            # The Scheduler is ready
-            if not self.is_initialized:
-                self.is_initialized = True
-                if self.func_on_initialized:
-                    self.func_on_initialized()
-
-            # The SoundSystem is ready
-            if self.soundsystem.is_ready():
-                self.queue_programme()
+            try:
+                seconds_to_wait = int(self.config.get("fetching_frequency"))
+                next_time = datetime.datetime.now() + datetime.timedelta(seconds=seconds_to_wait)
+                self.logger.info("Fetch new programmes every %ss. Next fetching in %ss." % (str(seconds_to_wait), str(next_time)))
+                self.fetch_new_programme()
+
+                # The scheduler is ready
+                if not self.is_initialized:
+                    self.is_initialized = True
+                    if self.func_on_initialized:
+                        self.func_on_initialized()
+
+                # The soundsystem is ready
+                if self.soundsystem.is_ready():
+                    self.queue_programme()
+            except Exception as e:
+                self.logger.critical(SimpleUtil.red("Unhandled error while fetching & scheduling new programme! (%s)" % str(e)), e)
 
             self.print_message_queue()
             self.exit_event.wait(seconds_to_wait)
-
+            self.logger.info(SimpleUtil.cyan("== start fetching new schedules =="))
 
 
 
@@ -242,7 +245,6 @@ class AuraScheduler(threading.Thread):
 
         # Load programme if necessary
         if not self.programme:
-            self.logger.info("Next track requested: Need to load programme from database first.")
             self.load_programme_from_db()
 
         # Check for scheduled playlist
@@ -590,19 +592,23 @@ class AuraScheduler(threading.Thread):
                 index += 1
                 entry_groups.append([])
                 entry_groups[index].append(entry)
-
             previous_entry = entry
         self.logger.info("Built %s entry group(s)" % len(entry_groups))
          
         # Schedule function calls
-        for entries in entry_groups:
-
-            self.set_entry_timer(entries, fade_in, fade_out)
+        if len(clean_entries) > 0 and len(entry_groups) > 0:
+            for entries in entry_groups:
+                if not isinstance(entries, list):
+                    raise ValueError("Invalid Entry Group: %s" % str(entries))
+                
+                self.set_entry_timer(entries, fade_in, fade_out)
 
-            # Check if it's the last item, which needs special handling
-            if entries[-1] == clean_entries[-1]:
-                # The end of schedule is the actual end of the track
-                self.queue_end_of_schedule(entries[-1], fade_out)
+                # Check if it's the last item, which needs special handling
+                if entries[-1] == clean_entries[-1]:
+                    # The end of schedule is the actual end of the track
+                    self.queue_end_of_schedule(entries[-1], fade_out)
+        else:
+            self.logger.warn(SimpleUtil.red("Nothing to schedule..."))
 
 
 
@@ -635,7 +641,7 @@ class AuraScheduler(threading.Thread):
 
         if play_timer:
             # Check if the Playlist IDs are different
-            if play_timer.entries[0].entry_id != entries[0].entry_id:
+            if play_timer.entries[0].playlist.playlist_id == entries[0].playlist.playlist_id:
                 # If not, stop and remove the old timer, create a new one
                 self.stop_timer(play_timer)                
             else:
@@ -644,7 +650,7 @@ class AuraScheduler(threading.Thread):
                 return
         
         # If nothing is planned at given time, create a new timer
-        (entries[0].switchtimer, entries[0].loadtimer) = self.create_timer(diff, do_play, [entries], switcher=True)
+        (entries[0].switchtimer, entries[0].loadtimer) = self.create_timer(diff, do_play, entries, switcher=True)
 
 
 
@@ -762,7 +768,7 @@ class AuraScheduler(threading.Thread):
         self.programme = Schedule.select_programme()
 
         if not self.programme:
-            self.logger.critical("Could not load programme from database. We are in big trouble my friend!")
+            self.logger.critical(SimpleUtil.red("Could not load programme from database. We are in big trouble my friend!"))
             return
 
 
@@ -772,32 +778,34 @@ class AuraScheduler(threading.Thread):
         Checks for existing timers at the given time.
         """
         for t in self.message_timer:
-            if t.entry[0].start_unix == given_time and (t.fade_in or t.switcher):
+            if t.entries[0].start_unix == given_time and (t.fadein or t.switcher):
                 return t
         return False
 
 
 
-    def create_timer(self, diff, func, parameters, fadein=False, fadeout=False, switcher=False):
+    def create_timer(self, diff, func, entries, fadein=False, fadeout=False, switcher=False):
         """
         Creates a new timer for timed execution of mixer commands.
 
         Args:
             diff (Integer):     The difference in seconds from now, when the call should happen
             func (Function):    The function to call
-            parameters ([]):    The function parameters
+            entries ([]):       The entries to be scheduled
 
         """
         if not fadein and not fadeout and not switcher or fadein and fadeout or fadein and switcher or fadeout and switcher:
-            raise Exception("You have to call me with either fadein=true, fadeout=true or switcher=True")
+            raise ValueError("You have to call me with either fadein=true, fadeout=true or switcher=True")
+        if not isinstance(entries, list):
+            raise ValueError("No list of entries passed!")
 
-        t = CallFunctionTimer(diff, func, parameters, fadein, fadeout, switcher)
+        t = CallFunctionTimer(diff=diff, func=func, param=entries, fadein=fadein, fadeout=fadeout, switcher=switcher)
         self.message_timer.append(t)
         t.start()
 
         if switcher:
-            # Load function to be called by timer
-            def do_load(entries):
+            # Pre-roll function to be called by timer
+            def do_preroll(entries):
                 try:
                     if entries[0].type == ChannelType.FILESYSTEM:
                         self.logger.info(SimpleUtil.cyan("=== preroll_group('%s') ===" % EngineUtil.get_entries_string(entries)))
@@ -810,7 +818,7 @@ class AuraScheduler(threading.Thread):
                     # TODO Fallback logic here
 
             loader_diff = diff - self.config.get("preload_offset")
-            loader = CallFunctionTimer(loader_diff, do_load, parameters, fadein, fadeout, switcher)
+            loader = CallFunctionTimer(diff=loader_diff, func=do_preroll, param=entries, fadein=fadein, fadeout=fadeout, switcher=False, loader=True)
             self.message_timer.append(loader)
             loader.start()
             return (t, loader)
@@ -827,22 +835,26 @@ class AuraScheduler(threading.Thread):
             timer (Timer):  The timer to stop.
         """
         timer.cancel()
+        count = 1
 
-        if timer.entry.loadtimer is not None:
-            timer.entry.loadtimer.cancel()
-            self.message_timer.remove(timer.entry.loadtimer)
+        if timer.entries[0].loadtimer is not None:
+            timer.entries[0].loadtimer.cancel()
+            self.message_timer.remove(timer.entries[0].loadtimer)
+            count += 1
 
-        if timer.entry.fadeintimer is not None:
-            timer.entry.fadeintimer.cancel()
-            self.message_timer.remove(timer.entry.fadeintimer)
+        # if timer.entries[0].fadeintimer is not None:
+        #     timer.entries[0].fadeintimer.cancel()
+        #     self.message_timer.remove(timer.entries[0].fadeintimer)
+        #     count += 1
 
-        if timer.entry.fadeouttimer is not None:
-            timer.entry.fadeouttimer.cancel()
-            self.message_timer.remove(timer.entry.fadeouttimer)
+        if timer.entries[0].fadeouttimer is not None:
+            timer.entries[0].fadeouttimer.cancel()
+            self.message_timer.remove(timer.entries[0].fadeouttimer)
+            count += 1
 
         # Remove it from message queue
         self.message_timer.remove(timer)
-        self.logger.info("Removed timer for " + str(timer.entry))
+        self.logger.info("Removed %s timers for: %s" % (str(count), EngineUtil.get_entries_string(timer.entries)))
 
 
 
@@ -909,29 +921,30 @@ class AuraScheduler(threading.Thread):
 class CallFunctionTimer(threading.Timer):
     logger = None
     param = None
-    entry = None
+    entries = None
     diff = None
     fadein = False
     fadeout = False
     switcher = False
+    loader = False
 
-    def __init__(self, diff, func, param, fadein=False, fadeout=False, switcher=False, loader=False):
+    def __init__(self, diff=None, func=None, param=None, fadein=False, fadeout=False, switcher=False, loader=False):
 
         self.logger = logging.getLogger("AuraEngine")
         self.logger.debug("Executing soundsystem command '%s' in %s seconds..." % (str(func.__name__), str(diff)))
-        threading.Timer.__init__(self, diff, func, args=param)
+        threading.Timer.__init__(self, diff, func, param)
 
         if not fadein and not fadeout and not switcher and not loader \
             or fadein and fadeout \
             or fadein and switcher \
             or fadeout and switcher:
             
-            raise Exception("You have to create me with either fadein=true, fadeout=true or switcher=True")
+            raise Exception("You have to create me with either fadein=True, fadeout=True or switcher=True")
 
         self.diff = diff
         self.func = func
         self.param = param
-        self.entry = param[0]
+        self.entries = param
         self.fadein = fadein
         self.fadeout = fadeout
         self.switcher = switcher
@@ -942,13 +955,17 @@ class CallFunctionTimer(threading.Timer):
         """
         String represenation of the timer.
         """
+        status = "Timer (Alive: %s)" % self.is_alive()
+        time_start = SimpleUtil.fmt_time(SimpleUtil.timestamp() + self.diff)
+        status += " starting in " + str(self.diff) + "s (" + time_start + ") "
+
         if self.fadein:
-            return "CallFunctionTimer starting in " + str(self.diff) + "s fading in source '" + str(self.entry)
+            return status + " fading in entries '" + EngineUtil.get_entries_string(self.entries)
         elif self.fadeout:
-            return "CallFunctionTimer starting in " + str(self.diff) + "s fading out source '" + str(self.entry)
+            return status + " fading out schedule '" + str(self.entries[0].playlist.schedule)
         elif self.switcher:
-            return "CallFunctionTimer starting in " + str(self.diff) + "s switching to source '" + str(self.entry)
+            return status + " switching to entries '" + EngineUtil.get_entries_string(self.entries)
         elif self.loader:
-            return "CallFunctionTimer starting in " + str(self.diff) + "s loading source '" + str(self.entry)
+            return status + " pre-rolling entries '" + EngineUtil.get_entries_string(self.entries)
         else:
             return "CORRUPTED CallFunctionTimer around! How can that be?"
-- 
GitLab