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

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
59
60
61
62
63
    is_liquidsoap_running = False    
    plugins = None

    # Mixer
    mixer = None
    mixer_fallback = None

    connector = None
64

65
    def __init__(self, config):
66
        """
67
        Constructor
68
69
70

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

        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")        
84
        
85
86
87

    def start(self):
        """
88
89
        Starts the engine. Called when the connection to the sound-system implementation
        has been established.
90
        """
91
        self.event_dispatcher = EngineEventDispatcher(self, self.scheduler)
92
        
93

94
95
        # Sleep needed, because the socket is created too slowly by Liquidsoap
        time.sleep(1)
96
97
98
99
100
        self.player = Player(self.config, self.event_dispatcher)

        # self.mixer = Mixer(self.config, MixerType.MAIN, self.connector)
        # self.mixer_fallback = Mixer(self.config, MixerType.FALLBACK, self.connector)

101
        self.is_liquidsoap_running = True
102
103
104
        self.event_dispatcher.on_initialized()
        self.logger.info(SU.green("Engine Core ------[ connected ]-------- Liquidsoap"))
        self.event_dispatcher.on_boot()
105
        self.logger.info(EngineSplash.splash_screen("Engine Core", meta.__version__))
106
        self.event_dispatcher.on_ready()
David Trattnig's avatar
David Trattnig committed
107
108


109
110
111

    #
    #   Basic Methods
112
    #
113

114

115
    def init_player(self):
116
        """
117
118
119
120
        Initializes the LiquidSoap Player after startup of the engine.

        Returns:
            (String):   Message that the player is started.
121
        """
122
123
124
125
126
127
128
129
        t = StartupThread(self)
        t.start()

        return "Engine Core startup done!"



    def is_active(self):
David Trattnig's avatar
David Trattnig committed
130
        """
131
        Checks if Liquidsoap is running
David Trattnig's avatar
David Trattnig committed
132
        """
133
134
135
136
137
138
139
140
141
        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
142

143
        return self.is_liquidsoap_running
144
145


146
147
148
149
150
151
    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
152

David Trattnig's avatar
David Trattnig committed
153

154
    def version(self):
155
        """
156
        Get the version of Liquidsoap.
157
        """
158
159
        data = self.player.connector.send_lqc_command("version", "")
        return data
160
161


162
    def uptime(self):
David Trattnig's avatar
David Trattnig committed
163
        """
164
        Retrieves the uptime of Liquidsoap.
David Trattnig's avatar
David Trattnig committed
165
        """
166
167
        data = self.player.connector.send_lqc_command("uptime", "")
        return data
168

David Trattnig's avatar
David Trattnig committed
169

170
171
172
173
174
175
176
177
    @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.
178

179
180
181
182
        Returns:
            (Integer):  the Unix epoch timestamp including the offset
        """
        return SU.timestamp() + Engine.engine_time_offset
183

184

David Trattnig's avatar
David Trattnig committed
185
186
187
188
189
190
191
192
    @staticmethod
    def get_instance():
        """
        Returns the one and only engine.
        """
        return Engine.instance


193
    def terminate(self):
David Trattnig's avatar
David Trattnig committed
194
        """
195
        Terminates the engine and all related processes.
David Trattnig's avatar
David Trattnig committed
196
        """
197
        if self.eci: self.eci.terminate()
198
199
200



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#
#   PLAYER
#


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

    # Mixer
    mixer = None
    mixer_fallback = None



    def __init__(self, config, event_dispatcher):
David Trattnig's avatar
David Trattnig committed
225
        """
226
        Constructor
227

228
229
230
231
232
233
234
235
236
237
        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)
238
239
240



241
    def preload(self, entry):
David Trattnig's avatar
David Trattnig committed
242
        """
243
        Pre-Load the entry. This is required before the actual `play(..)` can happen.
David Trattnig's avatar
David Trattnig committed
244
245
246

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

David Trattnig's avatar
David Trattnig committed
249
250
251
252
253
254
        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
255
256
257
        """
        entry.status = EntryPlayState.LOADING
        self.logger.info("Loading entry '%s'" % entry)
258
        is_ready = False
259

David Trattnig's avatar
David Trattnig committed
260
        # LIVE
261
        if entry.get_content_type() in ResourceClass.LIVE.types:
262
            entry.channel = "linein_" + entry.source.split("line://")[1]
263
            is_ready = True
264
        else:
265
266
            channel_type = self.channel_router.type_for_resource(entry.get_content_type())
            entry.previous_channel, entry.channel = self.channel_router.channel_swap(channel_type)
267

268
269
270
        # 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
271
272
            
        # STREAM
273
        elif entry.get_content_type() in ResourceClass.STREAM.types:
274
            is_ready = self.stream_load_entry(entry)
David Trattnig's avatar
David Trattnig committed
275

276
        if is_ready:
277
            entry.status = EntryPlayState.READY
278

279
        self.event_dispatcher.on_queue([entry])
David Trattnig's avatar
David Trattnig committed
280
281
282



283
    def preload_group(self, entries, channel_type=ChannelType.QUEUE):
David Trattnig's avatar
David Trattnig committed
284
        """
285
        Pre-Load multiple filesystem entries at once. This call is required before the 
David Trattnig's avatar
David Trattnig committed
286
        actual `play(..)` can happen. Due to their nature, non-filesystem entries cannot be queued
287
        using this method. In this case use `preload(self, entry)` instead. This method also allows
David Trattnig's avatar
David Trattnig committed
288
289
290
291
292
293
294
        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:
295
296
            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
297
        """
298
        channels = None
David Trattnig's avatar
David Trattnig committed
299
300
301

        # Validate entry type
        for entry in entries:
302
            if entry.get_content_type() != ResourceType.FILE:
David Trattnig's avatar
David Trattnig committed
303
304
                raise InvalidChannelException
        
305
306
        # Determine channel        
        channels = self.channel_router.channel_swap(channel_type)
David Trattnig's avatar
David Trattnig committed
307
308
309
310
311
312
313

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

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

316
            if self.queue_push(entry.channel, entry.source) == True:
David Trattnig's avatar
David Trattnig committed
317
318
                entry.status = EntryPlayState.READY
        
319
        self.event_dispatcher.on_queue(entries)
320
        return channels
David Trattnig's avatar
David Trattnig committed
321

322
323

    def play(self, entry, transition):
David Trattnig's avatar
David Trattnig committed
324
        """
325
326
        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
327

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

David Trattnig's avatar
David Trattnig committed
332
        Args:
David Trattnig's avatar
David Trattnig committed
333
            entry (PlaylistEntry):          The audio source to be played
David Trattnig's avatar
David Trattnig committed
334
            transition (TransitionType):    The type of transition to use e.g. fade-in or instant volume level.
David Trattnig's avatar
David Trattnig committed
335
            queue (Boolean):                If `True` the entry is queued if the `ChannelType` does allow so; 
336
                otherwise a new channel of the same type is activated
David Trattnig's avatar
David Trattnig committed
337
338
        
        """
David Trattnig's avatar
David Trattnig committed
339
        with suppress(LQConnectionError):
340
341
342
343
344
            
            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
345
346

            # Instant activation or fade-in
347
            self.connector.enable_transaction()
348
            if transition == TransitionType.FADE:
349
350
                mixer.channel_select(entry.channel.value, True)
                mixer.fade_in(entry.channel, entry.volume)
351
            else:
352
353
                mixer.channel_activate(entry.channel.value, True)
            self.connector.disable_transaction()
354

355
356
            # Update active channel for the current channel type            
            self.channel_router.set_active(channel_type, entry.channel)
David Trattnig's avatar
David Trattnig committed
357
358

            # Dear filesystem channels, please leave the room as you would like to find it!
359
360
361
362
            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
363
364
365
366
                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)
367
368
369
                    self.connector.enable_transaction()
                    mixer.channel_activate(entry.previous_channel.value, False)
                    res = self.queue_clear(entry.previous_channel)
370
                    self.logger.info("Clear Queue Response: " + res)
371
                    self.connector.disable_transaction()
David Trattnig's avatar
David Trattnig committed
372
                Thread(target=clean_up).start()
373
374
            
            # Filesystem meta-changes trigger the event via Liquidsoap
375
            if not entry.channel in ChannelType.QUEUE.channels:
376
                self.on_play(entry)
377

David Trattnig's avatar
David Trattnig committed
378

379
380
381

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

        Args:
386
            source (String):    The `Entry` or URI or of the media source currently being played
387
        """
388
        self.event_dispatcher.on_play(source)
389
390
391
392



    def stop(self, entry, transition):
David Trattnig's avatar
David Trattnig committed
393
        """
394
        Stops the currently playing entry.
David Trattnig's avatar
David Trattnig committed
395
396

        Args:
397
398
            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
399
        """
David Trattnig's avatar
David Trattnig committed
400
        with suppress(LQConnectionError):
401
            self.connector.enable_transaction()
402

403
            if not entry.channel:
404
                self.logger.warn(SU.red("Trying to stop entry %s, but it has no channel assigned" % entry))
405
406
407
                return
            
            if transition == TransitionType.FADE:
408
                self.mixer.fade_out(entry.channel, entry.volume)
409
            else:
410
                self.mixer.channel_volume(entry.channel, 0)
411

412
            self.logger.info(SU.pink("Stopped channel '%s' for entry %s" % (entry.channel, entry)))
413
            self.connector.disable_transaction()
414
            self.event_dispatcher.on_stop(entry)
415

David Trattnig's avatar
David Trattnig committed
416

417

418
    def start_fallback_playlist(self, entries):
David Trattnig's avatar
David Trattnig committed
419
        """
420
        Sets any scheduled fallback playlist and performs a fade-in.
David Trattnig's avatar
David Trattnig committed
421
422

        Args:
423
            entries ([Entry]):    The playlist entries
David Trattnig's avatar
David Trattnig committed
424
        """
425
        self.preload_group(entries, ChannelType.FALLBACK_QUEUE)
426
        self.play(entries[0], TransitionType.FADE)
427

David Trattnig's avatar
David Trattnig committed
428

429

430
    def stop_fallback_playlist(self):
David Trattnig's avatar
David Trattnig committed
431
        """
432
        Performs a fade-out and clears any scheduled fallback playlist.
David Trattnig's avatar
David Trattnig committed
433
        """
434
        dirty_channel = self.channel_router.get_active(ChannelType.FALLBACK_QUEUE)
435

436
437
438
439
        self.logger.info(f"Fading out channel '{dirty_channel}'")
        self.connector.enable_transaction()
        self.mixer_fallback.fade_out(dirty_channel, 100)
        self.connector.disable_transaction()  
440

441
442
443
444
445
446
447
448
449
450
451
452
        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()
        
453

454
455

    #
456
    #   Channel Type - Stream
457
458
    #

David Trattnig's avatar
David Trattnig committed
459
460
461
462

    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
463
464
465

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

David Trattnig's avatar
Docs.    
David Trattnig committed
467
468
        Returns:
            (Boolean):  `True` if successfull
David Trattnig's avatar
David Trattnig committed
469
470
471
472
473
474
475
476
477
478
479
        """
        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
480
                raise LoadSourceException("Could not connect to stream while waiting for %s seconds!" % str(retries*retry_delay))
David Trattnig's avatar
David Trattnig committed
481
482
483
            time.sleep(retry_delay)
            retries += 1

484
        return True
David Trattnig's avatar
David Trattnig committed
485
486
487
488



    def stream_load(self, channel, url):
David Trattnig's avatar
David Trattnig committed
489
        """
David Trattnig's avatar
David Trattnig committed
490
491
        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
492
493
494
495
496
497
498

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

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

502
503
        self.connector.enable_transaction()
        result = self.connector.send_lqc_command(channel, "stream_stop")
David Trattnig's avatar
David Trattnig committed
504
505
        
        if result != LiquidsoapResponse.SUCCESS.value:
David Trattnig's avatar
David Trattnig committed
506
            self.logger.error("%s.stop result: %s" % (channel, result))
David Trattnig's avatar
David Trattnig committed
507
            raise LQStreamException("Error while stopping stream!")
508

509
        result = self.connector.send_lqc_command(channel, "stream_set_url", url)
510

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

David Trattnig's avatar
David Trattnig committed
515
516
        # Liquidsoap ignores commands sent without a certain timeout
        time.sleep(2)
517

518
        result = self.connector.send_lqc_command(channel, "stream_start")
David Trattnig's avatar
David Trattnig committed
519
        self.logger.info("%s.start result: %s" % (channel, result))
520

521
        self.connector.disable_transaction()
David Trattnig's avatar
David Trattnig committed
522
523
524
525
        return result



David Trattnig's avatar
David Trattnig committed
526
    def stream_is_ready(self, channel, url):
David Trattnig's avatar
David Trattnig committed
527
        """
David Trattnig's avatar
David Trattnig committed
528
529
        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
530
531
532
533
534
535
536

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

        Returns:
            (Boolean):  `True` if successful
David Trattnig's avatar
David Trattnig committed
537
538
539
        """
        result = None

540
        self.connector.enable_transaction()
David Trattnig's avatar
David Trattnig committed
541

542
        result = self.connector.send_lqc_command(channel, "stream_status")
David Trattnig's avatar
David Trattnig committed
543
        self.logger.info("%s.status result: %s" % (channel, result))
David Trattnig's avatar
David Trattnig committed
544
545
546
547
548
549
550
551
552

        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

553
        self.connector.disable_transaction()
David Trattnig's avatar
David Trattnig committed
554

David Trattnig's avatar
David Trattnig committed
555
556
557
        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
558
        return True
559
560


David Trattnig's avatar
David Trattnig committed
561

562
    #
563
    #   Channel Type - Queue 
564
    #
David Trattnig's avatar
David Trattnig committed
565

566

567
    def queue_push(self, channel, uri):
