events.py 7.23 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
23
24
25
26
27
28
29

from modules.base.utils         import SimpleUtil as SU
from modules.base.exceptions    import NoActiveEntryException
from modules.base.mail          import AuraMailer
from modules.plugins.monitor    import AuraMonitor
from modules.core.state         import PlayerStateService


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


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

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

        ```
        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
60
    def get_instance(self):
61
62
63
64
65
66
67
68
69
        """
        Returns the object within that binding.
        """
        return self.instance



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

    subscriber_registry = None
    mailer = None
    soundsystem = None
    player_state = None
    scheduler = None
David Trattnig's avatar
David Trattnig committed
80
    monitor = None
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96


    def __init__(self, soundsystem, scheduler):
        """
        Initialize EventDispatcher
        """
        self.subscriber_registry = dict()
        self.logger = logging.getLogger("AuraEngine")
        self.config = soundsystem.config
        self.mailer = AuraMailer(self.config)
        self.soundsystem = soundsystem
        self.scheduler = scheduler
        self.player_state = PlayerStateService(self.config)
        
        binding = self.attach(AuraMonitor)
        binding.subscribe("on_boot")
David Trattnig's avatar
David Trattnig committed
97
98
99
100
        binding.subscribe("on_sick")
        binding.subscribe("on_resurrect")

        binding = self.attach(TrackServiceHandler)
101
102
103
104
105
106
107
108
109
110
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
        binding.subscribe("on_play")


    def attach(self, clazz):
        """
        Creates an intance of the given Class.
        """
        instance = clazz(self.config, self.soundsystem)
        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.
        """
165
        self.logger.debug("on_ready(..)")
166
167
168
169
170
171
        self.scheduler.on_ready()


    def on_play(self, source):
        """
        Event Handler which is called by the soundsystem implementation (i.e. Liquidsoap)
172
173
        when some entry is actually playing. Note that this event resolves the source URI
        and passes an `PlaylistEntry` to event handlers.
174
175

        Args:
David Trattnig's avatar
Doc.    
David Trattnig committed
176
            source (String):    The `Entry` object *or* the URI of the media source currently playing
177
178
        """
        self.logger.debug("on_play(..)")
179
180
181
182
183
184
185
186
187
188
189
        entry = None

        if isinstance(source, str):
            try:
                self.logger.info(SU.pink("Source '%s' started playing. Resolving ..." % source))
                entry = self.player_state.resolve_entry(source)                
            except NoActiveEntryException:
                self.logger.error("Cannot resolve '%s'" % source)
        else:
            entry = source

190
191
        # Assign timestamp for play time
        entry.entry_start_actual = datetime.datetime.now()
192
        self.call_event("on_play", entry)
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207


    def on_stop(self, entry):
        """
        The entry on the assigned channel has been stopped playing.
        """
        self.logger.debug("on_stop(..)")
        self.call_event("on_stop", entry)


    def on_idle(self):
        """
        Callend when no entry is playing
        """
        self.logger.debug("on_idle(..)")
David Trattnig's avatar
David Trattnig committed
208
        self.logger.error(SU.red("Currently there's nothing playing!"))
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
        self.call_event("on_idle", None)


    def on_schedule_change(self, schedule):
        """
        Called when the playlist or entries of the current schedule have changed.
        """
        self.logger.debug("on_schedule_change(..)")
        self.call_event("on_schedule_change", schedule)


    def on_queue(self, entries):
        """
        One or more entries have been queued and are currently pre-loaded.
        """
        self.logger.debug("on_queue(..)")
        self.player_state.add_to_history(entries)
        self.call_event("on_queue", entries)


David Trattnig's avatar
David Trattnig committed
229
    def on_sick(self, data):
230
231
232
233
        """
        Called when the engine is in some unhealthy state.
        """
        self.logger.debug("on_sick(..)")
David Trattnig's avatar
David Trattnig committed
234
        self.call_event("on_sick", data)
235
236


David Trattnig's avatar
David Trattnig committed
237
    def on_resurrect(self, data):
238
239
240
241
        """
        Called when the engine turned healthy again after being sick.
        """
        self.logger.debug("on_resurrect(..)")
David Trattnig's avatar
David Trattnig committed
242
        self.call_event("on_resurrect", data)
243
244
245
246
247
248
249
250
251
252


    def on_critical(self, subject, message, data=None):
        """
        Callend when some critical event occurs
        """
        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))