diff --git a/modules/base/models.py b/modules/base/models.py index e062cf858c55aef1187e6aa6ea812022e06ed688..9f66f395bc33324f49542de6e128ca5eeada2bad 100644 --- a/modules/base/models.py +++ b/modules/base/models.py @@ -30,10 +30,9 @@ from sqlalchemy import BigInteger, Boolean, Column, Date from sqlalchemy.orm import relationship from sqlalchemy.ext.hybrid import hybrid_property - -from modules.scheduling.types import PlaylistType from modules.base.config import AuraConfig -from modules.base.utils import SimpleUtil, EngineUtil +from modules.base.utils import SimpleUtil +from modules.core.resources import ResourceUtil @@ -159,7 +158,6 @@ class Schedule(DB.Model, AuraDatabaseModel): schedule_fallback_id = Column(Integer) show_fallback_id = Column(Integer) station_fallback_id = Column(Integer) - fallback_state = PlaylistType.DEFAULT fadeouttimer = None # Used to fade-out the schedule, even when entries are longer @@ -215,6 +213,27 @@ class Schedule(DB.Model, AuraDatabaseModel): return schedules + def get_playlist(self): + """ + Returns the assigned playlist. + """ + # TODO Refactor to avoid storing array of playlists. + if self.playlist and self.playlist[0]: + return self.playlist[0] + return None + + + def has_queued_entries(self): + """ + Checks if entries of this timeslot have been queued at the engine. + """ + #TODO Make logic more transparent + if hasattr(self, "queued_entries"): + if len(self.queued_entries) > 0: + return True + return False + + @hybrid_property def start_unix(self): """ @@ -256,7 +275,6 @@ class Schedule(DB.Model, AuraDatabaseModel): "show": { "name": self.show_name, - "type": self.get_type(), "host": self.show_hosts }, @@ -289,7 +307,6 @@ class Playlist(DB.Model, AuraDatabaseModel): # data playlist_id = Column(Integer, autoincrement=False) # , ForeignKey("schedule.playlist_id")) show_name = Column(String(256)) - fallback_type = Column(Integer) entry_count = Column(Integer) @@ -490,8 +507,8 @@ class PlaylistEntry(DB.Model, AuraDatabaseModel): def volume(self): return 100 # FIXME Make DB Column - def get_type(self): - return EngineUtil.get_channel_type(self.uri) + def get_content_type(self): + return ResourceUtil.get_content_type(self.uri) def get_prev_entries(self): diff --git a/modules/scheduling/calendar.py b/modules/scheduling/calendar.py index 8d565888badb59fde18e7ad268a40b1b0fafe265..4d5efd73943dd55dd96e11dc7833c7476fb2adac 100644 --- a/modules/scheduling/calendar.py +++ b/modules/scheduling/calendar.py @@ -26,7 +26,7 @@ import logging from datetime import datetime -from modules.scheduling.types import PlaylistType +# from modules.scheduling.types import PlaylistType from modules.base.utils import SimpleUtil as SU from modules.base.models import Schedule, Playlist, PlaylistEntry, PlaylistEntryMetaData from modules.scheduling.calender_fetcher import CalendarFetcher @@ -140,13 +140,22 @@ class AuraCalendarService(threading.Thread): schedule_db = self.store_schedule(schedule) # Store playlists to play - self.store_playlist(schedule_db, schedule_db.playlist_id, schedule["playlist"], PlaylistType.DEFAULT.id) + self.store_playlist(schedule_db, schedule_db.playlist_id, schedule["playlist"]) if schedule_db.schedule_fallback_id: - self.store_playlist(schedule_db, schedule_db.schedule_fallback_id, schedule["schedule_fallback"], PlaylistType.TIMESLOT.id) + self.store_playlist(schedule_db, schedule_db.schedule_fallback_id, schedule["schedule_fallback"]) if schedule_db.show_fallback_id: - self.store_playlist(schedule_db, schedule_db.show_fallback_id, schedule["show_fallback"], PlaylistType.SHOW.id) + self.store_playlist(schedule_db, schedule_db.show_fallback_id, schedule["show_fallback"]) if schedule_db.station_fallback_id: - self.store_playlist(schedule_db, schedule_db.station_fallback_id, schedule["station_fallback"], PlaylistType.STATION.id) + self.store_playlist(schedule_db, schedule_db.station_fallback_id, schedule["station_fallback"]) + + + # self.store_playlist(schedule_db, schedule_db.playlist_id, schedule["playlist"], PlaylistType.DEFAULT.id) + # if schedule_db.schedule_fallback_id: + # self.store_playlist(schedule_db, schedule_db.schedule_fallback_id, schedule["schedule_fallback"], PlaylistType.TIMESLOT.id) + # if schedule_db.show_fallback_id: + # self.store_playlist(schedule_db, schedule_db.show_fallback_id, schedule["show_fallback"], PlaylistType.SHOW.id) + # if schedule_db.station_fallback_id: + # self.store_playlist(schedule_db, schedule_db.station_fallback_id, schedule["station_fallback"], PlaylistType.STATION.id) result.append(schedule_db) @@ -203,12 +212,14 @@ class AuraCalendarService(threading.Thread): - def store_playlist(self, schedule_db, playlist_id, fetched_playlist, fallbackplaylist_type=0): + # def store_playlist(self, schedule_db, playlist_id, fetched_playlist, fallbackplaylist_type=0): + def store_playlist(self, schedule_db, playlist_id, fetched_playlist): """ Stores the Playlist to the database. """ if not playlist_id or not fetched_playlist: - self.logger.debug("Playlist type %s with ID '%s' is not available!" % (fallbackplaylist_type, playlist_id)) + self.logger.debug(f"Playlist ID#{playlist_id} is not available!") + # self.logger.debug("Playlist type %s with ID '%s' is not available!" % (fallbackplaylist_type, playlist_id)) return playlist_db = Playlist.select_playlist_for_schedule(schedule_db.schedule_start, playlist_id) @@ -222,7 +233,7 @@ class AuraCalendarService(threading.Thread): playlist_db.playlist_id = playlist_id playlist_db.schedule_start = schedule_db.schedule_start playlist_db.show_name = schedule_db.show_name - playlist_db.fallback_type = fallbackplaylist_type + # playlist_db.fallback_type = fallbackplaylist_type if "entries" in fetched_playlist: playlist_db.entry_count = len(fetched_playlist["entries"]) else: diff --git a/modules/scheduling/fallback_manager.py b/modules/scheduling/fallback_manager.py index 072a31a35fd4631cdb4514287790cd276aa31d3f..9abbc31d731e82d94fac11aefa4392032b32bd83 100644 --- a/modules/scheduling/fallback_manager.py +++ b/modules/scheduling/fallback_manager.py @@ -19,15 +19,34 @@ +import logging -import os, os.path -import random +from enum import Enum +from threading import Thread, Timer +from datetime import datetime, timedelta -from accessify import private, protected -from modules.scheduling.types import PlaylistType -from modules.base.utils import SimpleUtil, EngineUtil +from modules.base.utils import SimpleUtil as SU from modules.base.mail import AuraMailer -from modules.core.channels import ChannelType +from modules.core.resources import ResourceClass +from modules.core.engine import Engine + + +class FallbackType(Enum): + """ + Types of playlists. + """ + NONE = { "id": 0, "name": "default" } # No fallback active, default playout + SCHEDULE = { "id": 1, "name": "schedule" } # The first played when some default playlist fails + SHOW = { "id": 2, "name": "show" } # The second played when the timeslot fallback fails + STATION = { "id": 3, "name": "station" } # The last played when everything else fails + + @property + def id(self): + return self.value["id"] + + def __str__(self): + return str(self.value["name"]) + class FallbackManager: @@ -40,21 +59,16 @@ class FallbackManager: 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 + message_timer = None - def __init__(self, config, logger, scheduler): + + def __init__(self, config, logger, scheduler, message_timer): """ Constructor @@ -65,162 +79,103 @@ class FallbackManager: self.logger = logger self.mailer = AuraMailer(self.config) self.scheduler = scheduler - self.logger = logger + # self.message_timer = message_timer + self.message_timer = [] # # PUBLIC METHODS # - def resolve_playlist(self, schedule): + def schedule_fallback_playlist(self, schedule, schedule_now=False): """ - 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`). + Evaluates the scheduled fallback and queues it using a timed thread. Args: - schedule (Schedule): The schedule to resolve the playlist for - - Returns: - (Playlist): The resolved playlist + schedule_now (Boolean): If `True` it is executed immediately """ - playlist = None - type = None - self.logger.info("Resolving playlist for schedule #%s ..." % schedule.schedule_id) - - if not self.validate_playlist(schedule, "playlist"): - if not self.validate_playlist(schedule, "schedule_fallback"): - if not self.validate_playlist(schedule, "show_fallback"): - if not self.validate_playlist(schedule, "station_fallback"): - self.logger.error(SimpleUtil.red("No (fallback) playlists for schedule #%s available - not even a single one!" % schedule.schedule_id)) - return None - else: - type = PlaylistType.STATION - playlist = schedule.station_fallback - else: - type = PlaylistType.TIMESLOT - playlist = schedule.schedule_fallback - else: - type = PlaylistType.SHOW - playlist = schedule.show_fallback - else: - type = PlaylistType.DEFAULT - playlist = schedule.playlist - - 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] - - + timer_start = None + timer_end = None + (fallback_type, playlist) = self.get_fallback_playlist(schedule) + + if playlist: + self.logger.info(f"Resolved {fallback_type.value} fallback") + + def do_schedule(entries): + self.logger.info(SU.cyan(f"=== set_fallback_playlist('{entries}') ===")) + self.scheduler.engine.player.start_fallback_playlist(entries) + def do_unschedule(): + self.logger.info(SU.cyan("=== clear_fallback_playlist() ===")) + self.scheduler.engine.player.stop_fallback_playlist() + + if schedule_now == True: + # Update queue immediately + thread = Thread(target = do_schedule, args = (playlist.entries,)) + thread.start() + else: + # Update queue at the beginning of the timeslot + timer_start = FallbackCommandTimer(schedule.start_unix, do_schedule, playlist.entries) + self.message_timer.append(timer_start) + timer_start.start() + + # Update fallback channel to be cleared at the end of the timeslot + timer_end = FallbackCommandTimer(schedule.end_unix, do_unschedule) + self.message_timer.append(timer_end) + timer_end.start() + return (timer_start, timer_end) - 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?!")) + msg = f"There is no schedule- or show-fallback defined for timeslot#{schedule.schedule_id}. " + msg += f"The station fallback will be used automatically." + self.logger.info(msg) - - def get_fallback_for(self, fallbackname): + def resolve_playlist(self, schedule): """ - Retrieves a random fallback audio source for any of the types: - - timeslot/schedule - - show - - station - - Args: - fallbackname (String): Fallback type + Retrieves the currently planned (fallback) playlist. If a normal playlist is available, + this one is returned. In case of station fallback no playlist is returned. + Args: + schedule (Schedule) + Returns: - (String): Absolute path to the file + (FallbackType, Playlist) """ - file = "" - media_type = "PLAYLIST" - active_schedule, active_playlist = self.scheduler.get_active_playlist() + planned_playlist = None + fallback_type = None - # Block access to avoid race-conditions - if self.is_processing: - return None + if self.validate_playlist(schedule, "playlist"): + planned_playlist = schedule.get_playlist() + fallback_type = FallbackType.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 = "../../test/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 = "-" - 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)) + (fallback_type, planned_playlist) = self.get_fallback_playlist(schedule) - self.is_processing = False - return file + return (fallback_type, planned_playlist) - def fallback_has_started(self, artist, title): + def get_fallback_playlist(self, schedule): """ - Called when a fallback track has actually started playing - """ - self.logger.info("Now playing: fallback track '%s - %s'." % (artist, title)) + Retrieves the playlist to be used in a fallback scenario. + Args: + schedule (Schedule) + Returns: + (Playlist) + """ + playlist = None + fallback_type = FallbackType.STATION + if self.validate_playlist(schedule, "schedule_fallback"): + playlist = schedule.schedule_fallback[0] + fallback_type = FallbackType.SCHEDULE + elif self.validate_playlist(schedule, "show_fallback"): + playlist = schedule.show_fallback[0] + fallback_type = FallbackType.SHOW + + return (fallback_type, playlist) @@ -229,10 +184,21 @@ class FallbackManager: # - def validate_playlist(self, schedule, playlist_type): """ Checks if a playlist is valid for play-out. + + Following checks are done for all playlists: + + - has one or more entries + + Fallback playlists should either: + + - have filesystem entries only + - reference a recording of a previous playout of a show (also filesystem) + + Otherwise, if a fallback playlist contains Live or Stream entries, + the exact playout behaviour can hardly be predicted. """ playlist = getattr(schedule, playlist_type) if playlist \ @@ -240,105 +206,116 @@ class FallbackManager: and playlist[0].entries \ and len(playlist[0].entries) > 0: - return True + # Default playlist + if playlist_type == "playlist": + return True + + # Fallback playlist + elif playlist[0].entries: + is_fs_only = True + for entry in playlist[0].entries: + if entry.get_content_type() not in ResourceClass.FILE.types: + self.logger.error(SU.red("Fallback playlist of type '%s' contains not only file-system entries! \ + Skipping fallback level..." % playlist_type)) + is_fs_only = False + break + return is_fs_only + 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 +class EngineCommandTimer(Timer): + """ + Timer for timed executing of Engine commands. + """ + timer_store = {} + logger = logging.getLogger("AuraEngine") + timer_id = None + timer_type = None + param = None + diff = None + dt = None - def get_playlist_items(self, schedule, fallback_key): + def __init__(self, timer_type="BASE", due_time=None, func=None, param=None): """ - Retrieves the list of tracks from a playlist defined by `fallback_key`. + Constructor """ - 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.source + "\n" + now_unix = Engine.engine_time() + self.timer_type = timer_type + self.timer_id = f"{timer_type}:{func.__name__}:{due_time}" + + diff = due_time - now_unix + if diff < 0.0: + msg = f"Trying to create timer in the past: {self.timer_id}" + self.logger.error(SU.red(msg)) + raise Exception(msg) + + self.diff = diff + self.dt = datetime.now() + timedelta(seconds=diff) + self.func = func + self.param = param + + def wrapper_func(param=None): + + # Remove from cache + self.logger.info(SU.green(f"Removing old timer with ID: {self.timer_id}")) + del EngineCommandTimer.timer_store[self.timer_id] + # Call actual function + if param: func(param,) + else: func() + + Timer.__init__(self, diff, wrapper_func, (param,)) + self.update_cache() + self.logger.info(SU.green(f"Created command timer with ID: {self.timer_id}")) + - return playlist_files + + def update_cache(self): + """ + Adds the instance to the cache and cancels any previously existing commands. + """ + existing_command = None + if self.timer_id in EngineCommandTimer.timer_store: + existing_command = EngineCommandTimer.timer_store[self.timer_id] + if existing_command: + self.logger.info(SU.green(f"Cancelling previous timer with ID: {self.timer_id}")) + existing_command.cancel() + EngineCommandTimer.timer_store[self.timer_id] = self - def get_random_local_track(self): + def print_active_timers(self): """ - Retrieves a random audio track from the local station-fallback directory. - - Returns: - (String): Absolute path to an audio file + Prints a list of active timers to the log. """ - dir = self.config.fallback_music_folder - files = os.listdir(dir) - 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) - 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.logger.info("Cleared 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) - (history) ) - self.logger.info("Left fallback audio-files: %d/%d" % (len(left_audio_files), len(audio_files))) - - # 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 + for id, timer in EngineCommandTimer.timer_store.values(): + EngineCommandTimer.logger.info(str(timer)) - # 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 __str__(self): + """ + String represenation of the timer. + """ + return f"[{self.timer_id}] COMMAND TIMER due at {str(self.dt)} (alive: {self.is_alive()})" - 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: - dir (String): - file (File): the file object. - Returns: - (Boolean): True, if it's an audio file. +class FallbackCommandTimer(EngineCommandTimer): + """ + Timer for executing timed scheduling of fallback playlists. + """ + def __init__(self, diff=None, func=None, param=None): """ - audio_extensions = [".wav", ".flac", ".mp3", ".ogg", ".m4a"] - ext = os.path.splitext(file)[1] + Constructor + """ + super().__init__("FALLBACK", diff, func, param) + self.logger.info("Executing scheduled fallback playlist update '%s' in %s seconds..." % \ + (str(func.__name__), str(diff))) - 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 diff --git a/modules/scheduling/scheduler.py b/modules/scheduling/scheduler.py index ecb22187878d2a88d763195e6bb229c9ec17546d..ee49e58de2d7545edec8ba40882eae72b5edba0b 100644 --- a/modules/scheduling/scheduler.py +++ b/modules/scheduling/scheduler.py @@ -26,16 +26,17 @@ import decimal import traceback import sqlalchemy +from enum import Enum from operator import attrgetter from datetime import datetime, timedelta -from modules.cli.redis.messenger import RedisMessenger - -from modules.base.utils import SimpleUtil, EngineUtil +from modules.cli.redis.messenger import RedisMessenger +from modules.base.utils import SimpleUtil as SU from modules.base.models import AuraDatabaseModel, Schedule, Playlist from modules.base.exceptions import NoActiveScheduleException, LoadSourceException +from modules.core.engine import Engine from modules.core.channels import ChannelType, TransitionType, EntryPlayState -from modules.scheduling.types import EntryQueueState +from modules.core.resources import ResourceClass, ResourceUtil from modules.scheduling.calendar import AuraCalendarService from modules.scheduling.fallback_manager import FallbackManager @@ -54,6 +55,17 @@ def alchemyencoder(obj): else: return str(obj) + +class EntryQueueState(Enum): + """ + Types of playlist entry behaviours. + """ + OKAY = "ok" + CUT = "cut" + OUT_OF_SCHEDULE = "oos" + + + class AuraScheduler(threading.Thread): """ Aura Scheduler Class @@ -65,7 +77,7 @@ class AuraScheduler(threading.Thread): config (AuraConfig): Holds the Engine Configuration logger: The logger exit_event(threading.Event): Used to exit the thread if requested - soundsystem: Virtual mixer + engine: Virtual mixer last_successful_fetch (datetime): Stores the last time a fetch from Steering/Tank was successful programme: The current radio programme to be played as defined in the local engine database @@ -78,7 +90,7 @@ class AuraScheduler(threading.Thread): config = None logger = None exit_event = None - soundsystem = None + engine = None last_successful_fetch = None programme = None message_timer = [] @@ -89,23 +101,23 @@ class AuraScheduler(threading.Thread): - def __init__(self, config, soundsystem, func_on_init): + def __init__(self, config, engine, func_on_init): """ Constructor Args: config (AuraConfig): Reads the engine configuration - soundsystem (SoundSystem): The soundsystem to play the schedule on + engine (Engine): The engine to play the schedule on func_on_init (Function): The function to be called when the scheduler is initialized """ self.config = config self.logger = logging.getLogger("AuraEngine") self.init_database() - self.fallback_manager = FallbackManager(config, self.logger, self) + self.fallback_manager = FallbackManager(config, self.logger, self, None) self.redismessenger = RedisMessenger(config) - self.soundsystem = soundsystem - self.soundsystem.scheduler = self + self.engine = engine + self.engine.scheduler = self self.is_soundsytem_init = False # Scheduler Initialization @@ -142,7 +154,7 @@ class AuraScheduler(threading.Thread): try: self.config.load_config() seconds_to_wait = int(self.config.get("fetching_frequency")) - self.logger.info(SimpleUtil.cyan("== start fetching new schedules ==")) + self.logger.info(SU.cyan("== start fetching new schedules ==")) next_time = str(datetime.now()) self.logger.info("Fetching new schedules every %ss. Next fetching at %ss." % (str(seconds_to_wait), next_time)) self.fetch_new_programme() @@ -153,12 +165,12 @@ class AuraScheduler(threading.Thread): if self.func_on_initialized: self.func_on_initialized() - # The soundsystem is ready + # The engine is ready if self.is_soundsytem_init: self.queue_programme() except Exception as e: - self.logger.critical(SimpleUtil.red("Unhandled error while fetching & scheduling new programme! (%s)" % str(e)), e) + self.logger.critical(SU.red("Unhandled error while fetching & scheduling new programme! (%s)" % str(e)), e) self.clean_timer_queue() self.print_timer_queue() @@ -179,7 +191,7 @@ class AuraScheduler(threading.Thread): def on_ready(self): """ - Called when the soundsystem is ready. + Called when the engine is ready. """ # self.queue_programme() self.logger.info(self.get_ascii_programme()) @@ -203,25 +215,33 @@ class AuraScheduler(threading.Thread): """ sleep_offset = 10 active_schedule = self.get_active_schedule() + + # Schedule any available fallback playlist + if active_schedule: + self.fallback_manager.schedule_fallback_playlist(active_schedule, True) + # Queue the fade-out of the schedule + if not active_schedule.fadeouttimer: + self.queue_end_of_schedule(active_schedule, True) + active_entry = self.get_active_entry() if not active_entry: raise NoActiveScheduleException # In case of a file-system source, we need to fast-foward to the current marker as per schedule - if active_entry.get_type() == ChannelType.FILESYSTEM: + if active_entry.get_content_type() in ResourceClass.FILE.types: # Calculate the seconds we have to fast-forward - now_unix = self.get_virtual_now() + now_unix = Engine.engine_time() seconds_to_seek = now_unix - active_entry.start_unix # If the seek exceeds the length of the current track, # there's no need to do anything - the scheduler takes care of the rest if (seconds_to_seek + sleep_offset) > active_entry.duration: self.logger.info("The FFWD [>>] range exceeds the length of the entry. Drink some tea and wait for the sound of the next entry.") - else: + else: # Pre-roll and play active entry - self.soundsystem.preroll(active_entry) - self.soundsystem.play(active_entry, TransitionType.FADE) + self.engine.player.preroll(active_entry) + self.engine.player.play(active_entry, TransitionType.FADE) # Check if this is the last item of the schedule # if active_entry.end_unix > active_entry.playlist.schedule.end_unix: @@ -230,30 +250,28 @@ class AuraScheduler(threading.Thread): # Fast-forward to the scheduled position if seconds_to_seek > 0: # Without plenty of timeout (10s) the seek doesn't work - seconds_to_seek += sleep_offset - time.sleep(sleep_offset) - self.logger.info("Going to fast-forward %s seconds" % seconds_to_seek) - self.soundsystem.enable_transaction() - response = self.soundsystem.playlist_seek(active_entry.channel, seconds_to_seek) - self.soundsystem.disable_transaction() - self.logger.info("Sound-system seek response: " + response) - - elif active_entry.get_type() == ChannelType.HTTP \ - or active_entry.get_type() == ChannelType.HTTPS \ - or active_entry.get_type() == ChannelType.LIVE: + def async_cue_seek(seconds_to_seek): + seconds_to_seek += sleep_offset + time.sleep(sleep_offset) + self.logger.info("Going to fast-forward %s seconds" % seconds_to_seek) + response = self.engine.player.queue_seek(active_entry.channel, seconds_to_seek) + self.logger.info("Sound-system seek response: " + response) + + thread = threading.Thread(target = async_cue_seek, args = (seconds_to_seek,)) + thread.start() + + elif active_entry.get_content_type() in ResourceClass.STREAM.types \ + or active_entry.get_content_type() in ResourceClass.LIVE.types: # Pre-roll and play active entry - self.soundsystem.preroll(active_entry) - self.soundsystem.play(active_entry, TransitionType.FADE) + self.engine.player.preroll(active_entry) + self.engine.player.play(active_entry, TransitionType.FADE) # self.queue_end_of_schedule(active_schedule, True) else: self.logger.critical("Unknown Entry Type: %s" % active_entry) - # Queue the fade-out of the schedule - if active_schedule and not active_schedule.fadeouttimer: - self.queue_end_of_schedule(active_schedule, True) @@ -264,7 +282,7 @@ class AuraScheduler(threading.Thread): Returns: (PlaylistEntry): The track which is (or should) currently being played """ - now_unix = self.get_virtual_now() + now_unix = Engine.engine_time() # Load programme if necessary if not self.programme: @@ -273,14 +291,14 @@ class AuraScheduler(threading.Thread): # Check for current schedule current_schedule = self.get_active_schedule() if not current_schedule: - self.logger.warning(SimpleUtil.red("There's no active schedule")) + self.logger.warning(SU.red("There's no active schedule")) return None # Check for scheduled playlist - current_playlist = self.fallback_manager.resolve_playlist(current_schedule) + current_playlist = current_schedule.get_playlist() if not current_playlist: - msg = "There's no active playlist for a current schedule. Most likely the playlist was never available or finished before the end of the schedule." - self.logger.warning(SimpleUtil.red(msg)) + msg = "There's no playlist assigned to the current schedule. Most likely a fallback will make things okay again." + self.logger.warning(SU.red(msg)) return None # Iterate over playlist entries and store the current one @@ -292,8 +310,8 @@ class AuraScheduler(threading.Thread): if not current_entry: # Nothing playing ... fallback will kick-in - msg = "There's no entry scheduled for playlist '%s' at %s" % (str(current_playlist), SimpleUtil.fmt_time(now_unix)) - self.logger.warning(SimpleUtil.red(msg)) + msg = "There's no entry scheduled for playlist '%s' at %s" % (str(current_playlist), SU.fmt_time(now_unix)) + self.logger.warning(SU.red(msg)) return None return current_entry @@ -308,7 +326,7 @@ class AuraScheduler(threading.Thread): (Schedule): The current schedule """ current_schedule = None - now_unix = self.get_virtual_now() + now_unix = Engine.engine_time() # Iterate over all schedules and find the one to be played right now if self.programme: @@ -330,7 +348,7 @@ class AuraScheduler(threading.Thread): Returns: ([Schedule]): The next schedules """ - now_unix = self.get_virtual_now() + now_unix = Engine.engine_time() next_schedules = [] for schedule in self.programme: @@ -351,8 +369,9 @@ class AuraScheduler(threading.Thread): (Playlist): The resolved playlist """ schedule = self.get_active_schedule() - playlist = self.fallback_manager.resolve_playlist(schedule) - return playlist + if schedule: + return schedule.get_playlist() + return None # FIXME Review relevance. @@ -390,7 +409,7 @@ class AuraScheduler(threading.Thread): message_queue = "" messages = sorted(self.message_timer, key=attrgetter('diff')) if not messages: - self.logger.warning(SimpleUtil.red("There's nothing in the Timer Queue!")) + self.logger.warning(SU.red("There's nothing in the Timer Queue!")) else: for msg in messages: message_queue += str(msg)+"\n" @@ -468,26 +487,26 @@ class AuraScheduler(threading.Thread): """ active_schedule = self.get_active_schedule() - s = "\n\n PLAYING NOW:" + s = "\n\n SCHEDULED NOW:" s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────" if active_schedule: planned_playlist = None if active_schedule.playlist: - planned_playlist = active_schedule.playlist[0] # FIXME Improve model without list - resolved_playlist = self.fallback_manager.resolve_playlist(active_schedule) + planned_playlist = active_schedule.playlist[0] + + (fallback_type, resolved_playlist) = self.fallback_manager.resolve_playlist(active_schedule) - s += "\n│ Playing schedule %s " % active_schedule + s += "\n│ Playing timeslot %s " % active_schedule if planned_playlist: if resolved_playlist and resolved_playlist.playlist_id != planned_playlist.playlist_id: s += "\n│ └── Playlist %s " % planned_playlist s += "\n│ " - s += SimpleUtil.red("↑↑↑ That's the originally planned playlist.") + ("Instead playing the fallback playlist below ↓↓↓") + s += SU.red("↑↑↑ That's the originally planned playlist.") + ("Instead playing the fallback playlist below ↓↓↓") if resolved_playlist: if not planned_playlist: - fallback_type = str(EngineUtil.get_playlist_type(resolved_playlist.fallback_type)) s += "\n│ " - s += SimpleUtil.red("No Playlist assigned to schedule. Instead playing the `%s` playlist below ↓↓↓" % SimpleUtil.cyan(fallback_type)) + s += SU.red("No playlist assigned to timeslot. Instead playing the `%s` playlist below ↓↓↓" % SU.cyan(str(fallback_type))) s += "\n│ └── Playlist %s " % resolved_playlist @@ -503,7 +522,7 @@ class AuraScheduler(threading.Thread): # Entry currently being played if active_entry: s += "\n│ └── Entry %s | %s " % \ - (str(entry.entry_num+1), SimpleUtil.green("PLAYING > "+str(active_entry))) + (str(active_entry.entry_num+1), SU.green("PLAYING > "+str(active_entry))) # Open entries for current playlist rest_of_playlist = active_entry.get_next_entries(False) @@ -511,27 +530,27 @@ class AuraScheduler(threading.Thread): s += self.build_playlist_string(entries) else: - s += "\n│ └── %s" % (SimpleUtil.red("No active playlist. There should be at least some fallback playlist running...")) + s += "\n│ └── %s" % (SU.red("No active playlist. There should be at least some fallback playlist running...")) else: s += "\n│ Nothing. " s += "\n└──────────────────────────────────────────────────────────────────────────────────────────────────────" - s += "\n PLAYING NEXT:" + s += "\n SCHEDULED NEXT:" s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────" next_schedules = self.get_next_schedules() if not next_schedules: s += "\n│ Nothing. " else: - for schedule in next_schedules: - resolved_playlist = self.fallback_manager.resolve_playlist(schedule) + for schedule in next_schedules: + (fallback_type, resolved_playlist) = self.fallback_manager.resolve_playlist(schedule) if resolved_playlist: - fallback_type = str(EngineUtil.get_playlist_type(resolved_playlist.fallback_type)) - s += "\n│ Queued schedule %s " % schedule - s += "\n│ └── Playlist %s (Type: %s)" % (resolved_playlist, SimpleUtil.cyan(fallback_type)) + + s += "\n│ Queued timeslot %s " % schedule + s += "\n│ └── Playlist %s (Type: %s)" % (resolved_playlist, SU.cyan(str(fallback_type))) if resolved_playlist.end_unix > schedule.end_unix: s += "\n│ %s! " % \ - (SimpleUtil.red("↑↑↑ Playlist #%s ends after Schedule #%s!" % (resolved_playlist.playlist_id, schedule.schedule_id))) + (SU.red("↑↑↑ Playlist #%s ends after timeslot #%s!" % (resolved_playlist.playlist_id, schedule.schedule_id))) entries = self.preprocess_entries(resolved_playlist.entries, False) s += self.build_playlist_string(entries) @@ -551,7 +570,7 @@ class AuraScheduler(threading.Thread): for entry in entries: if entry.queue_state == EntryQueueState.OUT_OF_SCHEDULE and not is_out_of_schedule: s += "\n│ %s" % \ - SimpleUtil.red("↓↓↓ These entries won't be played because they are out of schedule.") + SU.red("↓↓↓ These entries won't be played because they are out of schedule.") is_out_of_schedule = True s += self.build_entry_string("\n│ └── ", entry, is_out_of_schedule) @@ -566,10 +585,10 @@ class AuraScheduler(threading.Thread): """ s = "" if entry.queue_state == EntryQueueState.CUT: - s = "\n│ %s" % SimpleUtil.red("↓↓↓ This entry is going to be cut.") + s = "\n│ %s" % SU.red("↓↓↓ This entry is going to be cut.") if strike: - entry_str = SimpleUtil.strike(entry) + entry_str = SU.strike(entry) else: entry_str = str(entry) @@ -583,17 +602,17 @@ class AuraScheduler(threading.Thread): # - def get_virtual_now(self): - """ - Liquidsoap is slow in executing commands, therefore it's needed to schedule - actions by (n) seconds in advance, as defined in the configuration file by - the property `lqs_delay_offset`. + # def engine_time(self): + # """ + # Liquidsoap is slow in executing commands, therefore it's needed to schedule + # actions by (n) seconds in advance, as defined in the configuration file by + # the property `lqs_delay_offset`. - Returns: - (Integer): the Unix epoch timestamp including the offset - """ - time_offset = int(self.config.lqs_delay_offset) - return SimpleUtil.timestamp() + time_offset + # Returns: + # (Integer): the Unix epoch timestamp including the offset + # """ + # time_offset = int(self.config.lqs_delay_offset) + # return SU.timestamp() + time_offset @@ -603,7 +622,7 @@ class AuraScheduler(threading.Thread): is defined by the config option `scheduling_window_end`. This value defines the seconds minus the actual start time of the schedule. """ - now_unix = self.get_virtual_now() + now_unix = Engine.engine_time() len_before = len(schedules) window_start = self.config.get("scheduling_window_start") window_end = self.config.get("scheduling_window_end") @@ -619,7 +638,7 @@ class AuraScheduler(threading.Thread): """ Checks if the schedule is within the scheduling window. """ - now_unix = self.get_virtual_now() + now_unix = Engine.engine_time() window_start = self.config.get("scheduling_window_start") window_end = self.config.get("scheduling_window_end") @@ -644,14 +663,17 @@ class AuraScheduler(threading.Thread): # Queue the schedules, their playlists and entries if schedules: for next_schedule in schedules: - playlist = self.fallback_manager.resolve_playlist(next_schedule) - self.queue_playlist_entries(next_schedule, playlist.entries, False, True) - + # Schedule any available fallback playlist + self.fallback_manager.schedule_fallback_playlist(next_schedule, False) + + if next_schedule.playlist: + self.queue_playlist_entries(next_schedule, next_schedule.get_playlist().entries, False, True) + # Queue the fade-out of the schedule if not next_schedule.fadeouttimer: self.queue_end_of_schedule(next_schedule, True) - self.logger.info(SimpleUtil.green("Finished queuing programme.")) + self.logger.info(SU.green("Finished queuing programme.")) @@ -664,7 +686,7 @@ class AuraScheduler(threading.Thread): # Queue the (rest of the) currently playing schedule upon startup if current_schedule: - current_playlist = self.fallback_manager.resolve_playlist(current_schedule) + current_playlist = current_schedule.get_playlist() if current_playlist: active_entry = self.get_active_entry() @@ -682,7 +704,9 @@ class AuraScheduler(threading.Thread): self.queue_playlist_entries(current_schedule, rest_of_playlist, False, True) # Store them for later reference - current_schedule.queued_entries = rest_of_playlist + current_schedule.queued_entries = [active_entry] + if rest_of_playlist: + current_schedule.queued_entries.append(rest_of_playlist) @@ -711,8 +735,8 @@ class AuraScheduler(threading.Thread): for entry in clean_entries: if previous_entry == None or \ (previous_entry != None and \ - previous_entry.get_type() == entry.get_type() and \ - entry.get_type() == ChannelType.FILESYSTEM): + previous_entry.get_content_type() == entry.get_content_type() and \ + entry.get_content_type() in ResourceClass.FILE.types): entry_groups[index].append(entry) else: @@ -736,7 +760,7 @@ class AuraScheduler(threading.Thread): schedule.queued_entries = clean_entries else: - self.logger.warn(SimpleUtil.red("Nothing to schedule ...")) + self.logger.warn(SU.red("Nothing to schedule ...")) @@ -749,22 +773,22 @@ class AuraScheduler(threading.Thread): entries ([]): List of multiple filesystem entries, or a single entry of other types """ play_timer = self.is_something_planned_at_time(entries[0].start_unix) - now_unix = self.get_virtual_now() + now_unix = Engine.engine_time() diff = entries[0].start_unix - now_unix # Play function to be called by timer def do_play(entries): - self.logger.info(SimpleUtil.cyan("=== play('%s') ===" % EngineUtil.get_entries_string(entries))) + self.logger.info(SU.cyan("=== play('%s') ===" % ResourceUtil.get_entries_string(entries))) transition_type = TransitionType.INSTANT if fade_in: transition_type = TransitionType.FADE if entries[-1].status != EntryPlayState.READY: - self.logger.critical(SimpleUtil.red("PLAY: For some reason the entry/entries are not yet ready to be played (Entries: %s)" % EngineUtil.get_entries_string(entries))) + self.logger.critical(SU.red("PLAY: For some reason the entry/entries are not yet ready to be played (Entries: %s)" % ResourceUtil.get_entries_string(entries))) # At this point it's too late to do any pro-active fallback handling. Is it? Wait for the silence detector to deal with it. # TODO Observe the actual handling of this section and think about possible improvements. - self.soundsystem.play(entries[0], transition_type) + self.engine.player.play(entries[0], transition_type) self.logger.info(self.get_ascii_programme()) @@ -775,7 +799,7 @@ class AuraScheduler(threading.Thread): self.stop_timer(play_timer) else: # If the playlist entries do not differ => reuse the old timer and do nothing - self.logger.debug("Playlist Entry %s is already scheduled - no new timer created." % EngineUtil.get_entries_string(entries)) + self.logger.debug("Playlist Entry %s is already scheduled - no new timer created." % ResourceUtil.get_entries_string(entries)) return # If nothing is planned at given time, create a new timer @@ -827,7 +851,7 @@ class AuraScheduler(threading.Thread): if entry.entry_start >= entry.playlist.schedule.schedule_end: msg = "Filtered entry (%s) after end-of schedule (%s) ... SKIPPED" % (entry, entry.playlist.schedule) - self.logger.warn(SimpleUtil.red(msg)) + self.logger.warning(SU.red(msg)) entry.queue_state = EntryQueueState.OUT_OF_SCHEDULE elif entry.end_unix > entry.playlist.schedule.end_unix: entry.queue_state = EntryQueueState.CUT @@ -843,7 +867,7 @@ class AuraScheduler(threading.Thread): def queue_end_of_schedule(self, schedule, fade_out): """ - Queues a soundsystem action to stop/fade-out the given schedule. + Queues a engine action to stop/fade-out the given schedule. Args: schedule (PlaylistEntry): The schedule @@ -851,20 +875,21 @@ class AuraScheduler(threading.Thread): """ schedule_end = schedule.schedule_end schedule_end_unix = schedule.end_unix - now_unix = self.get_virtual_now() + now_unix = Engine.engine_time() fade_out_time = 0 # Stop function to be called when schedule ends def do_stop(schedule): - last_entry = schedule.queued_entries[-1] # FIXME sometimes an issue with startup queues - self.logger.info(SimpleUtil.cyan("=== stop('%s') ===" % str(last_entry.playlist.schedule))) - transition_type = TransitionType.INSTANT - if fade_out: - transition_type = TransitionType.FADE - self.soundsystem.stop(last_entry, transition_type) + if schedule.has_queued_entries(): + last_entry = schedule.queued_entries[-1] + self.logger.info(SU.cyan("=== stop('%s') ===" % str(last_entry.playlist.schedule))) + transition_type = TransitionType.INSTANT + if fade_out: + transition_type = TransitionType.FADE + self.engine.player.stop(last_entry, transition_type) if fade_out == True: - fade_out_time = int(round(float(self.config.get("fade_out_time")))) #FIXME Use float + fade_out_time = int(round(float(self.config.get("fade_out_time")))) #TODO Use float # Stop any existing fade-out timer if schedule.fadeouttimer: @@ -899,26 +924,26 @@ class AuraScheduler(threading.Thread): self.last_successful_fetch = None if response is None: - msg = SimpleUtil.red("Trying to load programme from Engine Database, because AuraCalendarService returned an empty response.") + msg = SU.red("Trying to load programme from Engine Database, because AuraCalendarService returned an empty response.") self.logger.warning(msg) elif type(response) is list: self.programme = response if self.programme is not None and len(self.programme) > 0: self.last_successful_fetch = datetime.now() - self.logger.info(SimpleUtil.green("Finished fetching current programme from API")) + self.logger.info(SU.green("Finished fetching current programme from API")) if len(self.programme) == 0: self.logger.critical("Programme fetched from Steering/Tank has no entries!") elif response.startswith("fetching_aborted"): - msg = SimpleUtil.red("Trying to load programme from database only, because fetching was being aborted from AuraCalendarService! Reason: ") + msg = SU.red("Trying to load programme from database only, because fetching was being aborted from AuraCalendarService! Reason: ") self.logger.warning(msg + response[16:]) else: - msg = SimpleUtil.red("Trying to load programme from database only, because of an unknown response from AuraCalendarService: " + response) + msg = SU.red("Trying to load programme from database only, because of an unknown response from AuraCalendarService: " + response) self.logger.warning(msg) # Always load latest programme from the database self.last_successful_fetch = lsf self.load_programme_from_db() - self.logger.info(SimpleUtil.green("Finished loading current programme from database (%s schedules)" % str(len(self.programme)))) + self.logger.info(SU.green("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))) @@ -934,7 +959,7 @@ class AuraScheduler(threading.Thread): self.programme = Schedule.select_programme() if not self.programme: - self.logger.critical(SimpleUtil.red("Could not load programme from database. We are in big trouble my friend!")) + self.logger.critical(SU.red("Could not load programme from database. We are in big trouble my friend!")) return @@ -977,23 +1002,23 @@ class AuraScheduler(threading.Thread): # Pre-roll function to be called by timer def do_preroll(entries): try: - if entries[0].get_type() == ChannelType.FILESYSTEM: - self.logger.info(SimpleUtil.cyan("=== preroll_group('%s') ===" % EngineUtil.get_entries_string(entries))) - self.soundsystem.preroll_group(entries) + if entries[0].get_content_type() in ResourceClass.FILE.types: + self.logger.info(SU.cyan("=== preroll_group('%s') ===" % ResourceUtil.get_entries_string(entries))) + self.engine.player.preroll_group(entries, ChannelType.QUEUE) else: - self.logger.info(SimpleUtil.cyan("=== preroll('%s') ===" % EngineUtil.get_entries_string(entries))) - self.soundsystem.preroll(entries[0]) + self.logger.info(SU.cyan("=== preroll('%s') ===" % ResourceUtil.get_entries_string(entries))) + self.engine.player.preroll(entries[0]) except LoadSourceException as e: - self.logger.critical(SimpleUtil.red("Could not pre-roll entries %s" % EngineUtil.get_entries_string(entries)), e) + self.logger.critical(SU.red("Could not pre-roll entries %s" % ResourceUtil.get_entries_string(entries)), e) - # Pro-active fallback handling, avoiding the need of the silence detector kicking-in. - self.fallback_manager.handle_proactive_fallback(self, entries[0].playlist) + # # Pro-active fallback handling, avoiding the need of the silence detector kicking-in. + # self.fallback_manager.handle_proactive_fallback(self, entries[0].playlist) if entries[-1].status != EntryPlayState.READY: - self.logger.critical(SimpleUtil.red("Entries didn't reach 'ready' state during pre-rolling (Entries: %s)" % EngineUtil.get_entries_string(entries))) + self.logger.critical(SU.red("Entries didn't reach 'ready' state during pre-rolling (Entries: %s)" % ResourceUtil.get_entries_string(entries))) - # Pro-active fallback handling, avoiding the need of the silence detector kicking-in. - self.fallback_manager.handle_proactive_fallback(self, entries[0].playlist) + # # Pro-active fallback handling, avoiding the need of the silence detector kicking-in. + # self.fallback_manager.handle_proactive_fallback(self, entries[0].playlist) loader_diff = diff - self.config.get("preroll_offset") loader = CallFunctionTimer(diff=loader_diff, func=do_preroll, param=param, fadein=fadein, fadeout=fadeout, switcher=False, loader=True) @@ -1033,7 +1058,7 @@ class AuraScheduler(threading.Thread): # Remove it from message queue self.message_timer.remove(timer) - self.logger.info("Stopped %s timers for: %s" % (str(count), EngineUtil.get_entries_string(timer.entries))) + self.logger.info("Stopped %s timers for: %s" % (str(count), ResourceUtil.get_entries_string(timer.entries))) @@ -1111,7 +1136,7 @@ class CallFunctionTimer(threading.Timer): def __init__(self, diff=None, func=None, param=None, fadein=False, fadeout=False, switcher=False, loader=False): self.logger = logging.getLogger("AuraEngine") - self.logger.debug("Executing soundsystem command '%s' in %s seconds..." % (str(func.__name__), str(diff))) + self.logger.debug("Executing engine command '%s' in %s seconds..." % (str(func.__name__), str(diff))) threading.Timer.__init__(self, diff, func, (param,)) if not fadein and not fadeout and not switcher and not loader \ @@ -1141,12 +1166,12 @@ class CallFunctionTimer(threading.Timer): status += " starting at " + str(self.dt) if self.fadein: - return status + " fading in entries '" + EngineUtil.get_entries_string(self.entries) + return status + " fading in entries '" + ResourceUtil.get_entries_string(self.entries) elif self.fadeout: return status + " fading out schedule '" + str(self.param) elif self.switcher: - return status + " switching to entries '" + EngineUtil.get_entries_string(self.entries) + return status + " switching to entries '" + ResourceUtil.get_entries_string(self.entries) elif self.loader: - return status + " pre-rolling entries '" + EngineUtil.get_entries_string(self.entries) + return status + " pre-rolling entries '" + ResourceUtil.get_entries_string(self.entries) else: return "CORRUPTED CallFunctionTimer around! How can that be?"