From ce5321186cca751a6c6e0eb4dfaf718dd8aa1839 Mon Sep 17 00:00:00 2001
From: David Trattnig <david.trattnig@o94.at>
Date: Fri, 13 Nov 2020 12:37:21 +0100
Subject: [PATCH] Extracted ASCII rendering. #41

---
 src/scheduling/scheduler.py | 170 ++----------------------------
 src/scheduling/utils.py     | 203 ++++++++++++++++++++++++++++++++++++
 2 files changed, 213 insertions(+), 160 deletions(-)
 create mode 100644 src/scheduling/utils.py

diff --git a/src/scheduling/scheduler.py b/src/scheduling/scheduler.py
index ebabe854..c969d2ec 100644
--- a/src/scheduling/scheduler.py
+++ b/src/scheduling/scheduler.py
@@ -23,7 +23,6 @@ import threading
 import time
 import sqlalchemy
 
-from enum                       import Enum
 from operator                   import attrgetter
 from datetime                   import datetime, timedelta
 
@@ -36,17 +35,11 @@ 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
 
 
 
 
-class EntryQueueState(Enum):
-    """
-    Types of playlist entry behaviours.
-    """
-    OKAY = "ok"
-    CUT = "cut"
-    OUT_OF_SCHEDULE = "oos"
 
 
 
@@ -104,6 +97,7 @@ class AuraScheduler(threading.Thread):
     is_initialized = None
 
     last_successful_fetch = None
+    timeslot_renderer = None
     programme = None
     message_timer = []
     fallback = None
@@ -122,7 +116,7 @@ class AuraScheduler(threading.Thread):
         """
         self.config = AuraConfig.config()
         self.logger = logging.getLogger("AuraEngine")
-
+        self.timeslot_renderer = TimeslotRenderer(self)
         AuraScheduler.init_database()
         self.fallback = fallback_manager
         self.engine = engine
@@ -194,7 +188,7 @@ class AuraScheduler(threading.Thread):
         """
         Called when the scheduler is ready.
         """
-        self.logger.info(self.get_ascii_programme())
+        self.logger.info(self.timeslot_renderer.get_ascii_timeslots())
 
         try:
             self.play_active_entry()
@@ -347,6 +341,7 @@ class AuraScheduler(threading.Thread):
 
         Args:
             max_count (Integer): Maximum of timeslots to return, if `0` all exitsing ones are returned
+
         Returns:
             ([Timeslot]): The next timeslots
         """
@@ -406,124 +401,6 @@ class AuraScheduler(threading.Thread):
 
 
 
-    def get_ascii_programme(self):
-        """
-        Creates a printable version of the current programme (playlists and entries as per timeslot) 
-
-        Returns:
-            (String):   An ASCII representation of the programme
-        """
-        active_timeslot = self.get_active_timeslot()
-
-        s = "\n\n   SCHEDULED NOW:"
-        s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
-        if active_timeslot:
-            planned_playlist = None
-            if active_timeslot.playlist:
-                planned_playlist = active_timeslot.playlist
-
-            (fallback_type, resolved_playlist) = self.fallback.resolve_playlist(active_timeslot)
-
-            s += "\n│   Playing timeslot %s         " % active_timeslot
-            if planned_playlist: 
-                if resolved_playlist and resolved_playlist.playlist_id != planned_playlist.playlist_id:
-                    s += "\n│       └── Playlist %s         " % planned_playlist
-                    s += "\n│       "
-                    s += SU.red("↑↑↑ That's the originally planned playlist.") + ("Instead playing the fallback playlist below ↓↓↓")
-
-            if resolved_playlist:                                  
-                if not planned_playlist:
-                    s += "\n│                         "
-                    s += SU.red("No playlist assigned to timeslot. Instead playing the `%s` playlist below ↓↓↓" % SU.cyan(str(fallback_type)))
-
-                s += "\n│       └── Playlist %s         " % resolved_playlist
-
-                active_entry = self.get_active_entry()
-
-                # Finished entries
-                for entry in resolved_playlist.entries:
-                    if active_entry == entry:
-                        break
-                    else:
-                        s += self.build_entry_string("\n│         └── ", entry, True)
-
-                # Entry currently being played
-                if active_entry:
-                    s += "\n│         └── Entry %s | %s         " % \
-                        (str(active_entry.entry_num+1), SU.green("PLAYING > "+str(active_entry)))
-
-                    # Open entries for current playlist
-                    rest_of_playlist = active_entry.get_next_entries(False)
-                    entries = self.preprocess_entries(rest_of_playlist, False)
-                    s += self.build_playlist_string(entries)
-                    
-            else:
-                s += "\n│       └── %s" % (SU.red("No active playlist. There should be at least some fallback playlist running..."))
-        else:
-            s += "\n│   Nothing.           "
-        s += "\n└──────────────────────────────────────────────────────────────────────────────────────────────────────"
-
-        s += "\n   SCHEDULED NEXT:"
-        s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
-
-        next_timeslots = self.get_next_timeslots()
-        if not next_timeslots:
-            s += "\n│   Nothing.         "
-        else:
-            for timeslot in next_timeslots:        
-                (fallback_type, resolved_playlist) = self.fallback.resolve_playlist(timeslot)
-                if resolved_playlist:
-
-                    s += "\n│   Queued timeslot %s         " % timeslot
-                    s += "\n│      └── Playlist %s         (Type: %s)" % (resolved_playlist, SU.cyan(str(fallback_type)))
-                    if resolved_playlist.end_unix > timeslot.end_unix:
-                        s += "\n│          %s!              " % \
-                        (SU.red("↑↑↑ Playlist #%s ends after timeslot #%s!" % (resolved_playlist.playlist_id, timeslot.timeslot_id)))
-                    
-                    entries = self.preprocess_entries(resolved_playlist.entries, False)
-                    s += self.build_playlist_string(entries)
-
-        s += "\n└──────────────────────────────────────────────────────────────────────────────────────────────────────\n\n"
-        return s
-
-
-
-    def build_playlist_string(self, entries):
-        """
-        Returns a stringified list of entries
-        """
-        s = ""
-        is_out_of_timeslot = False
-
-        for entry in entries:
-            if entry.queue_state == EntryQueueState.OUT_OF_SCHEDULE and not is_out_of_timeslot:
-                s += "\n│             %s" % \
-                    SU.red("↓↓↓ These entries won't be played because they are out of timeslot.")
-                is_out_of_timeslot = True
-
-            s += self.build_entry_string("\n│         └── ", entry, is_out_of_timeslot)
-
-        return s
-
-
-
-    def build_entry_string(self, prefix, entry, strike):
-        """
-        Returns an stringified entry.
-        """
-        s = ""
-        if entry.queue_state == EntryQueueState.CUT:
-            s = "\n│             %s" % SU.red("↓↓↓ This entry is going to be cut.")
-
-        if strike:
-            entry_str = SU.strike(entry)
-        else:
-            entry_str = str(entry)
-
-        entry_line = "%sEntry %s | %s" % (prefix, str(entry.entry_num+1), entry_str)
-        return s + entry_line
-
-
 
 #
 #   PRIVATE METHODS
@@ -663,10 +540,10 @@ class AuraScheduler(threading.Thread):
         index = 0
 
         # Mark entries which start after the end of their timeslot or are cut
-        clean_entries = self.preprocess_entries(entries, True)
+        # clean_entries = self.preprocess_entries(entries, True)
 
         # Group/aggregate all filesystem entries, allowing them to be queued at once
-        for entry in clean_entries:
+        for entry in entries:
             if previous_entry == None or \
                 (previous_entry != None and \
                  previous_entry.get_content_type() == entry.get_content_type() and \
@@ -682,7 +559,7 @@ class AuraScheduler(threading.Thread):
          
         # Timeslot function calls
         do_queue_timeslot_end = False
-        if len(clean_entries) > 0 and len(entry_groups) > 0:
+        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))
@@ -691,7 +568,7 @@ class AuraScheduler(threading.Thread):
                 self.set_entries_timer(entries, fade_in, fade_out)
 
                 # Store them for later reference
-                timeslot.queued_entries = clean_entries
+                timeslot.queued_entries = entries
 
         else:
             self.logger.warn(SU.red("Nothing to schedule ..."))
@@ -722,7 +599,7 @@ class AuraScheduler(threading.Thread):
                 # Let 'em play anyway ...
 
             self.engine.player.play(entries[0], transition_type)
-            self.logger.info(self.get_ascii_programme())
+            self.logger.info(self.timeslot_renderer.get_ascii_timeslots())
 
 
         if play_timer:
@@ -767,34 +644,7 @@ class AuraScheduler(threading.Thread):
         return False
 
 
-    def preprocess_entries(self, entries, cut_oos):
-        """
-        Analyses and marks entries which are going to be cut or excluded.
-
-        Args:
-            entries ([PlaylistEntry]):  The playlist entries to be scheduled for playout
-            cut_oos (Boolean):  If `True` entries which are 'out of timeslot' are not returned
-
-        Returns:
-            ([PlaylistEntry]):  The list of processed playlist entries
-        """
-        clean_entries = []
-
-        for entry in entries:
-
-            if entry.entry_start >= entry.playlist.timeslot.timeslot_end:
-                msg = "Filtered entry (%s) after end-of timeslot (%s) ... SKIPPED" % (entry, entry.playlist.timeslot)
-                self.logger.warning(SU.red(msg))
-                entry.queue_state = EntryQueueState.OUT_OF_SCHEDULE
-            elif entry.end_unix > entry.playlist.timeslot.end_unix:
-                entry.queue_state = EntryQueueState.CUT
-            else:
-                entry.queue_state = EntryQueueState.OKAY
-            
-            if not entry.queue_state == EntryQueueState.OUT_OF_SCHEDULE or not cut_oos:
-                clean_entries.append(entry)
 
-        return clean_entries
 
 
 
