Skip to content
Snippets Groups Projects
Commit f90f2f10 authored by David Trattnig's avatar David Trattnig
Browse files

Fallback handling.

parent de452cc0
No related branches found
No related tags found
No related merge requests found
#
# 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!")
# Send admin email to notify about the fallback state
if file:
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.critical("Sent admin email:\n──────────────────────────────────────\n" + msg)
self.is_processing = False
return file
#
# 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
\ No newline at end of file
......@@ -35,7 +35,6 @@ import datetime
import decimal
import traceback
import sqlalchemy
import logging
import threading
......@@ -44,6 +43,7 @@ from operator import attrgetter
from modules.base.simpleutil import SimpleUtil
from modules.communication.redis.messenger import RedisMessenger
from modules.scheduling.calendar import AuraCalendarService
from modules.scheduling.fallback_manager import FallbackManager
from libraries.database.broadcasts import Schedule, Playlist, AuraDatabaseModel
from libraries.exceptions.exception_logger import ExceptionLogger
from libraries.enum.auraenumerations import ScheduleEntryType, TimerType, TerminalColors
......@@ -90,11 +90,10 @@ class AuraScheduler(ExceptionLogger, threading.Thread):
exit_event = None
liquidsoapcommunicator = None
last_successful_fetch = None
programme = None
active_entry = None
message_timer = []
fallback_manager = None
#schedule_entries = None
client = None
......@@ -110,7 +109,7 @@ class AuraScheduler(ExceptionLogger, threading.Thread):
self.logger = logging.getLogger("AuraEngine")
self.init_error_messages()
self.init_database()
self.fallback_manager = FallbackManager(config, self.logger, self)
self.redismessenger = RedisMessenger(config)
# init threading
......@@ -276,35 +275,20 @@ class AuraScheduler(ExceptionLogger, threading.Thread):
def get_next_file_for(self, fallbackname):
"""
Evaluates the next **fallback files/folders** to be played for a given fallback-type.
Evaluates the next **fallback files/playlists** to be played for a given fallback-type.
Valid fallback-types are:
* timeslot
* show
* station
Args:
fallbackname (String): The name of the fallback-type
Returns:
(String): Absolute path to the file to be played as a fallback.
"""
file = None
# next_entry = None
# if not self.active_entry:
# self.get_active_entry()
# next_entry = self.active_entry
# else:
# next_entry = self.get_next_entry()
if fallbackname == "timeslot":
file = "/home/david/Music/ab.mp3"
elif fallbackname == "show":
file = "/home/david/Code/aura/engine2/testing/content/ernie_mayne_sugar.mp3"
elif fallbackname == "station":
file = "/home/david/Code/aura/engine2/testing/content/ernie_mayne_sugar.mp3"
else:
file = ""
self.logger.critical("Should set next fallback file for " + fallbackname + ", but this fallback is unknown!")
file = self.fallback_manager.get_fallback_for(fallbackname)
if file:
self.logger.info("Got next file '%s' (type: %s)" % (file, fallbackname))
......@@ -336,14 +320,15 @@ class AuraScheduler(ExceptionLogger, threading.Thread):
current_playlist = None
# Iterate over all shows and playlists and find the one to be played right now
for schedule in self.programme:
if schedule.start_unix < now_unix < schedule.end_unix:
current_schedule = schedule
for playlist in schedule.playlist:
if playlist.start_unix < now_unix < playlist.end_unix:
current_playlist = playlist
break
break
if self.programme:
for schedule in self.programme:
if schedule.start_unix < now_unix < schedule.end_unix:
current_schedule = schedule
for playlist in schedule.playlist:
if playlist.start_unix < now_unix < playlist.end_unix:
current_playlist = playlist
break
break
return (current_schedule, current_playlist)
......@@ -523,9 +508,9 @@ class AuraScheduler(ExceptionLogger, threading.Thread):
# Always load latest programme from the database
self.last_successful_fetch = lsf
self.load_programme_from_db()
self.logger.info("Finished loading current programme from database")
self.logger.info("Finished loading current programme from database (%s schedules)" % str(len(self.programme)))
for schedule in self.programme:
self.logger.debug("\tSchedule %s with Playlist %s" % (str(schedule), str(schedule.playlist[0])))
self.logger.debug("\tSchedule %s with Playlist %s" % (str(schedule), str(schedule.playlist)))
......@@ -542,31 +527,9 @@ class AuraScheduler(ExceptionLogger, threading.Thread):
self.logger.critical("Could not load programme from database. We are in big trouble my friend!")
return
# FIXME That's very likely not needed - review!
# planned_entries = []
# for schedule in self.programme:
# # playlist to play
# #schedule.playlist = [Playlist.select_playlist_for_schedule(schedule.schedule_start, schedule.playlist_id)]
# # show fallback is played when playlist fails
# #schedule.showfallback = Playlist.select_playlist(schedule.show_fallback_id)
# # timeslot fallback is played when show fallback fails
# #schedule.timeslotfallback = Playlist.select_playlist(schedule.timeslot_fallback_id)
# # station fallback is played when timeslot fallback fails
# #schedule.stationfallback = Playlist.select_playlist(schedule.station_fallback_id)
# for p in schedule.playlist:
# planned_entries.append(p)
# FIXME Same playlists are repeated over time - test with different schedules/timeslots/playlists
# Therefore only passing the first playlist for now:
# self.logger.warn("ONLY PASSING 1ST PLAYLIST OF PROGRAMME")
# self.enable_entries(planned_entries[0])
# FIXME Still needed?
def enable_entries(self, playlist):
"""
Iterates over all playlist entries and assigs their start time.
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment