engine.py 23.4 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

David Trattnig's avatar
David Trattnig committed
28
29
30
from src.base.config        import AuraConfig
from src.base.utils         import SimpleUtil as SU
from src.base.exceptions    import LQConnectionError, InvalidChannelException, LQStreamException, LoadSourceException
31
from src.resources          import ResourceClass, ResourceUtil
32
from src.channels           import ChannelType, TransitionType, LiquidsoapResponse, EntryPlayState, ResourceType, ChannelRouter, ChannelResolver
David Trattnig's avatar
David Trattnig committed
33
34
35
36
from src.events             import EngineEventDispatcher
from src.control            import EngineControlInterface
from src.mixer              import Mixer, MixerType
from src.client.connector   import PlayerConnector
37

38

39

40

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

56
57

    def __init__(self):
58
        """
59
        Constructor
60
        """
David Trattnig's avatar
David Trattnig committed
61
        if Engine.instance:
62
63
64
            raise Exception("Engine is already running!")
        Engine.instance = self
        self.logger = logging.getLogger("AuraEngine")
65
        self.config = AuraConfig.config()
David Trattnig's avatar
David Trattnig committed
66
        Engine.engine_time_offset = float(self.config.get("engine_latency_offset"))
67
68

        self.plugins = dict()
69
        self.channel_router = ChannelRouter(self.config, self.logger)
70
71
72
        self.start()


73
74
75

    def start(self):
        """
76
77
        Starts the engine. Called when the connection to the sound-system implementation
        has been established.
78
        """
79
80
        self.event_dispatcher = EngineEventDispatcher(self)
        self.eci = EngineControlInterface(self, self.event_dispatcher)
81
        self.connector = PlayerConnector(self.event_dispatcher)
82
        self.event_dispatcher.on_initialized()
83
84

        while not self.is_connected():
85
86
            self.logger.info(SU.yellow("Waiting for Liquidsoap to be running ..."))
            time.sleep(2)
87
        self.logger.info(SU.green("Engine Core ------[ connected ]-------- Liquidsoap"))
88

89
        self.player = Player(self.connector, self.event_dispatcher)
90
        self.event_dispatcher.on_boot()
91
        self.logger.info(EngineSplash.splash_screen("Engine Core", meta.__version__))
92
        self.event_dispatcher.on_ready()
David Trattnig's avatar
David Trattnig committed
93
94


95
96
97

    #
    #   Basic Methods
98
    #
99

100

101
    def is_connected(self):
102
        """
103
        Checks if there's a valid connection to Liquidsoap.
David Trattnig's avatar
David Trattnig committed
104
        """
105
        has_connection = False
106
107
        try:
            self.uptime()
108
            has_connection = True
109
110
111
112
        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))
113

114
        return has_connection
115
116


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

David Trattnig's avatar
David Trattnig committed
124

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


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

David Trattnig's avatar
David Trattnig committed
142

143
144
145
146
147
    @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
David Trattnig's avatar
David Trattnig committed
148
        the property `engine_latency_offset`. it's important to note that this method
149
150
        requires the class variable `EngineUtil.engine_time_offset` to be set on
        Engine initialization.
151

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

157

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


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



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


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



194
    def __init__(self, connector, event_dispatcher):
David Trattnig's avatar
David Trattnig committed
195
        """
196
        Constructor
197

198
199
200
        Args:
            config (AuraConfig):    The configuration
        """
201
        self.config = AuraConfig.config()
202
        self.logger = logging.getLogger("AuraEngine")
203
        self.event_dispatcher = event_dispatcher
204
        self.connector = connector
205
206
207
        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)
208
209
210



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

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

219
220
        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
David Trattnig's avatar
David Trattnig committed
221
222
223
224
        `entry.state`.

        Args:
            entries ([Entry]):    An array holding filesystem entries
David Trattnig's avatar
David Trattnig committed
225
226
227
        """
        entry.status = EntryPlayState.LOADING
        self.logger.info("Loading entry '%s'" % entry)
228
        is_ready = False
229

David Trattnig's avatar
David Trattnig committed
230
        # LIVE
231
        if entry.get_content_type() in ResourceClass.LIVE.types:
232
233
234
235
            entry.channel = ChannelResolver.live_channel_for_resource(entry.source)
            if entry.channel == None:
                self.logger.critical(SU.red("Invalid live channel '{entry.source}' requested!"))
            entry.previous_channel = None
236
            is_ready = True
237
        else:
238
239
            channel_type = self.channel_router.type_for_resource(entry.get_content_type())
            entry.previous_channel, entry.channel = self.channel_router.channel_swap(channel_type)
240

241
242
243
        # QUEUE
        if entry.get_content_type() in ResourceClass.FILE.types:
            is_ready = self.queue_push(entry.channel, entry.source)
244

David Trattnig's avatar
David Trattnig committed
245
        # STREAM
246
        elif entry.get_content_type() in ResourceClass.STREAM.types:
247
            is_ready = self.stream_load_entry(entry)
David Trattnig's avatar
David Trattnig committed
248

249
        if is_ready:
250
            entry.status = EntryPlayState.READY
251

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



256
    def preload_group(self, entries, channel_type=ChannelType.QUEUE):
David Trattnig's avatar
David Trattnig committed
257
        """
258
        Pre-Load multiple filesystem entries at once. This call is required before the
David Trattnig's avatar
David Trattnig committed
259
        actual `play(..)` can happen. Due to their nature, non-filesystem entries cannot be queued
260
        using this method. In this case use `preload(self, entry)` instead. This method also allows
David Trattnig's avatar
David Trattnig committed
261
262
        queuing of very short files, such as jingles.

263
264
        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
David Trattnig's avatar
David Trattnig committed
265
266
267
        `entry.state`.

        Args:
268
269
            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
270
        """
271
        channels = None
David Trattnig's avatar
David Trattnig committed
272
273
274

        # Validate entry type
        for entry in entries:
275
            if entry.get_content_type() != ResourceType.FILE:
David Trattnig's avatar
David Trattnig committed
276
                raise InvalidChannelException
277
278

        # Determine channel
279
        channels = self.channel_router.channel_swap(channel_type)
David Trattnig's avatar
David Trattnig committed
280
281
282
283
284
285
286

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

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

289
            if self.queue_push(entry.channel, entry.source) == True:
David Trattnig's avatar
David Trattnig committed
290
                entry.status = EntryPlayState.READY
291

292
        self.event_dispatcher.on_queue(entries)
293
        return channels
David Trattnig's avatar
David Trattnig committed
294

295
296

    def play(self, entry, transition):
David Trattnig's avatar
David Trattnig committed
297
        """
298
        Plays a new `Entry`. In case of a new timeslot (or some intented, immediate transition),
299
        a clean channel is selected and transitions between old and new channel is performed.
David Trattnig's avatar
David Trattnig committed
300

301
        This method expects that the entry is pre-loaded using `preload(..)` or `preload_group(self, entries)`
302
        before being played. In case the pre-roll has happened for a group of entries, only the
David Trattnig's avatar
David Trattnig committed
303
        first entry of the group needs to be passed.
David Trattnig's avatar
David Trattnig committed
304

David Trattnig's avatar
David Trattnig committed
305
        Args:
David Trattnig's avatar
David Trattnig committed
306
            entry (PlaylistEntry):          The audio source to be played
David Trattnig's avatar
David Trattnig committed
307
            transition (TransitionType):    The type of transition to use e.g. fade-in or instant volume level.
308
            queue (Boolean):                If `True` the entry is queued if the `ChannelType` does allow so;
309
                otherwise a new channel of the same type is activated
310

David Trattnig's avatar
David Trattnig committed
311
        """
David Trattnig's avatar
David Trattnig committed
312
        with suppress(LQConnectionError):
313

314
315
316
317
            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
318
319

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

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

            # Dear filesystem channels, please leave the room as you would like to find it!
332
333
334
335
            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
336
337
338
339
                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)
340
341
342
                    self.connector.enable_transaction()
                    mixer.channel_activate(entry.previous_channel.value, False)
                    res = self.queue_clear(entry.previous_channel)
343
                    self.logger.info("Clear Queue Response: " + res)
344
                    self.connector.disable_transaction()
David Trattnig's avatar
David Trattnig committed
345
                Thread(target=clean_up).start()
346

347
            self.event_dispatcher.on_play(entry)
348
349
350
351



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

        Args:
356
357
            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
358
        """
David Trattnig's avatar
David Trattnig committed
359
        with suppress(LQConnectionError):
360
            self.connector.enable_transaction()
361

362
            if not entry.channel:
363
                self.logger.warn(SU.red("Trying to stop entry %s, but it has no channel assigned" % entry))
364
                return
365

366
            if transition == TransitionType.FADE:
367
                self.mixer.fade_out(entry.channel)
368
            else:
369
                self.mixer.channel_volume(entry.channel, 0)
370

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

David Trattnig's avatar
David Trattnig committed
375

376

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

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

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
        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)
409
            self.connector.disable_transaction()
410
411
            self.event_dispatcher.on_fallback_cleaned(dirty_channel)
        Thread(target=clean_up).start()
412

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
        """
        self.stream_load(entry.channel, entry.source)
        time.sleep(1)

433
        retry_delay = self.config.get("input_stream_retry_delay")
David Trattnig's avatar
David Trattnig committed
434
435
436
437
438
439
        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

        Args:
            channel (Channel): The stream channel
455
            url (String):      The stream URL
David Trattnig's avatar
David Trattnig committed
456
457
458

        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")
464

David Trattnig's avatar
David Trattnig committed
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

        Args:
            channel (Channel): The stream channel
493
            url (String):      The stream URL
David Trattnig's avatar
David Trattnig committed
494
495
496

        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, source):
528
        """
529
        Adds an filesystem URI to the given `ChannelType.QUEUE` channel.
530

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

535
        Returns:
