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

Pro-active fallback handling.

parent 57f97f77
No related branches found
No related tags found
No related merge requests found
...@@ -21,15 +21,16 @@ ...@@ -21,15 +21,16 @@
import os, os.path import os, os.path
import ntpath
import logging import logging
import random import random
import librosa import librosa
from accessify import private, protected from accessify import private, protected
from modules.base.enum import FallbackType from modules.base.enum import PlaylistType
from modules.base.utils import SimpleUtil from modules.base.utils import SimpleUtil, EngineUtil
from modules.communication.mail import AuraMailer from modules.communication.mail import AuraMailer
from modules.base.enum import ChannelType
class FallbackManager: class FallbackManager:
...@@ -73,39 +74,83 @@ class FallbackManager: ...@@ -73,39 +74,83 @@ class FallbackManager:
# PUBLIC METHODS # PUBLIC METHODS
# #
def resolve_playlist(self, schedule):
def get_fallback(self, schedule, type):
""" """
Checks if the given schedule is valid and returns a valid fallback Resolves the (fallback) playlist for the given schedule in case of pro-active fallback scenarios.
if required.
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 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 self.validate_playlist(schedule, "playlist"):
if not schedule.show_fallback_id: if not self.validate_playlist(schedule, "show_fallback"):
if not schedule.schedule_fallback_id: if not self.validate_playlist(schedule, "schedule_fallback"):
if not schedule.station_fallback_id: if not self.validate_playlist(schedule, "station_fallback"):
raise Exception raise Exception("No (fallback) playlists for schedule #%s available - not even a single one!" % schedule.schedule_id)
else: else:
type = FallbackType.STATION type = PlaylistType.STATION
playlist_id = schedule.station_fallback_id playlist = schedule.station_fallback
else: else:
type = FallbackType.TIMESLOT type = PlaylistType.TIMESLOT
playlist_id = schedule.schedule_fallback_id playlist = schedule.schedule_fallback
else: else:
type = FallbackType.SHOW type = PlaylistType.SHOW
playlist_id = schedule.show_fallback_id playlist = schedule.show_fallback
else:
type = PlaylistType.DEFAULT
playlist = schedule.playlist
if type: if type and type != PlaylistType.DEFAULT:
self.logger.warn("Detected fallback type '%s' required for schedule %s" % (type, str(schedule))) 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): def get_fallback_for(self, fallbackname):
...@@ -186,11 +231,45 @@ class FallbackManager: ...@@ -186,11 +231,45 @@ class FallbackManager:
duration = librosa.get_duration(y=y, sr=sr) duration = librosa.get_duration(y=y, sr=sr)
return duration return duration
# #
# PRIVATE METHODS # 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): def get_playlist_items(self, schedule, fallback_key):
""" """
Retrieves the list of tracks from a playlist defined by `fallback_key`. Retrieves the list of tracks from a playlist defined by `fallback_key`.
...@@ -218,7 +297,7 @@ class FallbackManager: ...@@ -218,7 +297,7 @@ class FallbackManager:
""" """
dir = self.config.fallback_music_folder dir = self.config.fallback_music_folder
files = os.listdir(dir) 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: if not dir or not audio_files:
self.logger.error("Folder 'fallback_music_folder = %s' is empty!" % dir) self.logger.error("Folder 'fallback_music_folder = %s' is empty!" % dir)
...@@ -253,22 +332,22 @@ class FallbackManager: ...@@ -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 Checks if the passed file is an audio file i.e. has a file-extension
known for audio files. known for audio files.
Args: Args:
(File): file: the file object. dir (String):
file (File): the file object.
Returns: Returns:
(Boolean): True, if it's an audio file. (Boolean): True, if it's an audio file.
""" """
audio_extensions = [".wav", ".flac", ".mp3", ".ogg", ".m4a"] audio_extensions = [".wav", ".flac", ".mp3", ".ogg", ".m4a"]
ext = os.path.splitext(file)[1] 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): if any(ext in s for s in audio_extensions):
return True return True
return False return False
\ No newline at end of file
This diff is collapsed.
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