engine.py 34.1 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#
#  engine
#
#  Playout Daemon for autoradio project
#
#
#  Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
#  This file is part of engine.
#
#  engine is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  any later version.
#
#  engine 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 General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with engine. If not, see <http://www.gnu.org/licenses/>.
#

25
26
import time
import logging
27
import json
28

29
from modules.communication.liquidsoap.playerclient import LiquidSoapPlayerClient
30
# from modules.communication.liquidsoap.recorderclient import LiquidSoapRecorderClient
David Trattnig's avatar
David Trattnig committed
31
32
33
from modules.core.startup       import StartupThread
from modules.core.state         import PlayerStateService
from modules.core.monitor       import Monitoring
34
from modules.communication.mail import AuraMailer
35

David Trattnig's avatar
David Trattnig committed
36
from modules.base.enum          import ChannelType, Channel, TransitionType, LiquidsoapResponse, EntryPlayState
David Trattnig's avatar
David Trattnig committed
37
from modules.base.utils         import TerminalColors, SimpleUtil
David Trattnig's avatar
David Trattnig committed
38
from modules.base.exceptions    import LQConnectionError, InvalidChannelException, NoActiveEntryException, EngineMalfunctionException, LQStreamException
39

40

David Trattnig's avatar
David Trattnig committed
41
class SoundSystem():
42
    """ 
David Trattnig's avatar
David Trattnig committed
43
    SoundSystem Class
44
45

    Uses LiquidSoapClient, but introduces more complex commands, transactions and error handling.
46
    """
47
    client = None
48
    logger = None
49
    transaction = 0
50
    channels = None
51
    scheduler = None
David Trattnig's avatar
David Trattnig committed
52
    monitoring = None
53
    #error_data = None #FIXME Can be removed
54
    auramailer = None
55
    is_liquidsoap_running = False
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
56
    connection_attempts = 0
57
    disable_logging = False
58
59
    fade_in_active = False
    fade_out_active = False
60

61
62
63
64
65
66
67
    # Active Channel & Entry Handling
    active_channel_type = None
    active_channel = None
    player_state = None
    # active_entries = None


68
    def __init__(self, config):
69
        """
70
71
72
73
74
        Initializes the communicator by establishing a Socket connection
        to Liquidsoap.

        Args:
            config (AuraConfig):    The configuration
75
        """
76
        self.config = config
77
78
        self.logger = logging.getLogger("AuraEngine")

79
80
        self.client = LiquidSoapPlayerClient(config, "engine.sock")
        # self.lqcr = LiquidSoapRecorderClient(config, "record.sock")
81
        self.auramailer = AuraMailer(self.config)
82
83
        self.monitoring = Monitoring(config, self, self.auramailer)
        
David Trattnig's avatar
David Trattnig committed
84
        self.is_active()
85

86
87
88
        # Initialize Default Channels
        self.active_channel = {
            ChannelType.FILESYSTEM: Channel.FILESYSTEM_A,
David Trattnig's avatar
David Trattnig committed
89
            ChannelType.HTTP: Channel.HTTP_A,
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
            ChannelType.LIVE: Channel.LIVE_0
        }
        # self.active_entries = {}
        self.player_state = PlayerStateService(config)



    def start(self):
        """
        Starts the soundsystem.
        """
        # Sleep needed, because the socket is created too slowly by Liquidsoap
        time.sleep(1)
        self.enable_transaction()
        time.sleep(1)

        self.mixer_start()

        # Setting init params like a blank file
        install_dir = self.config.get("install_dir")
        channel = self.active_channel[ChannelType.FILESYSTEM]
        self.playlist_push(channel, install_dir + "/configuration/blank.flac")

        self.disable_transaction()
        self.is_liquidsoap_running = True
        self.logger.info(SimpleUtil.green("Engine Core ------[ connected ]-------- Liquidsoap"))

David Trattnig's avatar
David Trattnig committed
117
118
119
120
121
122
123
124
125
126
127
        # Start Monitoring
        is_valid = self.monitoring.has_valid_status(False)
        status = self.monitoring.get_status()
        self.logger.info("Status Monitor:\n%s" % json.dumps(status, indent=4))
        if not is_valid:
            self.logger.info("Engine Status: " + SimpleUtil.red(status["engine"]["status"]))
            raise EngineMalfunctionException
        else:
            self.logger.info("Engine Status: " + SimpleUtil.green("OK"))


