From 6eafd4ebfd074fd9bc3f53e8b0bce378ea1e9f80 Mon Sep 17 00:00:00 2001
From: David Trattnig <david.trattnig@o94.at>
Date: Wed, 18 Nov 2020 19:17:48 +0100
Subject: [PATCH] Scheduler cmds inherit from EngineExecutor. #41

---
 src/core/client/connector.py |   2 +-
 src/core/engine.py           |  15 +-
 src/core/events.py           |   7 +-
 src/scheduling/calendar.py   |   4 +-
 src/scheduling/fallback.py   |  13 +-
 src/scheduling/models.py     |  51 ++---
 src/scheduling/programme.py  |   2 +-
 src/scheduling/scheduler.py  | 430 ++++++++---------------------------
 8 files changed, 145 insertions(+), 379 deletions(-)

diff --git a/src/core/client/connector.py b/src/core/client/connector.py
index fc0ea7af..59665758 100644
--- a/src/core/client/connector.py
+++ b/src/core/client/connector.py
@@ -88,7 +88,7 @@ class PlayerConnector():
                     "queue_clear", 
                     "playlist_uri_set", 
                     "playlist_uri_clear",
-                    "stream_set_ur", 
+                    "stream_set_url", 
                     "stream_start", 
                     "stream_stop", 
                     "stream_status",
diff --git a/src/core/engine.py b/src/core/engine.py
index 52aed5bc..69c69dae 100644
--- a/src/core/engine.py
+++ b/src/core/engine.py
@@ -341,10 +341,7 @@ class Player:
                     self.connector.disable_transaction()
                 Thread(target=clean_up).start()
             
-            # Filesystem meta-changes trigger the event via Liquidsoap, so only
-            # issue event for LIVE and STREAM:
-            if not entry.channel in ChannelType.QUEUE.channels:
-                self.event_dispatcher.on_play(entry)
+            self.event_dispatcher.on_play(entry)
 
 
 
@@ -386,6 +383,7 @@ class Player:
         self.event_dispatcher.on_fallback_updated(entries)
 
 
+
     def stop_fallback_playlist(self):
         """
         Performs a fade-out and clears any scheduled fallback playlist.
@@ -548,7 +546,14 @@ class Player:
         self.connector.disable_transaction()
 
         # If successful, Liquidsoap returns a resource ID of the queued track
-        return int(result) >= 0
+        resource_id = -1
+        try:
+            resource_id = int(result)
+        except ValueError:
+            self.logger.error(SU.red("Got an invalid resource ID: '%s'" % result))
+            return False
+
+        return resource_id >= 0
 
 
 
diff --git a/src/core/events.py b/src/core/events.py
index ee0255d3..74aca24b 100644
--- a/src/core/events.py
+++ b/src/core/events.py
@@ -220,15 +220,18 @@ class EngineEventDispatcher():
 
     def on_play(self, entry):
         """
-        Event Handler which is called by the engine when some entry is actually playing. 
+        Event Handler which is called by the engine when some play command to Liquidsoap is issued.
+        This does not indiciate that Liquidsoap started playing actually, only that the command has
+        been issued. To get the metadata update issued by Liquidsoap use `on_metadata` instead.
 
         Args:
             source (String):    The `PlaylistEntry` object
         """
         def func(self, entry):        
             self.logger.debug("on_play(..)")
-            # Assign timestamp for play time
+            # Assign timestamp indicating start play time. Use the actual playtime when possible.
             entry.entry_start_actual = datetime.datetime.now()
+            self.scheduler.on_play(entry)
             self.call_event("on_play", entry)
 
         thread = Thread(target = func, args = (self, entry))
diff --git a/src/scheduling/calendar.py b/src/scheduling/calendar.py
index cffa0961..3d847011 100644
--- a/src/scheduling/calendar.py
+++ b/src/scheduling/calendar.py
@@ -100,7 +100,7 @@ class AuraCalendarService(threading.Thread):
                 return
             
             # Check if existing timeslots have been deleted
-            local_timeslots = Timeslot.select_programme(datetime.now())
+            local_timeslots = Timeslot.get_timeslots(datetime.now())
             for local_timeslot in local_timeslots:
 
                 # Only allow deletion of timeslots which are deleted before the start of the scheduling window
@@ -169,7 +169,7 @@ class AuraCalendarService(threading.Thread):
         Args:
             timeslot (Timeslot):    The timeslot
         """
-        timeslot_db = Timeslot.select_show_on_datetime(timeslot["start"])
+        timeslot_db = Timeslot.for_datetime(timeslot["start"])
         havetoadd = False
 
         if not timeslot_db:
diff --git a/src/scheduling/fallback.py b/src/scheduling/fallback.py
index cc6bde81..1dec3da8 100644
--- a/src/scheduling/fallback.py
+++ b/src/scheduling/fallback.py
@@ -26,7 +26,7 @@ from datetime               import timedelta
 
 from src.base.config        import AuraConfig
 from src.base.utils         import SimpleUtil as SU
-from src.core.resources     import ResourceClass
+from src.core.resources     import ResourceClass, ResourceUtil
 from src.core.channels      import Channel
 from src.core.control       import EngineExecutor
 
@@ -113,7 +113,12 @@ class FallbackManager:
 
         Args:
             source (String):    The `PlaylistEntry` object
-        """        
+        """     
+        content_class = ResourceUtil.get_content_class(entry.get_content_type())
+        if content_class == ResourceClass.FILE:
+            # Files are handled by "on_metadata" called via Liquidsoap
+            return
+
         self.update_fallback_state(entry.channel)
 
 
@@ -306,5 +311,5 @@ class FallbackCommand(EngineExecutor):
         end_time_offset = int(float(AuraConfig.config().get("fade_out_time")) / 2 * 1000 * -1)
         end_time = SU.timestamp(timeslot.timeslot_end + timedelta(milliseconds=end_time_offset))
         self.logger.info(f"Starting fade-out of scheduled fallback with an offset of {end_time_offset} milliseconds at {end_time}")
-        child_timer = EngineExecutor("FALLBACK", None, end_time, do_stop, None)
-        super().__init__("FALLBACK", child_timer, timeslot.start_unix, do_play, entries)
\ No newline at end of file
+        super().__init__("FALLBACK", None, timeslot.start_unix, do_play, entries)
+        EngineExecutor("FALLBACK", self, end_time, do_stop, None)
\ No newline at end of file
diff --git a/src/scheduling/models.py b/src/scheduling/models.py
index 90ab2f58..f0f751e9 100644
--- a/src/scheduling/models.py
+++ b/src/scheduling/models.py
@@ -167,16 +167,23 @@ class Timeslot(DB.Model, AuraDatabaseModel):
     musicfocus = Column(String(256))
     is_repetition = Column(Boolean())
     
-    fadeouttimer = None # Used to fade-out the timeslot, even when entries are longer
+    # Transients
+    active_entry = None
 
 
     @staticmethod
-    def select_show_on_datetime(date_time):
+    def for_datetime(date_time):
+        """
+        Select a timeslot at the given datetime.
+
+        Args:
+            date_time (datetime): date and time when the timeslot starts
+        """
         return DB.session.query(Timeslot).filter(Timeslot.timeslot_start == date_time).first()
 
 
     @staticmethod
-    def select_programme(date_from=datetime.date.today()):
+    def get_timeslots(date_from=datetime.date.today()):
         """
         Select all timeslots starting from `date_from` or from today if no
         parameter is passed.
@@ -189,34 +196,26 @@ class Timeslot(DB.Model, AuraDatabaseModel):
         """
         timeslots = DB.session.query(Timeslot).\
             filter(Timeslot.timeslot_start >= date_from).\
-            order_by(Timeslot.timeslot_start).all()
-
+            order_by(Timeslot.timeslot_start).all()        
         return timeslots
 
 
-    @staticmethod
-    def select_upcoming(n):
+    def set_active_entry(self, entry):
         """
-        Selects the (`n`) upcoming timeslots.
+        Sets the currently playing entry.
+
+        Args:
+            entry (PlaylistEntry): The entry playing right now
         """
-        now = datetime.datetime.now()
-        DB.session.commit() # Required since independend session is used.
-        timeslots = DB.session.query(Timeslot).\
-            filter(Timeslot.timeslot_start > str(now)).\
-            order_by(Timeslot.timeslot_start.asc()).limit(n).all()
-        
-        return timeslots
+        self.active_entry = entry
 
 
-    def has_queued_entries(self):
+    def get_recent_entry(self):
         """
-        Checks if entries of this timeslot have been queued at the engine.        
+        Retrieves the most recent played or currently playing entry. This is used to fade-out
+        the timeslot, when there is no other entry is following the current one.
         """
-        #TODO Make logic more transparent
-        if hasattr(self, "queued_entries"):
-            if len(self.queued_entries) > 0:    
-                return True
-        return False
+        return self.active_entry
 
 
     @hybrid_property
@@ -332,9 +331,7 @@ class Playlist(DB.Model, AuraDatabaseModel):
         """
         playlist = None
         playlists = DB.session.query(Playlist).filter(Playlist.timeslot_start == start_date).all()
-        # FIXME There are unknown issues with the native SQL query by ID
-        # playlists = DB.session.query(Playlist).filter(Playlist.timeslot_start == datetime and Playlist.playlist_id == playlist_id).all()
-        
+
         for p in playlists:
             if p.playlist_id == playlist_id:
                 playlist = p
@@ -450,13 +447,11 @@ class PlaylistEntry(DB.Model, AuraDatabaseModel):
     source = Column(String(1024))
     entry_start = Column(DateTime)
 
+    # Transients
     entry_start_actual = None # Assigned when the entry is actually played
     channel = None # Assigned when entry is actually played
     queue_state = None # Assigned when entry is about to be queued    
     status = None # Assigned when state changes
-    switchtimer = None
-    loadtimer = None
-    fadeouttimer = None
 
 
     @staticmethod
diff --git a/src/scheduling/programme.py b/src/scheduling/programme.py
index c8620342..bb477eb1 100644
--- a/src/scheduling/programme.py
+++ b/src/scheduling/programme.py
@@ -104,7 +104,7 @@ class Programme():
         them via `self.enable_entries(..)`. After that, the
         current message queue is printed to the console.
         """
-        self.programme = Timeslot.select_programme()
+        self.programme = Timeslot.get_timeslots()
 
         if not self.programme:
             self.logger.critical(SU.red("Could not load programme from database. We are in big trouble my friend!"))
diff --git a/src/scheduling/scheduler.py b/src/scheduling/scheduler.py
index 762324f4..5e72546d 100644
--- a/src/scheduling/scheduler.py
+++ b/src/scheduling/scheduler.py
@@ -28,7 +28,7 @@ from datetime                   import datetime, timedelta
 
 from src.base.config            import AuraConfig
 from src.base.utils             import SimpleUtil as SU
-from src.scheduling.models      import AuraDatabaseModel, Timeslot, Playlist
+from src.scheduling.models      import AuraDatabaseModel, Playlist
 from src.base.exceptions        import NoActiveTimeslotException, LoadSourceException
 from src.core.control           import EngineExecutor
 from src.core.engine            import Engine
@@ -46,14 +46,17 @@ class TimeslotCommand(EngineExecutor):
     Command for triggering start and end of timeslot events.
     """
     engine = None
+    config = None
 
     def __init__(self, engine, timeslot):
         """
         Constructor
 
         Args:
+            engine (Engine):        The engine
             timeslot (Timeslot):    The timeslot which is starting at this time
-        """        
+        """      
+        self.config = AuraConfig()  
         self.engine = engine
 
         def do_start_timeslot(timeslot):
@@ -64,8 +67,73 @@ class TimeslotCommand(EngineExecutor):
             self.logger.info(SU.cyan(f"=== on_timeslot_end('{timeslot}') ==="))
             self.engine.event_dispatcher.on_timeslot_end(timeslot)
 
-        child_timer = EngineExecutor("TIMESLOT", None, timeslot.end_unix, do_end_timeslot, timeslot)                            
-        super().__init__("TIMESLOT", child_timer, timeslot.start_unix, do_start_timeslot, timeslot)
+            recent_entry = timeslot.get_recent_entry()
+            if recent_entry:
+                self.engine.player.stop(recent_entry, TransitionType.FADE)
+            else:
+                self.logger.warning(SU.red(f"Interestingly timeslot {timeslot} has no entry to be faded out?"))
+
+        fade_out_time = float(self.config.get("fade_out_time"))
+        start_fade_out = timeslot.end_unix - fade_out_time
+        self.logger.info(f"Fading out timeslot in {start_fade_out} seconds at {timeslot.timeslot_end} | Timeslot: {timeslot}")                            
+        super().__init__("TIMESLOT", None, timeslot.start_unix, do_start_timeslot, timeslot)
+        EngineExecutor("TIMESLOT", self, start_fade_out, do_end_timeslot, timeslot)
+
+
+
+class PlayCommand(EngineExecutor):
+    """
+    Command for triggering start and end of timeslot events.
+    """
+    engine = None
+    config = None
+
+    def __init__(self, engine, entries):
+        """
+        Constructor
+
+        Args:
+            engine (Engine):            The engine
+            entries (PlaylistEntry):    One or more playlist entries to be started
+        """
+        self.config = AuraConfig()
+        self.engine = engine
+
+
+        def do_preload(entries):
+            try:
+                if entries[0].get_content_type() in ResourceClass.FILE.types:
+                    self.logger.info(SU.cyan("=== preload_group('%s') ===" % ResourceUtil.get_entries_string(entries)))
+                    self.engine.player.preload_group(entries, ChannelType.QUEUE)
+                else:
+                    self.logger.info(SU.cyan("=== preload('%s') ===" % ResourceUtil.get_entries_string(entries)))
+                    self.engine.player.preload(entries[0])
+            except LoadSourceException as e:
+                self.logger.critical(SU.red("Could not preload entries %s" % ResourceUtil.get_entries_string(entries)), e)
+
+            if entries[-1].status != EntryPlayState.READY:
+                self.logger.critical(SU.red("Entries didn't reach 'ready' state during preloading (Entries: %s)" % ResourceUtil.get_entries_string(entries)))
+
+
+        def do_play(entries):
+            self.logger.info(SU.cyan("=== play('%s') ===" % ResourceUtil.get_entries_string(entries)))
+            if entries[-1].status != EntryPlayState.READY:
+                # Let 'em play anyway ...                
+                self.logger.critical(SU.red("PLAY: The entry/entries are not yet ready to be played (Entries: %s)" % ResourceUtil.get_entries_string(entries)))
+                while (entries[-1].status != EntryPlayState.READY):                    
+                    self.logger.info("PLAY: Wait a little until preloading is done ...")
+                    time.sleep(2)
+
+            self.engine.player.play(entries[0], TransitionType.FADE)            
+            self.logger.info(engine.scheduler.timeslot_renderer.get_ascii_timeslots())
+
+
+        start_preload = entries[0].start_unix - self.config.get("preload_offset")
+        start_play = entries[0].start_unix
+        super().__init__("PRELOAD", None, start_preload, do_preload, entries)
+        EngineExecutor("PLAY", self, start_play, do_play, entries)
+
+
 
 
 
@@ -158,10 +226,6 @@ class AuraScheduler(threading.Thread):
                 self.logger.critical(SU.red(f"Unhandled error while fetching & scheduling new programme! ({str(e)})"), e)
                 # Keep on working anyway
 
-            self.clean_timer_queue()
-            self.print_timer_queue()
-                        
-
             EngineExecutor.log_commands()            
             self.exit_event.wait(seconds_to_wait)
 
@@ -177,14 +241,6 @@ class AuraScheduler(threading.Thread):
         Called when the engine has finished booting and is ready to play.
         """
         self.is_initialized = True
-        self.on_scheduler_ready()
-
-
-
-    def on_scheduler_ready(self):
-        """
-        Called when the scheduler is ready.
-        """
         self.logger.info(self.timeslot_renderer.get_ascii_timeslots())
 
         try:
@@ -192,8 +248,24 @@ class AuraScheduler(threading.Thread):
             self.queue_startup_entries()
         except NoActiveTimeslotException:
             # That's not good, but keep on working...
-            pass
+            pass        
+
+
+    def on_play(self, entry):
+        """
+        Event Handler which is called by the engine when some entry is actually playing. 
+        Ignores entries which are part of a scheduled fallback, because they handle their
+        stuff by themselves.
 
+        Args:
+            source (String):    The `PlaylistEntry` object
+        """
+        if entry.channel in ChannelType.FALLBACK_QUEUE.channels:
+            return
+
+        if entry.playlist and entry.playlist.timeslot:
+            timeslot = entry.playlist.timeslot
+            timeslot.set_active_entry(entry)
 
 
     #
@@ -206,7 +278,7 @@ class AuraScheduler(threading.Thread):
         Returns the current programme.
         """
         return self.programme
-        
+
 
     def play_active_entry(self):
         """
@@ -223,11 +295,7 @@ class AuraScheduler(threading.Thread):
         if active_timeslot:
             # Create command timer to indicate the start of the timeslot
             TimeslotCommand(self.engine, active_timeslot)
-
             self.fallback.queue_fallback_playlist(active_timeslot)
-            # Queue the fade-out of the timeslot
-            if not active_timeslot.fadeouttimer:
-                self.queue_end_of_timeslot(active_timeslot, True)
 
         active_entry = self.programme.get_current_entry()
         if not active_entry:
@@ -246,8 +314,7 @@ class AuraScheduler(threading.Thread):
                 self.logger.info("The FFWD [>>] range exceeds the length of the entry. Drink some tea and wait for the sound of the next entry.")
             else:                
                 # Preload and play active entry
-                self.engine.player.preload(active_entry)
-                self.engine.player.play(active_entry, TransitionType.FADE)
+                PlayCommand(self.engine, [active_entry])
 
                 # Fast-forward to the scheduled position
                 if seconds_to_seek > 0:
@@ -266,41 +333,13 @@ class AuraScheduler(threading.Thread):
             or active_entry.get_content_type() in ResourceClass.LIVE.types:
 
             # Preload and play active entry
-            self.engine.player.preload(active_entry)
-            self.engine.player.play(active_entry, TransitionType.FADE)
+            PlayCommand(self.engine, [active_entry])
 
         else:
             self.logger.critical("Unknown Entry Type: %s" % active_entry)
         
 
 
-    def print_timer_queue(self):
-        """
-        Prints the current timer queue i.e. playlists in the queue to be played.
-        """
-        message_queue = ""
-        messages = sorted(self.message_timer, key=attrgetter('diff'))
-        if not messages:
-            self.logger.warning(SU.red("There's nothing in the Timer Queue!"))
-        else:
-            for msg in messages:
-                message_queue += str(msg)+"\n"
-
-            self.logger.info("Timer queue: \n" + message_queue) 
-
-
-
-    def clean_timer_queue(self):
-        """
-        Removes inactive timers from the queue.
-        """
-        len_before = len(self.message_timer)
-        self.message_timer[:] = [m for m in self.message_timer if m.is_alive()]
-        len_after = len(self.message_timer)
-        self.logger.debug("Removed %s finished timer objects from queue" % (len_before - len_after))
-
-
-
     def get_active_playlist(self):
         """
         Retrieves the currently playing playlist.
@@ -329,17 +368,12 @@ class AuraScheduler(threading.Thread):
             for next_timeslot in timeslots:
                 # Create command timer to indicate the start of the timeslot
                 TimeslotCommand(self.engine, next_timeslot)
-
                 # Schedule any available fallback playlist
                 self.fallback.queue_fallback_playlist(next_timeslot)
 
                 if next_timeslot.playlist:
                     self.queue_playlist_entries(next_timeslot, next_timeslot.playlist.entries, False, True)
                                 
-                # Queue the fade-out of the timeslot
-                if not next_timeslot.fadeouttimer:
-                    self.queue_end_of_timeslot(next_timeslot, True)
-
         self.logger.info(SU.green("Finished queuing programme."))
 
 
@@ -357,24 +391,11 @@ class AuraScheduler(threading.Thread):
 
             if current_playlist:
                 active_entry = self.programme.get_current_entry()
-
-                # Finished entries
-                for entry in current_playlist.entries:
-                    if active_entry == entry:
-                        break
-
-                # Entry currently being played
                 if active_entry:
-
                     # Queue open entries for current playlist
                     rest_of_playlist = active_entry.get_next_entries(True)
                     self.queue_playlist_entries(current_timeslot, rest_of_playlist, False, True)
 
-                    # Store them for later reference
-                    current_timeslot.queued_entries = [active_entry]
-                    if rest_of_playlist:
-                        current_timeslot.queued_entries.append(rest_of_playlist)
-
 
 
     def queue_playlist_entries(self, timeslot, entries, fade_in, fade_out):
@@ -430,225 +451,18 @@ class AuraScheduler(threading.Thread):
         self.logger.info("Built %s entry group(s)" % len(entry_groups))
          
         # Timeslot function calls
-        do_queue_timeslot_end = False
         if len(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))
 
-                # Create timers for each entry group
-                self.set_entries_timer(entries, fade_in, fade_out)
-
-                # Store them for later reference
-                timeslot.queued_entries = entries
-
+                # Create command timers for each entry group
+                PlayCommand(self.engine, entries)
         else:
             self.logger.warn(SU.red("Nothing to schedule ..."))
 
 
 
