From 79e22a14a014f56642eadf2b8afeb02bbeae1225 Mon Sep 17 00:00:00 2001
From: David Trattnig <david.trattnig@o94.at>
Date: Fri, 13 Nov 2020 15:21:02 +0100
Subject: [PATCH] Programme as entity. #41

---
 src/plugins/trackservice.py |   2 +-
 src/scheduling/programme.py | 233 ++++++++++++++++++++++++++++++++
 src/scheduling/scheduler.py | 260 +++++-------------------------------
 src/scheduling/utils.py     |   9 +-
 4 files changed, 274 insertions(+), 230 deletions(-)
 create mode 100644 src/scheduling/programme.py

diff --git a/src/plugins/trackservice.py b/src/plugins/trackservice.py
index f62a67cb..80d2a0a7 100644
--- a/src/plugins/trackservice.py
+++ b/src/plugins/trackservice.py
@@ -296,7 +296,7 @@ class Playlog:
                 return # Avoid overwrite by multiple calls in a row
 
         data = {}
-        next_timeslot = self.engine.scheduler.get_next_timeslots(1)
+        next_timeslot = self.engine.scheduler.get_programme().get_next_timeslots(1)
         if next_timeslot: 
             next_timeslot = next_timeslot[0]
         else:
diff --git a/src/scheduling/programme.py b/src/scheduling/programme.py
new file mode 100644
index 00000000..c8620342
--- /dev/null
+++ b/src/scheduling/programme.py
@@ -0,0 +1,233 @@
+
+
+#
+# Aura Engine (https://gitlab.servus.at/aura/engine)
+#
+# Copyright (C) 2017-2020 - The Aura Engine Team.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+
+import logging
+
+from datetime import datetime
+
+from src.base.config import AuraConfig
+from src.base.utils import SimpleUtil as SU
+from src.core.engine import Engine
+
+from src.scheduling.calendar import AuraCalendarService
+from src.scheduling.models import Timeslot
+
+
+
+
+class Programme():
+    """
+    The current programme of the calendar. The programme is consisting of a set of timeslots.
+    """
+    config = None
+    logger = None
+    programme = None
+    last_successful_fetch = None
+
+
+    def __init__(self):
+        """
+        Constructor
+        """
+        self.config = AuraConfig.config()
+        self.logger = logging.getLogger("AuraEngine")
+
+
+
+    def refresh(self):
+        """
+        Fetch the latest programme from `AuraCalendarService` which stores it to the database.
+        After that, the programme is in turn loaded from the database and stored in `self.programme`.
+        """
+
+        # Fetch programme from API endpoints
+        self.logger.debug("Trying to fetch new programe from API endpoints...")
+        acs = AuraCalendarService(self.config)
+        queue = acs.get_queue()
+        acs.start() # start fetching thread
+        response = queue.get() # wait for the end
+        self.logger.debug("... Programme fetch via API done!")
+
+        # Reset last successful fetch state
+        lsf = self.last_successful_fetch
+        self.last_successful_fetch = None
+
+        if response is None:
+            msg = SU.red("Trying to load programme from Engine Database, because AuraCalendarService returned an empty response.")
+            self.logger.warning(msg)
+        elif type(response) is list:
+            self.programme = response
+            if self.programme is not None and len(self.programme) > 0:
+                self.last_successful_fetch = datetime.now()
+                self.logger.info(SU.green("Finished fetching current programme from API"))
+            if len(self.programme) == 0:
+                self.logger.critical("Programme fetched from Steering/Tank has no entries!")
+        elif response.startswith("fetching_aborted"):
+            msg = SU.red("Trying to load programme from database only, because fetching was being aborted from AuraCalendarService! Reason: ")
+            self.logger.warning(msg + response[16:])
+        else:
+            msg = SU.red("Trying to load programme from database only, because of an unknown response from AuraCalendarService: " + response)
+            self.logger.warning(msg)
+
+        # Always load latest programme from the database
+        self.last_successful_fetch = lsf
+        self.load_programme_from_db()
+        self.logger.info(SU.green("Finished loading current programme from database (%s timeslots)" % str(len(self.programme))))
+        for timeslot in self.programme:
+            self.logger.debug("\tTimeslot %s with Playlist %s" % (str(timeslot), str(timeslot.playlist)))
+
+
+
+
+    def load_programme_from_db(self):
+        """
+        Loads the programme from Engine's database and enables
+        them via `self.enable_entries(..)`. After that, the
+        current message queue is printed to the console.
+        """
+        self.programme = Timeslot.select_programme()
+
+        if not self.programme:
+            self.logger.critical(SU.red("Could not load programme from database. We are in big trouble my friend!"))
+            return
+
+
+
+    def get_current_entry(self):
+        """
+        Retrieves the current `PlaylistEntry` which should be played as per programme. 
+
+        Returns:
+            (PlaylistEntry): The track which is (or should) currently being played
+        """
+        now_unix = Engine.engine_time()
+
+        # Load programme if necessary
+        if not self.programme:
+            self.load_programme_from_db()
+
+        # Check for current timeslot
+        current_timeslot = self.get_current_timeslot()
+        if not current_timeslot:
+            self.logger.warning(SU.red("There's no active timeslot"))
+            return None
+
+        # Check for scheduled playlist
+        current_playlist = current_timeslot.playlist
+        if not current_playlist:
+            msg = "There's no playlist assigned to the current timeslot. Most likely a fallback will make things okay again."
+            self.logger.warning(SU.red(msg))
+            return None
+
+        # Iterate over playlist entries and store the current one
+        current_entry = None
+        for entry in current_playlist.entries:
+            if entry.start_unix <= now_unix and now_unix <= entry.end_unix:
+                current_entry = entry
+                break
+      
+        if not current_entry:
+            # Nothing playing ... fallback will kick-in
+            msg = "There's no entry scheduled for playlist '%s' at %s" % (str(current_playlist), SU.fmt_time(now_unix))
+            self.logger.warning(SU.red(msg))
+            return None
+
+        return current_entry
+ 
+
+
+    def get_current_timeslot(self):
+        """
+        Retrieves the timeslot currently to be played. 
+        
+        Returns:
+            (Timeslot): The current timeslot
+        """
+        current_timeslot = None
+        now_unix = Engine.engine_time()
+
+        # Iterate over all timeslots and find the one to be played right now
+        if self.programme:
+            for timeslot in self.programme:
+                if timeslot.start_unix <= now_unix and now_unix < timeslot.end_unix:
+                    current_timeslot = timeslot
+                    break
+        
+        return current_timeslot
+
+
+
+    def get_next_timeslots(self, max_count=0):
+        """
+        Retrieves the timeslots to be played after the current one.
+
+        Args:
+            max_count (Integer): Maximum of timeslots to return, if `0` all exitsing ones are returned
+
+        Returns:
+            ([Timeslot]): The next timeslots
+        """
+        now_unix = Engine.engine_time()
+        next_timeslots = []
+
+        for timeslot in self.programme:
+            if timeslot.start_unix > now_unix:
+                if (len(next_timeslots) < max_count) or max_count == 0:
+                    next_timeslots.append(timeslot)
+                else:
+                    break
+                
+        return self.filter_scheduling_window(next_timeslots)
+
+
+
+    def filter_scheduling_window(self, timeslots):
+        """
+        Ignore timeslots which are beyond the scheduling window. The end of the scheduling window
+        is defined by the config option `scheduling_window_end`. This value defines the seconds
+        minus the actual start time of the timeslot.
+        """
+        now_unix = Engine.engine_time()
+        len_before = len(timeslots)
+        window_start = self.config.get("scheduling_window_start")
+        window_end = self.config.get("scheduling_window_end")
+        timeslots = list(filter(lambda s: (s.start_unix - window_end) > now_unix and (s.start_unix - window_start) < now_unix, timeslots))
+        len_after = len(timeslots)
+        self.logger.info("For now, skipped %s future timeslot(s) which are out of the scheduling window (-%ss <-> -%ss)" % ((len_before - len_after), window_start, window_end))
+
+        return timeslots
+
+
+
+    def is_timeslot_in_window(self, timeslot):
+        """
+        Checks if the timeslot is within the scheduling window.
+        """
+        now_unix = Engine.engine_time()
+        window_start = self.config.get("scheduling_window_start")
+        window_end = self.config.get("scheduling_window_end")
+
+        if timeslot.start_unix - window_start < now_unix and \
+            timeslot.start_unix - window_end > now_unix:
+
+            return True
+        return False
\ No newline at end of file
diff --git a/src/scheduling/scheduler.py b/src/scheduling/scheduler.py
index c969d2ec..762324f4 100644
--- a/src/scheduling/scheduler.py
+++ b/src/scheduling/scheduler.py
@@ -28,17 +28,15 @@ 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, Timeslot, Playlist
 from src.base.exceptions        import NoActiveTimeslotException, LoadSourceException
 from src.core.control           import EngineExecutor
 from src.core.engine            import Engine
 from src.core.channels          import ChannelType, TransitionType, EntryPlayState
 from src.core.resources         import ResourceClass, ResourceUtil
-from src.scheduling.calendar    import AuraCalendarService
-from src.scheduling.utils       import TimeslotRenderer
-
-
 
+from src.scheduling.utils       import TimeslotRenderer
+from src.scheduling.programme   import Programme
 
 
 
@@ -71,6 +69,7 @@ class TimeslotCommand(EngineExecutor):
 
 
 
+
 class AuraScheduler(threading.Thread):
     """
     Aura Scheduler Class
@@ -93,15 +92,12 @@ class AuraScheduler(threading.Thread):
     logger = None
     engine = None
     exit_event = None    
-    is_initialized = None
-    is_initialized = None
-
-    last_successful_fetch = None
     timeslot_renderer = None
     programme = None
     message_timer = []
     fallback = None
-
+    is_initialized = None
+    is_initialized = None
 
 
 
@@ -116,6 +112,7 @@ class AuraScheduler(threading.Thread):
         """
         self.config = AuraConfig.config()
         self.logger = logging.getLogger("AuraEngine")
+        self.programme = Programme()
         self.timeslot_renderer = TimeslotRenderer(self)
         AuraScheduler.init_database()
         self.fallback = fallback_manager
@@ -151,7 +148,7 @@ class AuraScheduler(threading.Thread):
                 self.logger.info(SU.cyan(f"== start fetching new timeslots (every {seconds_to_wait} seconds) =="))
                 
                 # Load some stuff from the API in any case
-                self.fetch_new_programme()
+                self.programme.refresh()
                 
                 # Queue only when the engine is ready to play
                 if self.is_initialized == True:                
@@ -204,6 +201,13 @@ class AuraScheduler(threading.Thread):
     #
 
 
+    def get_programme(self):
+        """
+        Returns the current programme.
+        """
+        return self.programme
+        
+
     def play_active_entry(self):
         """
         Plays the entry scheduled for the very current moment and forwards to the scheduled position in time. 
@@ -213,7 +217,7 @@ class AuraScheduler(threading.Thread):
             (NoActiveTimeslotException):    If there's no timeslot in the programme, within the scheduling window
         """
         sleep_offset = 10
-        active_timeslot = self.get_active_timeslot()
+        active_timeslot = self.programme.get_current_timeslot()
 
         # Schedule any available fallback playlist
         if active_timeslot:
@@ -225,7 +229,7 @@ class AuraScheduler(threading.Thread):
             if not active_timeslot.fadeouttimer:
                 self.queue_end_of_timeslot(active_timeslot, True)
 
-        active_entry = self.get_active_entry()
+        active_entry = self.programme.get_current_entry()
         if not active_entry:
             raise NoActiveTimeslotException
 
@@ -241,7 +245,7 @@ class AuraScheduler(threading.Thread):
             if (seconds_to_seek + sleep_offset) > active_entry.duration:
                 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:                
-                # Pre-roll and play active entry
+                # Preload and play active entry
                 self.engine.player.preload(active_entry)
                 self.engine.player.play(active_entry, TransitionType.FADE)
 
@@ -261,7 +265,7 @@ class AuraScheduler(threading.Thread):
         elif active_entry.get_content_type() in ResourceClass.STREAM.types \
             or active_entry.get_content_type() in ResourceClass.LIVE.types:
 
-            # Pre-roll and play active entry
+            # Preload and play active entry
             self.engine.player.preload(active_entry)
             self.engine.player.play(active_entry, TransitionType.FADE)
 
@@ -270,109 +274,6 @@ class AuraScheduler(threading.Thread):
         
 
 
-
-    def get_active_entry(self):
-        """
-        Retrieves the current `PlaylistEntry` which should be played as per programme. 
-
-        Returns:
-            (PlaylistEntry): The track which is (or should) currently being played
-        """
-        now_unix = Engine.engine_time()
-
-        # Load programme if necessary
-        if not self.programme:
-            self.load_programme_from_db()
-
-        # Check for current timeslot
-        current_timeslot = self.get_active_timeslot()
-        if not current_timeslot:
-            self.logger.warning(SU.red("There's no active timeslot"))
-            return None
-
-        # Check for scheduled playlist
-        current_playlist = current_timeslot.playlist
-        if not current_playlist:
-            msg = "There's no playlist assigned to the current timeslot. Most likely a fallback will make things okay again."
-            self.logger.warning(SU.red(msg))
-            return None
-
-        # Iterate over playlist entries and store the current one
-        current_entry = None
-        for entry in current_playlist.entries:
-            if entry.start_unix <= now_unix and now_unix <= entry.end_unix:
-                current_entry = entry
-                break       
-      
-        if not current_entry:
-            # Nothing playing ... fallback will kick-in
-            msg = "There's no entry scheduled for playlist '%s' at %s" % (str(current_playlist), SU.fmt_time(now_unix))
-            self.logger.warning(SU.red(msg))
-            return None
-
-        return current_entry
-
-
-
-    def get_active_timeslot(self):
-        """
-        Retrieves the timeslot currently to be played. 
-        
-        Returns:
-            (Timeslot): The current timeslot
-        """
-        current_timeslot = None
-        now_unix = Engine.engine_time()
-
-        # Iterate over all timeslots and find the one to be played right now
-        if self.programme:
-            for timeslot in self.programme:
-                if timeslot.start_unix <= now_unix and now_unix < timeslot.end_unix:
-                    current_timeslot = timeslot
-                    break
-        
-        return current_timeslot
-
-
-
-    def get_next_timeslots(self, max_count=0):
-        """
-        Retrieves the timeslots to be played after the current one.
-
-        Args:
-            max_count (Integer): Maximum of timeslots to return, if `0` all exitsing ones are returned
-
-        Returns:
-            ([Timeslot]): The next timeslots
-        """
-        now_unix = Engine.engine_time()
-        next_timeslots = []
-
-        for timeslot in self.programme:
-            if timeslot.start_unix > now_unix:
-                if (len(next_timeslots) < max_count) or max_count == 0:
-                    next_timeslots.append(timeslot)
-                else:
-                    break
-                
-        return next_timeslots
-
-
-    def get_active_playlist(self):
-        """
-        Retrieves the currently playing playlist.
-
-        Returns:
-            (FallbackType, Playlist): The resolved playlist
-        """
-        timeslot = self.get_active_timeslot()
-        if timeslot:
-            # return timeslot.playlist
-            return self.fallback.resolve_playlist(timeslot)
-        return (None, None)
-
-
-
     def print_timer_queue(self):
         """
         Prints the current timer queue i.e. playlists in the queue to be played.
@@ -400,45 +301,17 @@ class AuraScheduler(threading.Thread):
 
 
 
-
-
-#
-#   PRIVATE METHODS
-#
-
-
-
-    def filter_scheduling_window(self, timeslots):
-        """
-        Ignore timeslots which are beyond the scheduling window. The end of the scheduling window
-        is defined by the config option `scheduling_window_end`. This value defines the seconds
-        minus the actual start time of the timeslot.
+    def get_active_playlist(self):
         """
-        now_unix = Engine.engine_time()
-        len_before = len(timeslots)
-        window_start = self.config.get("scheduling_window_start")
-        window_end = self.config.get("scheduling_window_end")
-        timeslots = list(filter(lambda s: (s.start_unix - window_end) > now_unix and (s.start_unix - window_start) < now_unix, timeslots))
-        len_after = len(timeslots)
-        self.logger.info("For now, skipped %s future timeslot(s) which are out of the scheduling window (-%ss <-> -%ss)" % ((len_before - len_after), window_start, window_end))
-
-        return timeslots
-
-
+        Retrieves the currently playing playlist.
 
-    def is_timeslot_in_window(self, timeslot):
-        """
-        Checks if the timeslot is within the scheduling window.
+        Returns:
+            (FallbackType, Playlist): The resolved playlist
         """
-        now_unix = Engine.engine_time()
-        window_start = self.config.get("scheduling_window_start")
-        window_end = self.config.get("scheduling_window_end")
-
-        if timeslot.start_unix - window_start < now_unix and \
-            timeslot.start_unix - window_end > now_unix:
-
-            return True
-        return False
+        timeslot = self.programme.get_current_timeslot()
+        if timeslot:
+            return self.fallback.resolve_playlist(timeslot)
+        return (None, None)
 
 
 
@@ -449,8 +322,7 @@ class AuraScheduler(threading.Thread):
         """
 
         # Get a clean set of the timeslots within the scheduling window
-        timeslots = self.get_next_timeslots()
-        timeslots = self.filter_scheduling_window(timeslots)
+        timeslots = self.programme.get_next_timeslots()
 
         # Queue the timeslots, their playlists and entries
         if timeslots:
@@ -477,14 +349,14 @@ class AuraScheduler(threading.Thread):
         Queues all entries after the one currently playing upon startup. Don't use
         this method in any other scenario, as it doesn't respect the scheduling window.
         """
-        current_timeslot = self.get_active_timeslot()
+        current_timeslot = self.programme.get_current_timeslot()
         
         # Queue the (rest of the) currently playing timeslot upon startup
         if current_timeslot:
             current_playlist = current_timeslot.playlist
 
             if current_playlist:
-                active_entry = self.get_active_entry()
+                active_entry = self.programme.get_current_entry()
 
                 # Finished entries
                 for entry in current_playlist.entries:
@@ -645,9 +517,6 @@ class AuraScheduler(threading.Thread):
 
 
 
-
-
-
     def queue_end_of_timeslot(self, timeslot, fade_out):
         """
         Queues a engine action to stop/fade-out the given timeslot.
@@ -688,65 +557,6 @@ class AuraScheduler(threading.Thread):
 
 
 
-    def fetch_new_programme(self):
-        """
-        Fetch the latest programme from `AuraCalendarService` which stores it to the database.
-        After that, the programme is in turn loaded from the database and stored in `self.programme`.
-        """
-
-        # Fetch programme from API endpoints
-        self.logger.debug("Trying to fetch new programe from API endpoints...")
-        acs = AuraCalendarService(self.config)
-        queue = acs.get_queue()
-        acs.start() # start fetching thread
-        response = queue.get() # wait for the end
-        self.logger.debug("... Programme fetch via API done!")
-
-        # Reset last successful fetch state
-        lsf = self.last_successful_fetch
-        self.last_successful_fetch = None
-
-        if response is None:
-            msg = SU.red("Trying to load programme from Engine Database, because AuraCalendarService returned an empty response.")
-            self.logger.warning(msg)
-        elif type(response) is list:
-            self.programme = response
-            if self.programme is not None and len(self.programme) > 0:
-                self.last_successful_fetch = datetime.now()
-                self.logger.info(SU.green("Finished fetching current programme from API"))
-            if len(self.programme) == 0:
-                self.logger.critical("Programme fetched from Steering/Tank has no entries!")
-        elif response.startswith("fetching_aborted"):
-            msg = SU.red("Trying to load programme from database only, because fetching was being aborted from AuraCalendarService! Reason: ")
-            self.logger.warning(msg + response[16:])
-        else:
-            msg = SU.red("Trying to load programme from database only, because of an unknown response from AuraCalendarService: " + response)
-            self.logger.warning(msg)
-
-        # Always load latest programme from the database
-        self.last_successful_fetch = lsf
-        self.load_programme_from_db()
-        self.logger.info(SU.green("Finished loading current programme from database (%s timeslots)" % str(len(self.programme))))
-        for timeslot in self.programme:
-            self.logger.debug("\tTimeslot %s with Playlist %s" % (str(timeslot), str(timeslot.playlist)))
-
-
-
-
-    def load_programme_from_db(self):
-        """
-        Loads the programme from Engine's database and enables
-        them via `self.enable_entries(..)`. After that, the
-        current message queue is printed to the console.
-        """
-        self.programme = Timeslot.select_programme()
-
-        if not self.programme:
-            self.logger.critical(SU.red("Could not load programme from database. We are in big trouble my friend!"))
-            return
-
-
-
     def is_something_planned_at_time(self, given_time):
         """
         Checks for existing timers at the given time.
@@ -769,7 +579,7 @@ class AuraScheduler(threading.Thread):
             param ([]):         A timeslot or list of entries
 
         Returns:
-            (CallFunctionTimer, CallFunctionTimer):     In case of a "switch" command, the switch and pre-roll timer is returned
+            (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:
@@ -782,7 +592,7 @@ class AuraScheduler(threading.Thread):
         t.start()
 
         if switcher:
-            # Pre-roll function to be called by timer
+            # Preload function to be called by timer
             def do_preload(entries):
                 try:
                     if entries[0].get_content_type() in ResourceClass.FILE.types:
@@ -792,10 +602,10 @@ class AuraScheduler(threading.Thread):
                         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 pre-roll entries %s" % ResourceUtil.get_entries_string(entries)), 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 pre-rolling (Entries: %s)" % ResourceUtil.get_entries_string(entries)))
+                    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)
@@ -926,6 +736,6 @@ class CallFunctionTimer(threading.Timer):
         elif self.switcher:
             return status + " switching to entries '" + ResourceUtil.get_entries_string(self.entries)
         elif self.loader:
-            return status + " pre-rolling entries '" + ResourceUtil.get_entries_string(self.entries)
+            return status + " preloading entries '" + ResourceUtil.get_entries_string(self.entries)
         else:
             return "CORRUPTED CallFunctionTimer around! How can that be?"
diff --git a/src/scheduling/utils.py b/src/scheduling/utils.py
index ad88e62a..0817a311 100644
--- a/src/scheduling/utils.py
+++ b/src/scheduling/utils.py
@@ -43,6 +43,7 @@ class TimeslotRenderer:
     """
     logger = None
     scheduler = None
