engine.py 23.5 KB
Newer Older
1
#
David Trattnig's avatar
David Trattnig committed
2
# Aura Engine (https://gitlab.servus.at/aura/engine)
3
#
David Trattnig's avatar
David Trattnig committed
4
# Copyright (C) 2017-2020 - The Aura Engine Team.
5
#
David Trattnig's avatar
David Trattnig committed
6
7
8
9
# 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.
10
#
David Trattnig's avatar
David Trattnig committed
11
12
13
14
# 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.
15
#
David Trattnig's avatar
David Trattnig committed
16
17
18
# 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/>.

19

20
21
import time
import logging
22

David Trattnig's avatar
David Trattnig committed
23
24
from contextlib                 import suppress
from threading                  import Thread
David Trattnig's avatar
David Trattnig committed
25

26
27
import meta

28
from modules.base.config        import AuraConfig
29
30
31
32
33
34
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
35
from modules.core.events        import EngineEventDispatcher
36
from modules.core.control       import EngineControlInterface
37
38
from modules.core.mixer         import Mixer, MixerType
from modules.core.liquidsoap.connector import PlayerConnector
39

40

41

42

43
44
45
class Engine():
    """
    The Engine.
46
    """
David Trattnig's avatar
David Trattnig committed
47
    instance = None
48
    engine_time_offset = 0.0
49
    logger = None
50
    eci = None
51
    channels = None
52
    channel_router = None
53
    scheduler = None
54
    event_dispatcher = None
55
56
    plugins = None
    connector = None
57

58
59

    def __init__(self):
60
        """
61
        Constructor
62
        """
David Trattnig's avatar
David Trattnig committed
63
        if Engine.instance:
64
65
66
67
68
            raise Exception("Engine is already running!")        
        Engine.instance = self         
        self.logger = logging.getLogger("AuraEngine")            
        self.config = AuraConfig.config()
        Engine.engine_time_offset = self.config.get("lqs_delay_offset")
69
        
70
        self.plugins = dict()                    
71
        self.channel_router = ChannelRouter(self.config, self.logger)
72
73
74
        self.start()


75
76
77

    def start(self):
        """
78
79
        Starts the engine. Called when the connection to the sound-system implementation
        has been established.
80
        """
81
82
        self.event_dispatcher = EngineEventDispatcher(self)
        self.eci = EngineControlInterface(self, self.event_dispatcher)
83
        self.player = Player(self.config, self.event_dispatcher)
84
        self.event_dispatcher.on_initialized()
85
86
87
88
        
        while not self.is_connected(): 
            self.logger.info(SU.yellow("Waiting for Liquidsoap to be running ..."))
            time.sleep(2)
89
        self.logger.info(SU.green("Engine Core ------[ connected ]-------- Liquidsoap"))
90

91
        self.event_dispatcher.on_boot()
92
        self.logger.info(EngineSplash.splash_screen("Engine Core", meta.__version__))
93
        self.event_dispatcher.on_ready()
David Trattnig's avatar
David Trattnig committed
94
95


96
97
98

    #
    #   Basic Methods
99
    #
100

101

102
    def is_connected(self):
103
        """
104
        Checks if there's a valid connection to Liquidsoap.
David Trattnig's avatar
David Trattnig committed
105
        """
106
        has_connection = False
107
108
        try:
            self.uptime()
109
            has_connection = True
110
111
112
113
        except LQConnectionError as e:
            self.logger.info("Liquidsoap is not running so far")
        except Exception as e:
            self.logger.error("Cannot check if Liquidsoap is running. Reason: " + str(e))
114

115
        return has_connection
116
117


118
119
120
121
122
123
    def engine_state(self):
        """
        Retrieves the state of all inputs and outputs.
        """
        state = self.player.connector.send_lqc_command("engine", "state")
        return state
David Trattnig's avatar
David Trattnig committed
124

David Trattnig's avatar
David Trattnig committed
125

126
    def version(self):
127
        """
128
        Get the version of Liquidsoap.
129
        """
130
131
        data = self.player.connector.send_lqc_command("version", "")
        return data
132
133


