engine.py 34.6 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
37
38
from modules.base.enum          import ChannelType, Channel, TransitionType
from modules.base.utils         import TerminalColors, SimpleUtil
from modules.base.exceptions    import LQConnectionError, InvalidChannelException, NoActiveEntryException, EngineMalfunctionException
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
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"))

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

229
    # ------------------------------------------------------------------------------------------ #
230
231
232
233



    def play(self, entry, transition):
David Trattnig's avatar
David Trattnig committed
234
        """
235
236
        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
237
238

        Args:
239
240
241
242
            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
243
244
245
246
        
        Raises:
            (LQConnectionError): In case connecting to LiquidSoap isn't possible
        """
247
        try:
248
            self.enable_transaction()
249

250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
            # 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
279
            else:
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
                # 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     
            
299

300
301
            self.disable_transaction()
            
302
        except LQConnectionError:
David Trattnig's avatar
David Trattnig committed
303
304
            # 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
305
            pass
306

David Trattnig's avatar
David Trattnig committed
307

308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323

    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
324
        """
325
        Stops the currently playing entry. 
David Trattnig's avatar
David Trattnig committed
326
327

        Args:
328
329
            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
330
        """
331

332
333
        try:
            self.enable_transaction()
334

335
336
337
338
339
340
341
342
            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)
343

344
345
            # self.playlist_clear(entry.channel)
            self.logger.info(SimpleUtil.pink("Stopped channel '%s' for entry %s" % (entry.channel, entry)))
346

347
            self.disable_transaction()
348

349
350
351
352
        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
353

David Trattnig's avatar
David Trattnig committed
354

355
356
357
358
    # 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
359

360
361
    #         # Set volume of channel
    #         self.channel_volume(target_channel, target_volume)
362

363
364
365
    #         # Mute source channel
    #         if target_channel != source_channel:
    #             self.channel_volume(source_channel, 0)
366

367
368
369
370
371
    #         # 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)
372

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
373

374

375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
    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
399
400
401



402
403
    # ------------------------------------------------------------------------------------------ #
    def all_inputs_but(self, input_type):
404
        try:
405
            activemixer_copy = self.get_all_channels().copy()
406
            activemixer_copy.remove(input_type)
407
408
        except ValueError as e:
            self.logger.error("Requested channel (" + input_type + ") not in channellist. Reason: " + str(e))
409
410
        except AttributeError:
            self.logger.critical("Channellist is None")
411

412
413
414
415
        return activemixer_copy

    # ------------------------------------------------------------------------------------------ #
    def get_all_channels(self):
416
        if self.channels is None or len(self.channels) == 0:
417
            self.channels = self.__send_lqc_command__(self.client, "mixer", "inputs")
418
419

        return self.channels
420

421
422
423
424
425
    # ------------------------------------------------------------------------------------------ #
    def reload_channels(self):
        self.channels = None
        return self.get_all_channels()

426

427

428
429
430
    # ------------------------------------------------------------------------------------------ #
    def channel_activate(self, channel, activate):
        channels = self.get_all_channels()
431
432
433
434

        try:
            index = channels.index(channel)
            if len(channel) < 1:
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
435
436
437
438
439
440
                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))
441

442
443
444
    # ------------------------------------------------------------------------------------------ #
    def channel_volume(self, channel, volume):
        """
445
446
447
448
449
        Set volume of a channel

        Args:
            channel (Channel):      The channel
            volume  (Integer)       Volume between 0 and 100
450
        """
451

452
        channel = str(channel)
453
        try:
454
455
456
457
458
459
            if str(volume) == "100":
                channels = self.get_all_channels()
                index = channels.index(channel)
            else:
                channels = self.get_all_channels()
                index = channels.index(channel)
460
        except ValueError as e:
461
462
            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
463
            self.logger.info("Available channels: %s" % str(channels))
464
465
466
            return

        try:
467
            if len(channel) < 1:
468
469
                msg = SimpleUtil.red("Cannot set volume of channel " + channel + " to " + str(volume) + "! There are no channels.")
                self.logger.warning(msg)
470
            else:
471
                message = self.__send_lqc_command__(self.client, "mixer", "volume", str(index), str(int(volume)))
472

473
474
                if not self.disable_logging:
                    if message.find('volume=' + str(volume) + '%'):
475
                        self.logger.info(SimpleUtil.pink("Set volume of channel '%s' to %s" % (channel, str(volume))))
476
                    else:
477
478
                        msg = SimpleUtil.red("Setting volume of channel " + channel + " gone wrong! Liquidsoap message: " + message)
                        self.logger.warning(msg)
479

480
                return message
481
        except AttributeError as e: #(LQConnectionError, AttributeError):
482
            self.disable_transaction(force=True)
483
484
            msg = SimpleUtil.red("Ran into exception when setting volume of channel " + channel + ". Reason: " + str(e))
            self.logger.error(msg)
485

486
487

    #
488
    #   Channel Type - Stream 
489
490
491
    #


492
493
494
495
496
497
498
499
500
    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
501
502


503
    def stream_stop(self, url):
504
        try:
505
506
507
508
509
510
            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
511
512


513
514
515
516
517
    def http_start_stop(self, start):
        if start:
            cmd = "start"
        else:
            cmd = "stop"
518

519
        try:
520
            self.enable_transaction()
521
            self.__send_lqc_command__(self.client, "http", cmd)
522
523
            self.disable_transaction()
        except LQConnectionError:
524
            # we already caught and handled this error in __send_lqc_command__, but we do not want to execute this function further
525
526
527
            pass


528
    # ------------------------------------------------------------------------------------------ #
529
530
    def set_http_url(self, uri):
        return self.__send_lqc_command__(self.client, "http", "url", uri)
David Trattnig's avatar
David Trattnig committed
531

532

533
534
535
    #
    #   Channel Type - Playlist 
    #
David Trattnig's avatar
David Trattnig committed
536

537
538
539
540
    # FIXME
    # def playlist_activate(self, playlist, cue_in=0.0):
    #     """
    #     Activates a new Playlist.
541

542
543
544
545
546
547
548
549
550
551
    #     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()
552

553
554
    #     # Set default channel, if no previous track is available
    #     current_channel = self.active_channel[ChannelType.FILESYSTEM]
555

556
557
    #     # if active_entry:
    #     #     current_channel = active_entry.channel
558

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

566
567
568
569
570
571
    #         # 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()
572
573


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

585
    #         self.disable_transaction()
586

587
588
589
590
591
592
    #         # 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
593
594
595



596
597
598
    def playlist_push(self, channel, uri):
        """
        Adds an filesystem URI to the given `ChannelType.FILESYSTEM` channel.
599

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



612
613
614
    def playlist_seek(self, channel, seconds_to_seek):
        """
        Forwards the player of the given `ChannelType.FILESYSTEM` channel by (n) seconds.
615

616
617
618
619
620
621
        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))
622
623
624



625
626
627
628
629
630
631
632
633
634
    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")
        
635
636
637
638
639
640
641



    #
    #   Fading 
    #

642
643
644
645
646
647

    def fade_in(self, entry):
        """
        Performs a fade-in for the given `entry` to the `entry.volume` loudness
        at channel `entry.channel`.
        """
648
649
650
651
652
        try:
            fade_in_time = float(self.config.get("fade_in_time"))

            if fade_in_time > 0:
                self.fade_in_active = True
653
                target_volume = entry.volume
654
655
656

                step = fade_in_time / target_volume

657
658
659
                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))
660

661
                # Enable logging, which might have been disabled in a previous fade-out
662
663
664
665
                self.disable_logging = True
                self.client.disable_logging = True

                for i in range(target_volume):
666
                    self.channel_volume(entry.channel.value, i + 1)
667
668
                    time.sleep(step)

669
670
                msg = "Finished with fading-in '%s'." % entry.channel
                self.logger.info(SimpleUtil.pink(msg))
671
672
673
674
675

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

677
678
679
680
681
        except LQConnectionError as e:
            self.logger.critical(str(e))

        return True

682
683
684
685
686
687


    def fade_out(self, entry):
        """
        Performs a fade-out for the given `entry` at channel `entry.channel`.
        """
688
689
690
691
        try:
            fade_out_time = float(self.config.get("fade_out_time"))

            if fade_out_time > 0:
692
                step = abs(fade_out_time) / entry.volume
693

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

697
                # Disable logging... it is going to be enabled again after fadein and -out is finished
698
699
700
                self.disable_logging = True
                self.client.disable_logging = True

701
702
                for i in range(entry.volume):
                    self.channel_volume(entry.channel.value, entry.volume-i-1)
703
704
                    time.sleep(step)

705
706
                msg = "Finished with fading-out '%s'" % entry.channel
                self.logger.info(SimpleUtil.pink(msg))
707

708
                # Enable logging again
709
710
711
712
                self.fade_out_active = False
                if not self.fade_in_active:
                    self.disable_logging = False
                    self.client.disable_logging = False
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
785
786
787
788
789
790
791
792
        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):
793
        """
794
795
796
797
        Initializes the LiquidSoap Player after startup of the engine.

        Returns:
            (String):   Message that the player is started.
798
        """
799
        t = StartupThread(self)
800
801
        t.start()

802
        return "Engine Core startup done!"
803

804
805

    # ------------------------------------------------------------------------------------------ #
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
806
    def __send_lqc_command__(self, lqs_instance, namespace, command, *args):
807
808
809
810
811
812
813
814
815
816
817
818
819
820
        """
        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:
821
822
            if not self.disable_logging:
                if namespace == "recorder":
823
                    self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + "_" + str(command) + "." + str(args))
824
                else:
825
                    if command == "":
826
                        self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + str(args))
827
                    else:
828
                        self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + "." + str(command) + str(args))
829

830
            # call wanted function ...
831
832
833
834
835
836
837
838
839

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

840

841
842
            if not self.disable_logging:
                self.logger.debug("LiquidSoapCommunicator got response " + str(result))
843

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
844
            self.connection_attempts = 0
845

846
847
            return result

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

David Trattnig's avatar
David Trattnig committed
878
879
880
881
882

    def is_active(self):
        """
        Checks if Liquidsoap is running
        """
883
884
885
886
887
888
889
890
891
        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
892
893
        
        return self.is_liquidsoap_running
894

David Trattnig's avatar
David Trattnig committed
895
896
897
898
899
900

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

David Trattnig's avatar
David Trattnig committed
903

904
    def liquidsoap_help(self):
David Trattnig's avatar
David Trattnig committed
905
906
907
        """
        Retrieves the Liquidsoap help.
        """
908
909
910
911
912
913
914
        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
915

916
917
    def version(self):
        """
David Trattnig's avatar
David Trattnig committed
918
        Get the version of Liquidsoap.
919
920
921
922
923
        """
        data = self.__send_lqc_command__(self.client, "version", "")
        self.logger.debug("Got Liquidsoap's version")
        return data

David Trattnig's avatar
David Trattnig committed
924

925
926
    def uptime(self):
        """
David Trattnig's avatar
David Trattnig committed
927
        Retrieves the uptime of Liquidsoap.
928
929
930
931
932
933
934
935
936
937
938
        """
        data = self.__send_lqc_command__(self.client, "uptime", "")
        self.logger.debug("Got Liquidsoap's uptime")
        return data


    #
    #   Connection and Transaction Handling
    #


939
    # ------------------------------------------------------------------------------------------ #
940
941
942
943
944
945
946
947
    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:
948
            socket = self.client
949

950
951
        self.transaction = self.transaction + 1

952
        self.logger.debug(TerminalColors.WARNING.value + "Enabling transaction! cnt: " + str(self.transaction) + TerminalColors.ENDC.value)
953
954
955
956

        if self.transaction > 1:
            return

957
958
959
960
961
        try:
            self.__open_conn(socket)
        except FileNotFoundError:
            self.disable_transaction(socket=socket, force=True)

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
962
963
964
            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)
965

966
    # ------------------------------------------------------------------------------------------ #
967
968
969
970
971
    def disable_transaction(self, socket=None, force=False):
        if not force:
            # nothing to disable
            if self.transaction == 0:
                return
972

973
974
            # decrease transaction counter
            self.transaction = self.transaction - 1
975

976
977
978
979
980
981
982
983
            # 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)
984

985
986
        # close conn and set transactioncounter to 0
        self.__close_conn(socket)
987
        self.transaction = 0
988

989
    # ------------------------------------------------------------------------------------------ #
990
991
    def __open_conn(self, socket):
        # already connected
992
        if self.transaction > 1:
993
            return
994

995
        self.logger.debug(TerminalColors.GREEN.value + "LiquidSoapCommunicator opening conn" + TerminalColors.ENDC.value)
996

997
        # try to connect
998
999
        socket.connect()

1000
    # ------------------------------------------------------------------------------------------ #