David Trattnig's avatar
David Trattnig committed
536
            (Boolean):  `True` if successful
537
        """
538
539
540
        if channel not in ChannelType.QUEUE.channels and \
            channel not in ChannelType.FALLBACK_QUEUE.channels:
                raise InvalidChannelException
541

542
        self.connector.enable_transaction()
543
        audio_store = self.config.abs_audio_store_path()
544
        extension = self.config.get("audio_source_extension")
545
546
        filepath = ResourceUtil.source_to_filepath(audio_store, source, extension)
        self.logger.info(SU.pink(f"{channel}.queue_push('{filepath}')"))
547
548
549
        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
        # If successful, Liquidsoap returns a resource ID of the queued track
552
553
554
555
556
557
558
559
        resource_id = -1
        try:
            resource_id = int(result)
        except ValueError:
            self.logger.error(SU.red("Got an invalid resource ID: '%s'" % result))
            return False

        return resource_id >= 0
560
561
562



563
    def queue_seek(self, channel, seconds_to_seek):
564
        """
565
        Forwards the player of the given `ChannelType.QUEUE` channel by (n) seconds.
566

567
        Args:
David Trattnig's avatar
David Trattnig committed
568
            channel (Channel): The channel to push the file to
569
            seconds_to_seeks (Float):   The seconds to skip
David Trattnig's avatar
David Trattnig committed
570
571
572

        Returns:
            (String):   Liquidsoap response
573
        """
574
575
576
        if channel not in ChannelType.QUEUE.channels and \
            channel not in ChannelType.FALLBACK_QUEUE.channels:
                raise InvalidChannelException
577

578
579
580
581
        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()
582
583

        return result
584
585


David Trattnig's avatar
David Trattnig committed
586

587
    def queue_clear(self, channel):
588
        """
589
        Removes all tracks currently queued in the given `ChannelType.QUEUE` channel.
David Trattnig's avatar
David Trattnig committed
590
591
592
593
594
595

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

        Returns:
            (String):   Liquidsoap response
596
        """
597
598
599
        if channel not in ChannelType.QUEUE.channels and \
            channel not in ChannelType.FALLBACK_QUEUE.channels:
                raise InvalidChannelException
600

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

603
604
605
606
        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()
607

608
        return result
609

David Trattnig's avatar
David Trattnig committed
610
611


612
    #
613
    #   Channel Type - Playlist
614
615
    #

616

617
    def playlist_set_uri(self, channel, playlist_uri):
618
        """
619
        Sets the URI of a playlist.
David Trattnig's avatar
David Trattnig committed
620
621

        Args:
622
623
            channel (Channel): The channel to push the file to
            playlist_uri (String): The path to the playlist
624

David Trattnig's avatar
David Trattnig committed
625
        Returns:
626
            (String):   Liquidsoap response
627
        """
628
        self.logger.info(SU.pink("Setting URI of playlist '%s' to '%s'" % (channel, playlist_uri)))
629

630
631
632
633
        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()
634

635
        return result
636
637
638



639
    def playlist_clear_uri(self, channel):
640
        """
641
        Clears the URI of a playlist.
642

643
644
        Args:
            channel (Channel): The channel to push the file to
David Trattnig's avatar
David Trattnig committed
645

646
647
        Returns:
            (String):   Liquidsoap response
David Trattnig's avatar
David Trattnig committed
648
        """
649
        self.logger.info(SU.pink("Clearing URI of playlist '%s'" % (channel)))
650

651
652
653
654
        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()
655

656
        return result
David Trattnig's avatar
David Trattnig committed
657

658

David Trattnig's avatar
David Trattnig committed
659

660

David Trattnig's avatar
David Trattnig committed
661

662
class EngineSplash:
663

664
665
    @staticmethod
    def splash_screen(component, version):
666
        """
667
        Prints the engine logo and version info.
668
        """
669
670
671
        return """\n
             █████╗ ██╗   ██╗██████╗  █████╗     ███████╗███╗   ██╗ ██████╗ ██╗███╗   ██╗███████╗
            ██╔══██╗██║   ██║██╔══██╗██╔══██╗    ██╔════╝████╗  ██║██╔════╝ ██║████╗  ██║██╔════╝
672
673
            ███████║██║   ██║██████╔╝███████║    █████╗  ██╔██╗ ██║██║  ███╗██║██╔██╗ ██║█████╗
            ██╔══██║██║   ██║██╔══██╗██╔══██║    ██╔══╝  ██║╚██╗██║██║   ██║██║██║╚██╗██║██╔══╝
674
675
676
            ██║  ██║╚██████╔╝██║  ██║██║  ██║    ███████╗██║ ╚████║╚██████╔╝██║██║ ╚████║███████╗
            ╚═╝  ╚═╝ ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝    ╚══════╝╚═╝  ╚═══╝ ╚═════╝ ╚═╝╚═╝  ╚═══╝╚══════╝
            %s v%s - Ready to play!
677
        \n""" % (component, version)
678

679