communicator.py 25.1 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#
#  engine
#
#  Playout Daemon for autoradio project
#
#
#  Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
#  This file is part of engine.
#
#  engine is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  any later version.
#
#  engine is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with engine. If not, see <http://www.gnu.org/licenses/>.
#

25
26
import time
import logging
27
import json
28

29
from modules.communication.liquidsoap.playerclient import LiquidSoapPlayerClient
30
# from modules.communication.liquidsoap.recorderclient import LiquidSoapRecorderClient
31
from modules.communication.liquidsoap.initthread import LiquidSoapInitThread
32
33
from modules.communication.mail.mail import AuraMailer

34
from libraries.enum.auraenumerations import TerminalColors, ScheduleEntryType
35
from libraries.exceptions.auraexceptions import LQConnectionError
36
#from libraries.database.broadcasts import TrackService
37
from libraries.exceptions.exception_logger import ExceptionLogger
38

39
40
41
42
""" 
    LiquidSoapCommunicator Class 
    Uses LiquidSoapClient, but introduces more complex commands, transactions and error handling
"""
43

44
class LiquidSoapCommunicator(ExceptionLogger):
45
    client = None
46
    logger = None
47
    transaction = 0
48
    channels = None
49
    scheduler = None
50
    error_data = None
51
    auramailer = None
52
    is_liquidsoap_running = False
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
53
    connection_attempts = 0
54
    active_channel = None
55
    disable_logging = False
56
57
    fade_in_active = False
    fade_out_active = False
58
59

    # ------------------------------------------------------------------------------------------ #
60
    def __init__(self, config):
61
        """
62
63
        Constructor
        """
64
        self.config = config
65
66
        self.logger = logging.getLogger("AuraEngine")

67
68
        self.client = LiquidSoapPlayerClient(config, "engine.sock")
        # self.lqcr = LiquidSoapRecorderClient(config, "record.sock")
69

70
        errors_file = self.config.get("install_dir") + "/errormessages/controller_error.js"
71
        f = open(errors_file)
72
        self.error_data = json.load(f)
73
        f.close()
74

75
        self.auramailer = AuraMailer(self.config)
76

77
78
79
80
81
82
83
84
85
86
87
88
89
90
        self.is_liquidsoap_up_and_running()

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

91
92
    # ------------------------------------------------------------------------------------------ #
    def set_volume(self, mixernumber, volume):
93
        return self.__send_lqc_command__(self.client, "mixer", "volume", mixernumber, volume)
94
95
96

    # ------------------------------------------------------------------------------------------ #
    def get_active_mixer(self):
97
98
99
100
        """
        get active mixer in liquidsoap server
        :return:
        """
101
102
        activeinputs = []

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

106
        inputs = self.get_all_channels()
107
108
109

        cnt = 0
        for input in inputs:
110
            status = self.__get_mixer_status__(cnt)
111
112
113
114
115
116

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

            cnt = cnt + 1

117
        self.disable_transaction()
118
119
120

        return activeinputs

121
122
123
124
125
126
    # ------------------------------------------------------------------------------------------ #
    def get_active_channel(self):
        """
        gets active channel from programme
        :return:
        """
127
        (show, active_entry) = self.scheduler.get_active_entry()
128
        if active_entry is None:
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
129
            return ""
130
        return active_entry.type
131

132
133
    # ------------------------------------------------------------------------------------------ #
    def get_mixer_status(self):
134
135
        inputstate = {}

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
136
        self.enable_transaction()
137

138
        inputs = self.get_all_channels()
139
140
141

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

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
145
        self.disable_transaction()
146
147
148

        return inputstate

149
150
151
152
    # ------------------------------------------------------------------------------------------ #
    def get_mixer_volume(self, channel):
        return False

153
154
    # ------------------------------------------------------------------------------------------ #
    def get_recorder_status(self):
155
156
157
        self.enable_transaction(self.client)
        recorder_state = self.__send_lqc_command__(self.client, "record", "status")
        self.disable_transaction(self.client)
158
159
160

        return recorder_state

161
162
163
164
165
166
167
168
169
    # ------------------------------------------------------------------------------------------ #
    def http_start_stop(self, start):
        if start:
            cmd = "start"
        else:
            cmd = "stop"

        try:
            self.enable_transaction()
170
            self.__send_lqc_command__(self.client, "http", cmd)
171
            self.disable_transaction()
172
        except LQConnectionError:
173
174
175
            # we already caught and handled this error in __send_lqc_command__, but we do not want to execute this function further
            pass

176
177
178
179
180
181
182
183
184
185
186
187
    # ------------------------------------------------------------------------------------------ #
    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):
188
189
190
191
192
193
194
195
        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

196
197
198
199
200
201
202
        self.enable_transaction()

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

203
204
        self.disable_transaction()

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

211
        self.enable_transaction()
212
213
        for i in range(5):
            self.recorder_start_one(i)
214
        self.disable_transaction()
215
216
217

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

221
222
223
224
225
        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")
226

227
    # ------------------------------------------------------------------------------------------ #
228
229
230
231
232
233
234
    def fade_in(self, new_entry):
        try:
            fade_in_time = float(self.config.get("fade_in_time"))

            if fade_in_time > 0:
                self.fade_in_active = True
                target_volume = new_entry.volume
David Trattnig's avatar
David Trattnig committed
235

236
                step = fade_in_time / target_volume
237

238
                self.logger.info("Starting to fading " + new_entry.type.value + " in. step is " + str(step) + "s. target volume is " + str(target_volume))
239

240
241
                self.disable_logging = True
                self.client.disable_logging = True
242

243
244
245
246
247
248
249
250
251
252
253
254
                for i in range(target_volume):
                    self.channel_volume(new_entry.type.value, i + 1)
                    time.sleep(step)

                self.logger.info("Finished with fading " + new_entry.type.value + " in.")

                self.fade_in_active = False
                if not self.fade_out_active:
                    self.disable_logging = False
                    self.client.disable_logging = False
        except LQConnectionError as e:
            self.logger.critical(str(e))
255
256
257
258

        return True

    # ------------------------------------------------------------------------------------------ #
259
260
261
262
263
264
    def fade_out(self, old_entry):
        try:
            fade_out_time = float(self.config.get("fade_out_time"))

            if fade_out_time > 0:
                step = abs(fade_out_time) / old_entry.volume
265

266
                self.logger.info("Starting to fading " + old_entry.type.value + " out. step is " + str(step) + "s")
267

268
269
270
                # disable logging... it is going to be enabled again after fadein and -out is finished
                self.disable_logging = True
                self.client.disable_logging = True
271

272
273
274
275
276
277
278
279
280
281
282
283
284
                for i in range(old_entry.volume):
                    self.channel_volume(old_entry.type.value, old_entry.volume-i-1)
                    time.sleep(step)

                self.logger.info("Finished with fading " + old_entry.type.value + " out.")

                # enable logging again
                self.fade_out_active = False
                if not self.fade_in_active:
                    self.disable_logging = False
                    self.client.disable_logging = False
        except LQConnectionError as e:
            self.logger.critical(str(e))
285

286
287
        return True

288
    # ------------------------------------------------------------------------------------------ #
289
290
    def activate(self, new_entry):
        # grab the actual active entry
291
        (show, old_entry) = self.scheduler.get_active_entry()
292
        # determine its type
293
294
295
        # TODO Move to <get_active_entry>
        # FIXME No need to switch if current type = new type
        old_type = ScheduleEntryType.FILESYSTEM # Set default if no previous track is available
David Trattnig's avatar
David Trattnig committed
296
297
        if old_entry:
            old_type = old_entry.type
298

299
300
        try:
            # enable transaction
301
302
            self.enable_transaction()

303
            if old_type == new_entry.type:
304
                # push something to active channel
305
                self.activate_same_channel(new_entry)
306
307
            else:
                # switch to another channel
308
                self.activate_different_channel(new_entry, old_type)
309

310
            # disable conn
311
            self.disable_transaction()
312

313
            # insert playlist entry
314
315
            self.logger.critical("Trackservice entry not written here anymore")
#            self.insert_track_service_entry(new_entry)
316
        except LQConnectionError:
317
            # 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
318
            pass
319

320
    # ------------------------------------------------------------------------------------------ #
321
322
    def activate_same_channel(self, entry, activate_different_channel=False):
        if not activate_different_channel:
323
324
325
326
            self.logger.info(TerminalColors.PINK.value + entry.type.value + " already active!" + TerminalColors.ENDC.value)

        # push to fs or stream
        if entry.type == ScheduleEntryType.FILESYSTEM:
David Trattnig's avatar
David Trattnig committed
327
            self.playlist_push(entry.filename)
328
329
330
            self.active_channel = entry.type

        elif entry.type == ScheduleEntryType.STREAM:
331
            self.set_http_url(entry.source)
332
            self.http_start_stop(True)
333
334
335
            self.active_channel = entry.type

        # else: # live
336
        # nothing to do when we are live => just leave it as is
337

338
339
        self.active_channel = entry.type

340
341
342
343
        # set active channel to wanted volume
        if not activate_different_channel:
            self.channel_volume(entry.type.value, entry.volume)

344
345
346
    # ------------------------------------------------------------------------------------------ #
    def activate_different_channel(self, entry, active_type):
        self.logger.info(TerminalColors.PINK.value + "LiquidSoapCommunicator is activating " + entry.type.value + " & deactivating " + active_type.value + "!" + TerminalColors.ENDC.value)
347

348
        # reuse of this function, because activate_same_channel and activate_different_channel are doing pretty the same except setting of the volume to zero
349
        self.activate_same_channel(entry, True)
350

351
        # set other channels to zero volume
352
        others = self.all_inputs_but(entry.getChannel())
353
354
        for o in others:
            self.channel_volume(o, 0)
355

356
357
        # set active channel to wanted volume
        self.channel_volume(entry.type.value, entry.volume)
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
358
359
360

    # ------------------------------------------------------------------------------------------ #
    def insert_track_service_entry(self, schedule_entry):
361
        # create trackservice entry
362
        trackservice_entry = TrackService()
363
364

        # set foreign keys
365
366
367
        trackservice_entry.playlist_id = schedule_entry.playlist_id
        trackservice_entry.entry_num = schedule_entry.entry_num
        trackservice_entry.source = schedule_entry.source
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
368

369
        # store
370
        trackservice_entry.store(add=True, commit=True)
371
372
373

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

382
383
384
385
        return activemixer_copy

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

        return self.channels
390

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

396
397
    # ------------------------------------------------------------------------------------------ #
    def __get_mixer_status__(self, mixernumber):
398
        return self.__send_lqc_command__(self.client, "mixer", "status", mixernumber)
399
400

    # ------------------------------------------------------------------------------------------ #
401
    def init_player(self):
402
403
404
        (_, active_entry) = self.scheduler.get_active_entry()

        t = LiquidSoapInitThread(self, active_entry)
405
        t.start()
406

407
        return "LiquidSoapInitThread started!"
408

409
    # ------------------------------------------------------------------------------------------ #
410
411
412
413
414
415
    def channel_activate(self, channel, activate):
        channels = self.get_all_channels()

        try:
            index = channels.index(channel)
            if len(channel) < 1:
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
416
417
418
419
420
421
                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))
422

423
424
425
    # ------------------------------------------------------------------------------------------ #
    def channel_volume(self, channel, volume):
        """
426
        set volume of a channel
427
        @type     channel:  string
428
        @param    channel:  Channel
429
        @type     volume:   int
430
        @param    volume:   Volume between 0 and 100
431
        """
432

433
        try:
434
            channels = self.get_all_channels()
435
            index = channels.index(channel)
436
        except ValueError as e:
437
            self.logger.error("Cannot set volume of channel " + channel + " to " + str(volume) + "!. Reason: " + str(e))
438
439
440
            return

        try:
441
            if len(channel) < 1:
442
                self.logger.warning("Cannot set volume of channel " + channel + " to " + str(volume) + "! There are no channels.")
443
            else:
444
                message = self.__send_lqc_command__(self.client, "mixer", "volume", str(index), str(int(volume)))
445

446
447
448
449
450
                if not self.disable_logging:
                    if message.find('volume=' + str(volume) + '%'):
                        self.logger.debug("Set volume of channel " + channel + " to " + str(volume))
                    else:
                        self.logger.warning("Setting volume of channel " + channel + " gone wrong! Liquidsoap message: " + message)
451

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

457
458
459
460
461
    # ------------------------------------------------------------------------------------------ #
    def auraengine_state(self):
        state = self.__send_lqc_command__(self.client, "auraengine", "state")
        return state

462
463
    # ------------------------------------------------------------------------------------------ #
    def liquidsoap_help(self):
464
        data = self.__send_lqc_command__(self.client, 'help')
465
        if not data:
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
466
            self.logger.warning("Could not get Liquidsoap's help")
467
        else:
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
468
469
            self.logger.debug("Got Liquidsoap's help")
        return data
470

471
472
    # ------------------------------------------------------------------------------------------ #
    def set_http_url(self, uri):
473
        return self.__send_lqc_command__(self.client, "http", "url", uri)
474

475
476
477
478
479
480
481
    # ------------------------------------------------------------------------------------------ #
    def playlist_push(self, uri):
        """
        Eine Uri in die Playlist einfügen
        @type   uri:   str
        @param  uri:   Die Uri
        """
482
        return self.__send_lqc_command__(self.client, "fs", "push", uri)
483
484
485
486

    # ------------------------------------------------------------------------------------------ #
    def playlist_seek(self, seconds_to_seek):
        return self.__send_lqc_command__(self.client, "fs", "seek", seconds_to_seek)
487
488
489
490
491
492

    # ------------------------------------------------------------------------------------------ #
    def version(self):
        """
        get version
        """
493
        data = self.__send_lqc_command__(self.client, "version", "")
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
494
495
        self.logger.debug("Got Liquidsoap's version")
        return data
496
497

    # ------------------------------------------------------------------------------------------ #
498
499
500
501
    def uptime(self):
        """
        get uptime
        """
502
503
        data = self.__send_lqc_command__(self.client, "uptime", "")
        self.logger.debug("Got Liquidsoap's uptime")
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
504
        return data
505
506

    # ------------------------------------------------------------------------------------------ #
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
507
    def __send_lqc_command__(self, lqs_instance, namespace, command, *args):
508
509
510
511
512
513
514
515
516
517
518
519
520
521
        """
        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:
522
523
524
            if not self.disable_logging:
                if namespace == "recorder":
                    self.logger.info("LiquidSoapCommunicator is calling " + str(namespace) + "_" + str(command) + "." + str(args))
525
                else:
526
527
528
529
                    if command == "":
                        self.logger.info("LiquidSoapCommunicator is calling " + str(namespace) + str(args))
                    else:
                        self.logger.info("LiquidSoapCommunicator is calling " + str(namespace) + "." + str(command) + str(args))
530

531
532
533
534
535
            # call wanted function ...
            func = getattr(lqs_instance, namespace)
            # ... and fetch the result
            result = func(command, *args)

536
537
            if not self.disable_logging:
                self.logger.debug("LiquidSoapCommunicator got response " + str(result))
538

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
539
            self.connection_attempts = 0
540

541
542
            return result

543
        except LQConnectionError as e:
544
            self.logger.error("Connection Error when sending " + str(namespace) + "." + str(command) + str(args))
545
            if self.try_to_reconnect():
546
                time.sleep(0.2)
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
547
548
                self.connection_attempts += 1
                if self.connection_attempts < 5:
549
550
551
552
553
554
555
556
557
                    # 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
558
                else:
559
560
561
562
563
564
                    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)
565
                    self.disable_transaction(socket=self.client, force=True)
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
566
                    raise e
567
            else:
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
568
                # also store when was last admin mail sent with which content...
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
569
                self.logger.critical("SEND ADMIN MAIL AT THIS POINT")
570
571
                raise e

572
    # ------------------------------------------------------------------------------------------ #
573
574
575
576
577
578
579
580
    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:
581
            socket = self.client
582

583
584
        self.transaction = self.transaction + 1

585
        self.logger.debug(TerminalColors.WARNING.value + "ENabling transaction! cnt: " + str(self.transaction) + TerminalColors.ENDC.value)
586
587
588
589

        if self.transaction > 1:
            return

590
591
592
593
594
        try:
            self.__open_conn(socket)
        except FileNotFoundError:
            self.disable_transaction(socket=socket, force=True)

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
595
596
597
            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)
598

599
    # ------------------------------------------------------------------------------------------ #
600
601
602
603
604
    def disable_transaction(self, socket=None, force=False):
        if not force:
            # nothing to disable
            if self.transaction == 0:
                return
605

606
607
            # decrease transaction counter
            self.transaction = self.transaction - 1
608

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

618
619
        # close conn and set transactioncounter to 0
        self.__close_conn(socket)
620
        self.transaction = 0
621

622
    # ------------------------------------------------------------------------------------------ #
623
624
    def __open_conn(self, socket):
        # already connected
625
        if self.transaction > 1:
626
            return
627

628
        self.logger.debug(TerminalColors.GREEN.value + "LiquidSoapCommunicator opening conn" + TerminalColors.ENDC.value)
629

630
        # try to connect
631
632
        socket.connect()

633
    # ------------------------------------------------------------------------------------------ #
634
635
    def __close_conn(self, socket):
        # set socket to playout
636
        if socket is None:
637
            socket = self.client
638

639
        # do not disconnect if a transaction is going on
640
        if self.transaction > 0:
641
            return
642

643
        # say bye
644
        socket.byebye()
645

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