Commit 79e22a14 authored by David Trattnig's avatar David Trattnig
Browse files

Programme as entity. #41

parent d32b466d
......@@ -296,7 +296,7 @@ class Playlog:
return # Avoid overwrite by multiple calls in a row
data = {}
next_timeslot = self.engine.scheduler.get_next_timeslots(1)
next_timeslot = self.engine.scheduler.get_programme().get_next_timeslots(1)
if next_timeslot:
next_timeslot = next_timeslot[0]
else:
......
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program 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 Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from datetime import datetime
from src.base.config import AuraConfig
from src.base.utils import SimpleUtil as SU
from src.core.engine import Engine
from src.scheduling.calendar import AuraCalendarService
from src.scheduling.models import Timeslot
class Programme():
"""
The current programme of the calendar. The programme is consisting of a set of timeslots.
"""
config = None
logger = None
programme = None
last_successful_fetch = None
def __init__(self):
"""
Constructor
"""
self.config = AuraConfig.config()
self.logger = logging.getLogger("AuraEngine")
def refresh(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.debug("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.debug("... Programme fetch via API done!")
# Reset last successful fetch state
lsf = self.last_successful_fetch
self.last_successful_fetch = None
if response is None:
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(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 = 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 = 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(SU.green("Finished loading current programme from database (%s timeslots)" % str(len(self.programme))))
for timeslot in self.programme:
self.logger.debug("\tTimeslot %s with Playlist %s" % (str(timeslot), str(timeslot.playlist)))
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 = Timeslot.select_programme()
if not self.programme:
self.logger.critical(SU.red("Could not load programme from database. We are in big trouble my friend!"))
return
def get_current_entry(self):
"""
Retrieves the current `PlaylistEntry` which should be played as per programme.
Returns:
(PlaylistEntry): The track which is (or should) currently being played
"""
now_unix = Engine.engine_time()
# Load programme if necessary
if not self.programme:
self.load_programme_from_db()
# Check for current timeslot
current_timeslot = self.get_current_timeslot()
if not current_timeslot:
self.logger.warning(SU.red("There's no active timeslot"))
return None
# Check for scheduled playlist
current_playlist = current_timeslot.playlist
if not current_playlist:
msg = "There's no playlist assigned to the current timeslot. 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
current_entry = None
for entry in current_playlist.entries:
if entry.start_unix <= now_unix and now_unix <= entry.end_unix:
current_entry = entry
break
if not current_entry:
# Nothing playing ... fallback will kick-in
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
def get_current_timeslot(self):
"""
Retrieves the timeslot currently to be played.
Returns:
(Timeslot): The current timeslot
"""
current_timeslot = None
now_unix = Engine.engine_time()
# Iterate over all timeslots and find the one to be played right now
if self.programme:
for timeslot in self.programme:
if timeslot.start_unix <= now_unix and now_unix < timeslot.end_unix:
current_timeslot = timeslot
break
return current_timeslot
def get_next_timeslots(self, max_count=0):
"""
Retrieves the timeslots to be played after the current one.
Args:
max_count (Integer): Maximum of timeslots to return, if `0` all exitsing ones are returned
Returns:
([Timeslot]): The next timeslots
"""
now_unix = Engine.engine_time()
next_timeslots = []
for timeslot in self.programme:
if timeslot.start_unix > now_unix:
if (len(next_timeslots) < max_count) or max_count == 0:
next_timeslots.append(timeslot)
else:
break
return self.filter_scheduling_window(next_timeslots)
def filter_scheduling_window(self, timeslots):
"""
Ignore timeslots which are beyond the scheduling window. The end of the scheduling window
is defined by the config option `scheduling_window_end`. This value defines the seconds
minus the actual start time of the timeslot.
"""
now_unix = Engine.engine_time()
len_before = len(timeslots)
window_start = self.config.get("scheduling_window_start")
window_end = self.config.get("scheduling_window_end")
timeslots = list(filter(lambda s: (s.start_unix - window_end) > now_unix and (s.start_unix - window_start) < now_unix, timeslots))
len_after = len(timeslots)
self.logger.info("For now, skipped %s future timeslot(s) which are out of the scheduling window (-%ss <-> -%ss)" % ((len_before - len_after), window_start, window_end))
return timeslots
def is_timeslot_in_window(self, timeslot):
"""
Checks if the timeslot is within the scheduling window.
"""
now_unix = Engine.engine_time()
window_start = self.config.get("scheduling_window_start")
window_end = self.config.get("scheduling_window_end")
if timeslot.start_unix - window_start < now_unix and \
timeslot.start_unix - window_end > now_unix:
return True
return False
\ No newline at end of file
......@@ -28,17 +28,15 @@ from datetime import datetime, timedelta
from src.base.config import AuraConfig
from src.base.utils import SimpleUtil as SU
from src.scheduling.models import AuraDatabaseModel, Timeslot, Playlist
from src.scheduling.models import AuraDatabaseModel, Timeslot, Playlist
from src.base.exceptions import NoActiveTimeslotException, LoadSourceException
from src.core.control import EngineExecutor
from src.core.engine import Engine
from src.core.channels import ChannelType, TransitionType, EntryPlayState
from src.core.resources import ResourceClass, ResourceUtil
from src.scheduling.calendar import AuraCalendarService
from src.scheduling.utils import TimeslotRenderer
from src.scheduling.utils import TimeslotRenderer
from src.scheduling.programme import Programme
......@@ -71,6 +69,7 @@ class TimeslotCommand(EngineExecutor):
class AuraScheduler(threading.Thread):
"""
Aura Scheduler Class
......@@ -93,15 +92,12 @@ class AuraScheduler(threading.Thread):
logger = None
engine = None
exit_event = None
is_initialized = None
is_initialized = None
last_successful_fetch = None
timeslot_renderer = None
programme = None
message_timer = []
fallback = None
is_initialized = None
is_initialized = None
......@@ -116,6 +112,7 @@ class AuraScheduler(threading.Thread):
"""
self.config = AuraConfig.config()
self.logger = logging.getLogger("AuraEngine")
self.programme = Programme()
self.timeslot_renderer = TimeslotRenderer(self)
AuraScheduler.init_database()
self.fallback = fallback_manager
......@@ -151,7 +148,7 @@ class AuraScheduler(threading.Thread):
self.logger.info(SU.cyan(f"== start fetching new timeslots (every {seconds_to_wait} seconds) =="))
# Load some stuff from the API in any case
self.fetch_new_programme()
self.programme.refresh()
# Queue only when the engine is ready to play
if self.is_initialized == True:
......@@ -204,6 +201,13 @@ class AuraScheduler(threading.Thread):
#
def get_programme(self):
"""
Returns the current programme.
"""
return self.programme
def play_active_entry(self):
"""
Plays the entry scheduled for the very current moment and forwards to the scheduled position in time.
......@@ -213,7 +217,7 @@ class AuraScheduler(threading.Thread):
(NoActiveTimeslotException): If there's no timeslot in the programme, within the scheduling window
"""
sleep_offset = 10
active_timeslot = self.get_active_timeslot()
active_timeslot = self.programme.get_current_timeslot()
# Schedule any available fallback playlist
if active_timeslot:
......@@ -225,7 +229,7 @@ class AuraScheduler(threading.Thread):
if not active_timeslot.fadeouttimer:
self.queue_end_of_timeslot(active_timeslot, True)
active_entry = self.get_active_entry()
active_entry = self.programme.get_current_entry()
if not active_entry:
raise NoActiveTimeslotException
......@@ -241,7 +245,7 @@ class AuraScheduler(threading.Thread):
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:
# Pre-roll and play active entry
# Preload and play active entry
self.engine.player.preload(active_entry)
self.engine.player.play(active_entry, TransitionType.FADE)
......@@ -261,7 +265,7 @@ class AuraScheduler(threading.Thread):
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
# Preload and play active entry
self.engine.player.preload(active_entry)
self.engine.player.play(active_entry, TransitionType.FADE)
......@@ -270,109 +274,6 @@ class AuraScheduler(threading.Thread):
def get_active_entry(self):
"""
Retrieves the current `PlaylistEntry` which should be played as per programme.
Returns:
(PlaylistEntry): The track which is (or should) currently being played
"""
now_unix = Engine.engine_time()
# Load programme if necessary
if not self.programme:
self.load_programme_from_db()
# Check for current timeslot
current_timeslot = self.get_active_timeslot()
if not current_timeslot:
self.logger.warning(SU.red("There's no active timeslot"))
return None
# Check for scheduled playlist
current_playlist = current_timeslot.playlist
if not current_playlist:
msg = "There's no playlist assigned to the current timeslot. 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
current_entry = None
for entry in current_playlist.entries:
if entry.start_unix <= now_unix and now_unix <= entry.end_unix:
current_entry = entry
break
if not current_entry:
# Nothing playing ... fallback will kick-in
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
def get_active_timeslot(self):
"""
Retrieves the timeslot currently to be played.
Returns:
(Timeslot): The current timeslot
"""
current_timeslot = None
now_unix = Engine.engine_time()
# Iterate over all timeslots and find the one to be played right now
if self.programme:
for timeslot in self.programme:
if timeslot.start_unix <= now_unix and now_unix < timeslot.end_unix:
current_timeslot = timeslot
break
return current_timeslot
def get_next_timeslots(self, max_count=0):
"""
Retrieves the timeslots to be played after the current one.
Args:
max_count (Integer): Maximum of timeslots to return, if `0` all exitsing ones are returned
Returns:
([Timeslot]): The next timeslots
"""
now_unix = Engine.engine_time()
next_timeslots = []
for timeslot in self.programme:
if timeslot.start_unix > now_unix:
if (len(next_timeslots) < max_count) or max_count == 0:
next_timeslots.append(timeslot)
else:
break
return next_timeslots
def get_active_playlist(self):
"""
Retrieves the currently playing playlist.
Returns:
(FallbackType, Playlist): The resolved playlist
"""
timeslot = self.get_active_timeslot()
if timeslot:
# return timeslot.playlist
return self.fallback.resolve_playlist(timeslot)
return (None, None)
def print_timer_queue(self):
"""
Prints the current timer queue i.e. playlists in the queue to be played.
......@@ -400,45 +301,17 @@ class AuraScheduler(threading.Thread):
#
# PRIVATE METHODS
#
def filter_scheduling_window(self, timeslots):
"""
Ignore timeslots which are beyond the scheduling window. The end of the scheduling window
is defined by the config option `scheduling_window_end`. This value defines the seconds
minus the actual start time of the timeslot.
def get_active_playlist(self):
"""
now_unix = Engine.engine_time()
len_before = len(timeslots)
window_start = self.config.get("scheduling_window_start")
window_end = self.config.get("scheduling_window_end")
timeslots = list(filter(lambda s: (s.start_unix - window_end) > now_unix and (s.start_unix - window_start) < now_unix, timeslots))
len_after = len(timeslots)
self.logger.info("For now, skipped %s future timeslot(s) which are out of the scheduling window (-%ss <-> -%ss)" % ((len_before - len_after), window_start, window_end))
return timeslots
Retrieves the currently playing playlist.
def is_timeslot_in_window(self, timeslot):
"""
Checks if the timeslot is within the scheduling window.
Returns:
(FallbackType, Playlist): The resolved playlist
"""
now_unix = Engine.engine_time()
window_start = self.config.get("scheduling_window_start")
window_end = self.config.get("scheduling_window_end")
if timeslot.start_unix - window_start < now_unix and \
timeslot.start_unix - window_end > now_unix:
return True
return False
timeslot = self.programme.get_current_timeslot()
if timeslot:
return self.fallback.resolve_playlist(timeslot)
return (None, None)
......@@ -449,8 +322,7 @@ class AuraScheduler(threading.Thread):
"""
# Get a clean set of the timeslots within the scheduling window
timeslots = self.get_next_timeslots()
timeslots = self.filter_scheduling_window(timeslots)
timeslots = self.programme.get_next_timeslots()
# Queue the timeslots, their playlists and entries
if timeslots:
......@@ -477,14 +349,14 @@ class AuraScheduler(threading.Thread):
Queues all entries after the one currently playing upon startup. Don't use
this method in any other scenario, as it doesn't respect the scheduling window.
"""
current_timeslot = self.get_active_timeslot()
current_timeslot = self.programme.get_current_timeslot()
# Queue the (rest of the) currently playing timeslot upon startup
if current_timeslot:
current_playlist = current_timeslot.playlist
if current_playlist:
active_entry = self.get_active_entry()
active_entry = self.programme.get_current_entry()
# Finished entries
for entry in current_playlist.entries:
......@@ -645,9 +517,6 @@ class AuraScheduler(threading.Thread):
def queue_end_of_timeslot(self, timeslot, fade_out):
"""
Queues a engine action to stop/fade-out the given timeslot.
......@@ -688,65 +557,6 @@ class AuraScheduler(threading.Thread):
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.debug("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.debug("... Programme fetch via API done!")
# Reset last successful fetch state
lsf = self.last_successful_fetch
self.last_successful_fetch = None
if response is None:
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(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 = 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 = 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(SU.green("Finished loading current programme from database (%s timeslots)" % str(len(self.programme))))
for timeslot in self.programme:
self.logger.debug("\tTimeslot %s with Playlist %s" % (str(timeslot), str(timeslot.playlist)))
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 = Timeslot.select_programme()
if not self.programme:
self.logger.critical(SU.red("Could not load programme from database. We are in big trouble my friend!"))
return
def is_something_planned_at_time(self, given_time):
"""
Checks for existing timers at the given time.
......@@ -769,7 +579,7 @@ class AuraScheduler(threading.Thread):
param ([]): A timeslot or list of entries
Returns:
(CallFunctionTimer, CallFunctionTimer): In case of a "switch" command, the switch and pre-roll timer is returned
(CallFunctionTimer, CallFunctionTimer): In case of a "switch" command, the switch and preload timer is returned
<