diff --git a/modules/core/engine.py b/modules/core/engine.py index 255d7ee311d65050873914c060839b42fe61ddfc..3a45ca2cfbf38a73c2db3f813ec4691194a9225c 100644 --- a/modules/core/engine.py +++ b/modules/core/engine.py @@ -25,52 +25,58 @@ from threading import Thread import meta -from modules.base.utils import TerminalColors, SimpleUtil as SU, EngineUtil -from modules.base.exceptions import LQConnectionError, InvalidChannelException, LQStreamException, LoadSourceException -from modules.core.channels import ChannelType, Channel, TransitionType, LiquidsoapResponse, EntryPlayState +from modules.base.utils import SimpleUtil as SU +from modules.base.exceptions import LQConnectionError, InvalidChannelException, LQStreamException, \ + LoadSourceException +from modules.core.resources import ResourceClass, ResourceUtil +from modules.core.channels import ChannelType, TransitionType, LiquidsoapResponse, \ + EntryPlayState, ResourceType, ChannelRouter from modules.core.startup import StartupThread from modules.core.events import EngineEventDispatcher -from modules.core.liquidsoap.playerclient import LiquidSoapPlayerClient -# from modules.core.liquidsoap.recorderclient import LiquidSoapRecorderClient +from modules.core.control import SocketControlInterface +from modules.core.mixer import Mixer, MixerType +from modules.core.liquidsoap.connector import PlayerConnector -class SoundSystem(): - """ - The Soundsystem Mixer Control. - This class represents a virtual mixer as an abstraction layer to the actual audio hardware. - It uses LiquidSoapClient, but introduces more complex commands, transactions and error handling. - From one layer above it is used by `AuraScheduler` as if a virtual DJ is remote controling the mixer. +class Engine(): + """ + The Engine. """ - client = None + engine_time_offset = 0.0 + logger = None - transaction = 0 + sci = None channels = None + channel_router = None scheduler = None event_dispatcher = None - is_liquidsoap_running = False - connection_attempts = 0 - disable_logging = False - fade_in_active = False - fade_out_active = False - active_channel = None - plugins=None + is_liquidsoap_running = False + plugins = None + + # Mixer + mixer = None + mixer_fallback = None + + connector = None def __init__(self, config): """ - Initializes the sound-system by establishing a Socket connection - to Liquidsoap. + Constructor Args: config (AuraConfig): The configuration """ self.config = config - self.logger = logging.getLogger("AuraEngine") - self.client = LiquidSoapPlayerClient(config, "engine.sock") - # self.lqcr = LiquidSoapRecorderClient(config, "record.sock") - self.is_active() # TODO Check if it makes sense to move it to the boot-phase self.plugins = dict() + self.logger = logging.getLogger("AuraEngine") + # self.sci = SocketControlInterface.get_instance(self.config, self.logger) + # self.sci.attach(self) + + self.is_active() # TODO Check if it makes sense to move it to the boot-phase + self.channel_router = ChannelRouter(self.config, self.logger) + Engine.engine_time_offset = self.config.get("lqs_delay_offset") def start(self): @@ -79,116 +85,145 @@ class SoundSystem(): has been established. """ self.event_dispatcher = EngineEventDispatcher(self, self.scheduler) + # Sleep needed, because the socket is created too slowly by Liquidsoap time.sleep(1) - self.mixer_initialize() + self.player = Player(self.config, self.event_dispatcher) + + # self.mixer = Mixer(self.config, MixerType.MAIN, self.connector) + # self.mixer_fallback = Mixer(self.config, MixerType.FALLBACK, self.connector) + self.is_liquidsoap_running = True self.event_dispatcher.on_initialized() self.logger.info(SU.green("Engine Core ------[ connected ]-------- Liquidsoap")) self.event_dispatcher.on_boot() - self.logger.info(EngineUtil.engine_info("Engine Core", meta.__version__)) + self.logger.info(EngineSplash.splash_screen("Engine Core", meta.__version__)) self.event_dispatcher.on_ready() + + # + # Basic Methods # - # MIXER : GENERAL - # - def mixer_initialize(self): + def init_player(self): """ - - Pull all faders down to volume 0. - - Initialize default channels per type + Initializes the LiquidSoap Player after startup of the engine. + + Returns: + (String): Message that the player is started. """ - self.enable_transaction() - time.sleep(1) # TODO Check is this is still required - channels = self.mixer_channels_reload() - for channel in channels: - self.channel_volume(channel, "0") - self.disable_transaction() - - self.active_channel = { - ChannelType.FILESYSTEM: Channel.FILESYSTEM_A, - ChannelType.HTTP: Channel.HTTP_A, - ChannelType.HTTPS: Channel.HTTPS_A, - ChannelType.LIVE: Channel.LIVE_0 - } - - - def mixer_status(self): + t = StartupThread(self) + t.start() + + return "Engine Core startup done!" + + + + def is_active(self): """ - Returns the state of all mixer channels + Checks if Liquidsoap is running """ - cnt = 0 - inputstate = {} + try: + self.uptime() + self.is_liquidsoap_running = True + except LQConnectionError as e: + self.logger.info("Liquidsoap is not running so far") + self.is_liquidsoap_running = False + except Exception as e: + self.logger.error("Cannot check if Liquidsoap is running. Reason: " + str(e)) + self.is_liquidsoap_running = False - self.enable_transaction() - inputs = self.mixer_channels() + return self.is_liquidsoap_running - for channel in inputs: - inputstate[channel] = self.channel_status(cnt) - cnt = cnt + 1 - self.disable_transaction() - return inputstate + def engine_state(self): + """ + Retrieves the state of all inputs and outputs. + """ + state = self.player.connector.send_lqc_command("engine", "state") + return state - def mixer_channels(self): + def version(self): """ - Retrieves all mixer channels + Get the version of Liquidsoap. """ - if self.channels is None or len(self.channels) == 0: - self.channels = self.__send_lqc_command__(self.client, "mixer", "inputs") - return self.channels + data = self.player.connector.send_lqc_command("version", "") + return data - def mixer_channels_selected(self): + def uptime(self): """ - Retrieves all selected channels of the mixer. + Retrieves the uptime of Liquidsoap. """ - cnt = 0 - activeinputs = [] + data = self.player.connector.send_lqc_command("uptime", "") + return data - self.enable_transaction() - inputs = self.mixer_channels() - for channel in inputs: - status = self.channel_status(cnt) - if "selected=true" in status: - activeinputs.append(channel) - cnt = cnt + 1 + @staticmethod + def engine_time(): + """ + Liquidsoap is slow in executing commands, therefore it's needed to schedule + actions by (n) seconds in advance, as defined in the configuration file by + the property `lqs_delay_offset`. it's important to note that this method + requires the class variable `EngineUtil.engine_time_offset` to be set on + Engine initialization. - self.disable_transaction() - return activeinputs + Returns: + (Integer): the Unix epoch timestamp including the offset + """ + return SU.timestamp() + Engine.engine_time_offset - def mixer_channels_except(self, input_type): + def terminate(self): """ - Retrieves all mixer channels except the ones of the given type. + Terminates the engine and all related processes. """ - try: - activemixer_copy = self.mixer_channels().copy() - activemixer_copy.remove(input_type) - except ValueError as e: - self.logger.error("Requested channel (%s) not in channel-list. Reason: %s" % (input_type, str(e))) - except AttributeError: - self.logger.critical("Empty channel list") + if self.sci: self.sci.terminate() - return activemixer_copy - def mixer_channels_reload(self): - """ - Reloads all mixer channels. +# +# PLAYER +# + + +class Player: + """ + Engine Player. + """ + config = None + logger = None + connector = None + channels = None + channel_router = None + event_dispatcher = None + + + # Mixer + mixer = None + mixer_fallback = None + + + + def __init__(self, config, event_dispatcher): """ - self.channels = None - return self.mixer_channels() + Constructor + Args: + config (AuraConfig): The configuration + """ + self.config = config + self.logger = logging.getLogger("AuraEngine") + self.event_dispatcher = event_dispatcher + self.connector = PlayerConnector(self.config, self.event_dispatcher) + self.channel_router = ChannelRouter(self.config, self.logger) + self.mixer = Mixer(self.config, MixerType.MAIN, self.connector) + self.mixer_fallback = Mixer(self.config, MixerType.FALLBACK, self.connector) - # - # MIXER : CONTROL SECTION - # def preroll(self, entry): @@ -211,19 +246,19 @@ class SoundSystem(): is_ready = False # LIVE - if entry.get_type() == ChannelType.LIVE: + if entry.get_content_type() in ResourceClass.LIVE.types: entry.channel = "linein_" + entry.source.split("line://")[1] is_ready = True else: - # Choose and save the input channel - entry.previous_channel, entry.channel = self.channel_swap(entry.get_type()) + channel_type = self.channel_router.type_for_resource(entry.get_content_type()) + entry.previous_channel, entry.channel = self.channel_router.channel_swap(channel_type) - # PLAYLIST - if entry.get_type() == ChannelType.FILESYSTEM: - is_ready = self.playlist_push(entry.channel, entry.source) + # QUEUE + if entry.get_content_type() in ResourceClass.FILE.types: + is_ready = self.queue_push(entry.channel, entry.source) # STREAM - elif entry.get_type() == ChannelType.HTTP or entry.get_type() == ChannelType.HTTPS: + elif entry.get_content_type() in ResourceClass.STREAM.types: is_ready = self.stream_load_entry(entry) if is_ready: @@ -233,7 +268,7 @@ class SoundSystem(): - def preroll_group(self, entries): + def preroll_group(self, entries, channel_type=ChannelType.QUEUE): """ 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 @@ -245,17 +280,18 @@ class SoundSystem(): `entry.state`. Args: - entries ([Entry]): An array holding filesystem entries + entries ([Entry]): An array holding filesystem entries + channel_type (ChannelType): The type of channel where it should be queued (optional) """ - channel = None + channels = None # Validate entry type for entry in entries: - if entry.get_type() != ChannelType.FILESYSTEM: + if entry.get_content_type() != ResourceType.FILE: raise InvalidChannelException - # Determine channel - channel = self.channel_swap(entries[0].get_type()) + # Determine channel + channels = self.channel_router.channel_swap(channel_type) # Queue entries for entry in entries: @@ -263,13 +299,13 @@ class SoundSystem(): self.logger.info("Loading entry '%s'" % entry) # Choose and save the input channel - entry.previous_channel, entry.channel = channel + entry.previous_channel, entry.channel = channels - if self.playlist_push(entry.channel, entry.source) == True: + if self.queue_push(entry.channel, entry.source) == True: entry.status = EntryPlayState.READY self.event_dispatcher.on_queue(entries) - + return channels def play(self, entry, transition): @@ -289,34 +325,42 @@ class SoundSystem(): """ with suppress(LQConnectionError): + + channel_type = self.channel_router.type_of_channel(entry.channel) + mixer = self.mixer + if channel_type == ChannelType.FALLBACK_QUEUE: + mixer = self.mixer_fallback # Instant activation or fade-in - self.enable_transaction() + self.connector.enable_transaction() if transition == TransitionType.FADE: - self.channel_select(entry.channel.value, True) - self.fade_in(entry) + mixer.channel_select(entry.channel.value, True) + mixer.fade_in(entry.channel, entry.volume) else: - self.channel_activate(entry.channel.value, True) - self.disable_transaction() + mixer.channel_activate(entry.channel.value, True) + self.connector.disable_transaction() - # Update active channel and type - self.active_channel[entry.get_type()] = entry.channel + # Update active channel for the current channel type + self.channel_router.set_active(channel_type, entry.channel) # 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: + if entry.previous_channel and \ + entry.previous_channel in ChannelType.QUEUE.channels and \ + entry.previous_channel in ChannelType.FALLBACK_QUEUE.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.connector.enable_transaction() + mixer.channel_activate(entry.previous_channel.value, False) + res = self.queue_clear(entry.previous_channel) self.logger.info("Clear Queue Response: " + res) - self.disable_transaction() + self.connector.disable_transaction() Thread(target=clean_up).start() # Filesystem meta-changes trigger the event via Liquidsoap - if not entry.channel in ChannelType.FILESYSTEM.channels: + if not entry.channel in ChannelType.QUEUE.channels: self.on_play(entry) @@ -342,171 +386,58 @@ class SoundSystem(): transition (TransitionType): The type of transition to use e.g. fade-out. """ with suppress(LQConnectionError): - self.enable_transaction() + self.connector.enable_transaction() if not entry.channel: self.logger.warn(SU.red("Trying to stop entry %s, but it has no channel assigned" % entry)) return if transition == TransitionType.FADE: - self.fade_out(entry) + self.mixer.fade_out(entry.channel, entry.volume) else: - self.channel_volume(entry.channel, 0) + self.mixer.channel_volume(entry.channel, 0) self.logger.info(SU.pink("Stopped channel '%s' for entry %s" % (entry.channel, entry))) - self.disable_transaction() + self.connector.disable_transaction() self.event_dispatcher.on_stop(entry) - # - # MIXER : CHANNEL - # - - - def channel_swap(self, channel_type): + def start_fallback_playlist(self, entries): """ - Returns the currently in-active channel for a given type. For example if the currently some - file on channel FILESYSTEM_A is playing, the channel FILESYSTEM B is returned for being used - to queue new entries. + Sets any scheduled fallback playlist and performs a fade-in. Args: - channel_type (ChannelType): The channel type such es filesystem, stream or live channel + entries ([Entry]): The playlist entries """ - previous_channel = self.active_channel[channel_type] - new_channel = None - msg = None - - if channel_type == ChannelType.FILESYSTEM: - if previous_channel == Channel.FILESYSTEM_A: - new_channel = Channel.FILESYSTEM_B - msg = "Swapped filesystem channel from A > B" - else: - new_channel = Channel.FILESYSTEM_A - msg = "Swapped filesystem channel from B > A" + self.preroll_group(entries, ChannelType.FALLBACK_QUEUE) + self.play(entries[0], TransitionType.FADE) - elif channel_type == ChannelType.HTTP: - if previous_channel == Channel.HTTP_A: - new_channel = Channel.HTTP_B - msg = "Swapped HTTP Stream channel from A > B" - else: - new_channel = Channel.HTTP_A - msg = "Swapped HTTP Stream channel from B > A" - elif channel_type == ChannelType.HTTPS: - if previous_channel == Channel.HTTPS_A: - new_channel = Channel.HTTPS_B - msg = "Swapped HTTPS Stream channel from A > B" - else: - new_channel = Channel.HTTPS_A - msg = "Swapped HTTPS Stream channel from B > A" - - if msg: self.logger.info(SU.pink(msg)) - return (previous_channel, new_channel) - - - def channel_status(self, channel_number): + def stop_fallback_playlist(self): """ - Retrieves the status of a channel identified by the channel number. + Performs a fade-out and clears any scheduled fallback playlist. """ - return self.__send_lqc_command__(self.client, "mixer", "status", channel_number) - + dirty_channel = self.channel_router.get_active(ChannelType.FALLBACK_QUEUE) + self.logger.info(f"Fading out channel '{dirty_channel}'") + self.connector.enable_transaction() + self.mixer_fallback.fade_out(dirty_channel, 100) + self.connector.disable_transaction() - def channel_select(self, channel, select): - """ - Selects/deselects some mixer channel - - Args: - pos (Integer): The channel number - select (Boolean): Select or deselect - - Returns: - (String): Liquidsoap server response - """ - channels = self.mixer_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): - """ - 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", "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 - - Args: - channel (Channel): The channel - volume (Integer) Volume between 0 and 100 - """ - channel = str(channel) - try: - if str(volume) == "100": - channels = self.mixer_channels() - index = channels.index(channel) - else: - channels = self.mixer_channels() - index = channels.index(channel) - except ValueError as e: - msg = SU.red("Cannot set volume of channel " + channel + " to " + str(volume) + "!. Reason: " + str(e)) - self.logger.error(msg) - self.logger.info("Available channels: %s" % str(channels)) - return - - try: - if len(channel) < 1: - msg = SU.red("Cannot set volume of channel " + channel + " to " + str(volume) + "! There are no channels.") - self.logger.warning(msg) - else: - message = self.__send_lqc_command__(self.client, "mixer", "volume", str(index), str(int(volume))) - - if not self.disable_logging: - if message.find('volume=' + str(volume) + '%'): - self.logger.info(SU.pink("Set volume of channel '%s' to %s" % (channel, str(volume)))) - else: - msg = SU.red("Setting volume of channel " + channel + " gone wrong! Liquidsoap message: " + message) - self.logger.warning(msg) - - return message - except AttributeError as e: #(LQConnectionError, AttributeError): - self.disable_transaction(force=True) - msg = SU.red("Ran into exception when setting volume of channel " + channel + ". Reason: " + str(e)) - self.logger.error(msg) + 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.connector.enable_transaction() + self.mixer_fallback.channel_activate(dirty_channel.value, False) + res = self.queue_clear(dirty_channel) + self.logger.info("Clear Fallback Queue Response: " + res) + self.connector.disable_transaction() + self.event_dispatcher.on_fallback_cleaned(dirty_channel) + Thread(target=clean_up).start() + # @@ -556,14 +487,14 @@ class SoundSystem(): """ result = None - self.enable_transaction() - result = self.__send_lqc_command__(self.client, channel, "stream_stop") + self.connector.enable_transaction() + result = self.connector.send_lqc_command(channel, "stream_stop") if result != LiquidsoapResponse.SUCCESS.value: self.logger.error("%s.stop result: %s" % (channel, result)) raise LQStreamException("Error while stopping stream!") - result = self.__send_lqc_command__(self.client, channel, "stream_set_url", url) + result = self.connector.send_lqc_command(channel, "stream_set_url", url) if result != LiquidsoapResponse.SUCCESS.value: self.logger.error("%s.set_url result: %s" % (channel, result)) @@ -572,10 +503,10 @@ class SoundSystem(): # Liquidsoap ignores commands sent without a certain timeout time.sleep(2) - result = self.__send_lqc_command__(self.client, channel, "stream_start") + result = self.connector.send_lqc_command(channel, "stream_start") self.logger.info("%s.start result: %s" % (channel, result)) - self.disable_transaction() + self.connector.disable_transaction() return result @@ -594,9 +525,9 @@ class SoundSystem(): """ result = None - self.enable_transaction() + self.connector.enable_transaction() - result = self.__send_lqc_command__(self.client, channel, "stream_status") + result = self.connector.send_lqc_command(channel, "stream_status") self.logger.info("%s.status result: %s" % (channel, result)) if not result.startswith(LiquidsoapResponse.STREAM_STATUS_CONNECTED.value): @@ -607,7 +538,7 @@ class SoundSystem(): self.logger.error("Wrong URL '%s' set for channel '%s', expected: '%s'." % (lqs_url, channel, url)) return False - self.disable_transaction() + self.connector.disable_transaction() stream_buffer = self.config.get("input_stream_buffer") self.logger.info("Ready to play stream, but wait %s seconds until the buffer is filled..." % str(stream_buffer)) @@ -616,16 +547,14 @@ class SoundSystem(): - # - # Channel Type - Filesystem + # Channel Type - Queue # - - def playlist_push(self, channel, uri): + def queue_push(self, channel, uri): """ - Adds an filesystem URI to the given `ChannelType.FILESYSTEM` channel. + Adds an filesystem URI to the given `ChannelType.QUEUE` channel. Args: channel (Channel): The channel to push the file to @@ -634,25 +563,27 @@ class SoundSystem(): Returns: (Boolean): `True` if successful """ - if channel not in ChannelType.FILESYSTEM.channels: - raise InvalidChannelException - self.logger.info(SU.pink("playlist.push('%s', '%s'" % (channel, uri))) + if channel not in ChannelType.QUEUE.channels and \ + channel not in ChannelType.FALLBACK_QUEUE.channels: + raise InvalidChannelException + self.logger.info(SU.pink("queue.push('%s', '%s'" % (channel, uri))) - self.enable_transaction() - audio_store = self.config.get("audiofolder") - filepath = EngineUtil.uri_to_filepath(audio_store, uri) - result = self.__send_lqc_command__(self.client, channel, "playlist_push", filepath) - self.logger.info("%s.playlist_push result: %s" % (channel, result)) - self.disable_transaction() + self.connector.enable_transaction() + audio_store = self.config.get("audio_source_folder") + extension = self.config.get("audio_source_extension") + filepath = ResourceUtil.uri_to_filepath(audio_store, uri, extension) + result = self.connector.send_lqc_command(channel, "queue_push", filepath) + self.logger.info("%s.queue_push result: %s" % (channel, result)) + self.connector.disable_transaction() # If successful, Liquidsoap returns a resource ID of the queued track return int(result) >= 0 - def playlist_seek(self, channel, seconds_to_seek): + def queue_seek(self, channel, seconds_to_seek): """ - Forwards the player of the given `ChannelType.FILESYSTEM` channel by (n) seconds. + Forwards the player of the given `ChannelType.QUEUE` channel by (n) seconds. Args: channel (Channel): The channel to push the file to @@ -661,21 +592,22 @@ class SoundSystem(): Returns: (String): Liquidsoap response """ - if channel not in ChannelType.FILESYSTEM.channels: - raise InvalidChannelException + if channel not in ChannelType.QUEUE.channels and \ + channel not in ChannelType.FALLBACK_QUEUE.channels: + raise InvalidChannelException - self.enable_transaction() - result = self.__send_lqc_command__(self.client, channel, "playlist_seek", str(seconds_to_seek)) - self.logger.info("%s.playlist_seek result: %s" % (channel, result)) - self.disable_transaction() + self.connector.enable_transaction() + result = self.connector.send_lqc_command(channel, "queue_seek", str(seconds_to_seek)) + self.logger.info("%s.seek result: %s" % (channel, result)) + self.connector.disable_transaction() return result - def playlist_clear(self, channel): + def queue_clear(self, channel): """ - Removes all tracks currently queued in the given `ChannelType.FILESYSTEM` channel. + Removes all tracks currently queued in the given `ChannelType.QUEUE` channel. Args: channel (Channel): The channel to push the file to @@ -683,406 +615,86 @@ class SoundSystem(): Returns: (String): Liquidsoap response """ - if channel not in ChannelType.FILESYSTEM.channels: - raise InvalidChannelException + if channel not in ChannelType.QUEUE.channels and \ + channel not in ChannelType.FALLBACK_QUEUE.channels: + raise InvalidChannelException self.logger.info(SU.pink("Clearing filesystem queue '%s'!" % channel)) - self.enable_transaction() - result = self.__send_lqc_command__(self.client, channel, "playlist_clear") - self.logger.info("%s.playlist_clear result: %s" % (channel, result)) - self.disable_transaction() + self.connector.enable_transaction() + result = self.connector.send_lqc_command(channel, "queue_clear") + self.logger.info("%s.clear result: %s" % (channel, result)) + self.connector.disable_transaction() return result # - # Fading + # Channel Type - Playlist # - def fade_in(self, entry): + def playlist_set_uri(self, channel, playlist_uri): """ - Performs a fade-in for the given `entry` to the `entry.volume` loudness - at channel `entry.channel`. + Sets the URI of a playlist. Args: - entry (Entry): The entry to fade - - Returns: - (Boolean): `True` if successful - """ - try: - fade_in_time = float(self.config.get("fade_in_time")) - - if fade_in_time > 0: - self.fade_in_active = True - target_volume = entry.volume - - step = fade_in_time / target_volume - - msg = "Starting to fading-in '%s'. Step is %ss and target volume is %s." % \ - (entry.channel, str(step), str(target_volume)) - self.logger.info(SU.pink(msg)) - - # Enable logging, which might have been disabled in a previous fade-out - self.disable_logging = True - self.client.disable_logging = True - - for i in range(target_volume): - self.channel_volume(entry.channel.value, i + 1) - time.sleep(step) - - msg = "Finished with fading-in '%s'." % entry.channel - self.logger.info(SU.pink(msg)) - - self.fade_in_active = False - if not self.fade_out_active: - self.disable_logging = False - self.client.disable_logging = False - - except LQConnectionError as e: - self.logger.critical(str(e)) - - return True - - + channel (Channel): The channel to push the file to + playlist_uri (String): The path to the playlist - def fade_out(self, entry): - """ - Performs a fade-out for the given `entry` at channel `entry.channel`. - - Args: - entry (Entry): The entry to fade - Returns: - (Boolean): `True` if successful + (String): Liquidsoap response """ - try: - fade_out_time = float(self.config.get("fade_out_time")) - - if fade_out_time > 0: - step = abs(fade_out_time) / entry.volume - - msg = "Starting to fading-out '%s'. Step is %ss." % (entry.channel, str(step)) - self.logger.info(SU.pink(msg)) - - # Disable logging... it is going to be enabled again after fadein and -out is finished - self.disable_logging = True - self.client.disable_logging = True - - for i in range(entry.volume): - self.channel_volume(entry.channel.value, entry.volume-i-1) - time.sleep(step) - - msg = "Finished with fading-out '%s'" % entry.channel - self.logger.info(SU.pink(msg)) - - # Enable logging again - self.fade_out_active = False - if not self.fade_in_active: - self.disable_logging = False - self.client.disable_logging = False - - except LQConnectionError as e: - self.logger.critical(str(e)) - - return True - - - - # - # Recording - # - - - # # ------------------------------------------------------------------------------------------ # - # def recorder_stop(self): - # self.enable_transaction() - - # for i in range(5): - # if self.config.get("rec_" + str(i)) == "y": - # self.__send_lqc_command__(self.client, "recorder_" + str(i), "stop") - - # self.disable_transaction() - - # # ------------------------------------------------------------------------------------------ # - # def recorder_start(self, num=-1): - # if not self.is_liquidsoap_running: - # if num==-1: - # msg = "Want to start recorder, but LiquidSoap is not running" - # else: - # msg = "Want to start recorder " + str(num) + ", but LiquidSoap is not running" - # self.logger.warning(msg) - # return False - - # self.enable_transaction() - - # if num == -1: - # self.recorder_start_all() - # else: - # self.recorder_start_one(num) - - # self.disable_transaction() + self.logger.info(SU.pink("Setting URI of playlist '%s' to '%s'" % (channel, playlist_uri))) - # # ------------------------------------------------------------------------------------------ # - # def recorder_start_all(self): - # if not self.is_liquidsoap_running: - # self.logger.warning("Want to start all recorder, but LiquidSoap is not running") - # return False + self.connector.enable_transaction() + result = self.connector.send_lqc_command(channel, "playlist_uri_set", playlist_uri) + self.logger.info("%s.playlist_uri result: %s" % (channel, result)) + self.connector.disable_transaction() - # self.enable_transaction() - # for i in range(5): - # self.recorder_start_one(i) - # self.disable_transaction() - - # # ------------------------------------------------------------------------------------------ # - # def recorder_start_one(self, num): - # if not self.is_liquidsoap_running: - # return False - - # if self.config.get("rec_" + str(num)) == "y": - # returnvalue = self.__send_lqc_command__(self.client, "recorder", str(num), "status") - - # if returnvalue == "off": - # self.__send_lqc_command__(self.client, "recorder", str(num), "start") - - # # ------------------------------------------------------------------------------------------ # - # def get_recorder_status(self): - # self.enable_transaction(self.client) - # recorder_state = self.__send_lqc_command__(self.client, "record", "status") - # self.disable_transaction(self.client) - - # return recorder_state - - - - # - # Basic Methods - # + return result - def init_player(self): - """ - Initializes the LiquidSoap Player after startup of the engine. - Returns: - (String): Message that the player is started. + def playlist_clear_uri(self, channel): """ - t = StartupThread(self) - t.start() - - return "Engine Core startup done!" - - - # ------------------------------------------------------------------------------------------ # - def __send_lqc_command__(self, lqs_instance, namespace, command, *args): - """ - Ein Kommando an Liquidsoap senden - @type lqs_instance: object - @param lqs_instance: Instance of LiquidSoap Client - @type namespace: string - @param namespace: Namespace of function - @type command: string - @param command: Function name - @type args: list - @param args: List of parameters - @rtype: string - @return: Response from LiquidSoap - """ - try: - if not self.disable_logging: - if namespace == "recorder": - self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + "_" + str(command) + "." + str(args)) - else: - if command == "": - self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + str(args)) - else: - self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + "." + str(command) + str(args)) - - # call wanted function ... - - # FIXME REFACTOR all calls in a common way - if command in ["playlist_push", "playlist_seek", "playlist_clear", "stream_set_url", "stream_start", "stream_stop", "stream_status"]: - func = getattr(lqs_instance, command) - result = func(str(namespace), *args) - else: - func = getattr(lqs_instance, namespace) - result = func(command, *args) - - - if not self.disable_logging: - self.logger.debug("LiquidSoapCommunicator got response " + str(result)) - - self.connection_attempts = 0 - - return result - - except LQConnectionError as e: - self.logger.error("Connection Error when sending " + str(namespace) + "." + str(command) + str(args)) - if self.try_to_reconnect(): - time.sleep(0.2) - self.connection_attempts += 1 - if self.connection_attempts < 5: - # reconnect - self.__open_conn(self.client) - self.logger.info("Trying to resend " + str(namespace) + "." + str(command) + str(args)) - # grab return value - retval = self.__send_lqc_command__(lqs_instance, namespace, command, *args) - # disconnect - self.__close_conn(self.client) - # return the val - return retval - else: - if command == "": - msg = "Rethrowing Exception while trying to send " + str(namespace) + str(args) - else: - msg = "Rethrowing Exception while trying to send " + str(namespace) + "." + str(command) + str(args) - - self.logger.info(msg) - self.disable_transaction(socket=self.client, force=True) - raise e - else: - self.event_dispatcher.on_critical("Criticial Liquidsoap connection issue", \ - "Could not connect to Liquidsoap (Multiple attempts)", e) - raise e + Clears the URI of a playlist. + Args: + channel (Channel): The channel to push the file to - def is_active(self): - """ - Checks if Liquidsoap is running + Returns: + (String): Liquidsoap response """ - try: - self.uptime() - self.is_liquidsoap_running = True - except LQConnectionError as e: - self.logger.info("Liquidsoap is not running so far") - self.is_liquidsoap_running = False - except Exception as e: - self.logger.error("Cannot check if Liquidsoap is running. Reason: " + str(e)) - self.is_liquidsoap_running = False + self.logger.info(SU.pink("Clearing URI of playlist '%s'" % (channel))) - return self.is_liquidsoap_running + self.connector.enable_transaction() + result = self.connector.send_lqc_command(channel, "playlist_uri_clear") + self.logger.info("%s.playlist_uri_clear result: %s" % (channel, result)) + self.connector.disable_transaction() + return result - def engine_state(self): - """ - Retrieves the state of all inputs and outputs. - """ - state = self.__send_lqc_command__(self.client, "engine", "state") - return state - def liquidsoap_help(self): - """ - Retrieves the Liquidsoap help. - """ - data = self.__send_lqc_command__(self.client, "help", "") - if not data: - self.logger.warning("Could not get Liquidsoap's help") - else: - self.logger.debug("Got Liquidsoap's help") - return data - def version(self): - """ - Get the version of Liquidsoap. +class EngineSplash: + + @staticmethod + def splash_screen(component, version): """ - data = self.__send_lqc_command__(self.client, "version", "") - self.logger.debug("Got Liquidsoap's version") - return data - - - def uptime(self): - """ - Retrieves the uptime of Liquidsoap. + Prints the engine logo and version info. """ - data = self.__send_lqc_command__(self.client, "uptime", "") - self.logger.debug("Got Liquidsoap's uptime") - return data - - - # - # Connection and Transaction Handling - # - - - # ------------------------------------------------------------------------------------------ # - def try_to_reconnect(self): - self.enable_transaction() - return self.transaction > 0 - - # ------------------------------------------------------------------------------------------ # - def enable_transaction(self, socket=None): - # set socket to playout if nothing else is given - if socket is None: - socket = self.client - - self.transaction = self.transaction + 1 - - self.logger.debug(TerminalColors.WARNING.value + "Enabling transaction! cnt: " + str(self.transaction) + TerminalColors.ENDC.value) - - if self.transaction > 1: - return - - try: - self.__open_conn(socket) - except FileNotFoundError: - self.disable_transaction(socket=socket, force=True) - subject = "CRITICAL Exception when connecting to Liquidsoap" - msg = "socket file " + socket.socket_path + " not found. Is liquidsoap running?" - self.logger.critical(SU.red(msg)) - self.event_dispatcher.on_critical(subject, msg, None) - - - # ------------------------------------------------------------------------------------------ # - def disable_transaction(self, socket=None, force=False): - if not force: - # nothing to disable - if self.transaction == 0: - return - - # decrease transaction counter - self.transaction = self.transaction - 1 - - # debug msg - self.logger.debug(TerminalColors.WARNING.value + "DISabling transaction! cnt: " + str(self.transaction) + TerminalColors.ENDC.value) - - # return if connection is still needed - if self.transaction > 0: - return - else: - self.logger.debug(TerminalColors.WARNING.value + "Forcefully DISabling transaction! " + TerminalColors.ENDC.value) - - # close conn and set transactioncounter to 0 - self.__close_conn(socket) - self.transaction = 0 - - # ------------------------------------------------------------------------------------------ # - def __open_conn(self, socket): - # already connected - if self.transaction > 1: - return - - self.logger.debug(TerminalColors.GREEN.value + "LiquidSoapCommunicator opening conn" + TerminalColors.ENDC.value) - - # try to connect - socket.connect() - - # ------------------------------------------------------------------------------------------ # - def __close_conn(self, socket): - # set socket to playout - if socket is None: - socket = self.client - - # do not disconnect if a transaction is going on - if self.transaction > 0: - return + return """\n + █████╗ ██╗ ██╗██████╗ █████╗ ███████╗███╗ ██╗ ██████╗ ██╗███╗ ██╗███████╗ + ██╔â•â•â–ˆâ–ˆâ•—██║ ██║██╔â•â•â–ˆâ–ˆâ•—██╔â•â•â–ˆâ–ˆâ•— ██╔â•â•â•â•â•â–ˆâ–ˆâ–ˆâ–ˆâ•— ██║██╔â•â•â•â•â• ██║████╗ ██║██╔â•â•â•â•â• + ███████║██║ ██║██████╔â•â–ˆâ–ˆâ–ˆâ–ˆâ–ˆâ–ˆâ–ˆâ•‘ █████╗ ██╔██╗ ██║██║ ███╗██║██╔██╗ ██║█████╗ + ██╔â•â•â–ˆâ–ˆâ•‘██║ ██║██╔â•â•â–ˆâ–ˆâ•—██╔â•â•â–ˆâ–ˆâ•‘ ██╔â•â•â• ██║╚██╗██║██║ ██║██║██║╚██╗██║██╔â•â•â• + ██║ ██║╚██████╔â•â–ˆâ–ˆâ•‘ ██║██║ ██║ ███████╗██║ ╚████║╚██████╔â•â–ˆâ–ˆâ•‘██║ ╚████║███████╗ + â•šâ•â• â•šâ•â• â•šâ•â•â•â•â•â• â•šâ•â• â•šâ•â•â•šâ•â• â•šâ•â• â•šâ•â•â•â•â•â•â•â•šâ•â• â•šâ•â•â•â• â•šâ•â•â•â•â•â• â•šâ•â•â•šâ•â• â•šâ•â•â•â•â•šâ•â•â•â•â•â•â• + %s v%s - Ready to play! + \n""" % (component, version) - # say bye - socket.byebye() - # debug msg - self.logger.debug(TerminalColors.BLUE.value + "LiquidSoapCommunicator closed conn" + TerminalColors.ENDC.value)