Commit 298ebf3c authored by David Trattnig's avatar David Trattnig
Browse files

Pro-active fallback handling.

parent 57f97f77
......@@ -21,15 +21,16 @@
import os, os.path
import ntpath
import logging
import random
import librosa
from accessify import private, protected
from modules.base.enum import FallbackType
from modules.base.utils import SimpleUtil
from modules.base.enum import PlaylistType
from modules.base.utils import SimpleUtil, EngineUtil
from modules.communication.mail import AuraMailer
from modules.base.enum import ChannelType
class FallbackManager:
......@@ -73,39 +74,83 @@ class FallbackManager:
# PUBLIC METHODS
#
def get_fallback(self, schedule, type):
def resolve_playlist(self, schedule):
"""
Checks if the given schedule is valid and returns a valid fallback
if required.
Resolves the (fallback) playlist for the given schedule in case of pro-active fallback scenarios.
A resolved playlist represents the state how it would currently be aired. For example the `FallbackManager`
evaluated, that the actually planned playlist cannot be played for various reasons (e.g. entries n/a).
Instead one of the fallback playlists should be played. If the method is called some time later,
it actually planned playlist might be valid, thus returned as the resolved playlist.
As long the adressed schedule is still within the scheduling window, the resolved playlist can
always change.
This method also updates `schedule.fallback_state` to the current fallback type (`PlaylistType`).
Args:
schedule (Schedule): The schedule to resolve the playlist for
Returns:
(Playlist): The resolved playlist
"""
playlist = None
type = None
playlist_id = schedule.playlist_id
self.logger.info("Resolving playlist for schedule #%s ..." % schedule.schedule_id)
if not schedule.playlist_id:
if not schedule.show_fallback_id:
if not schedule.schedule_fallback_id:
if not schedule.station_fallback_id:
raise Exception
if not self.validate_playlist(schedule, "playlist"):
if not self.validate_playlist(schedule, "show_fallback"):
if not self.validate_playlist(schedule, "schedule_fallback"):
if not self.validate_playlist(schedule, "station_fallback"):
raise Exception("No (fallback) playlists for schedule #%s available - not even a single one!" % schedule.schedule_id)
else:
type = FallbackType.STATION
playlist_id = schedule.station_fallback_id
type = PlaylistType.STATION
playlist = schedule.station_fallback
else:
type = FallbackType.TIMESLOT
playlist_id = schedule.schedule_fallback_id
type = PlaylistType.TIMESLOT
playlist = schedule.schedule_fallback
else:
type = FallbackType.SHOW
playlist_id = schedule.show_fallback_id
type = PlaylistType.SHOW
playlist = schedule.show_fallback
else:
type = PlaylistType.DEFAULT
playlist = schedule.playlist
if type:
self.logger.warn("Detected fallback type '%s' required for schedule %s" % (type, str(schedule)))
if type and type != PlaylistType.DEFAULT:
previous_type = schedule.fallback_state
if type == previous_type:
self.logger.info("Fallback state for schedule #%s is still '%s'" % (schedule.schedule_id, type))
else:
self.logger.warn("Detected fallback type switch from '%s' to '%s' is required for schedule %s." % (previous_type, type, str(schedule)))
schedule.fallback_state = type
return playlist[0]
return (type, playlist_id)
def handle_proactive_fallback(self, scheduler, playlist):
"""
This is the 1st level strategy for fallback handling. When playlist entries are pre-rolled their
state is validated. If any of them doesn't become "ready to play" in time, some fallback entries
are queued.
"""
resolved_playlist = self.resolve_playlist(playlist.schedule)
if playlist != resolved_playlist:
self.logger.info("Switching from playlist #%s to fallback playlist #%s ..." % (playlist.playlist_id, resolved_playlist.playlist_id))
# Destroy any existing queue timers
for entry in playlist.entries:
scheduler.stop_timer(entry.switchtimer)
self.logger.info("Stopped existing timers for entries")
# Queue the fallback playlist
scheduler.queue_playlist_entries(resolved_playlist.schedule, resolved_playlist.entries, False, True)
self.logger.info("Queued fallback playlist entries (Fallback type: %s)" % playlist.type)
else:
self.logger.critical(SimpleUtil.red("For some strange reason the fallback playlist equals the currently failed one?!"))
def validate_playlist(self, playlist_id):
pass
def get_fallback_for(self, fallbackname):
......@@ -186,11 +231,45 @@ class FallbackManager:
duration = librosa.get_duration(y=y, sr=sr)
return duration
#
# PRIVATE METHODS
#
def validate_playlist(self, schedule, playlist_type):
"""
Checks if a playlist is valid for play-out.
"""
playlist = getattr(schedule, playlist_type)
if playlist \
and isinstance(playlist, list) \
and playlist[0].entries \
and len(playlist[0].entries) > 0:
return self.validate_entries(playlist[0].entries)
return False
def validate_entries(self, entries):
"""
Checks if playlist entries are valid for play-out.
"""
for entry in entries:
if entry.get_type() == ChannelType.FILESYSTEM:
audio_store = self.config.get("audiofolder")
filepath = EngineUtil.uri_to_filepath(audio_store, entry.source)
if not self.is_audio_file(filepath):
self.logger.warn("Invalid filesystem path '%s' in entry '%s'" % (filepath, str(entry)))
return False
return True
def get_playlist_items(self, schedule, fallback_key):
"""
Retrieves the list of tracks from a playlist defined by `fallback_key`.
......@@ -218,7 +297,7 @@ class FallbackManager:
"""
dir = self.config.fallback_music_folder
files = os.listdir(dir)
audio_files = list(filter(lambda f: self.is_audio_file(dir, f), files))
audio_files = list(filter(lambda f: self.is_audio_file(os.path.join(dir, f)), files))
if not dir or not audio_files:
self.logger.error("Folder 'fallback_music_folder = %s' is empty!" % dir)
......@@ -253,22 +332,22 @@ class FallbackManager:
def is_audio_file(self, dir, file):
def is_audio_file(self, 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.
dir (String):
file (File): the file object.
Returns:
(Boolean): True, if it's an audio file.
"""
audio_extensions = [".wav", ".flac", ".mp3", ".ogg", ".m4a"]
ext = os.path.splitext(file)[1]
abs_path = os.path.join(dir, file)
if os.path.isfile(abs_path):
if os.path.isfile(file):
if any(ext in s for s in audio_extensions):
return True
return False
\ No newline at end of file
This diff is collapsed.
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment