From 08ed7c25f5f0a9fe0b3556a24d8bb206bd7c8329 Mon Sep 17 00:00:00 2001
From: Chris Pastl <chris@crispybits.app>
Date: Wed, 15 May 2024 00:19:50 +0200
Subject: [PATCH] refactor: add timeout to Channel.load()

---
 src/aura_engine/core/channels.py        | 22 +++++++++++++---------
 src/aura_engine/engine.py               |  2 +-
 src/aura_engine/scheduling/domain.py    |  1 +
 src/aura_engine/scheduling/scheduler.py | 15 +++++++--------
 4 files changed, 22 insertions(+), 18 deletions(-)

diff --git a/src/aura_engine/core/channels.py b/src/aura_engine/core/channels.py
index 469475ea..ee087b1c 100644
--- a/src/aura_engine/core/channels.py
+++ b/src/aura_engine/core/channels.py
@@ -213,7 +213,7 @@ class GenericChannel:
         self.volume = volume
         self.remaining = remain
 
-    def load(self, metadata: dict = None) -> bool:
+    def load(self, timeout: int = None, metadata: dict = None) -> bool:
         """
         Interface definition for loading a channel track.
 
@@ -330,7 +330,7 @@ class QueueChannel(GenericChannel):
         self.type = ChannelType.QUEUE
         super().__init__(channel_index, channel_name, mixer)
 
-    def load(self, uri: str = None, metadata: dict = None):
+    def load(self, uri: str = None, timeout: int = None, metadata: dict = None):
         """
         Load the provided URI and pass metadata.
 
@@ -432,7 +432,7 @@ class StreamChannel(GenericChannel):
         self.type = ChannelType.HTTP
         super().__init__(channel_index, channel_name, mixer)
 
-    def load(self, uri: str = None, metadata: dict = None):
+    def load(self, uri: str = None, timeout: int = None, metadata: dict = None):
         """
         Load the given stream item and updates the playlist items's status codes.
 
@@ -444,18 +444,22 @@ class StreamChannel(GenericChannel):
             (bool): True if track loaded successfully
 
         """
+        if timeout is None:
+            timeout = 0
+
         self.logger.debug(SU.pink(f"Loading stream '{uri}'"))
-        retry_delay = self.config.scheduler.input_stream.retry_delay
-        max_retries = self.config.scheduler.input_stream.max_retries
-        retries = 0
 
         self.stop()
         self.set_url(uri)
         self.start()
 
+        retry_until = SU.timestamp() + timeout
+        retry_delay = self.config.scheduler.input_stream.retry_delay
+        retries = 0
+
         while not self.is_ready(uri):
-            if retries >= max_retries:
-                msg = f"Stream connection failed after {retries * retry_delay} seconds!"
+            if SU.timestamp() > retry_until:
+                msg = f"Stream connection failed after {retries} retries in {timeout} seconds"
                 raise LoadSourceException(msg)
             time.sleep(retry_delay)
             retries += 1
@@ -552,7 +556,7 @@ class LineChannel(GenericChannel):
         self.type = ChannelType.LIVE
         super().__init__(channel_index, channel_name, mixer)
 
-    def load(self, uri: str = None, metadata: dict = None):
+    def load(self, uri: str = None, timeout: int = None, metadata: dict = None):
         """
         Load the line channel.
 
diff --git a/src/aura_engine/engine.py b/src/aura_engine/engine.py
index 94906cc6..f10ef3a2 100644
--- a/src/aura_engine/engine.py
+++ b/src/aura_engine/engine.py
@@ -329,7 +329,7 @@ class Player:
             item.play.set_loading(chosen_channel)
             self.logger.info(SU.pink(msg))
 
-        is_ready = item.play.channel.load(uri, metadata=metadata)
+        is_ready = item.play.channel.load(uri, timeout=item.duration, metadata=metadata)
 
         if is_ready:
             item.play.set_ready()
diff --git a/src/aura_engine/scheduling/domain.py b/src/aura_engine/scheduling/domain.py
index 31487d01..481994e5 100644
--- a/src/aura_engine/scheduling/domain.py
+++ b/src/aura_engine/scheduling/domain.py
@@ -538,6 +538,7 @@ class PlayState:
         READY = "ready"
         PLAYING = "playing"
         DONE = "done"
+        FAILED = "failed"
 
     state: PlayStateType
     play_start: float
diff --git a/src/aura_engine/scheduling/scheduler.py b/src/aura_engine/scheduling/scheduler.py
index 645d683b..7082a8b4 100644
--- a/src/aura_engine/scheduling/scheduler.py
+++ b/src/aura_engine/scheduling/scheduler.py
@@ -41,7 +41,7 @@ from aura_engine.base.utils import SimpleUtil as SU
 from aura_engine.control import EngineExecutor
 from aura_engine.core.channels import LoadSourceException
 from aura_engine.resources import ResourceClass, ResourceUtil
-from aura_engine.scheduling.domain import PlaylistItem, Timeslot
+from aura_engine.scheduling.domain import PlaylistItem, PlayState, Timeslot
 
 #
 #   EngineExecutor Commands
@@ -183,6 +183,7 @@ class PlayCommand(EngineExecutor):
         last_item: PlaylistItem = items[-1]
         if not last_item.play.is_ready():
             msg = f"Items didn't reach 'ready' state during preloading (Items: {items_str})"
+            last_item.play.state = PlayState.PlayStateType.FAILED
             self.logger.warning(SU.red(msg))
 
     def do_play(self, items: list[PlaylistItem]):
@@ -195,15 +196,13 @@ class PlayCommand(EngineExecutor):
         items_str = ResourceUtil.get_items_string(items)
         self.logger.info(SU.cyan(f"=== play('{items_str}') ==="))
         last_item: PlaylistItem = items[-1]
-        if not last_item.play.is_ready():
-            # Let 'em play anyway ...
+        while not last_item.play.is_ready():
             msg = f"PLAY: Item(s) not yet ready to be played" f" (Items: {items_str})"
             self.logger.critical(SU.red(msg))
-            now = SU.timestamp()
-            while not last_item.play.is_ready() and SU.timestamp() <= now + last_item.duration:
-                self.logger.info("PLAY: Wait a little bit until preloading is done ...")
-                time.sleep(2)
-
+            if last_item.play.state == PlayState.PlayStateType.FAILED:
+                self.logger.info("PLAY: Preloading failed - skipping play")
+                return
+            time.sleep(2)
         self.engine.player.play(items[0], engine.Player.TransitionType.FADE)
         timetable_renderer: utils.TimetableRenderer = self.engine.scheduler.timetable_renderer
         self.logger.info(timetable_renderer.get_ascii_timeslots())
-- 
GitLab