From 900ff081e3631c591b7f9fc6746372437db8cd9c Mon Sep 17 00:00:00 2001
From: David Trattnig <david.trattnig@o94.at>
Date: Sat, 31 Oct 2020 21:32:46 +0100
Subject: [PATCH] Trigger new "on_fallback_active" event. #38

---
 src/scheduling/fallback.py | 122 +++++++++++++++++++++++++++++++------
 1 file changed, 104 insertions(+), 18 deletions(-)

diff --git a/src/scheduling/fallback.py b/src/scheduling/fallback.py
index a8375e78..cc6bde81 100644
--- a/src/scheduling/fallback.py
+++ b/src/scheduling/fallback.py
@@ -34,17 +34,26 @@ from src.core.control       import EngineExecutor
 
 class FallbackType(Enum):
     """
-    Types of playlists.
+    Types of fallbacks.
+
+        NONE:       No fallback active, default playout
+        SCHEDULE:   The first played when some default playlist fails
+        SHOW:       The second played when the timeslot fallback fails
+        STATION:    The last played when everything else fails
     """
-    NONE        = { "id": 0, "name": "default", "lqs_sources": [ Channel.QUEUE_A, Channel.QUEUE_A] }    # No fallback active, default playout
-    SCHEDULE    = { "id": 1, "name": "schedule", "lqs_sources": [ Channel.FALLBACK_QUEUE_A, Channel.FALLBACK_QUEUE_B]}   # The first played when some default playlist fails
-    SHOW        = { "id": 2, "name": "show", "lqs_sources": [ "station_folder", "station_playlist"]}       # The second played when the timeslot fallback fails    
-    STATION     = { "id": 3, "name": "station", "lqs_sources": [ "station_folder", "station_playlist"] }    # The last played when everything else fails
+    NONE        = { "id": 0, "name": "default", "channels": [ Channel.QUEUE_A, Channel.QUEUE_B ] }
+    SCHEDULE    = { "id": 1, "name": "schedule", "channels": [ Channel.FALLBACK_QUEUE_A, Channel.FALLBACK_QUEUE_B ] }   
+    SHOW        = { "id": 2, "name": "show", "channels": [ Channel.FALLBACK_QUEUE_A, Channel.FALLBACK_QUEUE_B ] }
+    STATION     = { "id": 3, "name": "station", "channels": [ Channel.FALLBACK_STATION_FOLDER, Channel.FALLBACK_STATION_PLAYLIST ] }
 
     @property
     def id(self):
         return self.value["id"]
 
+    @property
+    def channels(self):
+        return self.value["channels"]
+
     def __str__(self):
         return str(self.value["name"])
 
@@ -52,30 +61,80 @@ class FallbackType(Enum):
 
 class FallbackManager:
     """
-    Handles all types of fallbacks in case there is an outage
-    for the regular radio programme.
-
-    Attributes:
-        config (AuraConfig):        The engine configuration
-        logger (AuraLogger):        The logger
-        mail   (AuraMailer):        Mail service
-        scheduler (AuraScheduler):  The scheduler
+    Handles all types of fallbacks in case there is an outage or missing schedules
+    for the radio programme.
     """    
     config = None
     logger = None
-    scheduler = None
-
+    engine = None
+    state = None
     
-    def __init__(self, scheduler):
+
+    def __init__(self, engine):
         """
         Constructor
 
         Args:
-
+            scheduler (Scheduler):  The scheduler
         """
         self.config = AuraConfig.config()
         self.logger = logging.getLogger("AuraEngine")
-        self.scheduler = scheduler
+        self.engine = engine
+        self.state = {            
+            "fallback_type": FallbackType.NONE,
+            "previous_fallback_type": None, 
+            "timeslot": None
+        }
+
+
+    #
+    #   EVENTS
+    #
+
+
+    def on_timeslot_start(self, timeslot=None):
+        """
+        Some new timeslot has just started.
+        """        
+        self.state["timeslot"] = timeslot
+
+
+    def on_timeslot_end(self, timeslot):
+        """
+        The timeslot has ended and the state is updated. The method ensures that any intermediate state update doesn't get overwritten.
+        """        
+        if self.state["timeslot"] == timeslot:
+            self.state["timeslot"] = None
+
+
+    def on_play(self, entry):
+        """
+        Event Handler which is called by the engine when some entry is actually playing. 
+
+        Args:
+            source (String):    The `PlaylistEntry` object
+        """        
+        self.update_fallback_state(entry.channel)
+
+
+    def on_metadata(self, data):
+        """
+        Event called by the soundsystem implementation (i.e. Liquidsoap) when some entry is actually playing. 
+        This does not include live or stream sources, since they ain't have metadata and are triggered from 
+        engine core (see `on_play(..)`).
+
+        Args:
+            data (dict):    A collection of metadata related to the current track
+        """
+        channel = data.get("source")
+        fallback_type = self.update_fallback_state(channel)
+
+        # If we turned into a fallback state we issue an event
+        if fallback_type is not FallbackType.NONE:
+            # Only trigger the event the upon first state change
+            if fallback_type != self.state.get("previous_fallback_type"):
+                self.engine.event_dispatcher.on_fallback_active(self.state["timeslot"], fallback_type)
+
 
 
     #
@@ -83,6 +142,32 @@ class FallbackManager:
     #
 
 
+    def update_fallback_state(self, channel):
+        """
+        Update the current and previously active fallback state.
+
+        Returns:
+            (FallbackType): The current fallback
+        """
+        fallback_type = self.type_for_channel(channel)
+        self.state["previous_fallback_type"] = self.state["fallback_type"]
+        self.state["fallback_type"] = fallback_type
+        return fallback_type
+
+
+    def type_for_channel(self, source):
+        """
+        Retrieves the matching fallback type for the given source.
+        """
+        if source in [str(i) for i in FallbackType.SCHEDULE.channels]:
+            return FallbackType.SCHEDULE
+        if source in [str(i) for i in FallbackType.SHOW.channels]:
+            return FallbackType.SHOW
+        if source in [str(i) for i in FallbackType.STATION.channels]:
+            return FallbackType.STATION
+        return FallbackType.NONE
+
+
     def queue_fallback_playlist(self, timeslot):
         """
         Evaluates the scheduled fallback and queues it using a timed thread.
@@ -198,6 +283,7 @@ class FallbackCommand(EngineExecutor):
     are created.
     """
 
+
     def __init__(self, timeslot, entries):
         """
         Constructor
-- 
GitLab