calender_fetcher.py 10.2 KB
Newer Older
David Trattnig's avatar
David Trattnig committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# 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/>.



22
import logging
23
import requests
24

25
from datetime import datetime
26
27
28

from src.base.utils import SimpleUtil as SU

29

David Trattnig's avatar
David Trattnig committed
30

31
class CalendarFetcher:
David Trattnig's avatar
David Trattnig committed
32
    """
33
    Fetches the timeslots, playlists and playlist entries as JSON
34
    via the API endpoints of Steering and Tank.
David Trattnig's avatar
David Trattnig committed
35
    """
36
37
38
    config = None
    logging = None
    has_already_fetched = False
39
    fetched_timeslot_data = None
David Trattnig's avatar
David Trattnig committed
40

41
42
43
44
45
46
    # Config for API Endpoints
    steering_calendar_url = None
    tank_playlist_url = None
    tank_session = None
    tank_secret = None

47

David Trattnig's avatar
David Trattnig committed
48

49
    def __init__(self, config):
David Trattnig's avatar
David Trattnig committed
50
51
52
53
54
55
        """
        Constructor

        Args:
            config (AuraConfig):    Holds the engine configuration
        """
56
57
        self.config = config
        self.logger = logging.getLogger("AuraEngine")
58
59
60
61
62
        self.steering_calendar_url = self.config.get("api_steering_calendar")
        self.tank_playlist_url = self.config.get("api_tank_playlist")
        self.tank_session = self.config.get("api_tank_session")
        self.tank_secret = self.config.get("api_tank_secret")

David Trattnig's avatar
David Trattnig committed
63
64
65
66
67

    #
    #   PUBLIC METHODS
    #

68
69

    def fetch(self):
David Trattnig's avatar
David Trattnig committed
70
71
72
        """
        Retrieve all required data from the API.
        """
73
        return_data = []
David Trattnig's avatar
David Trattnig committed
74

75
76
77
78
        self.logger.debug("Fetching timeslots from STEERING")
        self.fetched_timeslot_data = self.fetch_timeslot_data()
        if not self.fetched_timeslot_data:
            self.logger.critical(SU.red("No timeslots fetched from API!"))
79
            return None
80

81
82
        self.logger.debug("Fetching playlists from TANK")
        self.fetch_playlists()
83
84

        try:
85
86
87
88
89
90
91
92
93
94
95
96
            for timeslot in self.fetched_timeslot_data:
                # Skip timeslot if no start or end is given
                if "start" not in timeslot:
                    self.logger.warning("No start of timeslot given. Skipping timeslot: " + str(timeslot))
                    timeslot = None
                if "end" not in timeslot:
                    self.logger.warning("No end of timeslot given. Skipping timeslot: " + str(timeslot))
                    timeslot = None
                if "playlist" not in timeslot \
                    and "show_fallback" not in timeslot \
                    and "schedule_fallback" not in timeslot \
                    and "station_fallback" not in timeslot:
97
                    
98
99
                    self.logger.warning("No playlist for timeslot given. Skipping timeslot: " + str(timeslot))
                    timeslot = None
100

101
102
                if timeslot:
                    return_data.append(timeslot)
103
104
        except TypeError:
            self.logger.error(SU.red("Nothing fetched ..."))
105
            self.fetched_timeslot_data = None
106
            return None
107
108
109

        return return_data

David Trattnig's avatar
David Trattnig committed
110
111
112
113
114
115
116


    #
    #   PRIVATE METHODS
    #


117
    def fetch_timeslot_data(self):
David Trattnig's avatar
David Trattnig committed
118
        """
119
        Fetches timeslot data from Steering.
David Trattnig's avatar
David Trattnig committed
120
121

        Returns:
122
            ([Timeslot]):   An array of timeslots
David Trattnig's avatar
David Trattnig committed
123
        """
124
        timeslot = None
125
126
127
        headers = { "content-type": "application/json" }
       
        try:
128
            self.logger.debug("Fetch timeslots from Steering API...")                
129
130
            response = requests.get(self.steering_calendar_url, data=None, headers=headers)
            if not response.status_code == 200:
131
                self.logger.critical(SU.red("HTTP Status: %s | Timeslots could not be fetched! Response: %s" % \
132
133
                    (str(response.status_code), response.text)))
                return None            
134
            timeslot = response.json()                
135
        except Exception as e:
136
            self.logger.critical(SU.red("Error while requesting timeslots from Steering!"), e)
137

138
139
        if not timeslot:
            self.logger.error(SU.red("Got no timeslot via Playout API (Steering)!"))
140
            return None
141

142
        return self.remove_unnecessary_data(timeslot)
143

David Trattnig's avatar
David Trattnig committed
144
145


146
    def fetch_playlists(self):
David Trattnig's avatar
David Trattnig committed
147
        """
148
149
150
        Fetches all playlists including fallback playlists for every timeslot.
        This method used the class member `fetched_timeslot_data`` to iterate
        over and extend timeslot data.
David Trattnig's avatar
David Trattnig committed
151
        """
152
153
154
        # store fetched entries => do not have to fetch playlist_id more than once
        fetched_entries=[]

155
        try:
156
            for timeslot in self.fetched_timeslot_data:
157

158
                # Get IDs of playlists
159
160
161
162
                playlist_id = self.get_playlist_id(timeslot, "playlist_id")
                schedule_fallback_id = self.get_playlist_id(timeslot, "schedule_fallback_id")
                show_fallback_id = self.get_playlist_id(timeslot, "show_fallback_id")
                station_fallback_id = self.get_playlist_id(timeslot, "station_fallback_id")
