engine.py 24.2 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
29
30
31
32
33
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
David Trattnig's avatar
David Trattnig committed
34
from modules.core.startup       import StartupThread
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
49
    engine_time_offset = 0.0

50
    logger = None
51
    eci = None
52
    channels = None
53
    channel_router = None
54
    scheduler = None
55
    event_dispatcher = None
56
57
58
    is_liquidsoap_running = False    
    plugins = None
    connector = None
59

60
    def __init__(self, config):
61
        """
62
        Constructor
63
64
65

        Args:
            config (AuraConfig):    The configuration
66
        """
David Trattnig's avatar
David Trattnig committed
67
68
69
        if Engine.instance:
            raise Exception("Engine is already running!")
        Engine.instance = self
70
        self.config = config
71
        self.plugins = dict()
72
        self.logger = logging.getLogger("AuraEngine")        
73
74
        self.eci = EngineControlInterface(self)
        
75
76
77
78

        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")        
79
        
80
81
82

    def start(self):
        """
83
84
        Starts the engine. Called when the connection to the sound-system implementation
        has been established.
85
        """
86
        self.event_dispatcher = EngineEventDispatcher(self, self.scheduler)
87
        
88
89
        # Sleep needed, because the socket is created too slowly by Liquidsoap
        time.sleep(1)
90
91
        self.player = Player(self.config, self.event_dispatcher)

92
        self.is_liquidsoap_running = True
93
94
95
        self.event_dispatcher.on_initialized()
        self.logger.info(SU.green("Engine Core ------[ connected ]-------- Liquidsoap"))
        self.event_dispatcher.on_boot()
96
        self.logger.info(EngineSplash.splash_screen("Engine Core", meta.__version__))
97
        self.event_dispatcher.on_ready()
David Trattnig's avatar
David Trattnig committed
98
99


100
101
102

    #
    #   Basic Methods
103
    #
104

105

106
    def init_player(self):
107
        """
108
109
110
111
        Initializes the LiquidSoap Player after startup of the engine.

        Returns:
            (String):   Message that the player is started.
112
        """
113
114
115
116
117
118
119
120
        t = StartupThread(self)
        t.start()

        return "Engine Core startup done!"



    def is_active(self):
David Trattnig's avatar
David Trattnig committed
121
        """
122
        Checks if Liquidsoap is running
David Trattnig's avatar
David Trattnig committed
123
        """
124
125
126
127
128
129
130
131
132
        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
133

134
        return self.is_liquidsoap_running
135
136


137
138
139
140
141
142
    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
143

David Trattnig's avatar
David Trattnig committed
144

145
    def version(self):
146
        """
147
        Get the version of Liquidsoap.
148
        """
149
150
        data = self.player.connector.send_lqc_command("version", "")
        return data
151
152


153
    def uptime(self):
David Trattnig's avatar
David Trattnig committed
154
        """
155
        Retrieves the uptime of Liquidsoap.
David Trattnig's avatar
David Trattnig committed
156
        """
157
158
        data = self.player.connector.send_lqc_command("uptime", "")
        return data
159

David Trattnig's avatar
David Trattnig committed
160

161
162
163
164
165
166
167
168
    @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.
169

170
171
172
173
        Returns:
            (Integer):  the Unix epoch timestamp including the offset
        """
        return SU.timestamp() + Engine.engine_time_offset
174

175

David Trattnig's avatar
David Trattnig committed
176
177
178
179
180
181
182
183
    @staticmethod
    def get_instance():
        """
        Returns the one and only engine.
        """
        return Engine.instance


184
    def terminate(self):
David Trattnig's avatar
David Trattnig committed
185
        """
186
        Terminates the engine and all related processes.
David Trattnig's avatar
David Trattnig committed
187
        """
188
        if self.eci: self.eci.terminate()
189
190
191



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#
#   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
213
        """
214
        Constructor
215

216
217
218
219
220
221
222
223
224
225
        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)
226
227
228



229
    def preload(self, entry):
David Trattnig's avatar
David Trattnig committed
230
        """
231
        Pre-Load the entry. This is required before the actual `play(..)` can happen.
David Trattnig's avatar
David Trattnig committed
232
233
234

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

David Trattnig's avatar
David Trattnig committed
237
238
239
240
241
242
        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
243
244
245
        """
        entry.status = EntryPlayState.LOADING
        self.logger.info("Loading entry '%s'" % entry)
246
        is_ready = False
247

David Trattnig's avatar
David Trattnig committed
248
        # LIVE
249
        if entry.get_content_type() in ResourceClass.LIVE.types:
250
            entry.channel = "linein_" + entry.source.split("line://")[1]
251
            is_ready = True
252
        else:
253
254
            channel_type = self.channel_router.type_for_resource(entry.get_content_type())
            entry.previous_channel, entry.channel = self.channel_router.channel_swap(channel_type)
255

256
257
258
        # 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
259
260
            
        # STREAM
261
        elif entry.get_content_type() in ResourceClass.STREAM.types:
262
            is_ready = self.stream_load_entry(entry)
David Trattnig's avatar
David Trattnig committed
263

264
        if is_ready:
265
            entry.status = EntryPlayState.READY
266

267
        self.event_dispatcher.on_queue([entry])
David Trattnig's avatar
David Trattnig committed
268
269
270



271
    def preload_group(self, entries, channel_type=ChannelType.QUEUE):
David Trattnig's avatar
David Trattnig committed
272
        """
273
        Pre-Load multiple filesystem entries at once. This call is required before the 
David Trattnig's avatar
David Trattnig committed
274
        actual `play(..)` can happen. Due to their nature, non-filesystem entries cannot be queued
275
        using this method. In this case use `preload(self, entry)` instead. This method also allows
David Trattnig's avatar
David Trattnig committed
276
277
278
279
280
281
282
        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:
283
284
            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
285
        """
286
        channels = None
David Trattnig's avatar
David Trattnig committed
287
288
289

        # Validate entry type
        for entry in entries:
290
            if entry.get_content_type() != ResourceType.FILE:
David Trattnig's avatar
David Trattnig committed
291
292
                raise InvalidChannelException
        
293
294
        # Determine channel        
        channels = self.channel_router.channel_swap(channel_type)
David Trattnig's avatar
David Trattnig committed
295
296
297
298
299
300
301

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

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

304
            if self.queue_push(entry.channel, entry.source) == True:
David Trattnig's avatar
David Trattnig committed
305
306
                entry.status = EntryPlayState.READY
        
307
        self.event_dispatcher.on_queue(entries)
308
        return channels
David Trattnig's avatar
David Trattnig committed
309

310
311

    def play(self, entry, transition):
David Trattnig's avatar
David Trattnig committed
312
        """
313
314
        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
315

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

David Trattnig's avatar
David Trattnig committed
320
        Args:
David Trattnig's avatar
David Trattnig committed
321
            entry (PlaylistEntry):          The audio source to be played
David Trattnig's avatar
David Trattnig committed
322
            transition (TransitionType):    The type of transition to use e.g. fade-in or instant volume level.
David Trattnig's avatar
David Trattnig committed
323
            queue (Boolean):                If `True` the entry is queued if the `ChannelType` does allow so; 
324
                otherwise a new channel of the same type is activated
David Trattnig's avatar
David Trattnig committed
325
326
        
        """
David Trattnig's avatar
David Trattnig committed
327
        with suppress(LQConnectionError):
328
329
330
331
332
            
            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
333
334

            # Instant activation or fade-in
335
            self.connector.enable_transaction()
336
            if transition == TransitionType.FADE:
337
338
                mixer.channel_select(entry.channel.value, True)
                mixer.fade_in(entry.channel, entry.volume)
339
            else:
340
341
                mixer.channel_activate(entry.channel.value, True)
            self.connector.disable_transaction()
342

343
344
            # Update active channel for the current channel type            
            self.channel_router.set_active(channel_type, entry.channel)
David Trattnig's avatar
David Trattnig committed
345
346

            # Dear filesystem channels, please leave the room as you would like to find it!
347
348
349
350
            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
351
352
353
354
                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)
355
356
357
                    self.connector.enable_transaction()
                    mixer.channel_activate(entry.previous_channel.value, False)
                    res = self.queue_clear(entry.previous_channel)
358
                    self.logger.info("Clear Queue Response: " + res)
359
                    self.connector.disable_transaction()
David Trattnig's avatar
David Trattnig committed
360
                Thread(target=clean_up).start()
361
362
            
            # Filesystem meta-changes trigger the event via Liquidsoap
363
            if not entry.channel in ChannelType.QUEUE.channels:
364
                self.on_play(entry)
365

David Trattnig's avatar
David Trattnig committed
366

367
368
369

    def on_play(self, source):
        """
370
        Event Handler which is called by the soundsystem implementation (i.e. Liquidsoap)
371
        when some entry is actually playing.
David Trattnig's avatar
David Trattnig committed
372
373

        Args:
374
            source (String):    The `Entry` or URI or of the media source currently being played
375
        """
376
        self.event_dispatcher.on_play(source)
377
378
379
380



    def stop(self, entry, transition):
David Trattnig's avatar
David Trattnig committed
381
        """
382
        Stops the currently playing entry.
David Trattnig's avatar
David Trattnig committed
383
384

        Args:
385
386
            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
387
        """
