Skip to content
Snippets Groups Projects
scheduler.py 32.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • #  Playout Daemon for autoradio project
    
    #  Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.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/>.
    
    __license__ = "GNU General Public License (GPL) Version 3"
    
    __version_info__ = (0, 0, 1)
    __author__ = 'Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>'
    
    Gottfried Gaisbauer's avatar
    Gottfried Gaisbauer committed
    import traceback
    
    from modules.base.simpleutil import SimpleUtil
    
    from modules.communication.redis.messenger import RedisMessenger
    
    from modules.scheduling.calendar import AuraCalendarService
    
    from libraries.database.broadcasts import Schedule, Playlist, AuraDatabaseModel
    
    from libraries.exceptions.exception_logger import ExceptionLogger
    
    from libraries.enum.auraenumerations import ScheduleEntryType, TimerType, TerminalColors
    
    def alchemyencoder(obj):
        """JSON encoder function for SQLAlchemy special classes."""
        if isinstance(obj, datetime.date):
            return obj.isoformat()
        elif isinstance(obj, decimal.Decimal):
            return float(obj)
    
        elif isinstance(obj, sqlalchemy.orm.state.InstanceState):
            return ""
    
            return json.dumps([obj._asdict()], default=alchemyencoder)
    
    # ------------------------------------------------------------------------------------------ #
    
    class AuraScheduler(ExceptionLogger, threading.Thread):
    
        - Retrieves data from Steering and Tank
    
        - Stores and fires events for LiquidSoap
    
        Attributes:
            config (AuraConfig):                    Holds the Engine Configuration
            logger:                                 The logger
            exit_event(threading.Event):            Used to exit the thread if requested                             
            liquidsoapcommunicator:                 Stores the connection to LiquidSoap 
            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
            active_entry(Show, Track):              This is a Tuple consisting of the currently played `Show` and `Track`
    
            message_timer(Array<threading.Timer>):  The message queue of Liquidsoap commands for playlists/tracks to be played
    
        redismessenger = None
    
        config = None
        logger = None
        exit_event = None
    
        liquidsoapcommunicator = None
    
        last_successful_fetch = None
    
    
        active_entry = None
        message_timer = []
    
        #schedule_entries = None
    
    
            Args:
                config (AuraConfig):    Reads the engine configuration
    
            self.logger = logging.getLogger("AuraEngine")
            self.init_error_messages()
    
            self.redismessenger = RedisMessenger(config)
    
            # init threading
            threading.Thread.__init__(self)
    
    
            # init messenger.. FIXME probably not needed anymore
    
    Gottfried Gaisbauer's avatar
    Gottfried Gaisbauer committed
            self.redismessenger.set_channel('scheduler')
            self.redismessenger.set_section('execjob')
    
            #self.redismessenger.send('Scheduler started', '0000', 'success', 'initApp', None, 'appinternal')
    
            # Create exit event
    
            Called when thread is started via `start()`. It does following:
            
                1. `self.fetch_new_programme()` periodically from the API depending on the `fetching_frequency` defined in the engine configuration. 
                2. Loads the latest programme from the database and sets the instance state `self.programme` with current schedules.
                3. Queues all playlists of the programm, other than the playlist currently to be played (This is triggered by Liquidsoap itself).
    
                seconds_to_wait = int(self.config.get("fetching_frequency"))
    
                next_time = datetime.datetime.now() + datetime.timedelta(seconds=seconds_to_wait)
    
                self.logger.info("Fetch new programmes every %ss. Next fetching in %ss." % (str(seconds_to_wait), str(next_time)))
    
                self.queue_programme()
                self.print_message_queue()
    
        def get_active_entry(self):
    
            Retrieves the current `PlaylistEntry` which should be played as per programme. 
            Publically called via `LiquidSoapCommunicator`.
    
            Important note: This method also updates the state variable `active_entry`.
    
                (PlaylistEntry): The track which is (or should) currently being played
    
            now_unix = time.mktime(datetime.datetime.now().timetuple())
            
    
            # Load programme if necessary
            if not self.programme:
                self.logger.info("Next track requested: Need to load programme from database first.")
                self.load_programme_from_db()
    
            # Check for scheduled playlist
            current_schedule, current_playlist = self.get_active_playlist()
            if not current_playlist:
                if not current_schedule:
                    self.logger.critical("There's no active playlist nor schedule. It's probably time to play some fallback...")
                else:
                    self.logger.warning("There's no active playlist for a current schedule. Most likely the playlist finished before the end of the schedule.")
                return None
    
            time_start = SimpleUtil.fmt_time(current_playlist.start_unix)
            time_end = SimpleUtil.fmt_time(current_playlist.start_unix+current_playlist.duration)
            time_now = SimpleUtil.fmt_time(now_unix)
            self.logger.info("Current Playlist (%d:%d) for show '%s' scheduled to be played at %s until %s (Now: %s)" % (current_playlist.playlist_id, current_playlist.artificial_id, current_playlist.show_name, time_start, time_end, time_now))
    
            # Iterate over playlist entries and store the current one
            time_marker = current_playlist.start_unix
            current_entry = None
            for entry in current_playlist.entries:
                self.logger.info(entry)
                if entry.start_unix < now_unix < entry.start_unix + entry.duration:
                    current_entry = entry
                    break       
                time_marker += entry.duration         
    
            if current_entry:
                time_start = SimpleUtil.fmt_time(current_entry.start_unix)
                time_end = SimpleUtil.fmt_time(current_entry.start_unix+current_entry.duration)
                time_now = SimpleUtil.fmt_time(now_unix)
                self.logger.info("Track '%s' is expected playing from %s to %s (Now: %s)" % (current_entry.filename, time_start, time_end, time_now))
                if not self.active_entry:
                    self.logger.warn("Activate track '%s' and [>> FFWD] to current point in time" % (current_entry.filename))
                elif self.active_entry.filename != current_entry.filename:
                    self.logger.critical("--- SOMETHING UNEXPECTED IS PLAYING: %s --vs-- %s" % (self.active_entry.filename, current_entry.filename))
                self.active_entry = current_entry
                return (current_entry)
            else:
                # Nothing playing ... fallback will kick-in
                self.logger.warning("There's no entry scheduled for playlist '%s'. Is currently -nothing- or a fallback playing?" % str(current_playlist))
                return None
    
        # FIXME Review relevance.
    
        def get_act_programme_as_string(self):
            """
            Fetches the latest programme and returns it as `String`.
            Also used by `ServerRedisAdapter`.
    
            Return:
                (String):       Programme
    
            Raises:
                (Exception):    In case the programme cannot be converted to String
            """
            programme_as_string = ""
    
            if self.programme is None or len(self.programme) == 0:
    
    David Trattnig's avatar
    David Trattnig committed
                self.fetch_new_programme()
    
    
            try:
                programme_as_string = json.dumps([p._asdict() for p in self.programme], default=alchemyencoder)
                # FIXME Change to more specific exception
            except Exception as e:
                self.logger.error("Cannot transform programme into JSON String. Reason: " + str(e))
                traceback.print_exc()
    
            return programme_as_string
    
    
    
        def print_message_queue(self):
            """
    
            Prints the current message queue i.e. playlists in the queue to be played.
    
            """
            message_queue = ""
            messages = sorted(self.message_timer, key=attrgetter('diff'))
            if not messages:
                self.logger.warning("There's nothing in the Message Queue!")
            else:
                for msg in messages:
                    message_queue += str(msg)+"\n"
    
    
                self.logger.info("Message Queue: \n" + message_queue) 
    
    
        # ------------------------------------------------------------------------------------------ #
    
        def set_next_file_for(self, playlistname):
            self.logger.critical("HAVE TO <SET> NEXT FILE FOR: " + playlistname)
            self.logger.critical(str(self.get_active_entry()))
    
            if playlistname == "station":
                file = "/home/david/Code/aura/engine2/testing/content/ernie_mayne_sugar.mp3"
            elif playlistname == "timeslot":
                file = "/home/david/Code/aura/engine2/testing/content/ernie_mayne_sugar.mp3"
            elif playlistname == "show":
                file = "/home/david/Code/aura/engine2/testing/content/ernie_mayne_sugar.mp3"
            else:
                file = ""
                self.logger.critical("Should set next fallback file for " + playlistname + ", but this playlist is unknown!")
    
            self.logger.info("Set next fallback file for " + playlistname + ": " + file)
            self.redismessenger.set_next_file_for(playlistname, file)
            return file
    
    
        def get_next_file_for(self, fallbackname):
            """
    
            Evaluates the next **fallback files/folders** to be played for a given fallback-type.
    
            Valid fallback-types are:
    
                * timeslot
                * show
                * station
    
            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!")
    
            if file:
                self.logger.info("Got next file '%s' (type: %s)" % (file, fallbackname))
    
    
            #set_next_file_thread = SetNextFile(fallbackname, show)
            #set_next_file_thread.start()
    
            #self.redismessenger.set_next_file_for(playlistname, file)
    
    
        def get_active_playlist(self):
            """
            Retrieves the schedule and playlist currently to be played as per 
            schedule. If the current point in time has no playlist assigned,
            only the matching schedule is returned.
    
            Returns:
                (Schedule, Playlist): The current schedule and playlist tuple.
            """
            now_unix = time.mktime(datetime.datetime.now().timetuple())
            current_schedule = None
            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
    
            return (current_schedule, current_playlist)
    
    
    
        def get_next_playlists(self):
            """
            Retrieves the playlists to be played after the current one.
    
            Returns:
                ([Playlist]): The next playlists
            """
            now_unix = time.mktime(datetime.datetime.now().timetuple())
            next_playlists = []
    
            for schedule in self.programme:
                if schedule.end_unix > now_unix:
                    for playlist in schedule.playlist:
                        if playlist.start_unix > now_unix:
                            next_playlists.append(playlist)
    
            return next_playlists
    
    
    
        def get_next_entry(self):
            """
            Retrieves the playlist entry to be played next.
    
            Returns:
                (Playlist): The next playlist track
            """
            next_entry = None
            current_schedule, current_playlist = self.get_active_playlist()
    
            if not self.active_entry:
                self.logger.warn("For some reason there is no active playlist entry set... Fetching it now!")
                self.get_active_entry()
            if not self.active_entry:
                self.logger.warn("Looks like nothing is currently scheduled...")
                return None
    
            # Check if there is a next entry in the current playlist
            for i, entry in enumerate(self.active_entry.playlist.entries):
                if entry is self.active_entry:
                    if i+1 < len(self.active_entry.playlist.entries):
                        next_entry = self.active_entry.playlist.entries[i+1]
                        break
            
            # It might be in the next playlist...
            if not next_entry:
                next_playlist = None
                found_current = False
                # Iterate over all schedule and playlists and find the one to be played next
                for schedule in self.programme:
                    for playlist in schedule.playlist:
                        if playlist is current_playlist:
                            found_current = True
                        elif found_current:
                            next_playlist = playlist
                            break
    
                if next_playlist:
                    next_entry = next_playlist.entries[0]
            
            if not next_entry:
                self.logger.fatal("There is no next playlist-entry in the programme!")
            
            return next_entry
    
    
    
        def queue_programme(self):
            """
            Queues the current programme (playlists as per schedule) by creating
            timed commands to Liquidsoap to enable the individuals tracks of playlists. 
            """
            active_schedule, active_playlist = self.get_active_playlist()
            playlists = self.get_next_playlists()
    
            s = "\n\n   PLAYING NOW:"
            s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
            if active_schedule:
                s += "\n│   Playing schedule %s         " % active_schedule
                if active_playlist:
                    s += "\n│       └── Playlist %s         " % active_playlist
                else:
                    s += "\n│       └── %s No Playlist active. Did it finish before the end of the schedule? %s" % (TerminalColors.ORANGE.value, TerminalColors.ENDC.value)
            else:
                s += "\n│   Nothing.           "
            s += "\n└──────────────────────────────────────────────────────────────────────────────────────────────────────"
    
            s += "\n   PLAYING NEXT:"
            s += "\n┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
    
            if not playlists:
                s += "\n│   Nothing.         "
            else:
                for next_playlist in playlists:
                    s += "\n│   Queued schedule %s         " % next_playlist.schedule
                    s += "\n│      └── Playlist %s         " % next_playlist
                    if next_playlist.end_unix > next_playlist.schedule.end_unix:
                        s += "\n│         %s ↑↑↑ Playlist #%s ends after Schedule #%s!%s              " % (TerminalColors.RED.value, next_playlist.playlist_id, next_playlist.schedule.schedule_id, TerminalColors.ENDC.value)
                    self.schedule_playlist(next_playlist)
            
            s += "\n└──────────────────────────────────────────────────────────────────────────────────────────────────────\n\n"
            self.logger.info(s)
    
    
    
        def schedule_playlist(self, playlist):
            """
            Creates a schedule at the planned time for the LiquidSoap player.
            
            Args:
                playlist(Playlist):     The playlist to be scheduled for playout
            """        
            now_unix = time.mktime(datetime.datetime.now().timetuple())
            diff = playlist.start_unix - now_unix
            def func(playlist):
                self.logger.info("=== Executing scheduled LQS command: activate_playlist(...) ===")
                self.liquidsoapcommunicator.activate_playlist(playlist)
    
            planned_timer = self.is_something_planned_at_time(playlist.start_unix)
            timer = None
    
            if planned_timer:
                # Check if the playlist_id's are different
                if planned_timer.entry.playlist_id != playlist.playlist_id:
                    # If not, stop and remove the old timer, create a new one
                    self.stop_timer(planned_timer)
                    timer = self.create_timer(diff, func, [playlist], switcher=True)
                else:
                    # If the playlists do not differ => reuse the old timer and do nothing
                    self.logger.info("Playlist %s is already scheduled - No new timer created!" % playlist)
            else:
                # If nothing is planned at given time, create a new timer
                timer = self.create_timer(diff, func, [playlist], switcher=True)
    
            if timer:
                playlist.switchtimer = timer
    
    
    
    
        def fetch_new_programme(self):
            """
    
            Fetch the latest programme from `AuraCalendarService` which stores it to the database.
            After that, the programme is in turn loaded from the database and stored in `self.programme`.
    
            # Fetch programme from API endpoints
            self.logger.info("Trying to fetch new programe from API endpoints...")
    
            acs = AuraCalendarService(self.config)
            queue = acs.get_queue()
            acs.start() # start fetching thread
            response = queue.get() # wait for the end
    
            self.logger.info("... Programme fetch via API done!")
    
    
            # Reset last successful fetch state
            lsf = self.last_successful_fetch
            self.last_successful_fetch = None
    
            if response is None:
                self.logger.warning("Trying to load programme from Engine Database, because AuraCalendarService returned an empty response.")
            elif type(response) is list:
                self.programme = response
                if self.programme is not None and len(self.programme) > 0:
                    self.last_successful_fetch = datetime.datetime.now()
    
                    self.logger.info("+++ Successfully fetched 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"):
                self.logger.warning("Trying to load programme from database, because fetching was being aborted from AuraCalendarService! Reason: " + response[16:])
            else:
                self.logger.warning("Trying to load programme from database, because i got an unknown response from AuraCalendarService: " + response)
    
    
            # 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")
            for schedule in self.programme:
                self.logger.debug("\tSchedule %s with Playlist %s" % (str(schedule), str(schedule.playlist[0])))
    
    
    
    
    
        def load_programme_from_db(self):
            """
            Loads the programme from Engine's database and enables
            them via `self.enable_entries(..)`. After that, the
            current message queue is printed to the console.
            """
    
            self.programme = Schedule.select_act_programme()
    
    
            if not self.programme:
    
                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])
    
            """
            Iterates over all playlist entries and assigs their start time.
            Additionally timers for fadings are created.
    
            Args:
                playlist(Playlist):     The playlist to be scheduled for playout
            """
    
    Gottfried Gaisbauer's avatar
    Gottfried Gaisbauer committed
            now_unix = time.mktime(datetime.datetime.now().timetuple())
    
            time_marker = playlist.start_unix
    
            # Old entry for fading out
            # FIXME retrieve active entry from previous playlist
    
            for entry in playlist.entries:
    
                diff=3
                entry.start_unix = time_marker
                self.enable_timer(diff, entry, old_entry)
                old_entry = entry
                # time_marker += 1 # FIXME ???
    
                # # Since we also get entries from the past, filter these out
                # if time_marker > now_unix:
    
                #     # when do we have to start?
                #     diff = time_marker - now_unix
                #     diff = 3 # FIXME test
                #     entry.start_unix = time_marker
    
                #     # enable the three timer
                #     self.enable_timer(diff, entry, old_entry)
                #     old_entry = entry
    
        # ------------------------------------------------------------------------------------------ #
    
        def enable_timer(self, diff, entry, old_entry):
    
            """
            Create threads to send track-activation messages to LiquidSoap.
            Those tracks can be delayed by `diff` seconds.
    
            Args:
                diff (Integer): seconds after tracks should be activated
            """
    
    
            self.logger.critical("ENABLING SWITCHTIMER FOR " + str(entry))
            entry.switchtimer = self.add_or_update_timer(diff, self.liquidsoapcommunicator.activate, [entry])
    
    
            # FIXME Fade In/Out logic: Not sure if that's functional
            #self.enable_fading(diff, entry, old_entry)
    
    
        # ------------------------------------------------------------------------------------------ #
    
        def enable_fading(self, diff, new_entry, old_entry):
            # fading times
            fade_out_time = float(self.config.get("fade_out_time"))
    
    
            # enable fading when entry types are different
            if old_entry is not None:
                if old_entry.type != new_entry.type:
    
                    #self.add_or_update_timer(diff, self.liquidsoapcommunicator.fade_out, [old_entry])
                    old_entry.fadeouttimer = self.create_timer(diff-fade_out_time, self.liquidsoapcommunicator.fade_out, [old_entry], fadeout=True)
                    self.logger.critical("ENABLING FADEOUTTIMER FOR " + str(old_entry))
    
            # same for fadein except old_entry can be None
    
                #self.add_or_update_timer(diff, self.liquidsoapcommunicator.fade_in, [new_entry])
                new_entry.fadeintimer = self.create_timer(diff, self.liquidsoapcommunicator.fade_in, [new_entry], fadein=True)
                self.logger.critical("ENABLING FADEINTIMER FOR " + str(new_entry))
    
        # ------------------------------------------------------------------------------------------ #
    
        def add_or_update_timer(self, diff, func, parameters):
    
            planned_timer = self.is_something_planned_at_time(entry.start_unix)
    
            # if something is planned on entry.entry_start
    
            if planned_timer:
                planned_entry = planned_timer.entry
    
                # check if the playlist_id's are different
    
                if planned_entry.playlist.playlist_id != entry.playlist.playlist_id:
    
                    # if not stop the old timer and remove it from the list
                    self.stop_timer(planned_timer)
    
                    # and create a new one
    
                    timer = self.create_timer(diff, func, parameters, switcher=True)
    
                # if the playlist id's do not differ => reuse the old timer and do nothing, they are the same
    
            # if nothing is planned at given time, create a new timer
    
                timer = self.create_timer(diff, func, parameters, switcher=True)
    
            if timer is None:
                return planned_timer
            return timer
    
    
        # ------------------------------------------------------------------------------------------ #
        def stop_timer(self, timer):
            # stop timer
            timer.cancel()
    
            if timer.entry.fadeintimer is not None:
                timer.entry.fadeintimer.cancel()
                self.message_timer.remove(timer.entry.fadeintimer)
            if timer.entry.fadeouttimer is not None:
                timer.entry.fadeouttimer.cancel()
                self.message_timer.remove(timer.entry.fadeouttimer)
    
    
            self.logger.critical("REMOVED TIMER for " + str(timer.entry))
    
    
        # ------------------------------------------------------------------------------------------ #
    
        def create_timer(self, diff, func, parameters, fadein=False, fadeout=False, switcher=False):
            if not fadein and not fadeout and not switcher or fadein and fadeout or fadein and switcher or fadeout and switcher:
                raise Exception("You have to call me with either fadein=true, fadeout=true or switcher=True")
    
            t = CallFunctionTimer(diff, func, parameters, fadein, fadeout, switcher)
    
    
        # ------------------------------------------------------------------------------------------ #
    
        def is_something_planned_at_time(self, given_time):
    
                if t.entry.start_unix == given_time:
    
        def init_error_messages(self):
            """
            Load error messages
            """
            error_file = self.config.get("install_dir") + "/errormessages/scheduler_error.js"
            f = open(error_file)
            self.error_data = json.load(f)
            f.close()
    
        def init_database(self):
            """
            Initializes the database.
    
            Raises:
                sqlalchemy.exc.ProgrammingError:    In case the DB model is invalid
            """
            if self.config.get("recreate_db") is not None:
                AuraDatabaseModel.recreate_db(systemexit=True)
    
            # Check if tables exists, if not create them
            try:
                Playlist.select_all()
            except sqlalchemy.exc.ProgrammingError as e:
                errcode = e.orig.args[0]
    
                if errcode == 1146: # Error for no such table
                    x = AuraDatabaseModel()
                    x.recreate_db()
                else:
                    raise
    
        def stop(self):
            """
            Called when thread is stopped.
            """
            self.exit_event.set()
    
    
    
    # ------------------------------------------------------------------------------------------ #
    
    # class SetNextFile(threading.Thread):
    #     fallbackname = None
    #     show = None
    
    #     def __init__(self, fallbackname, show):
    #         threading.Thread.__init__(self)
    #         self.fallbackname = fallbackname
    #         self.show = show
    
    #     def run(self):
    #         if self.fallbackname == "show":
    #             self.detect_next_file_for(self.show.showfallback)
    #         elif self.fallbackname == "timeslow":
    #             self.detect_next_file_for(self.show.timeslotfallback)
    #         elif self.fallbackname == "station":
    #             self.detect_next_file_for(self.show.stationfallback)
    
    #     def detect_next_file_for(self, playlist):
    #         return ""
    #         #if playlist.startswith("pool"):
    #         #    self.find_next_file_in_pool(playlist)
    
    #     #def find_next_file_in_pool(self, pool):
    #     #    return ""
    
    # ------------------------------------------------------------------------------------------ #
    
    class CallFunctionTimer(threading.Timer):
    
        def __init__(self, diff, func, param, fadein=False, fadeout=False, switcher=False):
    
    
            self.logger = logging.getLogger("AuraEngine")
            self.logger.debug("CallFunctionTimer: Executing LiquidSoap command '%s' in %s seconds..." % (str(func.__name__), str(diff)))
    
            # TODO Review usage of the fading-attributes:
    
            if not fadein and not fadeout and not switcher or fadein and fadeout or fadein and switcher or fadeout and switcher:
                raise Exception("You have to create me with either fadein=true, fadeout=true or switcher=True")
    
    
            self.fadein = fadein
            self.fadeout = fadeout
            self.switcher = switcher
    
        # ------------------------------------------------------------------------------------------ #
        def __str__(self):
    
            if self.fadein:
                return "CallFunctionTimer starting in " + str(self.diff) + "s fading in source '" + str(self.entry)
            elif self.fadeout:
                return "CallFunctionTimer starting in " + str(self.diff) + "s fading out source '" + str(self.entry)
            elif self.switcher:
                return "CallFunctionTimer starting in " + str(self.diff) + "s switching to source '" + str(self.entry)
    
                return "CORRUPTED CallFunctionTimer around! How can that be?"