engine.py 34.4 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
31
32
from modules.core.startup import StartupThread
from modules.core.state import PlayerStateService
33
from modules.communication.mail import AuraMailer
34

35
36
37
from modules.base.enum import ChannelType, Channel, TransitionType
from modules.base.utils import TerminalColors, SimpleUtil
from modules.base.exceptions import LQConnectionError, InvalidChannelException, NoActiveEntryException
38

39

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

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

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


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

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

77
78
        self.client = LiquidSoapPlayerClient(config, "engine.sock")
        # self.lqcr = LiquidSoapRecorderClient(config, "record.sock")
79

80
81
82
83
84
        #FIXME Can be removed
        # errors_file = self.config.get("install_dir") + "/errormessages/controller_error.js"
        # f = open(errors_file)
        # self.error_data = json.load(f)
        # f.close()
85

86
        self.auramailer = AuraMailer(self.config)
87
88
        self.is_liquidsoap_up_and_running()

89
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
117
118
119
120
121
122
        # Initialize Default Channels
        self.active_channel = {
            ChannelType.FILESYSTEM: Channel.FILESYSTEM_A,
            ChannelType.STREAM: Channel.STREAM_A,
            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"))


    def is_ready(self):
        """
123
        Returns `True` if the soundsystem is connected to Liquidsoap and is ready to be used.
124
125
126
        """
        return self.is_liquidsoap_running

127

128
    #
129
130
131
132
133
134
135
136
137
138
139
140
141
142
    #   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)
143
144


145
    # ------------------------------------------------------------------------------------------ #
146
147
148
    # 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
149

150
151
152
153
154
    # ------------------------------------------------------------------------------------------ #
    def get_active_mixer(self):
        """
        get active mixer in liquidsoap server
        :return:
155
        """
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
        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
175

176
    # ------------------------------------------------------------------------------------------ #
177
178
    def get_mixer_status(self):
        inputstate = {}
179

180
181
182
183
184
185
186
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
216
217
218
219
        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
    #


    # FIXME Currently not used, except for test class
    # def get_active_channel(self):
    #     """
    #     Retrieves the active channel from programme.

    #     Returns:
    #         (String):   The channel type, empty string if no channel is active.
    #     """
    #     active_entry = self.scheduler.get_active_entry()
    #     if active_entry is None:
    #         return ""
    #     return active_entry.channel
220

221
    # ------------------------------------------------------------------------------------------ #
222
223
224
225



    def play(self, entry, transition):
David Trattnig's avatar
David Trattnig committed
226
        """
227
228
        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
229
230

        Args:
231
232
233
234
            entry (PlaylistEntry):  The audio source to be played
            transition (TransitionType):    The type of transition to use e.g. fade-out.
            queue (Boolean):        If `True` the entry is queued if the `ChannelType` does allow so; 
                otherwise a new channel of the same type is activated
David Trattnig's avatar
David Trattnig committed
235
236
237
238
        
        Raises:
            (LQConnectionError): In case connecting to LiquidSoap isn't possible
        """
239
        try:
240
            self.enable_transaction()
241

242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
            # channel = self.active_channel[entry.type]
            # prev_channel = channel
            # already_active = False
            
            #FIXME
            # queue=False

            # if self.active_channel_type == entry.type:
            #     msg = SimpleUtil.pink("Channel type %s already active!" % str(entry.type))
            #     self.logger.info(msg)
                # already_active = True

            self.player_state.set_active_entry(entry)

            entry.channel = self.channel_swap(entry.type)
            # entry.channel = channel

            # PLAYLIST
            if entry.type == ChannelType.FILESYSTEM:
                # if not queue:
                
                self.playlist_push(entry.channel, entry.filename)
                
            # STREAM
            elif entry.type == ChannelType.STREAM:
                self.set_http_url(entry.channel, entry.source)
                self.http_start_stop(entry.channel, True)
                
            # LIVE
271
            else:
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
                # TODO Select correct LINE-OUT channels as per entry
                pass

            # if not already_active:
            #     self.channel_transition(prev_channel, channel, entry.volume, 0)

            # Assign selected channel
            

            # Move channel volume all the way up
            if transition == TransitionType.FADE:
                self.fade_in(entry)
            else:
                self.channel_volume(entry.channel, entry.volume)

            # Update active channel and type
            #self.active_channel_type = entry.type
            self.active_channel[entry.type] = entry.channel     
            
291

292
293
            self.disable_transaction()
            
294
        except LQConnectionError:
David Trattnig's avatar
David Trattnig committed
295
296
            # 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
297
            pass
298

David Trattnig's avatar
David Trattnig committed
299

300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315

    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
316
        """
317
        Stops the currently playing entry. 
David Trattnig's avatar
David Trattnig committed
318
319

        Args:
320
321
            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
322
        """
323

324
325
        try:
            self.enable_transaction()
326

327
328
329
330
331
332
333
334
            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)
335

336
337
            # self.playlist_clear(entry.channel)
            self.logger.info(SimpleUtil.pink("Stopped channel '%s' for entry %s" % (entry.channel, entry)))
338

339
            self.disable_transaction()
340

341
342
343
344
        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
345

David Trattnig's avatar
David Trattnig committed
346

347
348
349
350
    # def channel_transition(self, source_channel, target_channel, target_volume=100, transition_type=0):
        
    #     # Default: target_channel = 100% volume, source_channel = 0% volume
    #     if transition_type == 0:
David Trattnig's avatar
David Trattnig committed
351

352
353
    #         # Set volume of channel
    #         self.channel_volume(target_channel, target_volume)
354

355
356
357
    #         # Mute source channel
    #         if target_channel != source_channel:
    #             self.channel_volume(source_channel, 0)
358

359
360
361
362
363
    #         # Set other channels to zero volume
    #         # others = self.all_inputs_but(target_channel)
    #         # self.logger.info("Setting Volume=0 for channels: %s" % str(others))
    #         # for o in others:
    #         #     self.channel_volume(o, 0)
364

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
365

366

367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
    def channel_swap(self, channel_type):
        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"
            
        elif channel_type == ChannelType.STREAM:
            if active_channel == Channel.STREAM_A:
                channel = Channel.STREAM_B
                msg = "Swapped stream channel from A > B"
            else:
                channel = Channel.STREAM_A
                msg = "Swapped stream channel from B > A"

        if msg: self.logger.info(SimpleUtil.pink(msg))
        # self.active_channel[channel_type] = channel
        return channel
391
392
393



394
395
    # ------------------------------------------------------------------------------------------ #
    def all_inputs_but(self, input_type):
396
        try:
397
            activemixer_copy = self.get_all_channels().copy()
398
            activemixer_copy.remove(input_type)
399
400
        except ValueError as e:
            self.logger.error("Requested channel (" + input_type + ") not in channellist. Reason: " + str(e))
401
402
        except AttributeError:
            self.logger.critical("Channellist is None")
403

404
405
406
407
        return activemixer_copy

    # ------------------------------------------------------------------------------------------ #
    def get_all_channels(self):
408
        if self.channels is None or len(self.channels) == 0:
409
            self.channels = self.__send_lqc_command__(self.client, "mixer", "inputs")
410
411

        return self.channels
412

413
414
415
416
417
    # ------------------------------------------------------------------------------------------ #
    def reload_channels(self):
        self.channels = None
        return self.get_all_channels()

418

419

420
421
422
    # ------------------------------------------------------------------------------------------ #
    def channel_activate(self, channel, activate):
        channels = self.get_all_channels()
423
424
425
426

        try:
            index = channels.index(channel)
            if len(channel) < 1:
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
427
428
429
430
431
432
                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))
433

434
435
436
    # ------------------------------------------------------------------------------------------ #
    def channel_volume(self, channel, volume):
        """
437
438
439
440
441
        Set volume of a channel

        Args:
            channel (Channel):      The channel
            volume  (Integer)       Volume between 0 and 100
442
        """
443

444
        channel = str(channel)
445
        try:
446
447
448
449
450
451
            if str(volume) == "100":
                channels = self.get_all_channels()
                index = channels.index(channel)
            else:
                channels = self.get_all_channels()
                index = channels.index(channel)
452
        except ValueError as e:
453
454
            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
455
            self.logger.info("Available channels: %s" % str(channels))
456
457
458
            return

        try:
459
            if len(channel) < 1:
460
461
                msg = SimpleUtil.red("Cannot set volume of channel " + channel + " to " + str(volume) + "! There are no channels.")
                self.logger.warning(msg)
462
            else:
463
                message = self.__send_lqc_command__(self.client, "mixer", "volume", str(index), str(int(volume)))
464

465
466
                if not self.disable_logging:
                    if message.find('volume=' + str(volume) + '%'):
467
                        self.logger.info(SimpleUtil.pink("Set volume of channel '%s' to %s" % (channel, str(volume))))
468
                    else:
469
470
                        msg = SimpleUtil.red("Setting volume of channel " + channel + " gone wrong! Liquidsoap message: " + message)
                        self.logger.warning(msg)
471

472
                return message
473
        except AttributeError as e: #(LQConnectionError, AttributeError):
474
            self.disable_transaction(force=True)
475
476
            msg = SimpleUtil.red("Ran into exception when setting volume of channel " + channel + ". Reason: " + str(e))
            self.logger.error(msg)
477

478
479

    #
480
    #   Channel Type - Stream 
481
482
483
    #


484
485
486
487
488
489
490
491
492
    def stream_start(self, url):
        try:
            self.enable_transaction()
            self.__send_lqc_command__(self.client, "http", "url", url)
            self.__send_lqc_command__(self.client, "http", "start")
            self.disable_transaction()
        except LQConnectionError:
            # we already caught and handled this error in __send_lqc_command__, but we do not want to execute this function further
            pass
493
494


495
    def stream_stop(self, url):
496
        try:
497
498
499
500
501
502
            self.enable_transaction()
            self.__send_lqc_command__(self.client, "http", "start")
            self.disable_transaction()
        except LQConnectionError:
            # we already caught and handled this error in __send_lqc_command__, but we do not want to execute this function further
            pass
503
504


505
506
507
508
509
    def http_start_stop(self, start):
        if start:
            cmd = "start"
        else:
            cmd = "stop"
510

511
        try:
512
            self.enable_transaction()
513
            self.__send_lqc_command__(self.client, "http", cmd)
514
515
            self.disable_transaction()
        except LQConnectionError:
516
            # we already caught and handled this error in __send_lqc_command__, but we do not want to execute this function further
517
518
519
            pass


520
    # ------------------------------------------------------------------------------------------ #
521
522
    def set_http_url(self, uri):
        return self.__send_lqc_command__(self.client, "http", "url", uri)
David Trattnig's avatar
David Trattnig committed
523

524

525
526
527
    #
    #   Channel Type - Playlist 
    #
David Trattnig's avatar
David Trattnig committed
528

529
530
531
532
    # FIXME
    # def playlist_activate(self, playlist, cue_in=0.0):
    #     """
    #     Activates a new Playlist.
533

534
535
536
537
538
539
540
541
542
543
    #     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()
544

545
546
    #     # Set default channel, if no previous track is available
    #     current_channel = self.active_channel[ChannelType.FILESYSTEM]
547

548
549
    #     # if active_entry:
    #     #     current_channel = active_entry.channel
550

551
552
553
554
555
556
    #     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.
557

558
559
560
561
562
563
    #         # 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()
564
565


566
567
568
569
570
571
572
573
574
575
    #         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
576

577
    #         self.disable_transaction()
578

579
580
581
582
583
584
    #         # 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
585
586
587



588
589
590
    def playlist_push(self, channel, uri):
        """
        Adds an filesystem URI to the given `ChannelType.FILESYSTEM` channel.
591

592
593
594
595
596
597
598
599
600
        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)
601
602
603



604
605
606
    def playlist_seek(self, channel, seconds_to_seek):
        """
        Forwards the player of the given `ChannelType.FILESYSTEM` channel by (n) seconds.
607

608
609
610
611
612
613
        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))
614
615
616



617
618
619
620
621
622
623
624
625
626
    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")
        
627
628
629
630
631
632
633



    #
    #   Fading 
    #

634
635
636
637
638
639

    def fade_in(self, entry):
        """
        Performs a fade-in for the given `entry` to the `entry.volume` loudness
        at channel `entry.channel`.
        """
640
641
642
643
644
        try:
            fade_in_time = float(self.config.get("fade_in_time"))

            if fade_in_time > 0:
                self.fade_in_active = True
645
                target_volume = entry.volume
646
647
648

                step = fade_in_time / target_volume

649
650
651
                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))
652

653
                # Enable logging, which might have been disabled in a previous fade-out
654
655
656
657
                self.disable_logging = True
                self.client.disable_logging = True

                for i in range(target_volume):
658
                    self.channel_volume(entry.channel.value, i + 1)
659
660
                    time.sleep(step)

661
662
                msg = "Finished with fading-in '%s'." % entry.channel
                self.logger.info(SimpleUtil.pink(msg))
663
664
665
666
667

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

669
670
671
672
673
        except LQConnectionError as e:
            self.logger.critical(str(e))

        return True

674
675
676
677
678
679


    def fade_out(self, entry):
        """
        Performs a fade-out for the given `entry` at channel `entry.channel`.
        """
680
681
682
683
        try:
            fade_out_time = float(self.config.get("fade_out_time"))

            if fade_out_time > 0:
684
                step = abs(fade_out_time) / entry.volume
685

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

689
                # Disable logging... it is going to be enabled again after fadein and -out is finished
690
691
692
                self.disable_logging = True
                self.client.disable_logging = True

693
694
                for i in range(entry.volume):
                    self.channel_volume(entry.channel.value, entry.volume-i-1)
695
696
                    time.sleep(step)

697
698
                msg = "Finished with fading-out '%s'" % entry.channel
                self.logger.info(SimpleUtil.pink(msg))
699

700
                # Enable logging again
701
702
703
704
                self.fade_out_active = False
                if not self.fade_in_active:
                    self.disable_logging = False
                    self.client.disable_logging = False
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
780
781
782
783
784
        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):
785
        """
786
787
788
789
        Initializes the LiquidSoap Player after startup of the engine.

        Returns:
            (String):   Message that the player is started.
790
        """
791
        t = StartupThread(self)
792
793
        t.start()

794
        return "Engine Core startup done!"
795

796
797

    # ------------------------------------------------------------------------------------------ #
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
798
    def __send_lqc_command__(self, lqs_instance, namespace, command, *args):