134
    def uptime(self):
David Trattnig's avatar
David Trattnig committed
135
        """
136
        Retrieves the uptime of Liquidsoap.
David Trattnig's avatar
David Trattnig committed
137
        """
138
        self.player.connector.enable_transaction()
139
        data = self.player.connector.send_lqc_command("uptime", "")
140
        self.player.connector.disable_transaction()
141
        return data
142

David Trattnig's avatar
David Trattnig committed
143

144
145
146
147
148
149
150
151
    @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.
152

153
154
155
156
        Returns:
            (Integer):  the Unix epoch timestamp including the offset
        """
        return SU.timestamp() + Engine.engine_time_offset
157

158

David Trattnig's avatar
David Trattnig committed
159
160
161
162
163
164
165
166
    @staticmethod
    def get_instance():
        """
        Returns the one and only engine.
        """
        return Engine.instance


167
    def terminate(self):
David Trattnig's avatar
David Trattnig committed
168
        """
169
        Terminates the engine and all related processes.
David Trattnig's avatar
David Trattnig committed
170
        """
171
        if self.eci: self.eci.terminate()
172
173
174



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#
#   PLAYER
#


class Player:
    """
    Engine Player.
    """
    config = None
    logger = None
    connector = None
    channels = None
    channel_router = None
    event_dispatcher = None
    mixer = None
    mixer_fallback = None



    def __init__(self, config, event_dispatcher):
David Trattnig's avatar
David Trattnig committed
196
        """
197
        Constructor
198

199
200
201
202
203
204
205
206
207
208
        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)
209
210
211



212
    def preload(self, entry):
David Trattnig's avatar
David Trattnig committed
213
        """
214
        Pre-Load the entry. This is required before the actual `play(..)` can happen.
David Trattnig's avatar
David Trattnig committed
215
216
217

        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
218
        one queue using `preload_playlist(self, entries)`.
219

David Trattnig's avatar
David Trattnig committed
220
221
222
223
224
225
        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
David Trattnig's avatar
David Trattnig committed
226
227
228
        """
        entry.status = EntryPlayState.LOADING
        self.logger.info("Loading entry '%s'" % entry)
229
        is_ready = False
230

David Trattnig's avatar
David Trattnig committed
231
        # LIVE
232
        if entry.get_content_type() in ResourceClass.LIVE.types:
233
            entry.channel = "linein_" + entry.source.split("line://")[1]
234
            is_ready = True
235
        else:
236
237
            channel_type = self.channel_router.type_for_resource(entry.get_content_type())
            entry.previous_channel, entry.channel = self.channel_router.channel_swap(channel_type)
238

239
240
241
        # QUEUE
        if entry.get_content_type() in ResourceClass.FILE.types:
            is_ready = self.queue_push(entry.channel, entry.source)
David Trattnig's avatar
David Trattnig committed
242
243
            
        # STREAM
244
        elif entry.get_content_type() in ResourceClass.STREAM.types:
245
            is_ready = self.stream_load_entry(entry)
David Trattnig's avatar
David Trattnig committed
246

247
        if is_ready:
248
            entry.status = EntryPlayState.READY
249

250
        self.event_dispatcher.on_queue([entry])
David Trattnig's avatar
David Trattnig committed
251
252
253



254
    def preload_group(self, entries, channel_type=ChannelType.QUEUE):
David Trattnig's avatar
David Trattnig committed
255
        """
256
        Pre-Load multiple filesystem entries at once. This call is required before the 
David Trattnig's avatar
David Trattnig committed
257
        actual `play(..)` can happen. Due to their nature, non-filesystem entries cannot be queued
258
        using this method. In this case use `preload(self, entry)` instead. This method also allows
David Trattnig's avatar
David Trattnig committed
259
260
261
262
263
264
265
        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:
266
267
            entries ([Entry]):          An array holding filesystem entries
            channel_type (ChannelType): The type of channel where it should be queued (optional)
David Trattnig's avatar
David Trattnig committed
268
        """
269
        channels = None
David Trattnig's avatar
David Trattnig committed
270
271
272

        # Validate entry type
        for entry in entries:
