events.py 9.13 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


import logging
21
import datetime
22

David Trattnig's avatar
David Trattnig committed
23
24
from threading                  import Thread

25
from modules.base.config import AuraConfig
26
27
28
29
30
from modules.base.utils         import SimpleUtil as SU
from modules.base.mail          import AuraMailer
from modules.plugins.monitor    import AuraMonitor


David Trattnig's avatar
David Trattnig committed
31
from modules.plugins.trackservice import TrackServiceHandler
32
33
34
35
36
37


class EventBinding():
    """
    A binding between the event dispatcher and some event handler.

38
    It allows you to subscribe to events in a chained way:
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

        ```
        binding = dispatcher.attach(AuraMonitor)
        binding.subscribe("on_boot").subscribe("on_play")
        ```
    """
    dispatcher = None
    instance = None

    def __init__(self, dispatcher, instance):
        self.dispatcher = dispatcher
        self.instance = instance


    def subscribe(self, event_type):
        """
        Subscribes the instance to some event identified by the `event_type` string.
        """
        self.dispatcher.subscribe(self.instance, event_type)
        return self


David Trattnig's avatar
David Trattnig committed
61
    def get_instance(self):
62
63
64
65
66
67
68
69
70
        """
        Returns the object within that binding.
        """
        return self.instance



class EngineEventDispatcher():
    """
71
    Executes handlers for engine events.
72
73
74
75
76
77
    """
    logger = None
    config = None

    subscriber_registry = None
    mailer = None
78
    engine = None
79
    scheduler = None
David Trattnig's avatar
David Trattnig committed
80
    monitor = None
81
82


83
    def __init__(self, engine, scheduler):
84
85
86
87
88
        """
        Initialize EventDispatcher
        """
        self.subscriber_registry = dict()
        self.logger = logging.getLogger("AuraEngine")
89
        self.config = AuraConfig.config()
90
        self.mailer = AuraMailer(self.config)
91
        self.engine = engine
92
93
94
95
        self.scheduler = scheduler
        
        binding = self.attach(AuraMonitor)
        binding.subscribe("on_boot")
David Trattnig's avatar
David Trattnig committed
96
97
98
99
        binding.subscribe("on_sick")
        binding.subscribe("on_resurrect")

        binding = self.attach(TrackServiceHandler)
100
        binding.subscribe("on_timeslot")
101
        binding.subscribe("on_play")
102
103
        binding.subscribe("on_metadata")
        binding.subscribe("on_queue")
104
105
106
107
108
109


    def attach(self, clazz):
        """
        Creates an intance of the given Class.
        """
110
        instance = clazz(self.engine)
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
        return EventBinding(self, instance)


    def subscribe(self, instance, event_type):
        """
        Subscribes to some event type. Preferably use it via `EventBinding.subscribe(..)`.
        """
        if not event_type in self.subscriber_registry:
            self.subscriber_registry[event_type] = []
        self.subscriber_registry[event_type].append(instance)


    def call_event(self, event_type, args):
        """
        Calls all subscribers for the given event type.
        """
        if not event_type in self.subscriber_registry:
            return
        listeners = self.subscriber_registry[event_type]
        if not listeners:
            return
        for listener in listeners:
            method = getattr(listener, event_type)
            if method:
                if args:
                    method(args)
                else:
                    method()


    #
    # Events
    #


    def on_initialized(self):
        """
        Called when the engine is initialized e.g. connected to Liquidsoap.
        """
        self.logger.debug("on_initialized(..)")
        self.scheduler.on_initialized()
        self.call_event("on_initialized", None)


    def on_boot(self):
        """
        Called when the engine is starting up. This happens after the initialization step.
        """
        self.logger.debug("on_boot(..)")
        self.call_event("on_boot", None)


    def on_ready(self):
        """
        Called when the engine is booted and ready to play.
        """
167
        self.logger.debug("on_ready(..)")
168
169
170
        self.scheduler.on_ready()


171
    def on_timeslot(self, timeslot):
172
        """
173
        Event Handler which is called by the scheduler when the current timeslot is refreshed.
174
175

        Args:
176
            source (String):    The `PlaylistEntry` object
177
        """
178
179
180
181
182
183
184
        def func(self, timeslot):        
            self.logger.debug("on_timeslot(..)")
            self.call_event("on_timeslot", timeslot)

        thread = Thread(target = func, args = (self, timeslot))
        thread.start()    

David Trattnig's avatar
David Trattnig committed
185

186
187
188
    def on_play(self, entry):
        """
        Event Handler which is called by the engine when some entry is actually playing. 
189

190
191
192
193
194
        Args:
            source (String):    The `PlaylistEntry` object
        """
        def func(self, entry):        
            self.logger.debug("on_play(..)")
David Trattnig's avatar
David Trattnig committed
195
196
197
            # Assign timestamp for play time
            entry.entry_start_actual = datetime.datetime.now()
            self.call_event("on_play", entry)
198

199
        thread = Thread(target = func, args = (self, entry))
David Trattnig's avatar
David Trattnig committed
200
        thread.start()    
201
202


203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
    def on_metadata(self, data):
        """
        Event Handler which is called by the soundsystem implementation (i.e. Liquidsoap)
        when some entry is actually playing.

        Args:
            data (dict):    A collection of metadata related to the current track
        """
        def func(self, data):        
            self.logger.debug("on_metadata(..)")
            self.call_event("on_metadata", data)

        thread = Thread(target = func, args = (self, data))
        thread.start() 


219
220
221
222
    def on_stop(self, entry):
        """
        The entry on the assigned channel has been stopped playing.
        """
David Trattnig's avatar
David Trattnig committed
223
224
225
226
227
228
        def func(self, entry):        
            self.logger.debug("on_stop(..)")
            self.call_event("on_stop", entry)
        
        thread = Thread(target = func, args = (self, entry))
        thread.start() 
229
230


231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
    def on_fallback_updated(self, playlist_uri):
        """
        Called when the scheduled fallback playlist has been updated.
        """
        self.logger.debug("on_fallback_updated(..)")
        self.call_event("on_fallback_updated", playlist_uri)


    def on_fallback_cleaned(self, cleaned_channel):
        """
        Called when the scheduled fallback queue has been cleaned up.
        """
        self.logger.debug("on_fallback_cleaned(..)")
        self.call_event("on_fallback_cleaned", cleaned_channel)


247
248
249
250
    def on_idle(self):
        """
        Callend when no entry is playing
        """
David Trattnig's avatar
David Trattnig committed
251
252
253
254
255
256
257
        def func(self):
            self.logger.debug("on_idle(..)")
            self.logger.error(SU.red("Currently there's nothing playing!"))
            self.call_event("on_idle", None)

        thread = Thread(target = func, args = (self, ))
        thread.start() 
258
259
260
261
262
263


    def on_schedule_change(self, schedule):
        """
        Called when the playlist or entries of the current schedule have changed.
        """
David Trattnig's avatar
David Trattnig committed
264
265
266
267
268
269
        def func(self, schedule):        
            self.logger.debug("on_schedule_change(..)")
            self.call_event("on_schedule_change", schedule)

        thread = Thread(target = func, args = (self, schedule))
        thread.start() 
270
271
272
273
274
275


    def on_queue(self, entries):
        """
        One or more entries have been queued and are currently pre-loaded.
        """
David Trattnig's avatar
David Trattnig committed
276
277
278
279
280
281
        def func(self, entries):        
            self.logger.debug("on_queue(..)")
            self.call_event("on_queue", entries)

        thread = Thread(target = func, args = (self, entries))
        thread.start() 
282
283


David Trattnig's avatar
David Trattnig committed
284
    def on_sick(self, data):
285
286
287
        """
        Called when the engine is in some unhealthy state.
        """
David Trattnig's avatar
David Trattnig committed
288
289
290
291
292
293
        def func(self, data):        
            self.logger.debug("on_sick(..)")
            self.call_event("on_sick", data)

        thread = Thread(target = func, args = (self, data))
        thread.start() 
294
295


David Trattnig's avatar
David Trattnig committed
296
    def on_resurrect(self, data):
297
298
299
        """
        Called when the engine turned healthy again after being sick.
        """
David Trattnig's avatar
David Trattnig committed
300
301
302
303
304
305
        def func(self, data):        
            self.logger.debug("on_resurrect(..)")
            self.call_event("on_resurrect", data)

        thread = Thread(target = func, args = (self, data))
        thread.start() 
306
307
308
309
310
311


    def on_critical(self, subject, message, data=None):
        """
        Callend when some critical event occurs
        """
David Trattnig's avatar
David Trattnig committed
312
313
314
315
316
317
318
319
        def func(self, subject, message, data):        
            self.logger.debug("on_critical(..)")
            if not data: data = ""
            self.mailer.send_admin_mail(subject, message + "\n\n" + str(data))
            self.call_event("on_critical", (subject, message, data))

        thread = Thread(target = func, args = (self, subject, message, data))
        thread.start()