api.py 8.77 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 src.base.config import AuraConfig
27
from src.base.utils import SimpleUtil as SU
28
from src.scheduling.utils import TimeslotFilter
29

30

David Trattnig's avatar
David Trattnig committed
31

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

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

48

David Trattnig's avatar
David Trattnig committed
49

50
    def __init__(self):
David Trattnig's avatar
David Trattnig committed
51
52
53
        """
        Constructor
        """
54
        self.config = AuraConfig.config()
55
        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 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!"))
77
            return None
78

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

        try:
83
84
85
86
87
88
89
90
91
92
93
94
            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:
95
                    
96
97
                    self.logger.warning("No playlist for timeslot given. Skipping timeslot: " + str(timeslot))
                    timeslot = None
98

99
100
                if timeslot:
                    return_data.append(timeslot)
101
102
        except TypeError:
            self.logger.error(SU.red("Nothing fetched ..."))
103
            self.fetched_timeslot_data = None
104
            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_timeslot_data(self):
David Trattnig's avatar
David Trattnig committed
116
        """
117
        Fetches timeslot data from Steering.
David Trattnig's avatar
David Trattnig committed
118
119

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

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

140
        return self.polish_timeslots(timeslots)
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
        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
149
        """
150
151
152
        # store fetched entries => do not have to fetch playlist_id more than once
        fetched_entries=[]

153
        try:
154
            for timeslot in self.fetched_timeslot_data:
155

156
                # Get IDs of playlists
157
158
159
160
                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")
161

162
                # Retrieve playlist and the fallback playlists for every timeslot.
David Trattnig's avatar
David Trattnig committed
163
                # If a playlist (like station_fallback) is already fetched, it is not fetched again but reused
164
165
166
167
                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)
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
        Fetches the playlist for a given timeslot.
David Trattnig's avatar
David Trattnig committed
177
178
179
180
181
182

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


229
    def polish_timeslots(self, timeslots):
230
        """
231
232
        Removes all timeslots which are not relevant for further processing,
        and transparent timeslot ID assigment for more expressive use.
233
        """
234
        count_before = len(timeslots)
235
236
        timeslots = TimeslotFilter.filter_24h(timeslots)
        timeslots = TimeslotFilter.filter_past(timeslots)
237
238
239
240
241
242
        count_after = len(timeslots)
        self.logger.debug("Removed %d unnecessary timeslots from response. Timeslots left: %d" % ((count_before - count_after), count_after))

        for t in timeslots:
            t["timeslot_id"] = t["id"]
        return timeslots
243

244
245
246
247