128
129
130

    def is_ready(self):
        """
131
        Returns `True` if the soundsystem is connected to Liquidsoap and is ready to be used.
132
133
134
        """
        return self.is_liquidsoap_running

135

136
    #
137
138
139
140
141
142
143
144
145
146
147
148
149
150
    #   MIXER : GENERAL
    # 


    def mixer_start(self):
        # Reset channels and reload them
        channels = self.reload_channels()

        # For all available channels
        for c in channels:
            # Set volume to zero
            self.channel_volume(c, "0")
            # And activate this channel
            self.channel_activate(c, True)
151
152


153
    # ------------------------------------------------------------------------------------------ #
154
155
156
    # def set_volume(self, mixernumber, volume):
    #     #return self.client.command("mixer", 'volume', mixernumber, str(volume))
    #     return self.__send_lqc_command__(self.client, "mixer", "volume", mixernumber, volume)
David Trattnig's avatar
David Trattnig committed
157

158
159
160
161
162
    # ------------------------------------------------------------------------------------------ #
    def get_active_mixer(self):
        """
        get active mixer in liquidsoap server
        :return:
163
        """
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
        activeinputs = []

        # enable more control over the connection
        self.enable_transaction()

        inputs = self.get_all_channels()

        cnt = 0
        for input in inputs:
            status = self.__get_mixer_status__(cnt)

            if "selected=true" in status:
                activeinputs.append(input)

            cnt = cnt + 1

        self.disable_transaction()

        return activeinputs
183

184
    # ------------------------------------------------------------------------------------------ #
185
186
    def get_mixer_status(self):
        inputstate = {}
187

188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
        self.enable_transaction()

        inputs = self.get_all_channels()

        cnt = 0
        for input in inputs:
            inputstate[input] = self.__get_mixer_status__(cnt)
            cnt = cnt + 1

        self.disable_transaction()

        return inputstate


    # ------------------------------------------------------------------------------------------ #
    def get_mixer_volume(self, channel):
        return False

    # ------------------------------------------------------------------------------------------ #
    def __get_mixer_status__(self, mixernumber):
        return self.__send_lqc_command__(self.client, "mixer", "status", mixernumber)


    #
    #   MIXER : CHANNELS
    #


David Trattnig's avatar
David Trattnig committed
216
217
218
    def load(self, entry):
        """
        Preloads the entry. This is required before the actual `play(..)` can happen.
219

David Trattnig's avatar
David Trattnig committed
220
221
222
223
224
        Note his method is blocking until loading has finished. If this method is called 
        asynchroniously, the progress on the preloading state can be looked up in `entry.state`.
        """
        entry.status = EntryPlayState.LOADING
        self.logger.info("Loading entry '%s'" % entry)
225

David Trattnig's avatar
David Trattnig committed
226
227
228
229
        self.enable_transaction()
        self.player_state.set_active_entry(entry)
        entry.channel = self.channel_swap(entry.type)
        self.disable_transaction()
230

David Trattnig's avatar
David Trattnig committed
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
        # PLAYLIST
        if entry.type == ChannelType.FILESYSTEM:
            self.playlist_push(entry.channel, entry.source)
            
        # STREAM
        elif entry.type == ChannelType.HTTP:
            self.http_load(entry.channel, entry.source)
            time.sleep(1)

            while not self.http_is_ready(entry.channel, entry.source):
                self.logger.info("Loading Stream ...")
                time.sleep(1)

            entry.status = EntryPlayState.READY

        # LIVE
        else:
            # TODO Select correct LINE-OUT channels as per entry
            pass
  
            
252
253
254


    def play(self, entry, transition):
David Trattnig's avatar
David Trattnig committed
255
        """
256
257
        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
258
259

        Args:
David Trattnig's avatar
David Trattnig committed
260
            entry (PlaylistEntry):          The audio source to be played
261
            transition (TransitionType):    The type of transition to use e.g. fade-out.
David Trattnig's avatar
David Trattnig committed
262
            queue (Boolean):                If `True` the entry is queued if the `ChannelType` does allow so; 
263
                otherwise a new channel of the same type is activated
David Trattnig's avatar
David Trattnig committed
264
265
        
        """
266
        try:
267
268
            
            # Move channel volume all the way up
David Trattnig's avatar
David Trattnig committed
269
            self.enable_transaction()
270
271
272
273
            if transition == TransitionType.FADE:
                self.fade_in(entry)
            else:
                self.channel_volume(entry.channel, entry.volume)
David Trattnig's avatar
David Trattnig committed
274
            self.disable_transaction()
275
276
277
278
279

            # Update active channel and type
            self.active_channel[entry.type] = entry.channel     
            
            
280
        except LQConnectionError:
David Trattnig's avatar
David Trattnig committed
281
282
            # we already caught and handled this error in __send_lqc_command__, 
            # but we do not want to execute this function further and pass the exception
283
            pass
284

David Trattnig's avatar
David Trattnig committed
285

286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301

    def on_play(self, source):
        """
        Event Handler which is called by soundsystem implementation (i.e. Liquidsoap) 
        when some entry is actually playing.
        """
        self.logger.info(SimpleUtil.pink("Source '%s' started playing" % source))

        try:
            self.player_state.store_trackservice_entry(source)
        except NoActiveEntryException:
            self.logger.warn(SimpleUtil.red("Currently there's nothing playing!"))



    def stop(self, entry, transition):
David Trattnig's avatar
David Trattnig committed
302
        """
303
        Stops the currently playing entry. 
David Trattnig's avatar
David Trattnig committed
304
305

        Args:
306
307
            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
308
        """
309

310
311
        try:
            self.enable_transaction()
312

313
314
315
316
317
318
319
320
            if not entry.channel:
                self.logger.warn("Trying to stop entry %s, but it has no channel assigned" % entry)
                return
            
            if transition == TransitionType.FADE:
                self.fade_out(entry)
            else:
                self.channel_volume(entry.channel, 0)
321

322
323
            # self.playlist_clear(entry.channel)
            self.logger.info(SimpleUtil.pink("Stopped channel '%s' for entry %s" % (entry.channel, entry)))
324

325
            self.disable_transaction()
326

327
328
329
330
        except LQConnectionError:
            # we already caught and handled this error in __send_lqc_command__, 
            # but we do not want to execute this function further and pass the exception
            pass
331

David Trattnig's avatar
David Trattnig committed
332

333

334
    def channel_swap(self, channel_type):
David Trattnig's avatar
David Trattnig committed
335
336
337
338
339
340
341
342
        """
        Returns the currently in-active channel for a given type. For example if the currently some
        file on channel FILESYSTEM_A is playing, the channel FILESYSTEM B is returned for being used
        to queue new entries.

        Args:
            channel_type (ChannelType): The channel type such es filesystem, stream or live channel
        """
343
344
345
346
347
348
349
350
351
352
353
354
        active_channel = self.active_channel[channel_type]
        channel = None
        msg = None

        if channel_type == ChannelType.FILESYSTEM:
            if active_channel == Channel.FILESYSTEM_A:
                channel = Channel.FILESYSTEM_B
                msg = "Swapped filesystem channel from A > B"
            else:
                channel = Channel.FILESYSTEM_A
                msg = "Swapped filesystem channel from B > A"
            
David Trattnig's avatar
David Trattnig committed
355
356
357
358
359
            # TODO Clear old channel
            
        elif channel_type == ChannelType.HTTP:
            if active_channel == Channel.HTTP_A:
                channel = Channel.HTTP_B
360
                msg = "Swapped stream channel from A > B"
David Trattnig's avatar
David Trattnig committed
361

362
            else:
David Trattnig's avatar
David Trattnig committed
363
                channel = Channel.HTTP_A
364
                msg = "Swapped stream channel from B > A"
David Trattnig's avatar
David Trattnig committed
365
            
366
367
368

        if msg: self.logger.info(SimpleUtil.pink(msg))
        return channel
369
370
371



372
373
    # ------------------------------------------------------------------------------------------ #
    def all_inputs_but(self, input_type):
374
        try:
375
            activemixer_copy = self.get_all_channels().copy()
376
            activemixer_copy.remove(input_type)
377
378
        except ValueError as e:
            self.logger.error("Requested channel (" + input_type + ") not in channellist. Reason: " + str(e))
379
380
        except AttributeError:
            self.logger.critical("Channellist is None")
381

382
383
384
385
        return activemixer_copy

    # ------------------------------------------------------------------------------------------ #
    def get_all_channels(self):
386
        if self.channels is None or len(self.channels) == 0:
387
            self.channels = self.__send_lqc_command__(self.client, "mixer", "inputs")
388
389

        return self.channels
390

391
392
393
394
395
    # ------------------------------------------------------------------------------------------ #
    def reload_channels(self):
        self.channels = None
        return self.get_all_channels()

396

397

398
399
400
    # ------------------------------------------------------------------------------------------ #
    def channel_activate(self, channel, activate):
        channels = self.get_all_channels()
401
402
403
404

        try:
            index = channels.index(channel)
            if len(channel) < 1:
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
405
406
407
408
409
410
                self.logger.critical("Cannot activate channel. There are no channels!")
            else:
                message = self.__send_lqc_command__(self.client, "mixer", "select", index, activate)
                return message
        except Exception as e:
            self.logger.critical("Ran into exception when activating channel. Reason: " + str(e))
411

412
413
414
    # ------------------------------------------------------------------------------------------ #
    def channel_volume(self, channel, volume):
        """
415
416
417
418
419
        Set volume of a channel

        Args:
            channel (Channel):      The channel
            volume  (Integer)       Volume between 0 and 100
420
        """
421

422
        channel = str(channel)
423
        try:
424
425
426
427
428
429
            if str(volume) == "100":
                channels = self.get_all_channels()
                index = channels.index(channel)
            else:
                channels = self.get_all_channels()
                index = channels.index(channel)
430
        except ValueError as e:
431
432
            msg = SimpleUtil.red("Cannot set volume of channel " + channel + " to " + str(volume) + "!. Reason: " + str(e))
            self.logger.error(msg)
David Trattnig's avatar
David Trattnig committed
433
            self.logger.info("Available channels: %s" % str(channels))
434
435
436
            return

        try:
437
            if len(channel) < 1:
438
439
                msg = SimpleUtil.red("Cannot set volume of channel " + channel + " to " + str(volume) + "! There are no channels.")
                self.logger.warning(msg)
440
            else:
441
                message = self.__send_lqc_command__(self.client, "mixer", "volume", str(index), str(int(volume)))
442

443
444
                if not self.disable_logging:
                    if message.find('volume=' + str(volume) + '%'):
445
                        self.logger.info(SimpleUtil.pink("Set volume of channel '%s' to %s" % (channel, str(volume))))
446
                    else:
447
448
                        msg = SimpleUtil.red("Setting volume of channel " + channel + " gone wrong! Liquidsoap message: " + message)
                        self.logger.warning(msg)
449

450
                return message
451
        except AttributeError as e: #(LQConnectionError, AttributeError):
452
            self.disable_transaction(force=True)
453
454
            msg = SimpleUtil.red("Ran into exception when setting volume of channel " + channel + ". Reason: " + str(e))
            self.logger.error(msg)
455

456
457

    #
458
    #   Channel Type - Stream 
459
460
    #

David Trattnig's avatar
David Trattnig committed
461
462
463
464
465
    def http_load(self, channel, url):
        """
        Preloads the stream URL on the given channel.
        """
        result = None
466

David Trattnig's avatar
David Trattnig committed
467
468
469
470
471
472
        self.enable_transaction()
        result = self.__send_lqc_command__(self.client, channel, "http_stop")
        
        if result != LiquidsoapResponse.SUCCESS.value:
            self.logger.error("stream.stop result: " + result)
            raise LQStreamException("Error while stopping stream!")
473

David Trattnig's avatar
David Trattnig committed
474
        result = self.__send_lqc_command__(self.client, channel, "http_set_url", url)
475

David Trattnig's avatar
David Trattnig committed
476
477
478
        if result != LiquidsoapResponse.SUCCESS.value:
            self.logger.error("stream.set_url result: " + result)
            raise LQStreamException("Error while setting stream URL!")
479

David Trattnig's avatar
David Trattnig committed
480
481
        # Liquidsoap ignores commands sent without a certain timeout
        time.sleep(2)
482

David Trattnig's avatar
David Trattnig committed
483
484
        result = self.__send_lqc_command__(self.client, channel, "http_start")
        self.logger.info("stream.start result: " + result)
485

David Trattnig's avatar
David Trattnig committed
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
        self.disable_transaction()
        return result



    def http_is_ready(self, channel, url):
        """
        Checks if the stream on the given channel is ready to play.
        """
        result = None

        self.enable_transaction()

        result = self.__send_lqc_command__(self.client, channel, "http_status")
        self.logger.info("stream.status result: " + result)

        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

        self.disable_transaction()

        # Wait another 10 (!) seconds, because even now the old source might *still* be playing
        self.logger.info("Ready to play stream, Liquidsoap wants you to wait another 10secs though...")
        time.sleep(10)
        return True
516
517


David Trattnig's avatar
David Trattnig committed
518

519

520
521
522
    #
    #   Channel Type - Playlist 
    #
David Trattnig's avatar
David Trattnig committed
523

524
525
526
527
    # FIXME
    # def playlist_activate(self, playlist, cue_in=0.0):
    #     """
    #     Activates a new Playlist.
528

529
530
531
532
533
534
535
536
537
538
    #     Args:
    #         new_entry (Playlist):       The playlist to be played
    #         cue_in (Float):             Start/cue-time of track (For some reason Liquidsoap doesn't acknowledge this yet)
        
    #     Raises:
    #         (LQConnectionError): In case connecting to LiquidSoap isn't possible
    #     """
        
    #     # Grab the actual active entry
    #     # active_entry = self.scheduler.get_active_entry()
539

540
541
    #     # Set default channel, if no previous track is available
    #     current_channel = self.active_channel[ChannelType.FILESYSTEM]
542

543
544
    #     # if active_entry:
    #     #     current_channel = active_entry.channel
545

546
547
548
549
550
551
    #     try:
    #         # FIXME clearing creates some serious timing issues
    #         # To activate this feature we'd need some more sophisticated
    #         # Liquidsoap logic, such as >= 2 filesystem channels and
    #         # possiblities to pause pre-queued channels or cleaning them
    #         # after each completed schedule.
552

553
554
555
556
557
558
    #         # self.enable_transaction()
    #         # #if active_entry:
    #         #     #self.fade_out(active_entry)
    #         # res = self.playlist_clear(current_channel)
    #         # self.logger.info("Clear Queue Response: "+res)
    #         # self.disable_transaction()
559
560


561
562
563
564
565
566
567
568
569
570
    #         self.enable_transaction()
    #         self.reload_channels()
    #         # self.fade_in(playlist.entries[0])
    #         # FIXME rework
    #         for new_entry in playlist.entries:
    #             if current_channel == new_entry.channel:
    #                 self.activate_same_channel(new_entry, cue_in)
    #             else:
    #                 self.activate_different_channel(new_entry, cue_in, current_channel)
    #                 current_channel = new_entry.channel
571

572
    #         self.disable_transaction()
573

574
575
576
577
578
579
    #         # self.logger.critical("FIXME: Implement TrackService")
    #         #self.scheduler.update_track_service(new_entry)
    #     except LQConnectionError:
    #         # we already caught and handled this error in __send_lqc_command__, 
    #         # but we do not want to execute this function further and pass the exception
    #         pass
580
581
582



583
584
585
    def playlist_push(self, channel, uri):
        """
        Adds an filesystem URI to the given `ChannelType.FILESYSTEM` channel.
586

587
588
589
590
591
592
593
594
595
        Args:
            uri (String):   The URI of the file
        Returns:
            LiquidSoap Response
        """
        if channel not in ChannelType.FILESYSTEM.channels:
            raise InvalidChannelException
        self.logger.info(SimpleUtil.pink("playlist.push('%s', '%s'" % (channel, uri)))
        return self.__send_lqc_command__(self.client, channel, "playlist_push", uri)
596
597
598



599
600
601
    def playlist_seek(self, channel, seconds_to_seek):
        """
        Forwards the player of the given `ChannelType.FILESYSTEM` channel by (n) seconds.
602

603
604
605
606
607
608
        Args:
            seconds_to_seeks (Float):   The seconds to skip
        """
        if channel not in ChannelType.FILESYSTEM.channels:
            raise InvalidChannelException
        return self.__send_lqc_command__(self.client, channel, "playlist_seek", str(seconds_to_seek))
609
610
611



612
613
614
615
616
617
618
619
620
621
    def playlist_clear(self, channel):
        """
        Removes all tracks currently queued in the given `ChannelType.FILESYSTEM` channel.
        """
        if channel not in ChannelType.FILESYSTEM.channels:
            raise InvalidChannelException

        self.logger.info(SimpleUtil.pink("Clearing filesystem queue '%s'!" % channel))
        return self.__send_lqc_command__(self.client, channel, "playlist_clear")
        
622
623
624
625
626
627
628



    #
    #   Fading 
    #

629
630
631
632
633
634

    def fade_in(self, entry):
        """
        Performs a fade-in for the given `entry` to the `entry.volume` loudness
        at channel `entry.channel`.
        """
635
636
637
638
639
        try:
            fade_in_time = float(self.config.get("fade_in_time"))

            if fade_in_time > 0:
                self.fade_in_active = True
640
                target_volume = entry.volume
641
642
643

                step = fade_in_time / target_volume

644
645
646
                msg = "Starting to fading-in '%s'. Step is %ss and target volume is %s." % \
                    (entry.channel, str(step), str(target_volume))
                self.logger.info(SimpleUtil.pink(msg))
647

648
                # Enable logging, which might have been disabled in a previous fade-out
649
650
651
652
                self.disable_logging = True
                self.client.disable_logging = True

                for i in range(target_volume):
653
                    self.channel_volume(entry.channel.value, i + 1)
654
655
                    time.sleep(step)

656
657
                msg = "Finished with fading-in '%s'." % entry.channel
                self.logger.info(SimpleUtil.pink(msg))
658
659
660
661
662

                self.fade_in_active = False
                if not self.fade_out_active:
                    self.disable_logging = False
                    self.client.disable_logging = False
663

664
665
666
667
668
        except LQConnectionError as e:
            self.logger.critical(str(e))

        return True

669
670
671
672
673
674


    def fade_out(self, entry):
        """
        Performs a fade-out for the given `entry` at channel `entry.channel`.
        """
675
676
677
678
        try:
            fade_out_time = float(self.config.get("fade_out_time"))

            if fade_out_time > 0:
679
                step = abs(fade_out_time) / entry.volume
680

681
682
                msg = "Starting to fading-out '%s'. Step is %ss." % (entry.channel, str(step))
                self.logger.info(SimpleUtil.pink(msg))
683

684
                # Disable logging... it is going to be enabled again after fadein and -out is finished
685
686
687
                self.disable_logging = True
                self.client.disable_logging = True

688
689
                for i in range(entry.volume):
                    self.channel_volume(entry.channel.value, entry.volume-i-1)
690
691
                    time.sleep(step)

692
693
                msg = "Finished with fading-out '%s'" % entry.channel
                self.logger.info(SimpleUtil.pink(msg))
694

695
                # Enable logging again
696
697
698
699
                self.fade_out_active = False
                if not self.fade_in_active:
                    self.disable_logging = False
                    self.client.disable_logging = False
700

701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
        except LQConnectionError as e:
            self.logger.critical(str(e))

        return True



    #
    #   Recording
    #


    # ------------------------------------------------------------------------------------------ #
    def recorder_stop(self):
        self.enable_transaction()

        for i in range(5):
            if self.config.get("rec_" + str(i)) == "y":
                self.__send_lqc_command__(self.client, "recorder_" + str(i), "stop")

        self.disable_transaction()

    # ------------------------------------------------------------------------------------------ #
    def recorder_start(self, num=-1):
        if not self.is_liquidsoap_running:
            if num==-1:
                msg = "Want to start recorder, but LiquidSoap is not running"
            else:
                msg = "Want to start recorder " + str(num) + ", but LiquidSoap is not running"
            self.logger.warning(msg)
            return False

        self.enable_transaction()

        if num == -1:
            self.recorder_start_all()
        else:
            self.recorder_start_one(num)

        self.disable_transaction()

    # ------------------------------------------------------------------------------------------ #
    def recorder_start_all(self):
        if not self.is_liquidsoap_running:
            self.logger.warning("Want to start all recorder, but LiquidSoap is not running")
            return False

        self.enable_transaction()
        for i in range(5):
            self.recorder_start_one(i)
        self.disable_transaction()

    # ------------------------------------------------------------------------------------------ #
    def recorder_start_one(self, num):
        if not self.is_liquidsoap_running:
            return False

        if self.config.get("rec_" + str(num)) == "y":
            returnvalue = self.__send_lqc_command__(self.client, "recorder", str(num), "status")

            if returnvalue == "off":
                self.__send_lqc_command__(self.client, "recorder", str(num), "start")

    # ------------------------------------------------------------------------------------------ #
    def get_recorder_status(self):
        self.enable_transaction(self.client)
        recorder_state = self.__send_lqc_command__(self.client, "record", "status")
        self.disable_transaction(self.client)

        return recorder_state



    #
    #   Basic Methods
    #


    def init_player(self):
780
        """
781
782
783
784
        Initializes the LiquidSoap Player after startup of the engine.

        Returns:
            (String):   Message that the player is started.
785
        """
786
        t = StartupThread(self)
787
788
        t.start()

789
        return "Engine Core startup done!"
790

791
792

    # ------------------------------------------------------------------------------------------ #
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
793
    def __send_lqc_command__(self, lqs_instance, namespace, command, *args):
794
795
796
797
798
799
800
801
802
803
804
805
806
807
        """
        Ein Kommando an Liquidsoap senden
        @type  lqs_instance: object
        @param lqs_instance: Instance of LiquidSoap Client
        @type  namespace:    string
        @param namespace:    Namespace of function
        @type  command:      string
        @param command:      Function name
        @type args:          list
        @param args:         List of parameters
        @rtype:              string
        @return:             Response from LiquidSoap
        """
        try:
808
809
            if not self.disable_logging:
                if namespace == "recorder":
810
                    self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + "_" + str(command) + "." + str(args))
811
                else:
812
                    if command == "":
813
                        self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + str(args))
814
                    else:
815
                        self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + "." + str(command) + str(args))
816

817
            # call wanted function ...
818
819

            # FIXME REFACTOR all calls in a common way
David Trattnig's avatar
David Trattnig committed
820
            if command in  ["playlist_push", "playlist_seek", "playlist_clear", "http_set_url", "http_start", "http_stop", "http_status"]:
821
822
823
824
825
826
                func = getattr(lqs_instance, command)
                result = func(str(namespace), *args)
            else:
                func = getattr(lqs_instance, namespace)
                result = func(command, *args)            

827

828
829
            if not self.disable_logging:
                self.logger.debug("LiquidSoapCommunicator got response " + str(result))
830

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
831
            self.connection_attempts = 0
832

833
834
            return result

835
        except LQConnectionError as e:
836
            self.logger.error("Connection Error when sending " + str(namespace) + "." + str(command) + str(args))
837
            if self.try_to_reconnect():
838
                time.sleep(0.2)
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
839
840
                self.connection_attempts += 1
                if self.connection_attempts < 5:
841
842
843
844
845
846
847
848
849
                    # reconnect
                    self.__open_conn(self.client)
                    self.logger.info("Trying to resend " + str(namespace) + "." + str(command) + str(args))
                    # grab return value
                    retval = self.__send_lqc_command__(lqs_instance, namespace, command, *args)
                    # disconnect
                    self.__close_conn(self.client)
                    # return the val
                    return retval
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
850
                else:
851
852
853
854
855
856
                    if command == "":
                        msg = "Rethrowing Exception while trying to send " + str(namespace) + str(args)
                    else:
                        msg = "Rethrowing Exception while trying to send " + str(namespace) + "." + str(command) + str(args)

                    self.logger.info(msg)
857
                    self.disable_transaction(socket=self.client, force=True)
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
858
                    raise e
859
            else:
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
860
                # also store when was last admin mail sent with which content...
David Trattnig's avatar
David Trattnig committed
861
                # FIXME implement admin mail sending
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
862
                self.logger.critical("SEND ADMIN MAIL AT THIS POINT")
863
864
                raise e

David Trattnig's avatar
David Trattnig committed
865
866
867
868
869

    def is_active(self):
        """
        Checks if Liquidsoap is running
        """
870
871
872
873
874
875
876
877
878
        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
David Trattnig's avatar
David Trattnig committed
879
880
        
        return self.is_liquidsoap_running
881

David Trattnig's avatar
David Trattnig committed
882
883
884
885
886
887

    def engine_state(self):
        """
        Retrieves the state of all inputs and outputs.
        """
        state = self.__send_lqc_command__(self.client, "engine", "state")
888
889
        return state

David Trattnig's avatar
David Trattnig committed
890

891
    def liquidsoap_help(self):
David Trattnig's avatar
David Trattnig committed
892
893
894
        """
        Retrieves the Liquidsoap help.
        """
895
896
897
898
899
900
901
        data = self.__send_lqc_command__(self.client, "help", "")
        if not data:
            self.logger.warning("Could not get Liquidsoap's help")
        else:
            self.logger.debug("Got Liquidsoap's help")
        return data

David Trattnig's avatar
David Trattnig committed
902

903
904
    def version(self):
        """
David Trattnig's avatar
David Trattnig committed
905
        Get the version of Liquidsoap.
906
907
908
909
910
        """
        data = self.__send_lqc_command__(self.client, "version", "")
        self.logger.debug("Got Liquidsoap's version")
        return data

David Trattnig's avatar
David Trattnig committed
911

912
913
    def uptime(self):
        """
David Trattnig's avatar
David Trattnig committed
914
        Retrieves the uptime of Liquidsoap.
915
916
917
918
919
920
921
922
923
924
925
        """
        data = self.__send_lqc_command__(self.client, "uptime", "")
        self.logger.debug("Got Liquidsoap's uptime")
        return data


    #
    #   Connection and Transaction Handling
    #


926
    # ------------------------------------------------------------------------------------------ #
927
928
929
930
931
932
933
934
    def try_to_reconnect(self):
        self.enable_transaction()
        return self.transaction > 0

    # ------------------------------------------------------------------------------------------ #
    def enable_transaction(self, socket=None):
        # set socket to playout if nothing else is given
        if socket is None:
935
            socket = self.client
936

937
938
        self.transaction = self.transaction + 1

939
        self.logger.debug(TerminalColors.WARNING.value + "Enabling transaction! cnt: " + str(self.transaction) + TerminalColors.ENDC.value)
940
941
942
943

        if self.transaction > 1:
            return

944
945
946
947
948
        try:
            self.__open_conn(socket)
        except FileNotFoundError:
            self.disable_transaction(socket=socket, force=True)

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
949
950
951
            msg = "socket file " + socket.socket_path + " not found. Is liquidsoap running?"
            self.logger.critical(TerminalColors.RED.value + msg + TerminalColors.ENDC.value)
            self.auramailer.send_admin_mail("CRITICAL Exception when connecting to Liquidsoap", msg)
952

953
    # ------------------------------------------------------------------------------------------ #
954
955
956
957
958
    def disable_transaction(self, socket=None, force=False):
        if not force:
            # nothing to disable
            if self.transaction == 0:
                return
959

960
961
            # decrease transaction counter
            self.transaction = self.transaction - 1
962

963
964
965
966
967
968
969
970
            # debug msg
            self.logger.debug(TerminalColors.WARNING.value + "DISabling transaction! cnt: " + str(self.transaction) + TerminalColors.ENDC.value)

            # return if connection is still needed
            if self.transaction > 0:
                return
        else:
            self.logger.debug(TerminalColors.WARNING.value + "Forcefully DISabling transaction! " + TerminalColors.ENDC.value)
971

972
973
        # close conn and set transactioncounter to 0
        self.__close_conn(socket)
974
        self.transaction = 0
975

976
    # ------------------------------------------------------------------------------------------ #
977
978
    def __open_conn(self, socket):
        # already connected
979
        if self.transaction > 1:
980
            return
981

982
        self.logger.debug(TerminalColors.GREEN.value + "LiquidSoapCommunicator opening conn" + TerminalColors.ENDC.value)
983

984
        # try to connect
985
986
        socket.connect()

987
    # ------------------------------------------------------------------------------------------ #
988
989
    def __close_conn(self, socket):
        # set socket to playout
990
        if socket is None:
991
            socket = self.client
992

993
        # do not disconnect if a transaction is going on
994
        if self.transaction > 0:
995
            return
996

997
        # say bye
998
        socket.byebye()
999

1000
        # debug msg