Commit 1e082c76 authored by david's avatar david
Browse files

Absolute audio path config. #72

parent 80f9985f
......@@ -30,15 +30,15 @@ from_mail="monitoring@aura.engine"
mailsubject_prefix="[AURA Engine]"
# Server where heartbeat info is sent to
heartbeat_server = "127.0.0.1"
heartbeat_server = "127.0.0.1"
# Some UDP port
heartbeat_port = 43334
heartbeat_port = 43334
# Seconds how often the vitality of the Engine should be checked (0 = disabled)
heartbeat_frequency = 0
[api]
## STEERING ##
# The URL to get the health status
# The URL to get the health status
api_steering_status="http://localhost:8000/api/v1/"
# The URL to get the Calendar via Steering
api_steering_calendar="http://localhost:8000/api/v1/playout"
......@@ -48,7 +48,7 @@ api_steering_calendar="http://localhost:8000/api/v1/playout"
api_tank_session="engine"
# The secret which is used to authenticate against Tank
api_tank_secret="rather-secret"
# The URL to get the health status
# The URL to get the health status
api_tank_status="http://localhost:8040/healthz/"
# The URL to get playlist details via Tank
api_tank_playlist="http://localhost:8040/api/v1/playlists/${ID}"
......@@ -68,10 +68,10 @@ api_engine_store_health="http://localhost:8008/api/v1/source/health/${ENGINE_NUM
[scheduler]
# Base path as seen by "engine-core", not accessed by "engine"; this is required to construct the absolute audio file path (check "Audio Store" in the docs)
# Either provide an absolute base path or a relative one starting in the `engine-core/src` directory. In case of `engine-core` running in docker use `/var/audio/source`
audio_source_folder="../audio/source"
audio_source_folder="audio/source"
audio_source_extension=".flac"
# Folder holding M3U Playlists to be scheduled in form of Engine Playlists (similar as audio source folder above)
audio_playlist_folder="../audio/playlist"
audio_playlist_folder="audio/playlist"
# Offset in seconds how long it takes for Liquidsoap to actually execute a scheduler command; Crucial to keep things in sync
engine_latency_offset=0.5
# How often should the calendar be fetched in seconds. This determines the time of the last changes applied, before a specific show aired
......@@ -83,7 +83,7 @@ db_pass="---SECRET--PASSWORD---"
db_host="localhost"
db_charset="utf8"
# The scheduling window defines when the entries of each timeslot are queued for play-out. The windows start at (timeslot.start - window_start) seconds
# and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window.
# and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window.
scheduling_window_start=60
scheduling_window_end=60
# How many seconds before the actual schedule time the entry should be pre-rolled. Note to provide enough timeout for
......@@ -91,7 +91,7 @@ scheduling_window_end=60
# the past the offset is ignored and the entry is played as soon as possible
preload_offset=15
# Sometimes it might take longer to get a stream connected. Here you can define a viable length.
# But note, that this may affect the preloading time (see `preload_offset`), hence affecting the
# But note, that this may affect the preloading time (see `preload_offset`), hence affecting the
# overall playout, it's delays and possible fallbacks
input_stream_retry_delay=1
input_stream_max_retries=10
......
......@@ -30,15 +30,15 @@ from_mail="monitoring@aura.engine"
mailsubject_prefix="[AURA Engine]"
# Server where heartbeat info is sent to
heartbeat_server = "127.0.0.1"
heartbeat_server = "127.0.0.1"
# Some UDP port
heartbeat_port = 43334
heartbeat_port = 43334
# Seconds how often the vitality of the Engine should be checked (0 = disabled)
heartbeat_frequency = 0
[api]
## STEERING ##
# The URL to get the health status
# The URL to get the health status
api_steering_status="http://aura.local:8000/api/v1/"
# The URL to get the Calendar via Steering
api_steering_calendar="http://aura.local:8000/api/v1/playout"
......@@ -48,7 +48,7 @@ api_steering_calendar="http://aura.local:8000/api/v1/playout"
api_tank_session="engine"
# The secret which is used to authenticate against Tank
api_tank_secret="rather-secret"
# The URL to get the health status
# The URL to get the health status
api_tank_status="http://aura.local:8040/healthz/"
# The URL to get playlist details via Tank
api_tank_playlist="http://aura.local:8040/api/v1/playlists/${ID}"
......@@ -68,10 +68,10 @@ api_engine_store_health="http://localhost:8008/api/v1/source/health/${ENGINE_NUM
[scheduler]
# Base path as seen by "engine-core", not accessed by "engine"; this is required to construct the absolute audio file path (check "Audio Store" in the docs)
# Either provide an absolute base path or a relative one starting in the `engine-core/src` directory. In case of `engine-core` running in docker use `/var/audio/source`
audio_source_folder="../audio/source"
audio_source_folder="audio/source"
audio_source_extension=".flac"
# Folder holding M3U Playlists to be scheduled in form of Engine Playlists (similar as audio source folder above)
audio_playlist_folder="../audio/playlist"
audio_playlist_folder="audio/playlist"
# Offset in seconds how long it takes for Liquidsoap to actually execute a scheduler command; Crucial to keep things in sync
engine_latency_offset=0.5
# How often should the calendar be fetched in seconds. This determines the time of the last changes applied, before a specific show aired
......@@ -83,7 +83,7 @@ db_pass="---SECRET--PASSWORD---"
db_host="localhost"
db_charset="utf8"
# The scheduling window defines when the entries of each timeslot are queued for play-out. The windows start at (timeslot.start - window_start) seconds
# and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window.
# and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window.
scheduling_window_start=60
scheduling_window_end=60
# How many seconds before the actual schedule time the entry should be pre-rolled. Note to provide enough timeout for
......@@ -91,7 +91,7 @@ scheduling_window_end=60
# the past the offset is ignored and the entry is played as soon as possible
preload_offset=15
# Sometimes it might take longer to get a stream connected. Here you can define a viable length.
# But note, that this may affect the preloading time (see `preload_offset`), hence affecting the
# But note, that this may affect the preloading time (see `preload_offset`), hence affecting the
# overall playout, it's delays and possible fallbacks
input_stream_retry_delay=1
input_stream_max_retries=10
......
......@@ -27,9 +27,9 @@ from configparser import ConfigParser
class AuraConfig:
"""
"""
AuraConfig Class
Holds the Engine Configuration as in the file `engine.ini`.
"""
instance = None
......@@ -37,7 +37,7 @@ class AuraConfig:
logger = None
def __init__(self, ini_path="/etc/aura/engine.ini"):
def __init__(self, ini_path="/etc/aura/engine.ini"):
"""
Initializes the configuration, defaults to `/etc/aura/engine.ini`.
If this file doesn't exist it uses `./config/engine.ini` from
......@@ -46,7 +46,7 @@ class AuraConfig:
Args:
ini_path(String): The path to the configuration file `engine.ini`
"""
self.logger = logging.getLogger("AuraEngine")
self.logger = logging.getLogger("AuraEngine")
config_file = Path(ini_path)
if not config_file.is_file():
ini_path = "%s/config/engine.ini" % Path(__file__).parent.parent.parent.absolute()
......@@ -56,7 +56,7 @@ class AuraConfig:
AuraConfig.instance = self
# Defaults
self.set("config_dir", os.path.dirname(ini_path))
self.set("config_dir", os.path.dirname(ini_path))
self.set("install_dir", os.path.realpath(__file__ + "../../../.."))
......@@ -152,4 +152,18 @@ class AuraConfig:
if path.startswith("/"):
return path
else:
return self.get("install_dir") + "/" + path
\ No newline at end of file
return self.get("install_dir") + "/" + path
def abs_audio_store_path(self):
"""
Returns the absolute path to the audio store, based on the `audio_source_folder` setting.
"""
return self.to_abs_path(self.get("audio_source_folder"))
def abs_playlist_path(self):
"""
Returns the absolute path to the playlist folder
"""
return self.to_abs_path(self.get("audio_playlist_folder"))
......@@ -28,7 +28,7 @@ import meta
from src.base.config import AuraConfig
from src.base.utils import SimpleUtil as SU
from src.base.exceptions import LQConnectionError, InvalidChannelException, LQStreamException, LoadSourceException
from src.resources import ResourceClass, ResourceUtil
from src.resources import ResourceClass, ResourceUtil
from src.channels import ChannelType, TransitionType, LiquidsoapResponse, EntryPlayState, ResourceType, ChannelRouter
from src.events import EngineEventDispatcher
from src.control import EngineControlInterface
......@@ -59,13 +59,13 @@ class Engine():
Constructor
"""
if Engine.instance:
raise Exception("Engine is already running!")
Engine.instance = self
self.logger = logging.getLogger("AuraEngine")
raise Exception("Engine is already running!")
Engine.instance = self
self.logger = logging.getLogger("AuraEngine")
self.config = AuraConfig.config()
Engine.engine_time_offset = float(self.config.get("engine_latency_offset"))
self.plugins = dict()
self.plugins = dict()
self.channel_router = ChannelRouter(self.config, self.logger)
self.start()
......@@ -80,8 +80,8 @@ class Engine():
self.eci = EngineControlInterface(self, self.event_dispatcher)
self.connector = PlayerConnector(self.event_dispatcher)
self.event_dispatcher.on_initialized()
while not self.is_connected():
while not self.is_connected():
self.logger.info(SU.yellow("Waiting for Liquidsoap to be running ..."))
time.sleep(2)
self.logger.info(SU.green("Engine Core ------[ connected ]-------- Liquidsoap"))
......@@ -200,7 +200,7 @@ class Player:
"""
self.config = AuraConfig.config()
self.logger = logging.getLogger("AuraEngine")
self.event_dispatcher = event_dispatcher
self.event_dispatcher = event_dispatcher
self.connector = connector
self.channel_router = ChannelRouter(self.config, self.logger)
self.mixer = Mixer(self.config, MixerType.MAIN, self.connector)
......@@ -216,8 +216,8 @@ class Player:
result in sitations with incorrect timing. In this case bundle multiple short entries as
one queue using `preload_playlist(self, entries)`.
It's important to note, that his method is blocking until loading has finished. If this
method is called asynchronously, the progress on the preloading state can be looked up in
It's important to note, that his method is blocking until loading has finished. If this
method is called asynchronously, the progress on the preloading state can be looked up in
`entry.state`.
Args:
......@@ -238,7 +238,7 @@ class Player:
# QUEUE
if entry.get_content_type() in ResourceClass.FILE.types:
is_ready = self.queue_push(entry.channel, entry.source)
# STREAM
elif entry.get_content_type() in ResourceClass.STREAM.types:
is_ready = self.stream_load_entry(entry)
......@@ -252,13 +252,13 @@ class Player:
def preload_group(self, entries, channel_type=ChannelType.QUEUE):
"""
Pre-Load multiple filesystem entries at once. This call is required before the
Pre-Load multiple filesystem entries at once. This call is required before the
actual `play(..)` can happen. Due to their nature, non-filesystem entries cannot be queued
using this method. In this case use `preload(self, entry)` instead. This method also allows
queuing of very short files, such as jingles.
It's important to note, that his method is blocking until loading has finished. If this
method is called asynchronously, the progress on the preloading state can be looked up in
It's important to note, that his method is blocking until loading has finished. If this
method is called asynchronously, the progress on the preloading state can be looked up in
`entry.state`.
Args:
......@@ -271,8 +271,8 @@ class Player:
for entry in entries:
if entry.get_content_type() != ResourceType.FILE:
raise InvalidChannelException
# Determine channel
# Determine channel
channels = self.channel_router.channel_swap(channel_type)
# Queue entries
......@@ -285,7 +285,7 @@ class Player:
if self.queue_push(entry.channel, entry.source) == True:
entry.status = EntryPlayState.READY
self.event_dispatcher.on_queue(entries)
return channels
......@@ -296,18 +296,18 @@ class Player:
a clean channel is selected and transitions between old and new channel is performed.
This method expects that the entry is pre-loaded using `preload(..)` or `preload_group(self, entries)`
before being played. In case the pre-roll has happened for a group of entries, only the
before being played. In case the pre-roll has happened for a group of entries, only the
first entry of the group needs to be passed.
Args:
entry (PlaylistEntry): The audio source to be played
transition (TransitionType): The type of transition to use e.g. fade-in or instant volume level.
queue (Boolean): If `True` the entry is queued if the `ChannelType` does allow so;
queue (Boolean): If `True` the entry is queued if the `ChannelType` does allow so;
otherwise a new channel of the same type is activated
"""
with suppress(LQConnectionError):
channel_type = self.channel_router.type_of_channel(entry.channel)
mixer = self.mixer
if channel_type == ChannelType.FALLBACK_QUEUE:
......@@ -322,7 +322,7 @@ class Player:
mixer.channel_activate(entry.channel.value, True)
self.connector.disable_transaction()
# Update active channel for the current channel type
# Update active channel for the current channel type
self.channel_router.set_active(channel_type, entry.channel)
# Dear filesystem channels, please leave the room as you would like to find it!
......@@ -340,7 +340,7 @@ class Player:
self.logger.info("Clear Queue Response: " + res)
self.connector.disable_transaction()
Thread(target=clean_up).start()
self.event_dispatcher.on_play(entry)
......@@ -359,7 +359,7 @@ class Player:
if not entry.channel:
self.logger.warn(SU.red("Trying to stop entry %s, but it has no channel assigned" % entry))
return
if transition == TransitionType.FADE:
self.mixer.fade_out(entry.channel)
else:
......@@ -393,7 +393,7 @@ class Player:
self.logger.info(f"Fading out channel '{dirty_channel}'")
self.connector.enable_transaction()
self.mixer_fallback.fade_out(dirty_channel)
self.connector.disable_transaction()
self.connector.disable_transaction()
def clean_up():
# Wait a little, if there is some long fade-out. Note, this also means,
......@@ -403,10 +403,10 @@ class Player:
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.connector.disable_transaction()
self.event_dispatcher.on_fallback_cleaned(dirty_channel)
Thread(target=clean_up).start()
#
......@@ -427,7 +427,7 @@ class Player:
self.stream_load(entry.channel, entry.source)
time.sleep(1)
retry_delay = self.config.get("input_stream_retry_delay")
retry_delay = self.config.get("input_stream_retry_delay")
max_retries = self.config.get("input_stream_max_retries")
retries = 0
......@@ -458,7 +458,7 @@ class Player:
self.connector.enable_transaction()
result = self.connector.send_lqc_command(channel, "stream_stop")
if result != LiquidsoapResponse.SUCCESS.value:
self.logger.error("%s.stop result: %s" % (channel, result))
raise LQStreamException("Error while stopping stream!")
......@@ -517,7 +517,7 @@ class Player:
#
# Channel Type - Queue
# Channel Type - Queue
#
......@@ -535,9 +535,9 @@ class Player:
if channel not in ChannelType.QUEUE.channels and \
channel not in ChannelType.FALLBACK_QUEUE.channels:
raise InvalidChannelException
self.connector.enable_transaction()
audio_store = self.config.get("audio_source_folder")
audio_store = self.config.abs_audio_store_path()
extension = self.config.get("audio_source_extension")
filepath = ResourceUtil.source_to_filepath(audio_store, source, extension)
self.logger.info(SU.pink(f"{channel}.queue_push('{filepath}')"))
......@@ -607,7 +607,7 @@ class Player:
#
# Channel Type - Playlist
# Channel Type - Playlist
#
......@@ -657,7 +657,7 @@ class Player:
class EngineSplash:
@staticmethod
def splash_screen(component, version):
"""
......@@ -666,11 +666,11 @@ class EngineSplash:
return """\n
█████╗ ██╗ ██╗██████╗ █████╗ ███████╗███╗ ██╗ ██████╗ ██╗███╗ ██╗███████╗
██╔══██╗██║ ██║██╔══██╗██╔══██╗ ██╔════╝████╗ ██║██╔════╝ ██║████╗ ██║██╔════╝
███████║██║ ██║██████╔╝███████║ █████╗ ██╔██╗ ██║██║ ███╗██║██╔██╗ ██║█████╗
██╔══██║██║ ██║██╔══██╗██╔══██║ ██╔══╝ ██║╚██╗██║██║ ██║██║██║╚██╗██║██╔══╝
███████║██║ ██║██████╔╝███████║ █████╗ ██╔██╗ ██║██║ ███╗██║██╔██╗ ██║█████╗
██╔══██║██║ ██║██╔══██╗██╔══██║ ██╔══╝ ██║╚██╗██║██║ ██║██║██║╚██╗██║██╔══╝
██║ ██║╚██████╔╝██║ ██║██║ ██║ ███████╗██║ ╚████║╚██████╔╝██║██║ ╚████║███████╗
╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝╚══════╝
%s v%s - Ready to play!
\n""" % (component, version)
\n""" % (component, version)
......@@ -100,7 +100,7 @@ class AuraMonitor:
self.heartbeat_socket = socket(AF_INET, SOCK_DGRAM)
self.engine_id = self.get_engine_id()
#
# EVENTS
#
......@@ -120,7 +120,7 @@ class AuraMonitor:
else:
self.logger.info("Engine Status: " + SU.green("[OK]"))
self.post_health(status, True)
def on_sick(self, data):
......@@ -165,7 +165,7 @@ class AuraMonitor:
self.update_vitality_status()
else:
self.update_status()
try:
if self.status["lqs"]["active"] \
and self.status["lqs"]["mixer"]["in_filesystem_0"] \
......@@ -179,7 +179,7 @@ class AuraMonitor:
except Exception as e:
self.logger.error("Exception while validating engine status: " + str(e))
self.status["engine"]["status"] = MonitorResponseCode.INVALID_STATE.value
return is_valid
......@@ -222,7 +222,7 @@ class AuraMonitor:
self.status["lqs"]["mixer"] = self.engine.player.mixer.mixer_status()
self.status["lqs"]["mixer_fallback"] = self.engine.player.mixer_fallback.mixer_status()
self.engine.player.connector.disable_transaction()
self.status["api"]["steering"]["url"] = self.config.get("api_steering_status")
self.status["api"]["steering"]["available"] = self.validate_url_connection(self.config.get("api_steering_status"))
......@@ -243,7 +243,7 @@ class AuraMonitor:
self.engine.player.connector.enable_transaction()
self.status["lqs"]["active"] = self.engine.is_connected()
self.engine.player.connector.disable_transaction()
self.status["audio_source"] = self.validate_directory(self.config.get("audio_source_folder"))
self.status["audio_source"] = self.validate_directory(self.config.abs_audio_store_path())
# After first update start the Heartbeat Monitor
if not self.heartbeat_running:
......@@ -259,14 +259,14 @@ class AuraMonitor:
"""
if self.has_valid_status(True):
self.heartbeat_socket.sendto(str.encode("OK"), (self.heartbeat_server, self.heartbeat_port))
# Engine resurrected into normal state
if self.already_invalid:
self.already_invalid = False
status = json.dumps(self.get_status())
self.logger.info(SU.green("OK - Engine turned back into some healthy state!")+"\n"+str(status))
# Route call of event via event dispatcher to provide ability for additional hooks
self.engine.event_dispatcher.on_resurrect({"engine_id": self.engine_id, "status": status})
self.engine.event_dispatcher.on_resurrect({"engine_id": self.engine_id, "status": status})
else:
# Engine turned into invalid state
if not self.already_invalid:
......@@ -329,7 +329,7 @@ class AuraMonitor:
Args:
url (String): The API endpoint to call
Returns:
(dict[]): A Python object representing the JSON structure
"""
......
......@@ -88,11 +88,11 @@ class TrackServiceHandler():
def on_play(self, entry):
"""
Some `PlaylistEntry` started playing. This is likely only a LIVE or STREAM entry.
"""
"""
content_class = ResourceUtil.get_content_class(entry.get_content_type())
if content_class == ResourceClass.FILE:
# Files are handled by "on_metadata" called via Liquidsoap
return
return
diff = (entry.entry_start_actual - entry.entry_start).total_seconds()
self.logger.info("There's a difference of %s seconds between planned and actual start of the entry" % diff)
......@@ -105,16 +105,16 @@ class TrackServiceHandler():
data["track_title"] = entry.meta_data.title
data["track_duration"] = entry.duration
data["track_num"] = entry.entry_num
data["track_type"] = content_class.numeric
data["track_type"] = content_class.numeric
data["playlist_id"] = entry.playlist.playlist_id
data["timeslot_id"] = entry.playlist.timeslot.timeslot_id
data["show_id"] = entry.playlist.timeslot.show_id
data["show_name"] = entry.playlist.timeslot.show_name
data["log_source"] = self.config.get("api_engine_number")
data["log_source"] = self.config.get("api_engine_number")
self.store_trackservice(data)
self.store_clock_info(data)
def on_metadata(self, meta):
......@@ -125,9 +125,9 @@ class TrackServiceHandler():
data["track_start"] = meta.get("on_air")
data["track_artist"] = meta.get("artist")
data["track_album"] = meta.get("album")
data["track_title"] = meta.get("title")
data["track_title"] = meta.get("title")
data["track_type"] = ResourceClass.FILE.numeric
#lqs_source = meta["source"]
#lqs_source = meta["source"]
if "duration" in meta:
duration = float(meta.get("duration"))
......@@ -150,28 +150,28 @@ class TrackServiceHandler():
if timeslot:
data = {**data, **timeslot}
data["playlist_id"] = -1
data["log_source"] = self.config.get("api_engine_number")
data = SU.clean_dictionary(data)
self.store_trackservice(data)
self.store_clock_info(data)
def store_trackservice(self, data):
"""
Posts the given `PlaylistEntry` to the Engine API Playlog.
"""
"""
data = SU.clean_dictionary(data)
self.logger.info("Posting playlog to Engine API...")
self.logger.info("Posting playlog to Engine API...")
url = self.config.get("api_engine_store_playlog")
headers = {'content-type': 'application/json'}
body = json.dumps(data, indent=4, sort_keys=True, default=str)
self.logger.debug("Playlog Data: " + body)
response = requests.post(url, data=body, headers=headers)
if response.status_code != 204 or response.status_code != 204:
msg = f"Error while posting playlog to Engine API: {response.reason} (Error {response.status_code})\n"
if response.status_code != 204 or response.status_code != 204:
msg = f"Error while posting playlog to Engine API: {response.reason} (Error {response.status_code})\n"
self.logger.info(SU.red(msg) + response.content.decode("utf-8"))
......@@ -182,12 +182,12 @@ class TrackServiceHandler():
planned_playlist = None
if self.engine.scheduler:
(fallback_type, planned_playlist) = self.engine.scheduler.get_active_playlist()
(past_timeslot, current_timeslot, next_timeslot) = self.playlog.get_timeslots()
(past_timeslot, current_timeslot, next_timeslot) = self.playlog.get_timeslots()
data = dict()
data["engine_source"] = self.config.get("api_engine_number")
data["engine_source"] = self.config.get("api_engine_number")
if current_timeslot:
if current_timeslot:
data["current_timeslot"] = current_timeslot
if planned_playlist:
......@@ -199,14 +199,14 @@ class TrackServiceHandler():
entry["track_start"] = e.entry_start
if e.meta_data:
entry["track_artist"] = e.meta_data.artist
entry["track_album"] = e.meta_data.album
entry["track_title"] = e.meta_data.title
entry["track_album"] = e.meta_data.album
entry["track_title"] = e.meta_data.title
entry["track_num"] = e.entry_num
entry["track_duration"] = e.duration
content_class = ResourceUtil.get_content_class(e.get_content_type())
entry["track_type"] = content_class.numeric
entry = SU.clean_dictionary(entry)
data["planned_playlist"]["entries"].append(entry)
data["planned_playlist"]["entries"].append(entry)
if next_timeslot:
data["next_timeslot"] = next_timeslot
......@@ -214,15 +214,15 @@ class TrackServiceHandler():
data = SU.clean_dictionary(data)
self.logger.info("Posting clock info update to Engine API...")
self.logger.info("Posting clock info update to Engine API...")
url = self.config.get("api_engine_store_clock")
headers = {'content-type': 'application/json'}
body = json.dumps(data, indent=4, sort_keys=True, default=str)
self.logger.debug("Clock Data: " + body)
response = requests.put(url, data=body, headers=headers)
if response.status_code != 204 or response.status_code != 204:
msg = f"Error while posting clock-info to Engine API: {response.reason} (Error {response.status_code})\n"
self.logger.info(SU.red(msg) + response.content.decode("utf-8"))
if response.status_code != 204 or response.status_code != 204:
msg = f"Error while posting clock-info to Engine API: {response.reason} (Error {response.status_code})\n"
self.logger.info(SU.red(msg) + response.content.decode("utf-8"))
......@@ -276,7 +276,7 @@ class Playlog:
if next_timeslot: