engine.py 34 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
        self.auramailer = AuraMailer(self.config)
David Trattnig's avatar
David Trattnig committed
81
        self.is_active()
82

83
84
85
86
87
88
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
        # 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):
        """
117
        Returns `True` if the soundsystem is connected to Liquidsoap and is ready to be used.
118
119
120
        """
        return self.is_liquidsoap_running

121

122
    #
123
124
125
126
127
128
129
130
131
132
133
134
135
136
    #   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)
137
138


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

144
145
146
147
148
    # ------------------------------------------------------------------------------------------ #
    def get_active_mixer(self):
        """
        get active mixer in liquidsoap server
        :return:
149
        """
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
        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
169

170
    # ------------------------------------------------------------------------------------------ #
171
172
    def get_mixer_status(self):
        inputstate = {}
173

174
175
176
177
178
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
        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
214

215
    # ------------------------------------------------------------------------------------------ #
216
217
218
219



    def play(self, entry, transition):
David Trattnig's avatar
David Trattnig committed
220
        """
221
222
        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
223
224

        Args:
225
226
227
228
            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
229
230
231
232
        
        Raises:
            (LQConnectionError): In case connecting to LiquidSoap isn't possible
        """
233
        try:
234
            self.enable_transaction()
235

236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
            # 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
265
            else:
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
                # 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     
            
285

286
287
            self.disable_transaction()
            
288
        except LQConnectionError:
David Trattnig's avatar
David Trattnig committed
289
290
            # 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
291
            pass
292

David Trattnig's avatar
David Trattnig committed
293

294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309

    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
310
        """
311
        Stops the currently playing entry. 
David Trattnig's avatar
David Trattnig committed
312
313

        Args:
314
315
            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
316
        """
317

318
319
        try:
            self.enable_transaction()
320

321
322
323
324
325
326
327
328
            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)
329

330
331
            # self.playlist_clear(entry.channel)
            self.logger.info(SimpleUtil.pink("Stopped channel '%s' for entry %s" % (entry.channel, entry)))
332

333
            self.disable_transaction()
334

335
336
337
338
        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
339

David Trattnig's avatar
David Trattnig committed
340

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

346
347
    #         # Set volume of channel
    #         self.channel_volume(target_channel, target_volume)
348

349
350
351
    #         # Mute source channel
    #         if target_channel != source_channel:
    #             self.channel_volume(source_channel, 0)
352

353
354
355
356
357
    #         # 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)
358

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
359

360

361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
    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
385
386
387



388
389
    # ------------------------------------------------------------------------------------------ #
    def all_inputs_but(self, input_type):
390
        try:
391
            activemixer_copy = self.get_all_channels().copy()
392
            activemixer_copy.remove(input_type)
393
394
        except ValueError as e:
            self.logger.error("Requested channel (" + input_type + ") not in channellist. Reason: " + str(e))
395
396
        except AttributeError:
            self.logger.critical("Channellist is None")
397

398
399
400
401
        return activemixer_copy

    # ------------------------------------------------------------------------------------------ #
    def get_all_channels(self):
402
        if self.channels is None or len(self.channels) == 0:
403
            self.channels = self.__send_lqc_command__(self.client, "mixer", "inputs")
404
405

        return self.channels
406

407
408
409
410
411
    # ------------------------------------------------------------------------------------------ #
    def reload_channels(self):
        self.channels = None
        return self.get_all_channels()

412

413

414
415
416
    # ------------------------------------------------------------------------------------------ #
    def channel_activate(self, channel, activate):
        channels = self.get_all_channels()
417
418
419
420

        try:
            index = channels.index(channel)
            if len(channel) < 1:
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
421
422
423
424
425
426
                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))
427

428
429
430
    # ------------------------------------------------------------------------------------------ #
    def channel_volume(self, channel, volume):
        """
431
432
433
434
435
        Set volume of a channel

        Args:
            channel (Channel):      The channel
            volume  (Integer)       Volume between 0 and 100
436
        """
437

438
        channel = str(channel)
439
        try:
440
441
442
443
444
445
            if str(volume) == "100":
                channels = self.get_all_channels()
                index = channels.index(channel)
            else:
                channels = self.get_all_channels()
                index = channels.index(channel)
446
        except ValueError as e:
447
448
            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
449
            self.logger.info("Available channels: %s" % str(channels))
450
451
452
            return

        try:
453
            if len(channel) < 1:
454
455
                msg = SimpleUtil.red("Cannot set volume of channel " + channel + " to " + str(volume) + "! There are no channels.")
                self.logger.warning(msg)
456
            else:
457
                message = self.__send_lqc_command__(self.client, "mixer", "volume", str(index), str(int(volume)))
458

459
460
                if not self.disable_logging:
                    if message.find('volume=' + str(volume) + '%'):
461
                        self.logger.info(SimpleUtil.pink("Set volume of channel '%s' to %s" % (channel, str(volume))))
462
                    else:
463
464
                        msg = SimpleUtil.red("Setting volume of channel " + channel + " gone wrong! Liquidsoap message: " + message)
                        self.logger.warning(msg)
465

466
                return message
467
        except AttributeError as e: #(LQConnectionError, AttributeError):
468
            self.disable_transaction(force=True)
469
470
            msg = SimpleUtil.red("Ran into exception when setting volume of channel " + channel + ". Reason: " + str(e))
            self.logger.error(msg)
471

472
473

    #
474
    #   Channel Type - Stream 
475
476
477
    #


478
479
480
481
482
483
484
485
486
    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
487
488


489
    def stream_stop(self, url):
490
        try:
491
492
493
494
495
496
            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
497
498


499
500
501
502
503
    def http_start_stop(self, start):
        if start:
            cmd = "start"
        else:
            cmd = "stop"
504

505
        try:
506
            self.enable_transaction()
507
            self.__send_lqc_command__(self.client, "http", cmd)
508
509
            self.disable_transaction()
        except LQConnectionError:
510
            # we already caught and handled this error in __send_lqc_command__, but we do not want to execute this function further
511
512
513
            pass


514
    # ------------------------------------------------------------------------------------------ #
515
516
    def set_http_url(self, uri):
        return self.__send_lqc_command__(self.client, "http", "url", uri)
David Trattnig's avatar
David Trattnig committed
517

518

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

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

528
529
530
531
532
533
534
535
536
537
    #     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()
538

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

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

545
546
547
548
549
550
    #     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.
551

552
553
554
555
556
557
    #         # 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()
558
559


560
561
562
563
564
565
566
567
568
569
    #         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
570

571
    #         self.disable_transaction()
572

573
574
575
576
577
578
    #         # 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
579
580
581



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

586
587
588
589
590
591
592
593
594
        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)
595
596
597



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

602
603
604
605
606
607
        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))
608
609
610



611
612
613
614
615
616
617
618
619
620
    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")
        
621
622
623
624
625
626
627



    #
    #   Fading 
    #

628
629
630
631
632
633

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

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

                step = fade_in_time / target_volume

643
644
645
                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))
646

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

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

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

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

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

        return True

668
669
670
671
672
673


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

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

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

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

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

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

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

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
        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):
779
        """
780
781
782
783
        Initializes the LiquidSoap Player after startup of the engine.

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

788
        return "Engine Core startup done!"
789

790
791

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

816
            # call wanted function ...
817
818
819
820
821
822
823
824
825

            # 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)            

826

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

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

832
833
            return result

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

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

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

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

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

David Trattnig's avatar
David Trattnig committed
889

890
    def liquidsoap_help(self):
David Trattnig's avatar
David Trattnig committed
891
892
893
        """
        Retrieves the Liquidsoap help.
        """
894
895
896
897
898
899
900
        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
901

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

David Trattnig's avatar
David Trattnig committed
910

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


    #
    #   Connection and Transaction Handling
    #


925
    # ------------------------------------------------------------------------------------------ #
926
927
928
929
930
931
932
933
    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:
934
            socket = self.client
935

936
937
        self.transaction = self.transaction + 1

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

        if self.transaction > 1:
            return

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

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
948
949
950
            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)
951

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

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

962
963
964
965
966
967
968
969
            # 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)
970

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

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

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

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

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

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

996
        # say bye
997
        socket.byebye()
998

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