# # Aura Engine # # Playout Daemon for autoradio project # # # Copyright (C) 2020 David Trattnig <david.trattnig@subsquare.at> # # This file is part of engine. # # engine is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # engine 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with engine. If not, see <http://www.gnu.org/licenses/>. # # Meta __version__ = '0.0.1' __license__ = "GNU General Public License (GPL) Version 3" __version_info__ = (0, 0, 1) __author__ = 'David Trattnig <david.trattnig@subsquare.at>' import os, os.path import logging import random from accessify import private, protected from modules.base.simpleutil import SimpleUtil from modules.communication.mail import AuraMailer class FallbackManager: """ Handles all types of fallbacks in case there is an outage for the regular radio programme. Attributes: config (AuraConfig): The engine configuration logger (AuraLogger): The logger mail (AuraMailer): Mail service scheduler (AuraScheduler): The scheduler fallback_history (Dict): Holds a 24h history of played, local tracks to avoid re-play last_fallback (Integer): Timestamp, when the last local file fallback was played is_processing (Boolean): Flag to avoid race-conditions, as Liquidsoap sends plenty of requests at once """ config = None logger = None mailer = None scheduler = None fallback_history = {} last_fallback = 0 is_processing = False def __init__(self, config, logger, scheduler): """ Constructor Args: config (AuraConfig): Holds the engine configuration """ self.config = config self.mailer = AuraMailer(self.config) self.scheduler = scheduler self.logger = logger # # PUBLIC METHODS # def get_fallback_for(self, fallbackname): """ Retrieves a random fallback audio source for any of the types: - timeslot/schedule - show - station Args: fallbackname (String): Fallback type Returns: (String): Absolute path to the file """ file = "" media_type = "PLAYLIST" active_schedule, active_playlist = self.scheduler.get_active_playlist() # Block access to avoid race-conditions if self.is_processing: return None else: self.is_processing = True # Get fallback track(s) by fallback-type if fallbackname == "timeslot": file = self.get_playlist_items(active_schedule, "schedule_fallback") elif fallbackname == "show": file = self.get_playlist_items(active_schedule, "show_fallback") elif fallbackname == "station": file = self.get_playlist_items(active_schedule, "station_fallback") if not file: media_type = "TRACK" file = self.get_random_local_track() if not file: self.logger.critical("Got no file for station fallback! Playing default test track, to play anything at all.") file = "../../testing/content/ernie_mayne_sugar.mp3" media_type = "DEFAULT TRACK" else: file = "" self.logger.critical("Should set next fallback file for " + fallbackname + ", but this fallback is unknown!") if file: # Send admin email to notify about the fallback state if not active_playlist: active_playlist = "n/a" msg = "AURA ENGINE %s FALLBACK DETECTED!\n\n" % fallbackname msg += "Expected, active Schedule: %s \n" % active_schedule msg += "Expected, active Playlist: %s \n\n" % active_playlist msg += "Providing FALLBACK-%s for %s '%s'\n\n" % (media_type, fallbackname, file) msg += "Please review the schedules or contact your Aura Engine administrator." self.mailer.send_admin_mail("CRITICAL - Detected fallback for %s" % fallbackname, msg) self.logger.warn("Providing fallback %s: '%s'. Sent admin email about fallback state" % (media_type, file)) self.is_processing = False return file def fallback_has_started(self, artist, title): """ Called when a fallback track has actually started playing """ self.logger.info("Now playing: fallback track '%s - %s'." % (artist, title)) # # PRIVATE METHODS # def get_playlist_items(self, schedule, fallback_key): """ Retrieves the list of tracks from a playlist defined by `fallback_key`. """ playlist_files = "" if hasattr(schedule, fallback_key): playlist = getattr(schedule, fallback_key) if len(playlist) > 0: playlist = playlist[0] if playlist and playlist.entries: for entry in playlist.entries: playlist_files += entry.filename + "\n" return playlist_files def get_random_local_track(self): """ Retrieves a random audio track from the local station-fallback directory. Returns: (String): Absolute path to an audio file """ dir = self.config.fallback_music_folder files = os.listdir(dir) audio_files = list(filter(lambda f: self.is_audio_file(dir, f), files)) if not dir or not audio_files: self.logger.error("Folder 'fallback_music_folder = %s' is empty!" % dir) return None # If last played fallback is > 24 hours ago, ignore play history # This should save used memory if the engine runs for a long time if self.last_fallback < SimpleUtil.timestamp() - (60*60*24): self.fallback_history = {} self.last_fallback = SimpleUtil.timestamp() # Retrieve files which haven't been played yet history = set(self.fallback_history.keys()) left_audio_files = list(set(audio_files).difference(history)) # If nothing left, clear history and start with all files again if not len(left_audio_files): self.fallback_history = {} left_audio_files = audio_files # Select random track from directory i = random.randint(0, len(left_audio_files)-1) file = os.path.join(dir, left_audio_files[i]) # Store track in history, to avoid playing it multiple times if file: self.fallback_history[left_audio_files[i]] = SimpleUtil.timestamp() return file def is_audio_file(self, dir, file): """ Checks if the passed file is an audio file i.e. has a file-extension known for audio files. Args: (File): file: the file object. Returns: (Boolean): True, if it's an audio file. """ audio_extensions = [".wav", ".flac", ".mp3", ".ogg"] ext = os.path.splitext(file)[1] abs_path = os.path.join(dir, file) if os.path.isfile(abs_path): if any(ext in s for s in audio_extensions): return True return False