163

164
                # Retrieve playlist and the fallback playlists for every timeslot.
David Trattnig's avatar
David Trattnig committed
165
                # If a playlist (like station_fallback) is already fetched, it is not fetched again but reused
166
167
168
169
                timeslot["playlist"]          = self.fetch_playlist(playlist_id,          fetched_entries)
                timeslot["schedule_fallback"] = self.fetch_playlist(schedule_fallback_id, fetched_entries)
                timeslot["show_fallback"]     = self.fetch_playlist(show_fallback_id,     fetched_entries)
                timeslot["station_fallback"]  = self.fetch_playlist(station_fallback_id,  fetched_entries)
170

171
        except Exception as e:
172
            self.logger.error("Error while fetching playlists from API endpoints: " + str(e), e)
173

David Trattnig's avatar
David Trattnig committed
174
175


176
    def fetch_playlist(self, playlist_id, fetched_playlists):
David Trattnig's avatar
David Trattnig committed
177
        """
178
        Fetches the playlist for a given timeslot.
David Trattnig's avatar
David Trattnig committed
179
180
181
182
183
184

        Args:
            id_name (String):       The type of playlist to fetch (e.g. normal vs. fallback)
            fetched_playlists ([]): Previously fetched playlists to avoid re-fetching

        Returns:
185
            (Playlist):             Playlist of type `id_name`
David Trattnig's avatar
David Trattnig committed
186
        """
187
188
        if not playlist_id:
            return None
189
190
191
192
193
194
        playlist = None
        url = self.tank_playlist_url.replace("${ID}", playlist_id) 
        headers = {
            "Authorization": "Bearer %s:%s" % (self.tank_session, self.tank_secret), 
            "content-type": "application/json"
        }
195

196
        # If playlist is already fetched in this round, use the existing one
David Trattnig's avatar
David Trattnig committed
197
        for playlist in fetched_playlists:
198
199
            if playlist["id"] == playlist_id:
                self.logger.debug("Playlist #%s already fetched" % playlist_id)
David Trattnig's avatar
David Trattnig committed
200
                return playlist
201
                   
202
        try:
203
204
205
206
207
208
209
            self.logger.debug("Fetch playlist from Tank API...")             
            response = requests.get(url, data=None, headers=headers)
            if not response.status_code == 200:
                self.logger.critical(SU.red("HTTP Status: %s | Playlist #%s could not be fetched or is not available! Response: %s" % \
                    (str(response.status_code), str(playlist_id), response.text)))
                return None          
            playlist = response.json()                  
210
        except Exception as e:
211
212
            self.logger.critical(SU.red("Error while requesting playlist #%s from Tank" % str(playlist_id)), e)
            return None
213

214
215
216
        fetched_playlists.append(playlist)
        return playlist
 
217
218


219
    def get_playlist_id(self, timeslot, id_name):
220
        """
221
        Extracts the playlist ID for a given playlist (fallback) type.
222
        """
223
        playlist_id = str(timeslot[id_name])
224
        if not playlist_id or playlist_id == "None":
225
            self.logger.debug("No value defined for '%s' in timeslot '#%s'" % (id_name, timeslot["timeslot_id"]))
226
227
228
            return None
        
        return playlist_id
229
230


231
    def remove_unnecessary_data(self, timeslot):
232
        """
233
        Removes all timeslots which are not relevant for 
234
235
        further processing.
        """
236
237
238
239
        count_before = len(timeslot)
        timeslot = self.remove_data_more_than_24h_in_the_future(timeslot)
        timeslot = self.remove_data_in_the_past(timeslot)
        count_after = len(timeslot)
240

241
242
        self.logger.debug("Removed %d unnecessary timeslots from response. Entries left: %d" % ((count_before - count_after), count_after))
        return timeslot
243
244


245
    def remove_data_more_than_24h_in_the_future(self, timeslots):
246
247
248
        """ 
        Removes entries 24h in the future and 12 hours in the past.
        Note: This might influence resuming (in case of a crash)  
249
        single timeslots which are longer than 12 hours long.
250
251
        Think e.g. live broadcasts.
        """
252
        items = []
253
        now = SU.timestamp()
254
255
        now_plus_24hours = now + (12*60*60)
        now_minus_12hours = now - (12*60*60)
256

257
        for s in timeslots:
258
            start_time = datetime.strptime(s["start"], "%Y-%m-%dT%H:%M:%S")
259
            start_time = SU.timestamp(start_time)
260

261
262
            if start_time <= now_plus_24hours and start_time >= now_minus_12hours:
                items.append(s)
263

264
        return items
265
266


267
    def remove_data_in_the_past(self, timeslots):
268
        """
269
        Removes all timeslots from the past, except the one which is 
270
271
        currently playing.
        """
272
        items = []
273
        now = SU.timestamp()
274
        for s in timeslots:
275
            start_time = datetime.strptime(s["start"], "%Y-%m-%dT%H:%M:%S")
276
            start_time = SU.timestamp(start_time)
277
            end_time = datetime.strptime(s["end"], "%Y-%m-%dT%H:%M:%S")
278
            end_time = SU.timestamp(end_time)
279

280
            # Append all elements in the future
281
282
283
284
285
            if start_time >= now:
                items.append(s)
             # Append the one which is playing now
            elif start_time < now < end_time:
                items.append(s)
286

287
        return items