568
        """
569
        Adds an filesystem URI to the given `ChannelType.QUEUE` channel.
570

571
        Args:
David Trattnig's avatar
David Trattnig committed
572
573
574
            channel (Channel): The channel to push the file to
            uri (String):      The URI of the file

575
        Returns:
David Trattnig's avatar
David Trattnig committed
576
            (Boolean):  `True` if successful
577
        """
578
579
580
581
        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)))
582

583
584
585
586
587
588
589
        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()
590

David Trattnig's avatar
David Trattnig committed
591
592
        # If successful, Liquidsoap returns a resource ID of the queued track
        return int(result) >= 0
593
594
595



596
    def queue_seek(self, channel, seconds_to_seek):
597
        """
598
        Forwards the player of the given `ChannelType.QUEUE` channel by (n) seconds.
599

600
        Args:
David Trattnig's avatar
David Trattnig committed
601
            channel (Channel): The channel to push the file to
602
            seconds_to_seeks (Float):   The seconds to skip
David Trattnig's avatar
David Trattnig committed
603
604
605

        Returns:
            (String):   Liquidsoap response
606
        """
607
608
609
        if channel not in ChannelType.QUEUE.channels and \
            channel not in ChannelType.FALLBACK_QUEUE.channels:
                raise InvalidChannelException
610

611
612
613
614
        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()
615
616

        return result
617
618


David Trattnig's avatar
David Trattnig committed
619

620
    def queue_clear(self, channel):
621
        """
622
        Removes all tracks currently queued in the given `ChannelType.QUEUE` channel.
David Trattnig's avatar
David Trattnig committed
623
624
625
626
627
628

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

        Returns:
            (String):   Liquidsoap response
629
        """
630
631
632
        if channel not in ChannelType.QUEUE.channels and \
            channel not in ChannelType.FALLBACK_QUEUE.channels:
                raise InvalidChannelException
633

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

636
637
638
639
        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()
640

641
        return result
642

David Trattnig's avatar
David Trattnig committed
643
644


645
    #
646
    #   Channel Type - Playlist 
647
648
    #

649

650
    def playlist_set_uri(self, channel, playlist_uri):
651
        """
652
        Sets the URI of a playlist.
David Trattnig's avatar
David Trattnig committed
653
654

        Args:
655
656
            channel (Channel): The channel to push the file to
            playlist_uri (String): The path to the playlist
657

David Trattnig's avatar
David Trattnig committed
658
        Returns:
659
            (String):   Liquidsoap response
660
        """
661
        self.logger.info(SU.pink("Setting URI of playlist '%s' to '%s'" % (channel, playlist_uri)))
662

663
664
665
666
        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()
667

668
        return result
669
670
671



672
    def playlist_clear_uri(self, channel):
673
        """
674
        Clears the URI of a playlist.
675

676
677
        Args:
            channel (Channel): The channel to push the file to
David Trattnig's avatar
David Trattnig committed
678

679
680
        Returns:
            (String):   Liquidsoap response
David Trattnig's avatar
David Trattnig committed
681
        """
682
        self.logger.info(SU.pink("Clearing URI of playlist '%s'" % (channel)))
683

684
685
686
687
        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()
688

689
        return result
David Trattnig's avatar
David Trattnig committed
690

691

David Trattnig's avatar
David Trattnig committed
692

693

David Trattnig's avatar
David Trattnig committed
694

695
696
697
698
class EngineSplash:
    
    @staticmethod
    def splash_screen(component, version):
699
        """
700
        Prints the engine logo and version info.
701
        """
702
703
704
705
706
707
708
709
710
        return """\n
             █████╗ ██╗   ██╗██████╗  █████╗     ███████╗███╗   ██╗ ██████╗ ██╗███╗   ██╗███████╗
            ██╔══██╗██║   ██║██╔══██╗██╔══██╗    ██╔════╝████╗  ██║██╔════╝ ██║████╗  ██║██╔════╝
            ███████║██║   ██║██████╔╝███████║    █████╗  ██╔██╗ ██║██║  ███╗██║██╔██╗ ██║█████╗  
            ██╔══██║██║   ██║██╔══██╗██╔══██║    ██╔══╝  ██║╚██╗██║██║   ██║██║██║╚██╗██║██╔══╝  
            ██║  ██║╚██████╔╝██║  ██║██║  ██║    ███████╗██║ ╚████║╚██████╔╝██║██║ ╚████║███████╗
            ╚═╝  ╚═╝ ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝    ╚══════╝╚═╝  ╚═══╝ ╚═════╝ ╚═╝╚═╝  ╚═══╝╚══════╝
            %s v%s - Ready to play!
        \n""" % (component, version)       
711

712