diff --git a/src/scheduling/utils.py b/src/scheduling/utils.py
new file mode 100644
index 00000000..ad88e62a
--- /dev/null
+++ b/src/scheduling/utils.py
@@ -0,0 +1,203 @@
+
+#
+# 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 enum import Enum
+
+from src.base.utils import SimpleUtil as SU
+
+
+
+
+class EntryQueueState(Enum):
+    """
+    Types of playlist entry behaviours.
+    """
+    OKAY = "ok"
+    CUT = "cut"
+    OUT_OF_SCHEDULE = "oos"
+
+
+
+class TimeslotRenderer:
+    """
+    Displays current and next timeslots in ASCII for maintainence and debugging.
+    """
+    logger = None
+    scheduler = None
+
+
+    def __init__(self, scheduler):
+        """
+        Constructor
+        """
+        self.logger = logging.getLogger("AuraEngine")
+        self.scheduler = scheduler
+
+
+
+    def get_ascii_timeslots(self):
+        """
+        Creates a printable version of the current programme (playlists and entries as per timeslot) 
+
+        Returns:
+            (String):   An ASCII representation of the current and next timeslots
+        """
+        active_timeslot = self.scheduler.get_active_timeslot()
+
+        s = "\n\n   SCHEDULED NOW:"
+        s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
+        if active_timeslot:
+            planned_playlist = None
+            if active_timeslot.playlist:
+                planned_playlist = active_timeslot.playlist
+
+            (fallback_type, resolved_playlist) = self.scheduler.fallback.resolve_playlist(active_timeslot)
+
+            s += "\n│   Playing timeslot %s         " % active_timeslot
+            if planned_playlist: 
+                if resolved_playlist and resolved_playlist.playlist_id != planned_playlist.playlist_id:
+                    s += "\n│       └── Playlist %s         " % planned_playlist
+                    s += "\n│       "
+                    s += SU.red("↑↑↑ That's the originally planned playlist.") + ("Instead playing the fallback playlist below ↓↓↓")
+
+            if resolved_playlist:                                  
+                if not planned_playlist:
+                    s += "\n│                         "
+                    s += SU.red("No playlist assigned to timeslot. Instead playing the `%s` playlist below ↓↓↓" % SU.cyan(str(fallback_type)))
+
+                s += "\n│       └── Playlist %s         " % resolved_playlist
+
+                active_entry = self.scheduler.get_active_entry()
+
+                # Finished entries
+                for entry in resolved_playlist.entries:
+                    if active_entry == entry:
+                        break
+                    else:
+                        s += self.build_entry_string("\n│         └── ", entry, True)
+
+                # Entry currently being played
+                if active_entry:
+                    s += "\n│         └── Entry %s | %s         " % \
+                        (str(active_entry.entry_num+1), SU.green("PLAYING > "+str(active_entry)))
+
+                    # Open entries for current playlist
+                    rest_of_playlist = active_entry.get_next_entries(False)
+                    entries = self.preprocess_entries(rest_of_playlist, False)
+                    s += self.build_playlist_string(entries)
+                    
+            else:
+                s += "\n│       └── %s" % (SU.red("No active playlist. There should be at least some fallback playlist running..."))
+        else:
+            s += "\n│   Nothing.           "
+        s += "\n└──────────────────────────────────────────────────────────────────────────────────────────────────────"
+
+        s += "\n   SCHEDULED NEXT:"
+        s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
+
+        next_timeslots = self.scheduler.get_next_timeslots()
+        if not next_timeslots:
+            s += "\n│   Nothing.         "
+        else:
+            for timeslot in next_timeslots:        
+                (fallback_type, resolved_playlist) = self.scheduler.fallback.resolve_playlist(timeslot)
+                if resolved_playlist:
+
+                    s += "\n│   Queued timeslot %s         " % timeslot
+                    s += "\n│      └── Playlist %s         (Type: %s)" % (resolved_playlist, SU.cyan(str(fallback_type)))
+                    if resolved_playlist.end_unix > timeslot.end_unix:
+                        s += "\n│          %s!              " % \
+                        (SU.red("↑↑↑ Playlist #%s ends after timeslot #%s!" % (resolved_playlist.playlist_id, timeslot.timeslot_id)))
+                    
+                    entries = self.preprocess_entries(resolved_playlist.entries, False)
+                    s += self.build_playlist_string(entries)
+
+        s += "\n└──────────────────────────────────────────────────────────────────────────────────────────────────────\n\n"
+        return s
+
+
+    
+    def build_playlist_string(self, entries):
+        """
+        Returns a stringified list of entries
+        """
+        s = ""
+        is_out_of_timeslot = False
+
+        for entry in entries:
+            if entry.queue_state == EntryQueueState.OUT_OF_SCHEDULE and not is_out_of_timeslot:
+                s += "\n│             %s" % \
+                    SU.red("↓↓↓ These entries won't be played because they are out of timeslot.")
+                is_out_of_timeslot = True
+
+            s += self.build_entry_string("\n│         └── ", entry, is_out_of_timeslot)
+
+        return s
+
+
+
+    def build_entry_string(self, prefix, entry, strike):
+        """
+        Returns an stringified entry.
+        """
+        s = ""
+        if entry.queue_state == EntryQueueState.CUT:
+            s = "\n│             %s" % SU.red("↓↓↓ This entry is going to be cut.")
+
+        if strike:
+            entry_str = SU.strike(entry)
+        else:
+            entry_str = str(entry)
+
+        entry_line = "%sEntry %s | %s" % (prefix, str(entry.entry_num+1), entry_str)
+        return s + entry_line
+
+
+
+    def preprocess_entries(self, entries, cut_oos):
+        """
+        Analyses and marks entries which are going to be cut or excluded.
+
+        Args:
+            entries ([PlaylistEntry]):  The playlist entries to be scheduled for playout
+            cut_oos (Boolean):  If `True` entries which are 'out of timeslot' are not returned
+
+        Returns:
+            ([PlaylistEntry]):  The list of processed playlist entries
+        """
+        clean_entries = []
+
+        for entry in entries:
+
+            if entry.entry_start >= entry.playlist.timeslot.timeslot_end:
+                msg = "Filtered entry (%s) after end-of timeslot (%s) ... SKIPPED" % (entry, entry.playlist.timeslot)
+                self.logger.warning(SU.red(msg))
+                entry.queue_state = EntryQueueState.OUT_OF_SCHEDULE
+            elif entry.end_unix > entry.playlist.timeslot.end_unix:
+                entry.queue_state = EntryQueueState.CUT
+            else:
+                entry.queue_state = EntryQueueState.OKAY
+            
+            if not entry.queue_state == EntryQueueState.OUT_OF_SCHEDULE or not cut_oos:
+                clean_entries.append(entry)
+
+        return clean_entries
\ No newline at end of file
-- 
GitLab