273
            if entry.get_content_type() != ResourceType.FILE:
David Trattnig's avatar
David Trattnig committed
274
275
                raise InvalidChannelException
        
276
277
        # Determine channel        
        channels = self.channel_router.channel_swap(channel_type)
David Trattnig's avatar
David Trattnig committed
278
279
280
281
282
283
284

        # Queue entries
        for entry in entries:
            entry.status = EntryPlayState.LOADING
            self.logger.info("Loading entry '%s'" % entry)

            # Choose and save the input channel
285
            entry.previous_channel, entry.channel = channels
David Trattnig's avatar
David Trattnig committed
286

287
            if self.queue_push(entry.channel, entry.source) == True:
David Trattnig's avatar
David Trattnig committed
288
289
                entry.status = EntryPlayState.READY
        
290
        self.event_dispatcher.on_queue(entries)
291
        return channels
David Trattnig's avatar
David Trattnig committed
292

293
294

    def play(self, entry, transition):
David Trattnig's avatar
David Trattnig committed
295
        """
296
297
        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.
David Trattnig's avatar
David Trattnig committed
298

299
        This method expects that the entry is pre-loaded using `preload(..)` or `preload_group(self, entries)`
David Trattnig's avatar
David Trattnig committed
300
301
        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.
David Trattnig's avatar
David Trattnig committed
302

David Trattnig's avatar
David Trattnig committed
303
        Args:
David Trattnig's avatar
David Trattnig committed
304
            entry (PlaylistEntry):          The audio source to be played
David Trattnig's avatar
David Trattnig committed
305
            transition (TransitionType):    The type of transition to use e.g. fade-in or instant volume level.
David Trattnig's avatar
David Trattnig committed
306
            queue (Boolean):                If `True` the entry is queued if the `ChannelType` does allow so; 
307
                otherwise a new channel of the same type is activated
David Trattnig's avatar
David Trattnig committed
308
309
        
        """
David Trattnig's avatar
David Trattnig committed
310
        with suppress(LQConnectionError):
311
312
313
314
315
            
            channel_type = self.channel_router.type_of_channel(entry.channel)
            mixer = self.mixer
            if channel_type == ChannelType.FALLBACK_QUEUE:
                mixer = self.mixer_fallback
David Trattnig's avatar
David Trattnig committed
316
317

            # Instant activation or fade-in
318
            self.connector.enable_transaction()
319
            if transition == TransitionType.FADE:
320
321
                mixer.channel_select(entry.channel.value, True)
                mixer.fade_in(entry.channel, entry.volume)
322
            else:
323
324
                mixer.channel_activate(entry.channel.value, True)
            self.connector.disable_transaction()
325

326
327
            # Update active channel for the current channel type            
            self.channel_router.set_active(channel_type, entry.channel)
David Trattnig's avatar
David Trattnig committed
328
329

            # Dear filesystem channels, please leave the room as you would like to find it!
330
331
332
333
            if entry.previous_channel and \
                entry.previous_channel in ChannelType.QUEUE.channels and \
                entry.previous_channel in ChannelType.FALLBACK_QUEUE.channels:

David Trattnig's avatar
David Trattnig committed
334
335
336
337
                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)
338
339
340
                    self.connector.enable_transaction()
                    mixer.channel_activate(entry.previous_channel.value, False)
                    res = self.queue_clear(entry.previous_channel)
341
                    self.logger.info("Clear Queue Response: " + res)
342
                    self.connector.disable_transaction()
David Trattnig's avatar
David Trattnig committed
343
                Thread(target=clean_up).start()
344
            
345
346
            # Filesystem meta-changes trigger the event via Liquidsoap, so only
            # issue event for LIVE and STREAM:
347
            if not entry.channel in ChannelType.QUEUE.channels:
348
                self.event_dispatcher.on_play(entry)
349
350
351
352



    def stop(self, entry, transition):
David Trattnig's avatar
David Trattnig committed
353
        """
354
        Stops the currently playing entry.
David Trattnig's avatar
David Trattnig committed
355
356

        Args:
357
358
            entry (Entry):                  The entry to stop playing
            transition (TransitionType):    The type of transition to use e.g. fade-out.
David Trattnig's avatar
David Trattnig committed
359
        """