David Trattnig's avatar
David Trattnig committed
388
        with suppress(LQConnectionError):
389
            self.connector.enable_transaction()
390

391
            if not entry.channel:
392
                self.logger.warn(SU.red("Trying to stop entry %s, but it has no channel assigned" % entry))
393
394
395
                return
            
            if transition == TransitionType.FADE:
396
                self.mixer.fade_out(entry.channel)
397
            else:
398
                self.mixer.channel_volume(entry.channel, 0)
399

400
            self.logger.info(SU.pink("Stopped channel '%s' for entry %s" % (entry.channel, entry)))
401
            self.connector.disable_transaction()
402
            self.event_dispatcher.on_stop(entry)
403

David Trattnig's avatar
David Trattnig committed
404

405

406
    def start_fallback_playlist(self, entries):
David Trattnig's avatar
David Trattnig committed
407
        """
408
        Sets any scheduled fallback playlist and performs a fade-in.
David Trattnig's avatar
David Trattnig committed
409
410

        Args:
411
            entries ([Entry]):    The playlist entries
David Trattnig's avatar
David Trattnig committed
412
        """
413
        self.preload_group(entries, ChannelType.FALLBACK_QUEUE)
414
        self.play(entries[0], TransitionType.FADE)
415

David Trattnig's avatar
David Trattnig committed
416

417

418
    def stop_fallback_playlist(self):
David Trattnig's avatar
David Trattnig committed
419
        """
420
        Performs a fade-out and clears any scheduled fallback playlist.
David Trattnig's avatar
David Trattnig committed
421
        """
422
        dirty_channel = self.channel_router.get_active(ChannelType.FALLBACK_QUEUE)
423

424
425
        self.logger.info(f"Fading out channel '{dirty_channel}'")
        self.connector.enable_transaction()
426
        self.mixer_fallback.fade_out(dirty_channel)
427
        self.connector.disable_transaction()  
428

429
430
431
432
433
434
435
436
437
438
439
440
        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()
        
441

442
443

    #
444
    #   Channel Type - Stream
445
446
    #

David Trattnig's avatar
David Trattnig committed
447
448
449
450

    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
451
452
453

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

David Trattnig's avatar
Docs.    
David Trattnig committed
455
456
        Returns:
            (Boolean):  `True` if successfull
David Trattnig's avatar
David Trattnig committed
457
458
459
460
461
462
463
464
465
466
467
        """
        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
468
                raise LoadSourceException("Could not connect to stream while waiting for %s seconds!" % str(retries*retry_delay))
David Trattnig's avatar
David Trattnig committed
469
470
471
            time.sleep(retry_delay)
            retries += 1

472
        return True
David Trattnig's avatar
David Trattnig committed
473
474
475
476



    def stream_load(self, channel, url):
David Trattnig's avatar
David Trattnig committed
477
        """
David Trattnig's avatar
David Trattnig committed
478
479
        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
480
481
482
483
484
485
486

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

        Returns:
            (Boolean):  `True` if successful
David Trattnig's avatar
David Trattnig committed
487
488
        """
        result = None
489

490
491
        self.connector.enable_transaction()
        result = self.connector.send_lqc_command(channel, "stream_stop")
David Trattnig's avatar
David Trattnig committed
492
493
        
        if result != LiquidsoapResponse.SUCCESS.value:
David Trattnig's avatar
David Trattnig committed
494
            self.logger.error("%s.stop result: %s" % (channel, result))
David Trattnig's avatar
David Trattnig committed
495
            raise LQStreamException("Error while stopping stream!")
496

497
        result = self.connector.send_lqc_command(channel, "stream_set_url", url)
498

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

David Trattnig's avatar
David Trattnig committed
503
504
        # Liquidsoap ignores commands sent without a certain timeout
        time.sleep(2)
505

506
        result = self.connector.send_lqc_command(channel, "stream_start")
David Trattnig's avatar
David Trattnig committed
507
        self.logger.info("%s.start result: %s" % (channel, result))
508

509
        self.connector.disable_transaction()
David Trattnig's avatar
David Trattnig committed
510
511
512
513
        return result



David Trattnig's avatar
David Trattnig committed
514
    def stream_is_ready(self, channel, url):
David Trattnig's avatar
David Trattnig committed
515
        """
David Trattnig's avatar
David Trattnig committed
516
517
        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
518
519
520
521
522
523
524

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

        Returns:
            (Boolean):  `True` if successful
David Trattnig's avatar
David Trattnig committed
525
526
527
        """
        result = None

528
        self.connector.enable_transaction()
David Trattnig's avatar
David Trattnig committed
529

530
        result = self.connector.send_lqc_command(channel, "stream_status")
David Trattnig's avatar
David Trattnig committed
531
        self.logger.info("%s.status result: %s" % (channel, result))
David Trattnig's avatar
David Trattnig committed
532
533
534
535
536
537
538
539
540

        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

541
        self.connector.disable_transaction()
David Trattnig's avatar
David Trattnig committed
542

David Trattnig's avatar
David Trattnig committed
543
544
545
        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
546
        return True
547
548


David Trattnig's avatar
David Trattnig committed
549

550
    #
551
    #   Channel Type - Queue 
552
    #
David Trattnig's avatar
David Trattnig committed
553

554

555
    def queue_push(self, channel, uri):
556
        """
557
        Adds an filesystem URI to the given `ChannelType.QUEUE` channel.
558

559
        Args:
David Trattnig's avatar
David Trattnig committed
560
561
562
            channel (Channel): The channel to push the file to
            uri (String):      The URI of the file

563
        Returns:
David Trattnig's avatar
David Trattnig committed
564
            (Boolean):  `True` if successful
565
        """
566
567
568
569
        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)))
570

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

David Trattnig's avatar
David Trattnig committed
579
580
        # If successful, Liquidsoap returns a resource ID of the queued track
        return int(result) >= 0
581
582
583



584
    def queue_seek(self, channel, seconds_to_seek):
585
        """
586
        Forwards the player of the given `ChannelType.QUEUE` channel by (n) seconds.
587

588
        Args:
David Trattnig's avatar
David Trattnig committed
589
            channel (Channel): The channel to push the file to
590
            seconds_to_seeks (Float):   The seconds to skip
David Trattnig's avatar
David Trattnig committed
591
592
593

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

599
600
601
602
        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()
603
604

        return result
605
606


David Trattnig's avatar
David Trattnig committed
607

608
    def queue_clear(self, channel):
609
        """
610
        Removes all tracks currently queued in the given `ChannelType.QUEUE` channel.
David Trattnig's avatar
David Trattnig committed
611
612
613
614
615
616

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

        Returns:
            (String):   Liquidsoap response
617
        """
618
619
620
        if channel not in ChannelType.QUEUE.channels and \
            channel not in ChannelType.FALLBACK_QUEUE.channels:
                raise InvalidChannelException
621

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

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

629
        return result
630

David Trattnig's avatar
David Trattnig committed
631
632


633
    #
634
    #   Channel Type - Playlist 
635
636
    #

637

638
    def playlist_set_uri(self, channel, playlist_uri):
639
        """
640
        Sets the URI of a playlist.
David Trattnig's avatar
David Trattnig committed
641
642

        Args:
643
644
            channel (Channel): The channel to push the file to
            playlist_uri (String): The path to the playlist
645

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

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

656
        return result
657
658
659



660
    def playlist_clear_uri(self, channel):
661
        """
662
        Clears the URI of a playlist.
663

664
665
        Args:
            channel (Channel): The channel to push the file to
David Trattnig's avatar
David Trattnig committed
666

667
668
        Returns:
            (String):   Liquidsoap response
David Trattnig's avatar
David Trattnig committed
669
        """
670
        self.logger.info(SU.pink("Clearing URI of playlist '%s'" % (channel)))
671

672
673
674
675
        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()
676

677
        return result
David Trattnig's avatar
David Trattnig committed
678

679

David Trattnig's avatar
David Trattnig committed
680

681

David Trattnig's avatar
David Trattnig committed
682

683
684
685
686
class EngineSplash:
    
    @staticmethod
    def splash_screen(component, version):
687
        """
688
        Prints the engine logo and version info.
689
        """
690
691
692
693
694
695
696
697
698
        return """\n
             █████╗ ██╗   ██╗██████╗  █████╗     ███████╗███╗   ██╗ ██████╗ ██╗███╗   ██╗███████╗
            ██╔══██╗██║   ██║██╔══██╗██╔══██╗    ██╔════╝████╗  ██║██╔════╝ ██║████╗  ██║██╔════╝
            ███████║██║   ██║██████╔╝███████║    █████╗  ██╔██╗ ██║██║  ███╗██║██╔██╗ ██║█████╗  
            ██╔══██║██║   ██║██╔══██╗██╔══██║    ██╔══╝  ██║╚██╗██║██║   ██║██║██║╚██╗██║██╔══╝  
            ██║  ██║╚██████╔╝██║  ██║██║  ██║    ███████╗██║ ╚████║╚██████╔╝██║██║ ╚████║███████╗
            ╚═╝  ╚═╝ ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝    ╚══════╝╚═╝  ╚═══╝ ╚═════╝ ╚═╝╚═╝  ╚═══╝╚══════╝
            %s v%s - Ready to play!
        \n""" % (component, version)       
699

700