Commit 94c38f18 authored by david's avatar david
Browse files

Default playlists for show and schedule-level. #52

parent e87be140
......@@ -374,41 +374,41 @@ class Player:
def start_fallback_playlist(self, entries):
"""
Sets any scheduled fallback playlist and performs a fade-in.
Args:
entries ([Entry]): The playlist entries
"""
self.preload_group(entries, ChannelType.FALLBACK_QUEUE)
self.play(entries[0], TransitionType.FADE)
self.event_dispatcher.on_fallback_updated(entries)
def stop_fallback_playlist(self):
"""
Performs a fade-out and clears any scheduled fallback playlist.
"""
dirty_channel = self.channel_router.get_active(ChannelType.FALLBACK_QUEUE)
self.logger.info(f"Fading out channel '{dirty_channel}'")
self.connector.enable_transaction()
self.mixer_fallback.fade_out(dirty_channel)
self.connector.disable_transaction()
def clean_up():
# Wait a little, if there is some long fade-out. Note, this also means,
# this channel should not be used for at least some seconds (including clearing time).
time.sleep(2)
self.connector.enable_transaction()
self.mixer_fallback.channel_activate(dirty_channel.value, False)
res = self.queue_clear(dirty_channel)
self.logger.info("Clear Fallback Queue Response: " + res)
self.connector.disable_transaction()
self.event_dispatcher.on_fallback_cleaned(dirty_channel)
Thread(target=clean_up).start()
# def start_fallback_playlist(self, entries):
# """
# Sets any scheduled fallback playlist and performs a fade-in.
# Args:
# entries ([Entry]): The playlist entries
# """
# self.preload_group(entries, ChannelType.FALLBACK_QUEUE)
# self.play(entries[0], TransitionType.FADE)
# self.event_dispatcher.on_fallback_updated(entries)
# def stop_fallback_playlist(self):
# """
# Performs a fade-out and clears any scheduled fallback playlist.
# """
# dirty_channel = self.channel_router.get_active(ChannelType.FALLBACK_QUEUE)
# self.logger.info(f"Fading out channel '{dirty_channel}'")
# self.connector.enable_transaction()
# self.mixer_fallback.fade_out(dirty_channel)
# self.connector.disable_transaction()
# def clean_up():
# # Wait a little, if there is some long fade-out. Note, this also means,
# # this channel should not be used for at least some seconds (including clearing time).
# time.sleep(2)
# self.connector.enable_transaction()
# self.mixer_fallback.channel_activate(dirty_channel.value, False)
# res = self.queue_clear(dirty_channel)
# self.logger.info("Clear Fallback Queue Response: " + res)
# self.connector.disable_transaction()
# self.event_dispatcher.on_fallback_cleaned(dirty_channel)
# Thread(target=clean_up).start()
......
......@@ -89,7 +89,7 @@ class EngineEventDispatcher():
self.logger = logging.getLogger("AuraEngine")
self.config = AuraConfig.config()
self.engine = engine
binding = self.attach(AuraMailer)
binding.subscribe("on_critical")
binding.subscribe("on_sick")
......@@ -183,33 +183,33 @@ class EngineEventDispatcher():
"""
Called when the engine has finished booting and is ready to play.
"""
def func(self, param):
def func(self, param):
self.logger.debug("on_ready(..)")
self.scheduler.on_ready()
self.call_event("on_ready", param)
thread = Thread(target = func, args = (self, None))
thread.start()
thread.start()
def on_timeslot_start(self, timeslot):
"""
Called when a timeslot starts.
"""
def func(self, timeslot):
def func(self, timeslot):
self.logger.debug("on_timeslot_start(..)")
self.fallback_manager.on_timeslot_start(timeslot)
self.call_event("on_timeslot_start", timeslot)
thread = Thread(target = func, args = (self, timeslot))
thread.start()
thread.start()
def on_timeslot_end(self, timeslot):
"""
Called when a timeslot ends.
"""
def func(self, timeslot):
def func(self, timeslot):
self.logger.debug("on_timeslot_end(..)")
self.fallback_manager.on_timeslot_end(timeslot)
self.call_event("on_timeslot_end", timeslot)
......@@ -227,7 +227,7 @@ class EngineEventDispatcher():
Args:
source (String): The `PlaylistEntry` object
"""
def func(self, entry):
def func(self, entry):
self.logger.debug("on_play(..)")
# Assign timestamp indicating start play time. Use the actual playtime when possible.
entry.entry_start_actual = datetime.datetime.now()
......@@ -235,63 +235,63 @@ class EngineEventDispatcher():
self.call_event("on_play", entry)
thread = Thread(target = func, args = (self, entry))
thread.start()
thread.start()
def on_metadata(self, data):
"""
Event called by the soundsystem implementation (i.e. Liquidsoap) when some entry is actually playing.
This does not include live or stream sources, since they ain't have metadata and are triggered from
Event called by the soundsystem implementation (i.e. Liquidsoap) when some entry is actually playing.
This does not include live or stream sources, since they ain't have metadata and are triggered from
engine core (see `on_play(..)`).
Args:
data (dict): A collection of metadata related to the current track
"""
def func(self, data):
def func(self, data):
self.logger.debug("on_metadata(..)")
self.fallback_manager.on_metadata(data)
self.call_event("on_metadata", data)
thread = Thread(target = func, args = (self, data))
thread.start()
thread.start()
def on_stop(self, entry):
"""
The entry on the assigned channel has been stopped playing.
"""
def func(self, entry):
def func(self, entry):
self.logger.debug("on_stop(..)")
self.call_event("on_stop", entry)
thread = Thread(target = func, args = (self, entry))
thread.start()
thread.start()
def on_fallback_updated(self, playlist_uri):
"""
Called when the scheduled fallback playlist has been updated.
This event does not indicate that the fallback is actually playing.
"""
def func(self, playlist_uri):
self.logger.debug("on_fallback_updated(..)")
self.call_event("on_fallback_updated", playlist_uri)
# def on_fallback_updated(self, playlist_uri):
# """
# Called when the scheduled fallback playlist has been updated.
# This event does not indicate that the fallback is actually playing.
# """
# def func(self, playlist_uri):
# self.logger.debug("on_fallback_updated(..)")
# self.call_event("on_fallback_updated", playlist_uri)
thread = Thread(target = func, args = (self, playlist_uri))
thread.start()
# thread = Thread(target = func, args = (self, playlist_uri))
# thread.start()
def on_fallback_cleaned(self, cleaned_channel):
"""
Called when the scheduled fallback queue has been cleaned up.
This event does not indicate that some fallback is actually playing.
"""
def func(self, cleaned_channel):
self.logger.debug("on_fallback_cleaned(..)")
self.call_event("on_fallback_cleaned", cleaned_channel)
# def on_fallback_cleaned(self, cleaned_channel):
# """
# Called when the scheduled fallback queue has been cleaned up.
# This event does not indicate that some fallback is actually playing.
# """
# def func(self, cleaned_channel):
# self.logger.debug("on_fallback_cleaned(..)")
# self.call_event("on_fallback_cleaned", cleaned_channel)
thread = Thread(target = func, args = (self, cleaned_channel))
thread.start()
# thread = Thread(target = func, args = (self, cleaned_channel))
# thread.start()
def on_fallback_active(self, timeslot, fallback_type):
......@@ -299,57 +299,57 @@ class EngineEventDispatcher():
Called when a fallback is activated for the given timeslot,
since no default playlist is available.
"""
def func(self, timeslot, fallback_type):
def func(self, timeslot, fallback_type):
self.logger.debug("on_fallback_active(..)")
self.call_event("on_fallback_active", timeslot, fallback_type)
thread = Thread(target = func, args = (self, timeslot, fallback_type))
thread.start()
thread.start()
def on_queue(self, entries):
"""
One or more entries have been queued and are currently pre-loaded.
"""
def func(self, entries):
def func(self, entries):
self.logger.debug("on_queue(..)")
self.call_event("on_queue", entries)
thread = Thread(target = func, args = (self, entries))
thread.start()
thread.start()
def on_sick(self, data):
"""
Called when the engine is in some unhealthy state.
"""
def func(self, data):
def func(self, data):
self.logger.debug("on_sick(..)")
self.call_event("on_sick", data)
thread = Thread(target = func, args = (self, data))
thread.start()
thread.start()
def on_resurrect(self, data):
"""
Called when the engine turned healthy again after being sick.
"""
def func(self, data):
def func(self, data):
self.logger.debug("on_resurrect(..)")
self.call_event("on_resurrect", data)
thread = Thread(target = func, args = (self, data))
thread.start()
thread.start()
def on_critical(self, subject, message, data=None):
"""
Callend when some critical event occurs
"""
def func(self, subject, message, data):
def func(self, subject, message, data):
self.logger.debug("on_critical(..)")
self.call_event("on_critical", (subject, message, data))
thread = Thread(target = func, args = (self, subject, message, data))
thread.start()
\ No newline at end of file
thread.start()
\ No newline at end of file
......@@ -22,7 +22,6 @@ import logging
import requests
from collections import deque
from datetime import datetime, timedelta
from src.base.config import AuraConfig
from src.base.utils import SimpleUtil as SU
......@@ -181,7 +180,7 @@ class TrackServiceHandler():
"""
planned_playlist = None
if self.engine.scheduler:
(fallback_type, planned_playlist) = self.engine.scheduler.get_active_playlist()
(playlist_type, planned_playlist) = self.engine.scheduler.get_active_playlist()
(past_timeslot, current_timeslot, next_timeslot) = self.playlog.get_timeslots()
data = dict()
......@@ -349,21 +348,29 @@ class Playlog:
data ({}): The dictionary holding the (virtual) timeslot
timeslot (Timeslot): The actual timeslot object to retrieve fallback info from
"""
fallback_type = None
playlist_type = None
playlist = None
if timeslot:
fallback_type, playlist = self.engine.scheduler.fallback.resolve_playlist(timeslot)
playlist_type, playlist = self.engine.scheduler.resolve_playlist(timeslot)
if playlist:
data["playlist_id"] = playlist.playlist_id
else:
data["playlist_id"] = -1
if fallback_type:
data["fallback_type"] = fallback_type.id
else:
data["fallback_type"] = FallbackType.STATION.id
#FIXME "fallback_type" should be a more generic "playout_state"? (compare meta#42)
#FIXME Add field for "playlist_type", which now differs from playout-state
#FIXME Remove dependency to "scheduler" and "scheduler.fallback" module
data["fallback_type"] = 0
if self.engine.scheduler:
playout_state = self.engine.scheduler.fallback.get_playout_state()
data["fallback_type"] = playout_state.id
# if playlist_type:
# data["fallback_type"] = playlist_type.id
# else:
# data["fallback_type"] = FallbackType.STATION.id
def get_timeslots(self):
......
......@@ -37,7 +37,7 @@ class ApiFetcher(threading.Thread):
"""
config = None
logging = None
queue = None
queue = None
has_already_fetched = False
fetched_timeslot_data = None
stop_event = None
......@@ -62,7 +62,7 @@ class ApiFetcher(threading.Thread):
self.tank_secret = self.config.get("api_tank_secret")
self.queue = queue.Queue()
self.stop_event = threading.Event()
threading.Thread.__init__(self)
threading.Thread.__init__(self)
......@@ -121,14 +121,13 @@ class ApiFetcher(threading.Thread):
for timeslot in self.fetched_timeslot_data:
# FIXME Workaround until https://gitlab.servus.at/aura/steering/-/issues/54 is implemented
if "schedule_fallback_id" in timeslot:
timeslot["default_schedule_playlist_id"] = timeslot["schedule_fallback_id"]
if "schedule_default_playlist_id" in timeslot:
timeslot["default_schedule_playlist_id"] = timeslot["schedule_default_playlist_id"]
timeslot["schedule_fallback_id"] = None
if "show_fallback_id" in timeslot:
timeslot["default_show_playlist_id"] = timeslot["show_fallback_id"]
if "show_default_playlist_id" in timeslot:
timeslot["default_show_playlist_id"] = timeslot["show_default_playlist_id"]
timeslot["show_fallback_id"] = None
self.logger.debug("Fetching playlists from TANK")
self.fetch_playlists()
......@@ -163,15 +162,15 @@ class ApiFetcher(threading.Thread):
"""
timeslots = None
headers = { "content-type": "application/json" }
try:
self.logger.debug("Fetch timeslots from Steering API...")
self.logger.debug("Fetch timeslots from Steering API...")
response = requests.get(self.steering_calendar_url, data=None, headers=headers)
if not response.status_code == 200:
self.logger.critical(SU.red("HTTP Status: %s | Timeslots could not be fetched! Response: %s" % \
(str(response.status_code), response.text)))
return None
timeslots = response.json()
return None
timeslots = response.json()
except Exception as e:
self.logger.critical(SU.red("Error while requesting timeslots from Steering!"), e)
......@@ -198,8 +197,8 @@ class ApiFetcher(threading.Thread):
# Get IDs of specific, default and fallback playlists
playlist_id = self.get_playlist_id(timeslot, "playlist_id")
default_schedule_playlist_id = self.get_playlist_id(timeslot, "default_schedule_playlist_id")
default_show_playlist_id = self.get_playlist_id(timeslot, "default_show_playlist_id")
schedule_fallback_id = self.get_playlist_id(timeslot, "schedule_fallback_id")
default_show_playlist_id = self.get_playlist_id(timeslot, "default_show_playlist_id")
schedule_fallback_id = self.get_playlist_id(timeslot, "schedule_fallback_id")
show_fallback_id = self.get_playlist_id(timeslot, "show_fallback_id")
station_fallback_id = self.get_playlist_id(timeslot, "station_fallback_id")
......@@ -208,7 +207,7 @@ class ApiFetcher(threading.Thread):
timeslot["playlist"] = self.fetch_playlist(playlist_id, fetched_entries)
timeslot["default_schedule_playlist"] = self.fetch_playlist(default_schedule_playlist_id, fetched_entries)
timeslot["default_show_playlist"] = self.fetch_playlist(default_show_playlist_id, fetched_entries)
timeslot["schedule_fallback"] = self.fetch_playlist(schedule_fallback_id, fetched_entries)
timeslot["schedule_fallback"] = self.fetch_playlist(schedule_fallback_id, fetched_entries)
timeslot["show_fallback"] = self.fetch_playlist(show_fallback_id, fetched_entries)
timeslot["station_fallback"] = self.fetch_playlist(station_fallback_id, fetched_entries)
......@@ -232,9 +231,9 @@ class ApiFetcher(threading.Thread):
return None
playlist = None
url = self.tank_playlist_url.replace("${ID}", playlist_id)
url = self.tank_playlist_url.replace("${ID}", playlist_id)
headers = {
"Authorization": "Bearer %s:%s" % (self.tank_session, self.tank_secret),
"Authorization": "Bearer %s:%s" % (self.tank_session, self.tank_secret),
"content-type": "application/json"
}
......@@ -243,22 +242,22 @@ class ApiFetcher(threading.Thread):
if playlist["id"] == playlist_id:
self.logger.debug("Playlist #%s already fetched" % playlist_id)
return playlist
try:
self.logger.debug("Fetch playlist from Tank API...")
self.logger.debug("Fetch playlist from Tank API...")
response = requests.get(url, data=None, headers=headers)
if not response.status_code == 200:
self.logger.critical(SU.red("HTTP Status: %s | Playlist #%s could not be fetched or is not available! Response: %s" % \
(str(response.status_code), str(playlist_id), response.text)))
return None
playlist = response.json()
return None
playlist = response.json()
except Exception as e:
self.logger.critical(SU.red("Error while requesting playlist #%s from Tank" % str(playlist_id)), e)
return None
fetched_playlists.append(playlist)
return playlist
def get_playlist_id(self, timeslot, id_name):
......@@ -279,7 +278,7 @@ class ApiFetcher(threading.Thread):
if not playlist_id or playlist_id == "None":
self.logger.debug("No value defined for '%s' in timeslot '#%s'" % (id_name, timeslot["id"]))
return None
return playlist_id
......
......@@ -36,14 +36,12 @@ class FallbackType(Enum):
"""
Types of fallbacks.
NONE: No fallback active, default playout
SCHEDULE: The first played when some default playlist fails
SHOW: The second played when the timeslot fallback fails
STATION: The last played when everything else fails
NONE: No fallback active, default playout as planned
STATION: The station fallback is played when everything else fails
"""
NONE = { "id": 0, "name": "default", "channels": [ Channel.QUEUE_A, Channel.QUEUE_B ] }
SCHEDULE = { "id": 1, "name": "schedule", "channels": [ Channel.FALLBACK_QUEUE_A, Channel.FALLBACK_QUEUE_B ] }
SHOW = { "id": 2, "name": "show", "channels": [ Channel.FALLBACK_QUEUE_A, Channel.FALLBACK_QUEUE_B ] }
# SCHEDULE = { "id": 1, "name": "schedule", "channels": [ Channel.FALLBACK_QUEUE_A, Channel.FALLBACK_QUEUE_B ] }
# SHOW = { "id": 2, "name": "show", "channels": [ Channel.FALLBACK_QUEUE_A, Channel.FALLBACK_QUEUE_B ] }
STATION = { "id": 3, "name": "station", "channels": [ Channel.FALLBACK_STATION_FOLDER, Channel.FALLBACK_STATION_PLAYLIST ] }
@property
......@@ -61,14 +59,13 @@ class FallbackType(Enum):
class FallbackManager:
"""
Handles all types of fallbacks in case there is an outage or missing schedules
for the radio programme.
"""
Manages if engine is in normal or fallback play-state.
"""
config = None
logger = None
engine = None
state = None
def __init__(self, engine):
"""
......@@ -80,9 +77,9 @@ class FallbackManager:
self.config = AuraConfig.config()
self.logger = logging.getLogger("AuraEngine")
self.engine = engine
self.state = {
self.state = {
"fallback_type": FallbackType.NONE,
"previous_fallback_type": None,
"previous_fallback_type": None,
"timeslot": None
}
......@@ -101,7 +98,7 @@ class FallbackManager:
def on_timeslot_end(self, timeslot):
"""
The timeslot has ended and the state is updated. The method ensures that any intermediate state
The timeslot has ended and the state is updated. The method ensures that any intermediate state
update doesn't get overwritten.
"""
if self.state["timeslot"] == timeslot:
......@@ -110,11 +107,11 @@ class FallbackManager:
def on_play(self, entry):
"""
Event Handler which is called by the engine when some entry is actually playing.
Event Handler which is called by the engine when some entry is actually playing.
Args:
source (String): The `PlaylistEntry` object
"""
"""
content_class = ResourceUtil.get_content_class(entry.get_content_type())
if content_class == ResourceClass.FILE:
# Files are handled by "on_metadata" called via Liquidsoap
......@@ -125,8 +122,8 @@ class FallbackManager:
def on_metadata(self, data):
"""
Event called by the soundsystem implementation (i.e. Liquidsoap) when some entry is actually playing.
This does not include live or stream sources, since they ain't have metadata and are triggered from
Event called by the soundsystem implementation (i.e. Liquidsoap) when some entry is actually playing.
This does not include live or stream sources, since they ain't have metadata and are triggered from
engine core (see `on_play(..)`).
Args:
......@@ -151,6 +148,13 @@ class FallbackManager:
#
def get_playout_state(self):
"""
Returns the current playout state, like normal or fallback.
"""
return self.state["fallback_type"]
def update_fallback_state(self, channel):
"""
Update the current and previously active fallback state.
......@@ -168,151 +172,150 @@ class FallbackManager:
"""
Retrieves the matching fallback type for the given source.
"""
if source in [str(i) for i in FallbackType.SCHEDULE.channels]:
return FallbackType.SCHEDULE
if source in [str(i) for i in FallbackType.SHOW.channels]:
return FallbackType.SHOW
# if source in [str(i) for i in FallbackType.SCHEDULE.channels]:
# return FallbackType.SCHEDULE
# if source in [str(i) for i in FallbackType.SHOW.channels]:
# return FallbackType.SHOW
if source in [str(i) for i in FallbackType.STATION.channels]:
return FallbackType.STATION
return FallbackType.NONE
def queue_fallback_playlist(self, timeslot):
"""
Evaluates the scheduled fallback and queues it using a timed thread.
"""
(fallback_type, playlist) = self.get_fallback_playlist(timeslot)
# def queue_fallback_playlist(self, timeslot):
# """
# Evaluates the scheduled fallback and queues it using a timed thread.
# """
# (fallback_type, playlist) = self.get_fallback_playlist(timeslot)
if playlist:
self.logger.info(f"Resolved {fallback_type.value} fallback")
return FallbackCommand(timeslot, playlist.entries)
else:
msg = f"There is no timeslot- or show-fallback defined for timeslot#{timeslot.timeslot_id}. "
msg += f"The station fallback will be used automatically."
self.logger.info(msg)
# if playlist:
# self.logger.info(f"Resolved {fallback_type.value} fallback")
# return FallbackCommand(timeslot, playlist.entries)
# else:
# msg = f"There is no fallback playlist on timeslot- or show-level defined for timeslot#{timeslot.timeslot_id}. "
# msg += f"The station fallback will be used automatically."
# self.logger.info(msg)
def resolve_playlist(self, timeslot):
"""
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.
# def resolve_playlist(self, timeslot):
# """
# Retrieves the currently planned, default or fallback playlist. If a normal playlist is available,
# this one is returned. In case of a station fallback to be triggered, no playlist is returned.
Args: