fallback.py 7.6 KB
Newer Older
David Trattnig's avatar
David Trattnig committed
1
2

#
David Trattnig's avatar
David Trattnig committed
3
# Aura Engine (https://gitlab.servus.at/aura/engine)
David Trattnig's avatar
David Trattnig committed
4
#
David Trattnig's avatar
David Trattnig committed
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Copyright (C) 2017-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/>.

David Trattnig's avatar
David Trattnig committed
20
21


David Trattnig's avatar
David Trattnig committed
22
import logging
David Trattnig's avatar
David Trattnig committed
23

David Trattnig's avatar
David Trattnig committed
24
from enum                       import Enum
25
from datetime                   import timedelta
David Trattnig's avatar
David Trattnig committed
26

27
from modules.base.config        import AuraConfig
David Trattnig's avatar
David Trattnig committed
28
from modules.base.utils         import SimpleUtil as SU
29
from modules.base.mail          import AuraMailer
David Trattnig's avatar
David Trattnig committed
30
from modules.core.resources     import ResourceClass
31
from modules.core.channels      import Channel
32
from modules.core.control       import EngineExecutor
33

David Trattnig's avatar
David Trattnig committed
34
35
36
37
38
39


class FallbackType(Enum):
    """
    Types of playlists.
    """
40
41
42
43
    NONE        = { "id": 0, "name": "default", "lqs_sources": [ Channel.QUEUE_A, Channel.QUEUE_A] }    # No fallback active, default playout
    SCHEDULE    = { "id": 1, "name": "schedule", "lqs_sources": [ Channel.FALLBACK_QUEUE_A, Channel.FALLBACK_QUEUE_B]}   # The first played when some default playlist fails
    SHOW        = { "id": 2, "name": "show", "lqs_sources": [ "station_folder", "station_playlist"]}       # The second played when the timeslot fallback fails    
    STATION     = { "id": 3, "name": "station", "lqs_sources": [ "station_folder", "station_playlist"] }    # The last played when everything else fails
David Trattnig's avatar
David Trattnig committed
44
45
46
47
48
49
50
51

    @property
    def id(self):
        return self.value["id"]

    def __str__(self):
        return str(self.value["name"])

David Trattnig's avatar
David Trattnig committed
52

David Trattnig's avatar
David Trattnig committed
53

David Trattnig's avatar
David Trattnig committed
54
55
56
57
58
59
60
61
62
63
class FallbackManager:
    """
    Handles all types of fallbacks in case there is an outage
    for the regular radio programme.

    Attributes:
        config (AuraConfig):        The engine configuration
        logger (AuraLogger):        The logger
        mail   (AuraMailer):        Mail service
        scheduler (AuraScheduler):  The scheduler
David Trattnig's avatar
David Trattnig committed
64
    """    
David Trattnig's avatar
David Trattnig committed
65
66
67
68
69
    config = None
    logger = None
    mailer = None
    scheduler = None

David Trattnig's avatar
David Trattnig committed
70
    
71
    def __init__(self, scheduler):
David Trattnig's avatar
David Trattnig committed
72
73
74
75
        """
        Constructor

        Args:
76

David Trattnig's avatar
David Trattnig committed
77
        """
78
79
        self.config = AuraConfig.config()
        self.logger = logging.getLogger("AuraEngine")
David Trattnig's avatar
David Trattnig committed
80
81
82
83
84
        self.mailer = AuraMailer(self.config)
        self.scheduler = scheduler


    #
85
    #   METHODS
David Trattnig's avatar
David Trattnig committed
86
87
    #

88
89

    def queue_fallback_playlist(self, timeslot):
David Trattnig's avatar
David Trattnig committed
90
        """
David Trattnig's avatar
David Trattnig committed
91
        Evaluates the scheduled fallback and queues it using a timed thread.
David Trattnig's avatar
David Trattnig committed
92
        """
93
        (fallback_type, playlist) = self.get_fallback_playlist(timeslot)
David Trattnig's avatar
David Trattnig committed
94
95

        if playlist:
96
97
            self.logger.info(f"Resolved {fallback_type.value} fallback")         
            return FallbackCommand(timeslot, playlist.entries)
David Trattnig's avatar
David Trattnig committed
98
        else:
99
            msg = f"There is no schedule- or show-fallback defined for timeslot#{timeslot.schedule_id}. "
David Trattnig's avatar
David Trattnig committed
100
101
            msg += f"The station fallback will be used automatically."
            self.logger.info(msg)
David Trattnig's avatar
David Trattnig committed
102
103
104



105
    def resolve_playlist(self, timeslot):
David Trattnig's avatar
David Trattnig committed
106
        """
David Trattnig's avatar
David Trattnig committed
107
108
        Retrieves the currently planned (fallback) playlist. If a normal playlist is available,
        this one is returned. In case of station fallback no playlist is returned.
David Trattnig's avatar
David Trattnig committed
109

David Trattnig's avatar
David Trattnig committed
110
        Args:
111
            timeslot (Schedule)
David Trattnig's avatar
David Trattnig committed
112
        
David Trattnig's avatar
David Trattnig committed
113
        Returns:
David Trattnig's avatar
David Trattnig committed
114
            (FallbackType, Playlist)
David Trattnig's avatar
David Trattnig committed
115
        """
David Trattnig's avatar
David Trattnig committed
116
117
        planned_playlist = None
        fallback_type = None
