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
26
from datetime import datetime
from modules.base.utils import SimpleUtil as SU
27

David Trattnig's avatar
David Trattnig committed
28

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

39
40
41
42
43
44
    # Config for API Endpoints
    steering_calendar_url = None
    tank_playlist_url = None
    tank_session = None
    tank_secret = None

45

David Trattnig's avatar
David Trattnig committed
46

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

        Args:
            config (AuraConfig):    Holds the engine configuration
        """
54
55
        self.config = config
        self.logger = logging.getLogger("AuraEngine")
56
57
58
59
60
        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
61
62
63
64
65

    #
    #   PUBLIC METHODS
    #

66
67

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

73
74
75
76
        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!"))
77
            return None
78

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

        try:
            for schedule in self.fetched_schedule_data:
David Trattnig's avatar
David Trattnig committed
84
                # Skip schedule if no start or end is given
85
                if "start" not in schedule:
David Trattnig's avatar
David Trattnig committed
86
                    self.logger.warning("No start of schedule given. Skipping schedule: " + str(schedule))
87
88
                    schedule = None
                if "end" not in schedule:
David Trattnig's avatar
David Trattnig committed
89
                    self.logger.warning("No end of schedule given. Skipping schedule: " + str(schedule))
90
                    schedule = None
91
92
93
94
95
                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
96
                    self.logger.warning("No playlist for schedule given. Skipping schedule: " + str(schedule))
97
98
99
100
                    schedule = None

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

        return return_data

David Trattnig's avatar
David Trattnig committed
108
109
110
111
112
113
114


    #
    #   PRIVATE METHODS
    #


115
    def fetch_schedule_data(self):
David Trattnig's avatar
David Trattnig committed
116
117
118
119
120
121
        """
        Fetches schedule data from Steering.

        Returns:
            ([Schedule]):   An array of schedules
        """
122
        schedule = None
123
124
125
126
127
128
129
130
131
132
133
134
        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)
135

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

140
        return self.remove_unnecessary_data(schedule)
141

David Trattnig's avatar
David Trattnig committed
142
143


144
    def fetch_playlists(self):
David Trattnig's avatar
David Trattnig committed
145
146
147
148
149
        """
        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.
        """
150
151
152
        # store fetched entries => do not have to fetch playlist_id more than once
        fetched_entries=[]

153
154
155
        try:
            for schedule in self.fetched_schedule_data:

156
157
158
159
160
161
                # 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
162
163
                # 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
164
165
166
167
                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)
168

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

David Trattnig's avatar
David Trattnig committed
172
173


174
    def fetch_playlist(self, playlist_id, fetched_playlists):
David Trattnig's avatar
David Trattnig committed
175
176
177
178
179
180
181
182
        """
        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:
183
            (Playlist):             Playlist of type `id_name`
David Trattnig's avatar
David Trattnig committed
184
        """
185
186
        if not playlist_id:
            return None
187
188
189
190
191
192
        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"
        }
193

194
        # If playlist is already fetched in this round, use the existing one
David Trattnig's avatar
David Trattnig committed
195
        for playlist in fetched_playlists:
196
197
            if playlist["id"] == playlist_id:
                self.logger.debug("Playlist #%s already fetched" % playlist_id)
David Trattnig's avatar
David Trattnig committed
198
                return playlist
199
                   
200
        try:
201
202
203
204
205
206
207
            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()                  
208
        except Exception as e:
209
210
            self.logger.critical(SU.red("Error while requesting playlist #%s from Tank" % str(playlist_id)), e)
            return None
211

212
213
214
        fetched_playlists.append(playlist)
        return playlist
 
215
216


217
    def get_playlist_id(self, schedule, id_name):
218
        """
219
        Extracts the playlist ID for a given playlist (fallback) type.
220
        """
221
222
223
224
225
226
        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
227
228


229
    def remove_unnecessary_data(self, schedule):
230
231
232
233
        """
        Removes all schedules which are not relevant for 
        further processing.
        """
234
235
236
237
        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)
238

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


243
    def remove_data_more_than_24h_in_the_future(self, schedules):
244
245
246
247
248
249
        """ 
        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.
        """
250
        items = []
251
        now = SU.timestamp()
252
253
        now_plus_24hours = now + (12*60*60)
        now_minus_12hours = now - (12*60*60)
254

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

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

262
        return items
263
264


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

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

285
        return items