+    programme = None
 
 
     def __init__(self, scheduler):
@@ -51,7 +52,7 @@ class TimeslotRenderer:
         """
         self.logger = logging.getLogger("AuraEngine")
         self.scheduler = scheduler
-
+        self.programme = scheduler.get_programme()
 
 
     def get_ascii_timeslots(self):
@@ -61,7 +62,7 @@ class TimeslotRenderer:
         Returns:
             (String):   An ASCII representation of the current and next timeslots
         """
-        active_timeslot = self.scheduler.get_active_timeslot()
+        active_timeslot = self.programme.get_current_timeslot()
 
         s = "\n\n   SCHEDULED NOW:"
         s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
@@ -86,7 +87,7 @@ class TimeslotRenderer:
 
                 s += "\n│       └── Playlist %s         " % resolved_playlist
 
-                active_entry = self.scheduler.get_active_entry()
+                active_entry = self.programme.get_current_entry()
 
                 # Finished entries
                 for entry in resolved_playlist.entries:
@@ -114,7 +115,7 @@ class TimeslotRenderer:
         s += "\n   SCHEDULED NEXT:"
         s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
 
-        next_timeslots = self.scheduler.get_next_timeslots()
+        next_timeslots = self.programme.get_next_timeslots()
         if not next_timeslots:
             s += "\n│   Nothing.         "
         else:
-- 
GitLab