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 schedules, 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
39
    config = None
    logging = None
    has_already_fetched = False
    fetched_schedule_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 schedules from STEERING")
        self.fetched_schedule_data = self.fetch_schedule_data()
        if not self.fetched_schedule_data:
            self.logger.critical(SU.red("No schedules fetched from API!"))
79
            return None
80

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

        try:
            for schedule in self.fetched_schedule_data:
David Trattnig's avatar
David Trattnig committed
86
                # Skip schedule if no start or end is given
87
                if "start" not in schedule:
David Trattnig's avatar
David Trattnig committed
88
                    self.logger.warning("No start of schedule given. Skipping schedule: " + str(schedule))
89
90
                    schedule = None
                if "end" not in schedule:
David Trattnig's avatar
David Trattnig committed
91
                    self.logger.warning("No end of schedule given. Skipping schedule: " + str(schedule))
92
                    schedule = None
93
94
95
96
97
                if "playlist" not in schedule \
                    and "show_fallback" not in schedule \
                    and "schedule_fallback" not in schedule \
                    and "station_fallback" not in schedule:
                    
David Trattnig's avatar
David Trattnig committed
98
                    self.logger.warning("No playlist for schedule given. Skipping schedule: " + str(schedule))
99
100
101
102
                    schedule = None

                if schedule:
                    return_data.append(schedule)
103
104
        except TypeError:
            self.logger.error(SU.red("Nothing fetched ..."))
105
106
            self.fetched_schedule_data = None
            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_schedule_data(self):
David Trattnig's avatar
David Trattnig committed
118
119
120
121
122
123
        """
        Fetches schedule data from Steering.

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

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

142
        return self.remove_unnecessary_data(schedule)
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
151
        """
        Fetches all playlists including fallback playlists for every schedule.
        This method used the class member `fetched_schedule_data`` to iterate
        over and extend schedule data.
        """
152
153
154
        # store fetched entries => do not have to fetch playlist_id more than once
        fetched_entries=[]

155
156
157
        try:
            for schedule in self.fetched_schedule_data:

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

David Trattnig's avatar
David Trattnig committed
164
165
                # Retrieve playlist and the fallback playlists for every schedule.
                # If a playlist (like station_fallback) is already fetched, it is not fetched again but reused
166
167
168
169
                schedule["playlist"]          = self.fetch_playlist(playlist_id,          fetched_entries)
                schedule["schedule_fallback"] = self.fetch_playlist(schedule_fallback_id, fetched_entries)
                schedule["show_fallback"]     = self.fetch_playlist(show_fallback_id,     fetched_entries)
                schedule["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
179
180
181
182
183
184
        """
        Fetches the playlist for a given schedule.

        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, schedule, id_name):
220
        """
221
        Extracts the playlist ID for a given playlist (fallback) type.
222
        """
223
224
225
226
227
228
        playlist_id = str(schedule[id_name])
        if not playlist_id or playlist_id == "None":
            self.logger.debug("No value defined for '%s' in schedule '#%s'" % (id_name, schedule["schedule_id"]))
            return None
        
        return playlist_id
229
230


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

David Trattnig's avatar
David Trattnig committed
241
        self.logger.debug("Removed %d unnecessary schedules from response. Entries left: %d" % ((count_before - count_after), count_after))
242
        return schedule
243
244


245
    def remove_data_more_than_24h_in_the_future(self, schedules):
246
247
248
249
250
251
        """ 
        Removes entries 24h in the future and 12 hours in the past.
        Note: This might influence resuming (in case of a crash)  
        single schedules which are longer than 12 hours long.
        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
258
        for s in schedules:
            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, schedules):
268
269
270
271
        """
        Removes all schedules from the past, except the one which is 
        currently playing.
        """
272
        items = []
273
        now = SU.timestamp()
274
275
        for s in schedules:
            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