David Trattnig's avatar
David Trattnig committed
360
        with suppress(LQConnectionError):
361
            self.connector.enable_transaction()
362

363
            if not entry.channel:
364
                self.logger.warn(SU.red("Trying to stop entry %s, but it has no channel assigned" % entry))
365
366
367
                return
            
            if transition == TransitionType.FADE:
368
                self.mixer.fade_out(entry.channel)
369
            else:
370
                self.mixer.channel_volume(entry.channel, 0)
371

372
            self.logger.info(SU.pink("Stopped channel '%s' for entry %s" % (entry.channel, entry)))
373
            self.connector.disable_transaction()
374
            self.event_dispatcher.on_stop(entry)
375

David Trattnig's avatar
David Trattnig committed
376

377

378
    def start_fallback_playlist(self, entries):
David Trattnig's avatar
David Trattnig committed
379
        """
380
        Sets any scheduled fallback playlist and performs a fade-in.
David Trattnig's avatar
David Trattnig committed
381
382

        Args:
383
            entries ([Entry]):    The playlist entries
David Trattnig's avatar
David Trattnig committed
384
        """
385
        self.preload_group(entries, ChannelType.FALLBACK_QUEUE)
386
        self.play(entries[0], TransitionType.FADE)
387

David Trattnig's avatar
David Trattnig committed
388

389

390
    def stop_fallback_playlist(self):
David Trattnig's avatar
David Trattnig committed
391
        """
392
        Performs a fade-out and clears any scheduled fallback playlist.
David Trattnig's avatar
David Trattnig committed
393
        """
394
        dirty_channel = self.channel_router.get_active(ChannelType.FALLBACK_QUEUE)
395

396
397
        self.logger.info(f"Fading out channel '{dirty_channel}'")
        self.connector.enable_transaction()
398
        self.mixer_fallback.fade_out(dirty_channel)
399
        self.connector.disable_transaction()  
400

401
402
403
404
405
406
407
408
409
410
411
412
        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()
        
413

414
415

    #
416
    #   Channel Type - Stream
417
418
    #

David Trattnig's avatar
David Trattnig committed
419
420
421
422

    def stream_load_entry(self, entry):
        """
        Loads the given stream entry and updates the entries's status codes.
David Trattnig's avatar
Docs.    
David Trattnig committed
423
424
425

        Args:
            entry (Entry):  The entry to be pre-loaded
David Trattnig's avatar
David Trattnig committed
426

David Trattnig's avatar
Docs.    
David Trattnig committed
427
428
        Returns:
            (Boolean):  `True` if successfull
David Trattnig's avatar
David Trattnig committed
429
430
431
432
433
434
435
436
437
438
439
        """
        self.stream_load(entry.channel, entry.source)
        time.sleep(1)

        retry_delay = self.config.get("input_stream_retry_delay") 
        max_retries =  self.config.get("input_stream_max_retries")
        retries = 0

        while not self.stream_is_ready(entry.channel, entry.source):
            self.logger.info("Loading Stream ...")
            if retries >= max_retries:
David Trattnig's avatar
David Trattnig committed
440
                raise LoadSourceException("Could not connect to stream while waiting for %s seconds!" % str(retries*retry_delay))
David Trattnig's avatar
David Trattnig committed
441
442
443
            time.sleep(retry_delay)
            retries += 1

444
        return True
David Trattnig's avatar
David Trattnig committed
445
446
447
448



    def stream_load(self, channel, url):
David Trattnig's avatar
David Trattnig committed
449
        """
David Trattnig's avatar
David Trattnig committed
450
451
        Preloads the stream URL on the given channel. Note this method is blocking
        some serious amount of time; hence it's worth being called asynchroneously.
David Trattnig's avatar
David Trattnig committed
452
453
454
455
456
457
458

        Args:
            channel (Channel): The stream channel
            uri (String):      The stream URL

        Returns:
            (Boolean):  `True` if successful
David Trattnig's avatar
David Trattnig committed
459
460
        """
        result = None