-    def set_entries_timer(self, entries, fade_in, fade_out):
-        """
-        Creates timer for loading and playing one or multiple entries. Existing timers are 
-        updated.
-
-        Args:
-            entries ([]): List of multiple filesystem entries, or a single entry of other types
-        """
-        play_timer = self.is_something_planned_at_time(entries[0].start_unix)
-        now_unix = Engine.engine_time()
-        diff = entries[0].start_unix - now_unix
-
-        # Play function to be called by timer
-        def do_play(entries):
-            self.logger.info(SU.cyan("=== play('%s') ===" % ResourceUtil.get_entries_string(entries)))
-            transition_type = TransitionType.INSTANT
-            if fade_in:
-                transition_type = TransitionType.FADE
-
-            if entries[-1].status != EntryPlayState.READY:
-                self.logger.critical(SU.red("PLAY: For some reason the entry/entries are not yet ready to be played (Entries: %s)" % ResourceUtil.get_entries_string(entries)))
-                # Let 'em play anyway ...
-
-            self.engine.player.play(entries[0], transition_type)
-            self.logger.info(self.timeslot_renderer.get_ascii_timeslots())
-
-
-        if play_timer:
-            # Check if the Playlist IDs are different
-            if self.have_entries_changed(play_timer, entries):
-                # If not, stop and remove the old timer, create a new one
-                self.stop_timer(play_timer)                
-            else:
-                # If the playlist entries do not differ => reuse the old timer and do nothing
-                self.logger.debug("Playlist Entry %s is already scheduled - no new timer created." % ResourceUtil.get_entries_string(entries))
-                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)
-
-
-
-    def have_entries_changed(self, timer, new_entries):
-        """
-        Checks if the new entries and playlists are matching the existing queued ones,
-        or if they should be updated.
-        
-        Args:
-            timer (CallFunctionTimer):      The timer holding queued entries
-            new_entries ([PlaylistEntry]):  The possibly updated entries
-
-        Returns:
-            (Boolean):  `True` if it has changed
-        """
-        old_entries = timer.entries
-
-        if old_entries[0].playlist and new_entries[0].playlist:
-            if old_entries[0].playlist.playlist_id != new_entries[0].playlist.playlist_id:
-                return True
-        if len(old_entries) != len(new_entries):
-            return True
-
-        for old_entry, new_entry in zip(old_entries, new_entries):
-            if old_entry.source != new_entry.source:
-                return True
-        
-        return False
-
-
-
-    def queue_end_of_timeslot(self, timeslot, fade_out):
-        """
-        Queues a engine action to stop/fade-out the given timeslot.
-
-        Args:
-            timeslot (PlaylistEntry):  The timeslot
-            fade_out (Boolean):        If the timeslot should be faded-out
-        """
-        timeslot_end = timeslot.timeslot_end
-        timeslot_end_unix = timeslot.end_unix
-        now_unix = Engine.engine_time()
-        fade_out_time = 0
-
-        # Stop function to be called when timeslot ends
-        def do_stop(timeslot):
-            if timeslot.has_queued_entries():
-                last_entry = timeslot.queued_entries[-1]
-                self.logger.info(SU.cyan("=== stop('%s') ===" % str(last_entry.playlist.timeslot)))
-                transition_type = TransitionType.INSTANT
-                if fade_out:
-                    transition_type = TransitionType.FADE
-                self.engine.player.stop(last_entry, transition_type)
-
-        if fade_out == True:
-            fade_out_time = int(round(float(self.config.get("fade_out_time")))) #TODO Use float
-        
-        # Stop any existing fade-out timer
-        if timeslot.fadeouttimer:
-            timeslot.fadeouttimer.cancel()
-            self.message_timer.remove(timeslot.fadeouttimer)
-
-        # Create timer to fade-out
-        start_fade_out = timeslot_end_unix - now_unix - fade_out_time
-        # last_entry = timeslot.queued_entries[-1]
-        timeslot.fadeouttimer = self.create_timer(start_fade_out, do_stop, timeslot, fadeout=True)
-
-        self.logger.info("Fading out timeslot in %s seconds at %s | Timeslot: %s" % (str(start_fade_out), str(timeslot_end), timeslot))
-
-
-
-    def is_something_planned_at_time(self, given_time):
-        """
-        Checks for existing timers at the given time.
-        """
-        for t in self.message_timer:
-            if t.fadein or t.switcher:
-                if t.entries[0].start_unix == given_time:
-                    return t
-        return False
-
-
-
-    def create_timer(self, diff, func, param, 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
-            param ([]):         A timeslot or list of entries
-
-        Returns:
-            (CallFunctionTimer, CallFunctionTimer):     In case of a "switch" command, the switch and preload timer is returned
-            (CallFunctionTimer):                        In all other cases only the timer for the command is returned
-        """
-        if not fadein and not fadeout and not switcher or fadein and fadeout or fadein and switcher or fadeout and switcher:
-            raise ValueError("You have to call me with either fadein=true, fadeout=true or switcher=True")
-        if not isinstance(param, list) and not isinstance(param, Timeslot):
-            raise ValueError("No list of entries nor timeslot passed!")
-
-        t = CallFunctionTimer(diff=diff, func=func, param=param, fadein=fadein, fadeout=fadeout, switcher=switcher)
-        self.message_timer.append(t)
-        t.start()
-
-        if switcher:
-            # Preload function to be called by timer
-            def do_preload(entries):
-                try:
-                    if entries[0].get_content_type() in ResourceClass.FILE.types:
-                        self.logger.info(SU.cyan("=== preload_group('%s') ===" % ResourceUtil.get_entries_string(entries)))
-                        self.engine.player.preload_group(entries, ChannelType.QUEUE)
-                    else:
-                        self.logger.info(SU.cyan("=== preload('%s') ===" % ResourceUtil.get_entries_string(entries)))
-                        self.engine.player.preload(entries[0])
-                except LoadSourceException as e:
-                    self.logger.critical(SU.red("Could not preload entries %s" % ResourceUtil.get_entries_string(entries)), e)
-
-                if entries[-1].status != EntryPlayState.READY:
-                    self.logger.critical(SU.red("Entries didn't reach 'ready' state during preloading (Entries: %s)" % ResourceUtil.get_entries_string(entries)))
-
-            loader_diff = diff - self.config.get("preload_offset")
-            loader = CallFunctionTimer(diff=loader_diff, func=do_preload, param=param, fadein=fadein, fadeout=fadeout, switcher=False, loader=True)
-            self.message_timer.append(loader)
-            loader.start()
-            return (t, loader)
-        else:
-            return t
-
-
-
-    def stop_timer(self, timer):
-        """
-        Stops the given timer.
-
-        Args:
-            timer (Timer):  The timer to stop.
-        """
-        timer.cancel()
-        count = 1
-
-        for entry in timer.entries:
-            if entry.loadtimer is not None:
-                entry.loadtimer.cancel()
-                self.message_timer.remove(entry.loadtimer)
-                count += 1
-
-            # if timer.entries[0].fadeintimer is not None:
-            #     timer.entries[0].fadeintimer.cancel()
-            #     self.message_timer.remove(timer.entries[0].fadeintimer)
-            #     count += 1
-
-            # if entry.fadeouttimer is not None:
-            #     entry.fadeouttimer.cancel()
-            #     self.message_timer.remove(entry.fadeouttimer)
-            #     count += 1
-
-        # Remove it from message queue
-        self.message_timer.remove(timer)
-        self.logger.info("Stopped %s timers for: %s" % (str(count), ResourceUtil.get_entries_string(timer.entries)))
-
-
-
     # FIXME Move to adequate module
     @staticmethod
     def init_database():
@@ -683,59 +497,3 @@ class AuraScheduler(threading.Thread):
         self.logger.info("Shutting down scheduler ...")
 
 
-
-
-# ------------------------------------------------------------------------------------------ #
-class CallFunctionTimer(threading.Timer):
-    logger = None
-    param = None
-    entries = None
-    diff = None
-    dt = None
-    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 engine command '%s' in %s seconds..." % (str(func.__name__), str(diff)))
-        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")
-
-        self.diff = diff
-        self.dt = datetime.now() + timedelta(seconds=diff)
-
-        self.func = func
-        self.param = param
-        self.entries = param # TODO Refactor since param can hold [entries] or a timeslot, depending on the timer type
-        self.fadein = fadein
-        self.fadeout = fadeout
-        self.switcher = switcher
-        self.loader = loader
-
-
-    def __str__(self):
-        """
-        String represenation of the timer.
-        """
-        status = "Timer (Alive: %s)" % self.is_alive()
-        status += " starting at " + str(self.dt)
-
-        if self.fadein:
-            return status + " fading in entries '" + ResourceUtil.get_entries_string(self.entries)
-        elif self.fadeout:
-            return status + " fading out timeslot '" + str(self.param)
-        elif self.switcher:
-            return status + " switching to entries '" + ResourceUtil.get_entries_string(self.entries)
-        elif self.loader:
-            return status + " preloading entries '" + ResourceUtil.get_entries_string(self.entries)
-        else:
-            return "CORRUPTED CallFunctionTimer around! How can that be?"
-- 
GitLab