799
800
801
802
803
804
805
806
807
808
809
810
811
812
        """
        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:
813
814
            if not self.disable_logging:
                if namespace == "recorder":
815
                    self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + "_" + str(command) + "." + str(args))
816
                else:
817
                    if command == "":
818
                        self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + str(args))
819
                    else:
820
                        self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + "." + str(command) + str(args))
821

822
            # call wanted function ...
823
824
825
826
827
828
829
830
831

            # FIXME REFACTOR all calls in a common way
            if command in  ["playlist_push", "playlist_seek", "playlist_clear"]:
                func = getattr(lqs_instance, command)
                result = func(str(namespace), *args)
            else:
                func = getattr(lqs_instance, namespace)
                result = func(command, *args)            

832

833
834
            if not self.disable_logging:
                self.logger.debug("LiquidSoapCommunicator got response " + str(result))
835

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
836
            self.connection_attempts = 0
837

838
839
            return result

840
        except LQConnectionError as e:
841
            self.logger.error("Connection Error when sending " + str(namespace) + "." + str(command) + str(args))
842
            if self.try_to_reconnect():
843
                time.sleep(0.2)
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
844
845
                self.connection_attempts += 1
                if self.connection_attempts < 5:
846
847
848
849
850
851
852
853
854
                    # 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
855
                else:
856
857
858
859
860
861
                    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)
862
                    self.disable_transaction(socket=self.client, force=True)
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
863
                    raise e
864
            else:
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
865
                # also store when was last admin mail sent with which content...
David Trattnig's avatar
David Trattnig committed
866
                # FIXME implement admin mail sending
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
867
                self.logger.critical("SEND ADMIN MAIL AT THIS POINT")
868
869
                raise e

870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
    # ------------------------------------------------------------------------------------------ #
    def is_liquidsoap_up_and_running(self):
        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

    # ------------------------------------------------------------------------------------------ #
    def auraengine_state(self):
        state = self.__send_lqc_command__(self.client, "auraengine", "state")
        return state

    # ------------------------------------------------------------------------------------------ #
    def liquidsoap_help(self):
        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

    # ------------------------------------------------------------------------------------------ #
    def version(self):
        """
        get version
        """
        data = self.__send_lqc_command__(self.client, "version", "")
        self.logger.debug("Got Liquidsoap's version")
        return data

    # ------------------------------------------------------------------------------------------ #
    def uptime(self):
        """
        get uptime
        """
        data = self.__send_lqc_command__(self.client, "uptime", "")
        self.logger.debug("Got Liquidsoap's uptime")
        return data


    #
    #   Connection and Transaction Handling
    #


920
    # ------------------------------------------------------------------------------------------ #
921
922
923
924
925
926
927
928
    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:
929
            socket = self.client
930

931
932
        self.transaction = self.transaction + 1

933
        self.logger.debug(TerminalColors.WARNING.value + "Enabling transaction! cnt: " + str(self.transaction) + TerminalColors.ENDC.value)
934
935
936
937

        if self.transaction > 1:
            return

938
939
940
941
942
        try:
            self.__open_conn(socket)
        except FileNotFoundError:
            self.disable_transaction(socket=socket, force=True)

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
943
944
945
            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)
946

947
    # ------------------------------------------------------------------------------------------ #
948
949
950
951
952
    def disable_transaction(self, socket=None, force=False):
        if not force:
            # nothing to disable
            if self.transaction == 0:
                return
953

954
955
            # decrease transaction counter
            self.transaction = self.transaction - 1
956

957
958
959
960
961
962
963
964
            # 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)
965

966
967
        # close conn and set transactioncounter to 0
        self.__close_conn(socket)
968
        self.transaction = 0
969

970
    # ------------------------------------------------------------------------------------------ #
971
972
    def __open_conn(self, socket):
        # already connected
973
        if self.transaction > 1:
974
            return
975

976
        self.logger.debug(TerminalColors.GREEN.value + "LiquidSoapCommunicator opening conn" + TerminalColors.ENDC.value)
977

978
        # try to connect
979
980
        socket.connect()

981
    # ------------------------------------------------------------------------------------------ #
982
983
    def __close_conn(self, socket):
        # set socket to playout
984
        if socket is None:
985
            socket = self.client
986

987
        # do not disconnect if a transaction is going on
988
        if self.transaction > 0:
989
            return
990

991
        # say bye
992
        socket.byebye()
993

994
995
        # debug msg
        self.logger.debug(TerminalColors.BLUE.value + "LiquidSoapCommunicator closed conn" + TerminalColors.ENDC.value)