From 861c16110feac29641c30a7b4f4f6502601e9e57 Mon Sep 17 00:00:00 2001 From: David Trattnig <david.trattnig@o94.at> Date: Thu, 17 Sep 2020 23:02:01 +0200 Subject: [PATCH] Bearer auth against Tank. Refactoring. #14 --- modules/scheduling/calender_fetcher.py | 351 ++++++------------------- 1 file changed, 77 insertions(+), 274 deletions(-) diff --git a/modules/scheduling/calender_fetcher.py b/modules/scheduling/calender_fetcher.py index e8a4ecba..087dbec8 100644 --- a/modules/scheduling/calender_fetcher.py +++ b/modules/scheduling/calender_fetcher.py @@ -19,31 +19,29 @@ -import os -import sys -import urllib import logging -import simplejson +import requests -from datetime import datetime, timedelta -#from modules.models.schedule import Schedule -from modules.base.utils import SimpleUtil +from datetime import datetime +from modules.base.utils import SimpleUtil as SU class CalendarFetcher: """ Fetches the schedules, playlists and playlist entries as JSON - via the API endpoints. + via the API endpoints of Steering and Tank. """ - url = dict() - url_parameter = dict() config = None logging = None has_already_fetched = False fetched_schedule_data = None - # FIXME another crutch because of the missing TANK - used_random_playlist_ids = list() + # Config for API Endpoints + steering_calendar_url = None + tank_playlist_url = None + tank_session = None + tank_secret = None + def __init__(self, config): @@ -55,9 +53,11 @@ class CalendarFetcher: """ self.config = config self.logger = logging.getLogger("AuraEngine") - self.__set_url__("api_steering_calendar") - self.__set_url__("api_tank_playlist") - self.__set_url__("api_steering_show") + 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") + # # PUBLIC METHODS @@ -68,38 +68,17 @@ class CalendarFetcher: """ Retrieve all required data from the API. """ + return_data = [] - # Fetch upcoming schedules from STEERING - try: - self.logger.debug("Fetching schedules from STEERING") - self.fetched_schedule_data = self.__fetch_schedule_data__() - if not self.fetched_schedule_data: - self.logger.critical(SimpleUtil.red("No schedules fetched from API!")) - return None - except urllib.error.HTTPError as e: - self.logger.critical("Cannot fetch from " + self.url["api_steering_calendar"] + "! Reason: " + str(e)) - self.fetched_schedule_data = None - return None - except (urllib.error.URLError, IOError, ValueError) as e: - self.logger.critical("Cannot connect to " + self.url["api_steering_calendar"] + "! Reason: " + str(e)) - self.fetched_schedule_data = None + 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!")) return None - # Fetch playlist and fallbacks to the schedules from TANK - try: - self.logger.debug("Fetching playlists from TANK") - self.__fetch_schedule_playlists__() - except urllib.error.HTTPError as e: - self.logger.critical("Cannot fetch from " + self.url["api_tank_playlist"] + "! Reason: " + str(e)) - self.fetched_schedule_data = None - return None - except (urllib.error.URLError, IOError, ValueError) as e: - self.logger.critical("Cannot connect to " + self.url["api_tank_playlist"] + "! Reason: " + str(e)) - self.fetched_schedule_data = None - return None + self.logger.debug("Fetching playlists from TANK") + self.fetch_playlists() - return_data = [] - # Gather returndata try: for schedule in self.fetched_schedule_data: # Skip schedule if no start or end is given @@ -119,8 +98,8 @@ class CalendarFetcher: if schedule: return_data.append(schedule) - except TypeError as e: - self.logger.error("Nothing fetched ...") + except TypeError: + self.logger.error(SU.red("Nothing fetched ...")) self.fetched_schedule_data = None return None @@ -133,57 +112,37 @@ class CalendarFetcher: # - # FIXME Refactor for more transparent API requests. - def __set_url__(self, type): - """ - Initializes URLs and parameters for API calls. - """ - url = self.config.get(type) - pos = url.find("?") - - if pos > 0: - self.url[type] = url[0:pos] - self.url_parameter[type] = url[pos:] - else: - self.url[type] = url - - - - def __fetch_schedule_data__(self): + def fetch_schedule_data(self): """ Fetches schedule data from Steering. Returns: ([Schedule]): An array of schedules """ - servicetype = "api_steering_calendar" schedule = None + 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) + self.logger.debug("Steering API response: %s" % response.status_code) + 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) - # fetch data from steering - url = self.__build_url__(servicetype) - html_response = self.__fetch_data__(servicetype, url) - - # use testdata if response fails or is empty - if not html_response or html_response == b"[]": - self.logger.critical("Got no response from Steering!") - - # FIXME move hardcoded test-data to separate testing logic. - html_response = self.get_test_schedules() - - # convert to dict - schedule = simplejson.loads(html_response) - - # check data if not schedule: - self.logger.warn("Got no schedule via Playout API (Steering)!") + self.logger.error(SU.red("Got no schedule via Playout API (Steering)!")) return None - #self.fetched_schedule_data = self.remove_unnecessary_data(schedule) return self.remove_unnecessary_data(schedule) - def __fetch_schedule_playlists__(self): + def fetch_playlists(self): """ Fetches all playlists including fallback playlists for every schedule. This method used the class member `fetched_schedule_data`` to iterate @@ -196,9 +155,6 @@ class CalendarFetcher: try: for schedule in self.fetched_schedule_data: - # Extend schedule with details of show (e.g. slug) - schedule = self.__fetch_show_details__(schedule) - # Get IDs of playlists playlist_id = self.get_playlist_id(schedule, "playlist_id") schedule_fallback_id = self.get_playlist_id(schedule, "schedule_fallback_id") @@ -207,15 +163,15 @@ class CalendarFetcher: # 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 - schedule["playlist"] = self.__fetch_schedule_playlist__(schedule, playlist_id, fetched_entries) - schedule["schedule_fallback"] = self.__fetch_schedule_playlist__(schedule, schedule_fallback_id, fetched_entries) - schedule["show_fallback"] = self.__fetch_schedule_playlist__(schedule, show_fallback_id, fetched_entries) - schedule["station_fallback"] = self.__fetch_schedule_playlist__(schedule, station_fallback_id, fetched_entries) + 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) # If Steering doesn't provide a station fallback, the local one is used if not schedule["station_fallback"] and int(local_station_fallback_id) > 0: schedule["station_fallback_id"] = local_station_fallback_id - schedule["station_fallback"] = self.__fetch_schedule_playlist__(schedule, local_station_fallback_id, fetched_entries) + schedule["station_fallback"] = self.fetch_playlist(local_station_fallback_id, fetched_entries) if schedule["station_fallback"]: self.logger.info("Assigned playlist #%s as local station fallback to schedule #%s" % (local_station_fallback_id, schedule["schedule_id"])) @@ -224,133 +180,60 @@ class CalendarFetcher: - def __fetch_show_details__(self, schedule): - """ - Fetches details of a show from Steering. - - Args: - schedule (Schedule): A schedule holding a valid `show_id` - - Returns: - (Schedule): The given schedule with additional show fields set. - """ - servicetype = "api_steering_show" - - url = self.__build_url__(servicetype, "${ID}", str(schedule["show_id"])) - json_response = self.__fetch_data__(servicetype, url) - show_details = simplejson.loads(json_response) - - # Extend "schedules" with details of "show" - schedule["show_slug"] = show_details["slug"] - ### ... - ### ... Add more properties here, if needed - ### ... - - return schedule - - - - def get_playlist_id(self, schedule, id_name): - """ - Extracts the playlist ID for a given playlist (fallback) type. - """ - 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 - - - - def __fetch_schedule_playlist__(self, schedule, playlist_id, fetched_playlists): + def fetch_playlist(self, playlist_id, fetched_playlists): """ Fetches the playlist for a given schedule. Args: - schedule (Schedule): The schedule to fetch playlists for id_name (String): The type of playlist to fetch (e.g. normal vs. fallback) fetched_playlists ([]): Previously fetched playlists to avoid re-fetching Returns: (Playlist): Playlist of type `id_name` """ - servicetype = "api_tank_playlist" - playlist = None - if not playlist_id: return None + 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" + } - # If playlist is already fetched, use the existing one + # If playlist is already fetched in this round, use the existing one for playlist in fetched_playlists: if playlist["id"] == playlist_id: self.logger.debug("Playlist #%s already fetched" % playlist_id) return playlist - - url = self.__build_url__(servicetype, "${ID}", playlist_id) - json_response = self.__fetch_data__(servicetype, url) - - if not json_response: - self.logger.critical(SimpleUtil.red("Playlist #%s could not be fetched or is not available! JSON response: '%s'" % (playlist_id, json_response))) - return None - + try: - playlist = simplejson.loads(json_response) + self.logger.debug("Fetch playlist from Tank API...") + response = requests.get(url, data=None, headers=headers) + self.logger.info("Tank API response: %s" % response.status_code) + 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() except Exception as e: - self.logger.critical(SimpleUtil.red("Error while parsing JSON response for playlist: %s" % json_response), e) + self.logger.critical(SU.red("Error while requesting playlist #%s from Tank" % str(playlist_id)), e) + return None fetched_playlists.append(playlist) return playlist - - def __fetch_data__(self, type, url): - """ - Fetches JSON data for the given URL. - - Args: - url (String): The API endpoint to call - - Returns: - (Byte[]): An UTF-8 encoded byte object holding the response - """ - html_response = b'' - - # Send request to the API and read the data - try: - if type not in self.url_parameter: - if self.url[type] == "": - return False - request = urllib.request.Request(url) - else: - request = urllib.request.Request(url, self.url_parameter[type]) - - response = urllib.request.urlopen(request) - html_response = response.read() - - except (urllib.error.URLError, IOError, ValueError) as e: - self.logger.error("Cannot connect to " + self.url[type] + - " (type: " + type + ")! Reason: " + str(e.reason)) - #if not self.has_already_fetched: # first fetch - # self.logger.critical("exiting fetch data thread..") - # sys.exit() - - self.has_already_fetched = True - return html_response.decode("utf-8") - - - - def __build_url__(self, type, placeholder=None, value=None): + def get_playlist_id(self, schedule, id_name): """ - Builds an API request URL using passed placeholder and value. + Extracts the playlist ID for a given playlist (fallback) type. """ - url = self.url[type] - if placeholder: - url = url.replace(placeholder, value) - self.logger.debug("Built API URL: "+url) - return url - + 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 def remove_unnecessary_data(self, schedule): @@ -367,7 +250,6 @@ class CalendarFetcher: return schedule - def remove_data_more_than_24h_in_the_future(self, schedules): """ Removes entries 24h in the future and 12 hours in the past. @@ -376,13 +258,13 @@ class CalendarFetcher: Think e.g. live broadcasts. """ items = [] - now = SimpleUtil.timestamp() + now = SU.timestamp() now_plus_24hours = now + (12*60*60) now_minus_12hours = now - (12*60*60) for s in schedules: start_time = datetime.strptime(s["start"], "%Y-%m-%dT%H:%M:%S") - start_time = SimpleUtil.timestamp(start_time) + start_time = SU.timestamp(start_time) if start_time <= now_plus_24hours and start_time >= now_minus_12hours: items.append(s) @@ -396,12 +278,12 @@ class CalendarFetcher: currently playing. """ items = [] - now = SimpleUtil.timestamp() + now = SU.timestamp() for s in schedules: start_time = datetime.strptime(s["start"], "%Y-%m-%dT%H:%M:%S") - start_time = SimpleUtil.timestamp(start_time) + start_time = SU.timestamp(start_time) end_time = datetime.strptime(s["end"], "%Y-%m-%dT%H:%M:%S") - end_time = SimpleUtil.timestamp(end_time) + end_time = SU.timestamp(end_time) # Append all elements in the future if start_time >= now: @@ -411,82 +293,3 @@ class CalendarFetcher: items.append(s) return items - - - - - # - # TESTING - # - - - def get_test_schedules(self): - html_response = "{}" - - # use testdata if wanted - if self.config.get("use_test_data"): - - html_response = '[{"id":1,"schedule_id":1,"automation-id":1,"className":"TestData","memo":"TestData","show_fundingcategory":"TestData","start":"' + (datetime.now() + timedelta(hours=0)).strftime('%Y-%m-%dT%H:00:00') + '","end":"' + (datetime.now() + timedelta(hours=1)).strftime('%Y-%m-%dT%H:00:00') + '","show_id":9,"show_name":"TestData: FROzine","show_hosts":"TestData: Sandra Hochholzer, Martina Schweiger","title":"TestData:title","is_repetition":false,"playlist_id":2,"schedule_fallback_id":12,"show_fallback_id":92,"station_fallback_id":1,"rtr_category":"string","comment":"TestData: Kommentar","show_languages":"TestData: Sprachen","show_type":"TestData: Typ","show_categories":"TestData: Kategorie","show_topics":"TestData: Topic","show_musicfocus":"TestData: Fokus"},' \ - '{"id":2,"schedule_id":2,"automation-id":1,"className":"TestData","memo":"TestData","show_fundingcategory":"TestData","start":"' + (datetime.now() + timedelta(hours=1)).strftime('%Y-%m-%dT%H:00:00') + '","end":"' + (datetime.now() + timedelta(hours=2)).strftime('%Y-%m-%dT%H:00:00') + '","show_id":10,"show_name":"TestData: FROMat","show_hosts":"TestData: Sandra Hochholzer, Martina Schweiger","title":"TestData:title","is_repetition":false,"playlist_id":4,"schedule_fallback_id":22,"show_fallback_id":102,"station_fallback_id":1,"rtr_category":"TestData: string","comment":"TestData: Kommentar","show_languages":"TestData: Sprachen","show_type":"TestData: Typ","show_categories":"TestData: Kategorie","show_topics":"TestData: Topic","show_musicfocus":"TestData: Fokus"},' \ - '{"id":3,"schedule_id":3,"automation-id":1,"className":"TestData","memo":"TestData","show_fundingcategory":"TestData","start":"' + (datetime.now() + timedelta(hours=2)).strftime('%Y-%m-%dT%H:00:00') + '","end":"' + (datetime.now() + timedelta(hours=3)).strftime('%Y-%m-%dT%H:00:00') + '","show_id":11,"show_name":"TestData: Radio für Senioren","show_hosts":"TestData: Sandra Hochholzer, Martina Schweiger","title":"TestData:title","is_repetition":false,"playlist_id":6,"schedule_fallback_id":32,"show_fallback_id":112,"station_fallback_id":1,"rtr_category":"TestData: string","comment":"TestData: Kommentar","show_languages":"TestData: Sprachen","show_type":"TestData: Typ","show_categories":"TestData: Kategorie","show_topics":"TestData: Topic","show_musicfocus":"TestData: Fokus"}]' - self.logger.critical("Using hardcoded Response!") - - return html_response - - - - def create_test_data(self, id_name, schedule): - import random - rand_id = random.randint(1, 10000) - - while rand_id in self.used_random_playlist_ids: - rand_id = random.randint(1, 10000) - - self.used_random_playlist_ids.append(rand_id) - - # FIXME move hardcoded test-data to separate testing logic. - # HARDCODED Testdata - if id_name != "playlist_id": - # FALLBACK TESTDATA - - if rand_id % 3 == 0: # playlist fallback - json_response = '{"playlist_id":' + str( - rand_id) + ',"entries":[{"source":"file:///var/audio/fallback/music.flac"},{"source":"file:///var/audio/fallback/NightmaresOnWax/DJ-Kicks/02 - Only Child - Breakneck.flac"}]}' - elif rand_id % 2 == 0: # stream fallback - json_response = '{"playlist_id":' + str( - rand_id) + ',"entries":[{"source":"http://chill.out.airtime.pro:8000/chill_a"}]}' - else: # pool fallback - json_response = '{"playlist_id":' + str(rand_id) + ',"entries":[{"source":"pool:///liedermacherei"}]}' - - schedule[id_name] = rand_id - - elif schedule[id_name] == 0 or schedule[id_name] is None: - # this happens when playlist id is not filled out in pv - # json_response = '{"playlist_id": 0}' - - if rand_id % 4 == 0: # playlist with two files - json_response = '{"playlist_id":' + str( - rand_id) + ',"entries":[{"source":"file:///var/audio/fallback/music.flac"},{"source":"file:///var/audio/fallback/NightmaresOnWax/DJ-Kicks/02 - Only Child - Breakneck.flac"}]}' - elif rand_id % 3 == 0: # playlist with jingle and then linein - json_response = '{"playlist_id":' + str( - rand_id) + ',"entries":[{"source":"file:///var/audio/fallback/music.flac"},{"source":"linein://1"}]}' - elif rand_id % 2 == 0: # playlist with jingle and then http stream - json_response = '{"playlist_id":' + str( - rand_id) + ',"entries":[{"source":"file:///var/audio/fallback/music.flac"},{"source":"http://chill.out.airtime.pro:8000/chill_a"}]}' - else: # pool playlist - json_response = '{"playlist_id":' + str(rand_id) + ',"entries":[{"source":"pool:///hiphop"}]}' - - schedule[id_name] = rand_id - - elif schedule[id_name] % 4 == 0: # playlist with two files - json_response = '{"playlist_id":' + str(schedule[id_name]) + ',"entries":[{"source":"file:///var/audio/fallback/music.flac"},{"source":"file:///var/audio/fallback/NightmaresOnWax/DJ-Kicks/01 - Type - Slow Process.flac"}]}' - elif schedule[id_name] % 3 == 0: # playlist with jingle and then http stream - json_response = '{"playlist_id":' + str(schedule[id_name]) + ',"entries":[{"source":"file:///var/audio/fallback/music.flac"},{"source":"linein://0"}]}' - elif schedule[id_name] % 2 == 0: # playlist with jingle and then linein - json_response = '{"playlist_id":' + str(schedule[id_name]) + ',"entries":[{"source":"file:///var/audio/fallback/music.flac"},{"source":"http://stream.fro.at:80/fro-128.ogg"}]}' - else: # pool playlist - json_response = '{"playlist_id":' + str(schedule[id_name]) + ',"entries":[{"source":"pool:///chillout"}]}' - - self.logger.info("Using 'randomized' playlist: " + json_response + " for " + id_name[:-3] + " for show " + schedule["show_name"] + " starting @ " + schedule["start"]) - - return json_response -- GitLab