From a5a5e72e26a7ad755fcb229c43bfd95bc8ded861 Mon Sep 17 00:00:00 2001 From: David Trattnig <david.trattnig@o94.at> Date: Thu, 28 May 2020 20:37:06 +0200 Subject: [PATCH] Finalized Engine Basics. --- guru.py | 4 +- modules/cli_tool/padavan.py | 16 +- modules/core/engine.py | 334 ++++++++++++++++++++------------ modules/core/monitor.py | 2 +- modules/core/state.py | 57 +++--- modules/database/model.py | 4 +- modules/scheduling/scheduler.py | 78 +++++--- testing/connection_tester.py | 2 +- 8 files changed, 312 insertions(+), 185 deletions(-) diff --git a/guru.py b/guru.py index 509aab29..b5343a33 100755 --- a/guru.py +++ b/guru.py @@ -110,8 +110,8 @@ class Guru(): # getter self.parser.add_argument("-pcs", "--print-connection-status", action="store_true", dest="get_connection_status", default=False, help="Prints the status of the connection to liquidsoap, pv and tank") - self.parser.add_argument("-gam", "--get-active-mixer", action="store_true", dest="get_active_mixer", default=False, help="Which mixer is activated?") - self.parser.add_argument("-pms", "--print-mixer-status", action="store_true", dest="get_mixer_status", default=False, help="Prints all mixer sources and their states") + self.parser.add_argument("-gam", "--get-active-mixer", action="store_true", dest="mixer_channels_selected",default=False, help="Which mixer channels are selected?") + self.parser.add_argument("-pms", "--print-mixer-status", action="store_true", dest="mixer_status", default=False, help="Prints all mixer sources and their states") self.parser.add_argument("-pap", "--print-act-programme", action="store_true", dest="get_act_programme", default=False, help="Prints the actual Programme, the controller holds") self.parser.add_argument("-s", "--status", action="store_true", dest="get_status", default=False, help="Returns the Engine Status as JSON") diff --git a/modules/cli_tool/padavan.py b/modules/cli_tool/padavan.py index 56c3c8eb..a368f465 100644 --- a/modules/cli_tool/padavan.py +++ b/modules/cli_tool/padavan.py @@ -50,11 +50,11 @@ class Padavan: if self.args.fetch_new_programme: self.fetch_new_programme() - elif self.args.get_active_mixer: - self.get_active_mixer() + elif self.args.mixer_channels_selected: + self.mixer_channels_selected() - elif self.args.get_mixer_status: - self.get_mixer_status() + elif self.args.mixer_status: + self.mixer_status() elif self.args.get_act_programme: self.get_act_programme() @@ -275,9 +275,9 @@ class Padavan: self.destroy_liquidsoap_communication() # ------------------------------------------------------------------------------------------ # - def get_active_mixer(self): + def mixer_channels_selected(self): self.init_liquidsoap_communication() - am = self.ss.get_active_mixer() + am = self.ss.mixer_channels_selected() if len(am) == 0: self.destroy_liquidsoap_communication() @@ -289,10 +289,10 @@ class Padavan: self.destroy_liquidsoap_communication() # ------------------------------------------------------------------------------------------ # - def get_mixer_status(self): + def mixer_status(self): self.init_liquidsoap_communication() - status = self.ss.get_mixer_status() + status = self.ss.mixer_status() for k, v in status.items(): self.stringreply += "source: " + k + "\t status: " + v + "\n" diff --git a/modules/core/engine.py b/modules/core/engine.py index 3819a3c3..8bd210fe 100644 --- a/modules/core/engine.py +++ b/modules/core/engine.py @@ -26,7 +26,9 @@ import time import logging import json -from urllib.parse import urlparse, ParseResult +from urllib.parse import urlparse, ParseResult +from contextlib import suppress +from threading import Thread from modules.base.enum import ChannelType, Channel, TransitionType, LiquidsoapResponse, EntryPlayState from modules.base.utils import TerminalColors, SimpleUtil, EngineUtil @@ -66,7 +68,7 @@ class SoundSystem(): def __init__(self, config): """ - Initializes the communicator by establishing a Socket connection + Initializes the sound-system by establishing a Socket connection to Liquidsoap. Args: @@ -96,19 +98,22 @@ class SoundSystem(): def start(self): """ - Starts the soundsystem. + Starts the sound-system. """ # Sleep needed, because the socket is created too slowly by Liquidsoap time.sleep(1) self.enable_transaction() time.sleep(1) - self.mixer_start() + # Initialize all channels + channels = self.mixer_channels_reload() + for c in channels: + self.channel_volume(c, "0") # Setting init params like a blank file - install_dir = self.config.get("install_dir") - channel = self.active_channel[ChannelType.FILESYSTEM] - self.playlist_push(channel, install_dir + "/configuration/blank.flac") + # install_dir = self.config.get("install_dir") + # channel = self.active_channel[ChannelType.FILESYSTEM] + # self.playlist_push(channel, install_dir + "/configuration/blank.flac") self.disable_transaction() self.is_liquidsoap_running = True @@ -133,104 +138,124 @@ class SoundSystem(): return self.is_liquidsoap_running + # # MIXER : GENERAL # - def mixer_start(self): - # Reset channels and reload them - channels = self.reload_channels() + def mixer_status(self): + """ + Returns the state of all mixer channels + """ + cnt = 0 + inputstate = {} - # For all available channels - for c in channels: - # Set volume to zero - self.channel_volume(c, "0") - # And activate this channel - self.channel_activate(c, True) + self.enable_transaction() + inputs = self.mixer_channels() + for input in inputs: + inputstate[input] = self.channel_status(cnt) + cnt = cnt + 1 - # ------------------------------------------------------------------------------------------ # - # def set_volume(self, mixernumber, volume): - # #return self.client.command("mixer", 'volume', mixernumber, str(volume)) - # return self.__send_lqc_command__(self.client, "mixer", "volume", mixernumber, volume) + self.disable_transaction() + return inputstate - # ------------------------------------------------------------------------------------------ # - def get_active_mixer(self): + + def mixer_channels(self): """ - get active mixer in liquidsoap server - :return: + Retrieves all mixer channels """ - activeinputs = [] + if self.channels is None or len(self.channels) == 0: + self.channels = self.__send_lqc_command__(self.client, "mixer", "inputs") - # enable more control over the connection - self.enable_transaction() + return self.channels - inputs = self.get_all_channels() + def mixer_channels_selected(self): + """ + Retrieves all selected channels of the mixer. + """ cnt = 0 - for input in inputs: - status = self.__get_mixer_status__(cnt) + activeinputs = [] + self.enable_transaction() + inputs = self.mixer_channels() + + for input in inputs: + status = self.channel_status(cnt) if "selected=true" in status: activeinputs.append(input) - cnt = cnt + 1 self.disable_transaction() return activeinputs - # ------------------------------------------------------------------------------------------ # - def get_mixer_status(self): - inputstate = {} - self.enable_transaction() + def mixer_channels_except(self, input_type): + """ + Retrieves all mixer channels except the ones of the given type. + """ + try: + activemixer_copy = self.mixer_channels().copy() + activemixer_copy.remove(input_type) + except ValueError as e: + self.logger.error("Requested channel (" + input_type + ") not in channellist. Reason: " + str(e)) + except AttributeError: + self.logger.critical("Channellist is None") - inputs = self.get_all_channels() + return activemixer_copy - cnt = 0 - for input in inputs: - inputstate[input] = self.__get_mixer_status__(cnt) - cnt = cnt + 1 - self.disable_transaction() + def mixer_channels_reload(self): + """ + Reloads all mixer channels. + """ + self.channels = None + return self.mixer_channels() - return inputstate # ------------------------------------------------------------------------------------------ # def get_mixer_volume(self, channel): + # FIXME Is this needed; even possible? return False - # ------------------------------------------------------------------------------------------ # - def __get_mixer_status__(self, mixernumber): - return self.__send_lqc_command__(self.client, "mixer", "status", mixernumber) + # - # MIXER : CHANNELS + # MIXER : CONTROL SECTION # - def load(self, entry): + def preroll(self, entry): """ - Preloads the entry. This is required before the actual `play(..)` can happen. + Pre-Rolls/Pre-Loads the entry. This is required before the actual `play(..)` can happen. + + Be aware when using this method to queue a very short entry (shorter than ``) this may + result in sitations with incorrect timing. In this case bundle multiple short entries as + one queue using `preroll_playlist(self, entries)`. - Note his method is blocking until loading has finished. If this method is called - asynchroniously, the progress on the preloading state can be looked up in `entry.state`. + It's important to note, that his method is blocking until loading has finished. If this + method is called asynchronously, the progress on the preloading state can be looked up in + `entry.state`. + + Args: + entries ([Entry]): An array holding filesystem entries """ entry.status = EntryPlayState.LOADING self.logger.info("Loading entry '%s'" % entry) is_ready = False - # Choose and save the input channel + # LIVE if entry.type == ChannelType.LIVE: entry.channel = "linein_" + entry.source.split("line://")[1] is_ready = True else: - self.player_state.set_active_entry(entry) - entry.channel = self.channel_swap(entry.type) + # Choose and save the input channel + entry.previous_channel, entry.channel = self.channel_swap(entry.type) # PLAYLIST if entry.type == ChannelType.FILESYSTEM: @@ -243,45 +268,99 @@ class SoundSystem(): if is_ready == True: entry.status = EntryPlayState.READY + # Store in play-log cache for later reference + self.player_state.add_to_history([entry]) + + + + def preroll_group(self, entries): + """ + Pre-Rolls/Pre-Loads multiple filesystem entries at once. This call is required before the + actual `play(..)` can happen. Due to their nature, non-filesystem entries cannot be queued + using this method. In this case use `preroll(self, entry)` instead. This method also allows + queuing of very short files, such as jingles. + + It's important to note, that his method is blocking until loading has finished. If this + method is called asynchronously, the progress on the preloading state can be looked up in + `entry.state`. + + Args: + entries ([Entry]): An array holding filesystem entries + """ + channel = None + + # Validate entry type + for entry in entries: + if entry.type != ChannelType.FILESYSTEM: + raise InvalidChannelException + + # Determine channel + channel = self.channel_swap(entry.type) + + # Queue entries + for entry in entries: + entry.status = EntryPlayState.LOADING + self.logger.info("Loading entry '%s'" % entry) + + # Choose and save the input channel + entry.previous_channel, entry.channel = channel + + if self.playlist_push(entry.channel, entry.source) == True: + entry.status = EntryPlayState.READY + + # Store in play-log cache for later reference + self.player_state.add_to_history(entries) + + def play(self, entry, transition): """ Plays a new `Entry`. In case of a new schedule (or some intented, immediate transition), a clean channel is selected and transitions between old and new channel is performed. - This method expects that the entry is pre-loaded using `load(..)` before being played. + This method expects that the entry is pre-loaded using `preroll(..)` or `preroll_group(self, entries)` + before being played. In case the pre-roll has happened for a group of entries, only the + first entry of the group needs to be passed. Args: entry (PlaylistEntry): The audio source to be played - transition (TransitionType): The type of transition to use e.g. fade-out. + transition (TransitionType): The type of transition to use e.g. fade-in or instant volume level. queue (Boolean): If `True` the entry is queued if the `ChannelType` does allow so; otherwise a new channel of the same type is activated """ - try: - - # Move channel volume all the way up + with suppress(LQConnectionError): + + # Instant activation or fade-in self.enable_transaction() if transition == TransitionType.FADE: + self.channel_select(entry.channel.value, True) self.fade_in(entry) else: - self.channel_volume(entry.channel, entry.volume) + self.channel_activate(entry.channel.value, True) self.disable_transaction() # Update active channel and type self.active_channel[entry.type] = entry.channel - - - except LQConnectionError: - # we already caught and handled this error in __send_lqc_command__, - # but we do not want to execute this function further and pass the exception - pass + + # Dear filesystem channels, please leave the room as you would like to find it! + if entry.previous_channel and entry.previous_channel in ChannelType.FILESYSTEM.channels: + def clean_up(): + # Wait a little, if there is some long fade-out. Note, this also means, + # this channel should not be used for at least some seconds (including clearing time). + time.sleep(2) + self.enable_transaction() + self.channel_activate(entry.previous_channel.value, False) + res = self.playlist_clear(entry.previous_channel) + self.logger.info("Clear Queue Response: "+res) + self.disable_transaction() + Thread(target=clean_up).start() def on_play(self, source): """ - Event Handler which is called by soundsystem implementation (i.e. Liquidsoap) + Event Handler which is called by the soundsystem implementation (i.e. Liquidsoap) when some entry is actually playing. Args: @@ -304,8 +383,7 @@ class SoundSystem(): entry (Entry): The entry to stop playing transition (TransitionType): The type of transition to use e.g. fade-out. """ - - try: + with suppress(LQConnectionError): self.enable_transaction() if not entry.channel: @@ -317,18 +395,17 @@ class SoundSystem(): else: self.channel_volume(entry.channel, 0) - # self.playlist_clear(entry.channel) self.logger.info(SimpleUtil.pink("Stopped channel '%s' for entry %s" % (entry.channel, entry))) - self.disable_transaction() - except LQConnectionError: - # we already caught and handled this error in __send_lqc_command__, - # but we do not want to execute this function further and pass the exception - pass + # + # MIXER : CHANNEL + # + + def channel_swap(self, channel_type): """ Returns the currently in-active channel for a given type. For example if the currently some @@ -338,87 +415,99 @@ class SoundSystem(): Args: channel_type (ChannelType): The channel type such es filesystem, stream or live channel """ - active_channel = self.active_channel[channel_type] - channel = None + previous_channel = self.active_channel[channel_type] + new_channel = None msg = None if channel_type == ChannelType.FILESYSTEM: - if active_channel == Channel.FILESYSTEM_A: - channel = Channel.FILESYSTEM_B + if previous_channel == Channel.FILESYSTEM_A: + new_channel = Channel.FILESYSTEM_B msg = "Swapped filesystem channel from A > B" else: - channel = Channel.FILESYSTEM_A + new_channel = Channel.FILESYSTEM_A msg = "Swapped filesystem channel from B > A" - # TODO Clear old channel - elif channel_type == ChannelType.HTTP: - if active_channel == Channel.HTTP_A: - channel = Channel.HTTP_B + if previous_channel == Channel.HTTP_A: + new_channel = Channel.HTTP_B msg = "Swapped HTTP Stream channel from A > B" - else: - channel = Channel.HTTP_A + new_channel = Channel.HTTP_A msg = "Swapped HTTP Stream channel from B > A" - elif channel_type == ChannelType.HTTPS: - if active_channel == Channel.HTTPS_A: - channel = Channel.HTTPS_B + if previous_channel == Channel.HTTPS_A: + new_channel = Channel.HTTPS_B msg = "Swapped HTTPS Stream channel from A > B" - else: - channel = Channel.HTTPS_A + new_channel = Channel.HTTPS_A msg = "Swapped HTTPS Stream channel from B > A" + if msg: self.logger.info(SimpleUtil.pink(msg)) + return (previous_channel, new_channel) - if msg: self.logger.info(SimpleUtil.pink(msg)) - return channel + def channel_status(self, channel_number): + """ + Retrieves the status of a channel identified by the channel number. + """ + return self.__send_lqc_command__(self.client, "mixer", "status", channel_number) - # ------------------------------------------------------------------------------------------ # - def all_inputs_but(self, input_type): - try: - activemixer_copy = self.get_all_channels().copy() - activemixer_copy.remove(input_type) - except ValueError as e: - self.logger.error("Requested channel (" + input_type + ") not in channellist. Reason: " + str(e)) - except AttributeError: - self.logger.critical("Channellist is None") - return activemixer_copy + def channel_select(self, channel, select): + """ + Selects/deselects some mixer channel - # ------------------------------------------------------------------------------------------ # - def get_all_channels(self): - if self.channels is None or len(self.channels) == 0: - self.channels = self.__send_lqc_command__(self.client, "mixer", "inputs") + Args: + pos (Integer): The channel number + select (Boolean): Select or deselect - return self.channels + Returns: + (String): Liquidsoap server response + """ + channels = self.mixer_channels() - # ------------------------------------------------------------------------------------------ # - def reload_channels(self): - self.channels = None - return self.get_all_channels() + try: + index = channels.index(channel) + if len(channel) < 1: + self.logger.critical("Cannot select channel. There are no channels!") + else: + message = self.__send_lqc_command__(self.client, "mixer", "select", index, select) + return message + except Exception as e: + self.logger.critical("Ran into exception when selecting channel. Reason: " + str(e)) - # ------------------------------------------------------------------------------------------ # def channel_activate(self, channel, activate): - channels = self.get_all_channels() + """ + Combined call of following to save execution time: + - Select some mixer channel + - Increase the volume to 100, + + Args: + pos (Integer): The channel number + activate (Boolean): Activate or deactivate + + Returns: + (String): Liquidsoap server response + """ + channels = self.mixer_channels() try: index = channels.index(channel) if len(channel) < 1: self.logger.critical("Cannot activate channel. There are no channels!") else: - message = self.__send_lqc_command__(self.client, "mixer", "select", index, activate) + message = self.__send_lqc_command__(self.client, "mixer", "activate", index, activate) return message except Exception as e: self.logger.critical("Ran into exception when activating channel. Reason: " + str(e)) - # ------------------------------------------------------------------------------------------ # + + def channel_volume(self, channel, volume): """ Set volume of a channel @@ -427,14 +516,13 @@ class SoundSystem(): channel (Channel): The channel volume (Integer) Volume between 0 and 100 """ - channel = str(channel) try: if str(volume) == "100": - channels = self.get_all_channels() + channels = self.mixer_channels() index = channels.index(channel) else: - channels = self.get_all_channels() + channels = self.mixer_channels() index = channels.index(channel) except ValueError as e: msg = SimpleUtil.red("Cannot set volume of channel " + channel + " to " + str(volume) + "!. Reason: " + str(e)) @@ -572,7 +660,7 @@ class SoundSystem(): # - # Channel Type - Playlist + # Channel Type - Filesystem # # FIXME @@ -613,7 +701,7 @@ class SoundSystem(): # self.enable_transaction() - # self.reload_channels() + # self.mixer_channels_reload() # # self.fade_in(playlist.entries[0]) # # FIXME rework # for new_entry in playlist.entries: @@ -654,7 +742,8 @@ class SoundSystem(): self.logger.info("%s.playlist_push result: %s" % (channel, result)) self.disable_transaction() - return result == "1" + # If successful, Liquidsoap returns a resource ID of the queued track + return int(result) >= 0 @@ -680,6 +769,7 @@ class SoundSystem(): return result + def playlist_clear(self, channel): """ Removes all tracks currently queued in the given `ChannelType.FILESYSTEM` channel. @@ -702,6 +792,8 @@ class SoundSystem(): return result + + # # Fading # diff --git a/modules/core/monitor.py b/modules/core/monitor.py index f4bbce0d..9c7b9966 100644 --- a/modules/core/monitor.py +++ b/modules/core/monitor.py @@ -159,7 +159,7 @@ class Monitoring: self.status["soundsystem"]["version"] = self.soundsystem.version() self.status["soundsystem"]["uptime"] = self.soundsystem.uptime() self.status["soundsystem"]["io"] = self.get_io_state() - self.status["soundsystem"]["mixer"] = self.soundsystem.get_mixer_status() + self.status["soundsystem"]["mixer"] = self.soundsystem.mixer_status() #self.status["soundsystem"]["recorder"] = self.soundsystem.get_recorder_status() self.soundsystem.disable_transaction(self.soundsystem.client) diff --git a/modules/core/state.py b/modules/core/state.py index 726c2ad2..a0f7fa0d 100644 --- a/modules/core/state.py +++ b/modules/core/state.py @@ -59,32 +59,24 @@ class PlayerStateService: # - def set_active_entry(self, entry): + def add_to_history(self, entries): """ - Saves the currently playing entry to the local cache. + Saves the currently pre-rolled [`Entry`] to the local cache. """ self.entry_history.pop() - self.entry_history.appendleft(entry) - - msg = "Active entry history:\n" - msg += "\n" + str(self.entry_history[0]) - msg += "\n" + str(self.entry_history[1]) - msg += "\n" + str(self.entry_history[2]) - self.logger.info(msg) + self.entry_history.appendleft(entries) - - def get_active_entry(self): + def get_recent_entries(self): """ - Retrieves the currently playing `Entry` from the local cache. + Retrieves the currently playing [`Entry`] from the local cache. """ return self.entry_history[0] - def store_trackservice_entry(self, source): """ - Stores the given entry in the Track Service. + Stores the entry identified by the given source in the Track Service. Args: source (String): The URI of the currently playing source @@ -92,25 +84,42 @@ class PlayerStateService: Raises: (NoActiveEntryException): In case currently nothing is playing """ - active_entry = self.get_active_entry() + found = False + entries = self.get_recent_entries() - if not active_entry: + if not entries: raise NoActiveEntryException - if active_entry.source == source: - trackservice = TrackService(active_entry) - trackservice.store(add=True, commit=True) + for active_entry in entries: + if active_entry.source == source: + trackservice = TrackService(active_entry) + trackservice.store(add=True, commit=True) + + active_entry.trackservice_id = trackservice.id + active_entry.store(add=False, commit=True) - active_entry.trackservice_id = trackservice.id - active_entry.store(add=False, commit=True) + self.logger.info("Stored active entry '%s' to TrackService as '%s'" % (active_entry, trackservice)) + found = True - self.logger.info("Stored active entry '%s' to TrackService as '%s'" % (active_entry, trackservice)) - else: - msg = "Active entry source '%s' != '%s' activated source." % (active_entry.source, source) + if not found: + msg = "Found no entry in the recent history which matches the given source '%s'" % (source) self.logger.critical(SimpleUtil.red(msg)) + def print_entry_history(self): + """ + Prints all recents entries of the history. + """ + msg = "Active entry history:\n" + for entries in self.entry_history: + msg += "[" + for e in entries: + msg += "\n" + str(e) + msg += "]" + self.logger.info(msg) + + # def adapt_trackservice_title(self, source): # """ diff --git a/modules/database/model.py b/modules/database/model.py index dd01882b..20d2d6c7 100644 --- a/modules/database/model.py +++ b/modules/database/model.py @@ -423,7 +423,7 @@ class PlaylistEntry(DB.Model, AuraDatabaseModel): entry_start = Column(DateTime) queue_state = None # Assigned when entry is about to be queued channel = None # Assigned when entry is actually played - state = None # Assigned when state changes + status = None # Assigned when state changes # relationships playlist = relationship("Playlist", uselist=False, back_populates="entries") @@ -720,7 +720,7 @@ class SingleEntry(DB.Model, AuraDatabaseModel): queue_state = None # Assigned when entry is about to be queued channel = None # Assigned when entry is actually played - state = None # Assigned when state changes + status = None # Assigned when state changes @hybrid_property diff --git a/modules/scheduling/scheduler.py b/modules/scheduling/scheduler.py index 4b2e5ab4..98d075f2 100644 --- a/modules/scheduling/scheduler.py +++ b/modules/scheduling/scheduler.py @@ -196,8 +196,8 @@ 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: - # Load and play active entry - self.soundsystem.load(active_entry) + # Pre-roll and play active entry + self.soundsystem.preroll(active_entry) self.soundsystem.play(active_entry, TransitionType.FADE) # Check if this is the last item of the schedule @@ -219,8 +219,8 @@ class AuraScheduler(threading.Thread): or active_entry.type == ChannelType.HTTPS \ or active_entry.type == ChannelType.LIVE: - # Load and play active entry - self.soundsystem.load(active_entry) + # Pre-roll and play active entry + self.soundsystem.preroll(active_entry) self.soundsystem.play(active_entry, TransitionType.FADE) self.queue_end_of_schedule(active_entry, True) @@ -558,7 +558,6 @@ class AuraScheduler(threading.Thread): - def queue_playlist_entries(self, entries, fade_in, fade_out): """ Creates Liquidsoap player commands for all playlist items to be executed at the scheduled time. @@ -571,58 +570,81 @@ class AuraScheduler(threading.Thread): Returns: (String): Formatted string to display playlist entries in log """ + entry_groups = [] + entry_groups.append([]) + previous_entry = None + index = 0 # Mark entries which start after the end of their schedule or are cut clean_entries = self.preprocess_entries(entries, True) - # Schedule function calls + # Group all filesystem entries, allowing them to be queued at once for entry in clean_entries: + if previous_entry == None or \ + (previous_entry != None and \ + previous_entry.type == entry.type and \ + entry.type == ChannelType.FILESYSTEM): + + entry_groups[index].append(entry) + else: + index += 1 + entry_groups.append([]) + entry_groups[index].append(entry) - self.set_entry_timer(entry, fade_in, fade_out) + previous_entry = entry + self.logger.info("Built %s entry group(s)" % len(entry_groups)) + + # Schedule function calls + for entries in entry_groups: + + self.set_entry_timer(entries, fade_in, fade_out) # Check if it's the last item, which needs special handling - if entry == clean_entries[-1]: + if entries[-1] == clean_entries[-1]: # The end of schedule is the actual end of the track - self.queue_end_of_schedule(entry, fade_out) + self.queue_end_of_schedule(entries[-1], fade_out) - def set_entry_timer(self, entry, fade_in, fade_out): + def set_entry_timer(self, entries, fade_in, fade_out): """ - Creates timer for loading and playing an entry. Existing timers are + 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(entry.start_unix) + play_timer = self.is_something_planned_at_time(entries[0].start_unix) now_unix = self.get_virtual_now() - diff = entry.start_unix - now_unix + diff = entries[0].start_unix - now_unix # Play function to be called by timer - def do_play(entry): - self.logger.info(SimpleUtil.cyan("=== play('%s') ===" % entry)) + def do_play(entries): + self.logger.info(SimpleUtil.cyan("=== play('%s') ===" % EngineUtil.get_entries_string(entries))) transition_type = TransitionType.INSTANT if fade_in: transition_type = TransitionType.FADE - if entry.status != EntryPlayState.READY: - self.logger.critical(SimpleUtil.red("PLAY: For some reason the entry is not yet ready or could not be loaded (Entry: %s)" % str(entry))) + if entries[-1].status != EntryPlayState.READY: + self.logger.critical(SimpleUtil.red("PLAY: For some reason the entry/entries is not yet ready or could not be loaded (Entries: %s)" % str(entries))) # TODO Pro-active fallback handling here - self.soundsystem.play(entry, transition_type) + self.soundsystem.play(entries[0], transition_type) self.logger.info(self.get_ascii_programme()) if play_timer: # Check if the Playlist IDs are different - if play_timer.entry.entry_id != entry.entry_id: + if play_timer.entries[0].entry_id != entries[0].entry_id: # If not, stop and remove the old timer, create a new one self.stop_timer(play_timer) else: # If the playlists do not differ => reuse the old timer and do nothing - self.logger.info("Playlist Entry %s is already scheduled - no new timer created!" % entry) + self.logger.info("Playlist Entry %s is already scheduled - no new timer created!" % str(entries)) return # If nothing is planned at given time, create a new timer - (entry.switchtimer, entry.loadtimer) = self.create_timer(diff, do_play, [entry], switcher=True) + (entries[0].switchtimer, entries[0].loadtimer) = self.create_timer(diff, do_play, [entries], switcher=True) @@ -750,7 +772,7 @@ class AuraScheduler(threading.Thread): Checks for existing timers at the given time. """ for t in self.message_timer: - if t.entry.start_unix == given_time and (t.fade_in or t.switcher): + if t.entry[0].start_unix == given_time and (t.fade_in or t.switcher): return t return False @@ -775,12 +797,16 @@ class AuraScheduler(threading.Thread): if switcher: # Load function to be called by timer - def do_load(entry): - self.logger.info(SimpleUtil.cyan("=== load('%s') ===" % entry)) + def do_load(entries): try: - self.soundsystem.load(entry) + if entries[0].type == ChannelType.FILESYSTEM: + self.logger.info(SimpleUtil.cyan("=== preroll_group('%s') ===" % EngineUtil.get_entries_string(entries))) + self.soundsystem.preroll_group(entries) + else: + self.logger.info(SimpleUtil.cyan("=== preroll('%s') ===" % EngineUtil.get_entries_string(entries))) + self.soundsystem.preroll(entries[0]) except LoadSourceException as e: - self.logger("Could not load entry %s:" % str(entry), e) + self.logger("Could not load entries %s:" % EngineUtil.get_entries_string(entries), e) # TODO Fallback logic here loader_diff = diff - self.config.get("preload_offset") diff --git a/testing/connection_tester.py b/testing/connection_tester.py index 4ab6c171..513e40d7 100644 --- a/testing/connection_tester.py +++ b/testing/connection_tester.py @@ -63,7 +63,7 @@ class ConnectionTester(AuraConfig): def test_lqs_conn(self): try: lsc = soundsystem(self.config) - lsc.get_mixer_status() + lsc.mixer_status() return True except Exception as e: -- GitLab