461

462
463
        self.connector.enable_transaction()
        result = self.connector.send_lqc_command(channel, "stream_stop")
David Trattnig's avatar
David Trattnig committed
464
465
        
        if result != LiquidsoapResponse.SUCCESS.value:
David Trattnig's avatar
David Trattnig committed
466
            self.logger.error("%s.stop result: %s" % (channel, result))
David Trattnig's avatar
David Trattnig committed
467
            raise LQStreamException("Error while stopping stream!")
468

469
        result = self.connector.send_lqc_command(channel, "stream_set_url", url)
470

David Trattnig's avatar
David Trattnig committed
471
        if result != LiquidsoapResponse.SUCCESS.value:
David Trattnig's avatar
David Trattnig committed
472
            self.logger.error("%s.set_url result: %s" % (channel, result))
David Trattnig's avatar
David Trattnig committed
473
            raise LQStreamException("Error while setting stream URL!")
474

David Trattnig's avatar
David Trattnig committed
475
476
        # Liquidsoap ignores commands sent without a certain timeout
        time.sleep(2)
477

478
        result = self.connector.send_lqc_command(channel, "stream_start")
David Trattnig's avatar
David Trattnig committed
479
        self.logger.info("%s.start result: %s" % (channel, result))
480

481
        self.connector.disable_transaction()
David Trattnig's avatar
David Trattnig committed
482
483
484
485
        return result



David Trattnig's avatar
David Trattnig committed
486
    def stream_is_ready(self, channel, url):
David Trattnig's avatar
David Trattnig committed
487
        """
David Trattnig's avatar
David Trattnig committed
488
489
        Checks if the stream on the given channel is ready to play. Note this method is blocking
        some serious amount of time even when successfull; hence it's worth being called asynchroneously.
David Trattnig's avatar
David Trattnig committed
490
491
492
493
494
495
496

        Args:
            channel (Channel): The stream channel
            uri (String):      The stream URL

        Returns:
            (Boolean):  `True` if successful
David Trattnig's avatar
David Trattnig committed
497
498
499
        """
        result = None

500
        self.connector.enable_transaction()
David Trattnig's avatar
David Trattnig committed
501

502
        result = self.connector.send_lqc_command(channel, "stream_status")
David Trattnig's avatar
David Trattnig committed
503
        self.logger.info("%s.status result: %s" % (channel, result))
David Trattnig's avatar
David Trattnig committed
504
505
506
507
508
509
510
511
512

        if not result.startswith(LiquidsoapResponse.STREAM_STATUS_CONNECTED.value):
            return False

        lqs_url = result.split(" ")[1]
        if not url == lqs_url:
            self.logger.error("Wrong URL '%s' set for channel '%s', expected: '%s'." % (lqs_url, channel, url))
            return False

513
        self.connector.disable_transaction()
David Trattnig's avatar
David Trattnig committed
514

David Trattnig's avatar
David Trattnig committed
515
516
517
        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))
        time.sleep(round(float(stream_buffer)))
David Trattnig's avatar
David Trattnig committed
518
        return True
519
520


David Trattnig's avatar
David Trattnig committed
521

522
    #
523
    #   Channel Type - Queue 
524
    #
David Trattnig's avatar
David Trattnig committed
525

526

527
    def queue_push(self, channel, uri):
528
        """
529
        Adds an filesystem URI to the given `ChannelType.QUEUE` channel.
530

531
        Args:
David Trattnig's avatar
David Trattnig committed
532
533
534
            channel (Channel): The channel to push the file to
            uri (String):      The URI of the file

535
        Returns:
David Trattnig's avatar
David Trattnig committed
536
            (Boolean):  `True` if successful
537
        """
538
539
540
541
        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)))
542

543
544
545
546
547
548
549
        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()
550

David Trattnig's avatar
David Trattnig committed
551
552
        # If successful, Liquidsoap returns a resource ID of the queued track
        return int(result) >= 0
553
554
555



556
    def queue_seek(self, channel, seconds_to_seek):
557
        """
558
        Forwards the player of the given `ChannelType.QUEUE` channel by (n) seconds.
559

560
        Args:
David Trattnig's avatar
David Trattnig committed
561
            channel (Channel): The channel to push the file to
562
            seconds_to_seeks (Float):   The seconds to skip
David Trattnig's avatar
David Trattnig committed
563
564
565

        Returns:
            (String):   Liquidsoap response
566
        """
567
568
569
        if channel not in ChannelType.QUEUE.channels and \
            channel not in ChannelType.FALLBACK_QUEUE.channels:
                raise InvalidChannelException
570

571
572
573
574
        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()
575
576

        return result
577
578


David Trattnig's avatar
David Trattnig committed
579

580
    def queue_clear(self, channel):
581
        """
582
        Removes all tracks currently queued in the given `ChannelType.QUEUE` channel.
David Trattnig's avatar
David Trattnig committed
583
584
585
586
587
588

        Args:
            channel (Channel): The channel to push the file to

        Returns:
            (String):   Liquidsoap response
589
        """
590
591
592
        if channel not in ChannelType.QUEUE.channels and \
            channel not in ChannelType.FALLBACK_QUEUE.channels:
                raise InvalidChannelException
593

594
        self.logger.info(SU.pink("Clearing filesystem queue '%s'!" % channel))
595

596
597
598
599
        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()
600

601
        return result
602

David Trattnig's avatar
David Trattnig committed
603
604


605
    #
606
    #   Channel Type - Playlist 
607
608
    #

609

610
    def playlist_set_uri(self, channel, playlist_uri):
611
        """
612
        Sets the URI of a playlist.
David Trattnig's avatar
David Trattnig committed
613
614

        Args:
615
616
            channel (Channel): The channel to push the file to
            playlist_uri (String): The path to the playlist
617

David Trattnig's avatar
David Trattnig committed
618
        Returns:
619
            (String):   Liquidsoap response
620
        """
621
        self.logger.info(SU.pink("Setting URI of playlist '%s' to '%s'" % (channel, playlist_uri)))
622

623
624
625
626
        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()
627

628
        return result
629
630
631



632
    def playlist_clear_uri(self, channel):
633
        """
634
        Clears the URI of a playlist.
635

636
637
        Args:
            channel (Channel): The channel to push the file to
David Trattnig's avatar
David Trattnig committed
638

639
640
        Returns:
            (String):   Liquidsoap response
David Trattnig's avatar
David Trattnig committed
641
        """
642
        self.logger.info(SU.pink("Clearing URI of playlist '%s'" % (channel)))
643

644
645
646
647
        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()
648

649
        return result
David Trattnig's avatar
David Trattnig committed
650

651

David Trattnig's avatar
David Trattnig committed
652

653

David Trattnig's avatar
David Trattnig committed
654

655
656
657
658
class EngineSplash:
    
    @staticmethod
    def splash_screen(component, version):
659
        """
660
        Prints the engine logo and version info.
661
        """
662
663
664
665
666
667
668
669
670
        return """\n
             █████╗ ██╗   ██╗██████╗  █████╗     ███████╗███╗   ██╗ ██████╗ ██╗███╗   ██╗███████╗
            ██╔══██╗██║   ██║██╔══██╗██╔══██╗    ██╔════╝████╗  ██║██╔════╝ ██║████╗  ██║██╔════╝
            ███████║██║   ██║██████╔╝███████║    █████╗  ██╔██╗ ██║██║  ███╗██║██╔██╗ ██║█████╗  
            ██╔══██║██║   ██║██╔══██╗██╔══██║    ██╔══╝  ██║╚██╗██║██║   ██║██║██║╚██╗██║██╔══╝  
            ██║  ██║╚██████╔╝██║  ██║██║  ██║    ███████╗██║ ╚████║╚██████╔╝██║██║ ╚████║███████╗
            ╚═╝  ╚═╝ ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝    ╚══════╝╚═╝  ╚═══╝ ╚═════╝ ╚═╝╚═╝  ╚═══╝╚══════╝
            %s v%s - Ready to play!
        \n""" % (component, version)       
671

672