David Trattnig's avatar
David Trattnig committed
118

119
120
        if self.validate_playlist(timeslot, "playlist"):
            planned_playlist = timeslot.get_playlist()
David Trattnig's avatar
David Trattnig committed
121
            fallback_type = FallbackType.NONE
David Trattnig's avatar
David Trattnig committed
122
        else:
123
            (fallback_type, planned_playlist) = self.get_fallback_playlist(timeslot)            
David Trattnig's avatar
David Trattnig committed
124

David Trattnig's avatar
David Trattnig committed
125
        return (fallback_type, planned_playlist)
David Trattnig's avatar
David Trattnig committed
126
127
128



129
    def get_fallback_playlist(self, timeslot):
130
        """
David Trattnig's avatar
David Trattnig committed
131
        Retrieves the playlist to be used in a fallback scenario.
132

David Trattnig's avatar
David Trattnig committed
133
        Args: 
134
            timeslot (Schedule)
135

David Trattnig's avatar
David Trattnig committed
136
137
138
139
140
        Returns:
            (Playlist)
        """        
        playlist = None
        fallback_type = FallbackType.STATION
David Trattnig's avatar
David Trattnig committed
141

142
143
        if self.validate_playlist(timeslot, "schedule_fallback"):
            playlist = timeslot.schedule_fallback[0]
David Trattnig's avatar
David Trattnig committed
144
            fallback_type = FallbackType.SCHEDULE
145
146
        elif self.validate_playlist(timeslot, "show_fallback"):
            playlist = timeslot.show_fallback[0]
David Trattnig's avatar
David Trattnig committed
147
148
149
            fallback_type = FallbackType.SHOW

        return (fallback_type, playlist)
David Trattnig's avatar
David Trattnig committed
150

David Trattnig's avatar
David Trattnig committed
151
152


153
    def validate_playlist(self, timeslot, playlist_type):
David Trattnig's avatar
David Trattnig committed
154
155
        """
        Checks if a playlist is valid for play-out.
David Trattnig's avatar
David Trattnig committed
156
157
158
159
160
161
162
163
164
165
166
167

        Following checks are done for all playlists:

            - has one or more entries

        Fallback playlists should either:
                
            - have filesystem entries only
            - reference a recording of a previous playout of a show (also filesystem)
        
        Otherwise, if a fallback playlist contains Live or Stream entries,
        the exact playout behaviour can hardly be predicted.
David Trattnig's avatar
David Trattnig committed
168
        """
169
        playlist = getattr(timeslot, playlist_type)
David Trattnig's avatar
David Trattnig committed
170
171
172
173
174
        if playlist \
            and isinstance(playlist, list) \
            and playlist[0].entries \
            and len(playlist[0].entries) > 0:

David Trattnig's avatar
David Trattnig committed
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
            # Default playlist
            if playlist_type == "playlist":
                return True

            # Fallback playlist
            elif playlist[0].entries:
                is_fs_only = True
                for entry in playlist[0].entries:
                    if entry.get_content_type() not in ResourceClass.FILE.types:
                        self.logger.error(SU.red("Fallback playlist of type '%s' contains not only file-system entries! \
                            Skipping fallback level..." % playlist_type))
                        is_fs_only = False
                        break                
                return is_fs_only

David Trattnig's avatar
David Trattnig committed
190
191
192
193
194
195
        return False





David Trattnig's avatar
David Trattnig committed
196

197
class FallbackCommand(EngineExecutor):
David Trattnig's avatar
David Trattnig committed
198
    """
199
200
201
202
    Command composition for executing timed scheduling and unscheduling of fallback playlists.

    Based on the `timeslot.start_date` and `timeslot.end_date` two `EngineExecutor commands
    are created.
David Trattnig's avatar
David Trattnig committed
203
    """
204
205

    def __init__(self, timeslot, entries):
David Trattnig's avatar
David Trattnig committed
206
        """
David Trattnig's avatar
David Trattnig committed
207
        Constructor
208
209
210
211

        Args:
            timeslot (Timeslot):    The timeslot any fallback entries should be scheduled for
            entries (List):         List of entries to be scheduled as fallback
David Trattnig's avatar
David Trattnig committed
212
        """        
213
214
215
216
217
218
219
220
221
        from modules.core.engine import Engine

        def do_play(entries):
            self.logger.info(SU.cyan(f"=== start_fallback_playlist('{entries}') ==="))
            Engine.get_instance().player.start_fallback_playlist(entries)

        def do_stop():
            self.logger.info(SU.cyan("=== stop_fallback_playlist() ==="))
            Engine.get_instance().player.stop_fallback_playlist()
David Trattnig's avatar
David Trattnig committed
222

223
224
225
226
227
        # Start fade-out 50% before the end of the timeslot for a more smooth transition
        end_time_offset = int(float(AuraConfig.config().get("fade_out_time")) / 2 * 1000 * -1)
        end_time = SU.timestamp(timeslot.schedule_end + timedelta(milliseconds=end_time_offset))
        self.logger.info(f"Starting fade-out of scheduled fallback with an offset of {end_time_offset} milliseconds at {end_time}")
        child_timer = EngineExecutor("FALLBACK", None, end_time, do_stop, None)
228
        super().__init__("FALLBACK", child_timer, timeslot.start_unix, do_play, entries)