diff --git a/config/sample-development.engine.ini b/config/sample-development.engine.ini index c8496bad2eee3c1ba76496f5f559d53733598182..9633492fcf2aad4ef3da4ae0e5d2543c2498ecb4 100644 --- a/config/sample-development.engine.ini +++ b/config/sample-development.engine.ini @@ -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 diff --git a/config/sample-production.engine.ini b/config/sample-production.engine.ini index 8a66740180a0af155a0d028088d4b8287e34ef2c..718ca0446636b6006feff74ea1cdf626395841b0 100644 --- a/config/sample-production.engine.ini +++ b/config/sample-production.engine.ini @@ -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 diff --git a/src/base/config.py b/src/base/config.py index 14ec5103cd61362f4c61a7d4b9d9bda780cb9ba7..1d4679ba11cd03912e9c782a1921b40c0a98db7b 100644 --- a/src/base/config.py +++ b/src/base/config.py @@ -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")) diff --git a/src/engine.py b/src/engine.py index d0829c7b0273b6f3c87e1c58741aab4984ed9510..07e5e9ebe2f761bf669ca9251185b91c71b4be38 100644 --- a/src/engine.py +++ b/src/engine.py @@ -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) diff --git a/src/plugins/monitor.py b/src/plugins/monitor.py index 5c5bf769e49796549563a8682c91e4c5a9de3b88..3ae57fbf71865499f1f6c1571a6dc9307787b0d2 100644 --- a/src/plugins/monitor.py +++ b/src/plugins/monitor.py @@ -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 """ diff --git a/src/plugins/trackservice.py b/src/plugins/trackservice.py index 1373d48f2f11d9100afae4a075983f8c677983cd..28d6d7f36b6c17e7fee7eee65c16ac0695a56533 100644 --- a/src/plugins/trackservice.py +++ b/src/plugins/trackservice.py @@ -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: data["timeslot_end"] = next_timeslot.timeslot_start - else: + else: data["timeslot_end"] = None self.current_timeslot = data @@ -300,31 +300,31 @@ class Playlog: data = {} next_timeslot = self.engine.scheduler.get_programme().get_next_timeslots(1) - if next_timeslot: + if next_timeslot: next_timeslot = next_timeslot[0] else: next_timeslot = None - + # A valid timeslot from the scheduler is available - if timeslot: + if timeslot: self.assign_fallback_playlist(data, timeslot) data["timeslot_id"] = timeslot.timeslot_id data["timeslot_start"] = timeslot.timeslot_start - data["timeslot_end"] = timeslot.timeslot_end + data["timeslot_end"] = timeslot.timeslot_end data["show_id"] = timeslot.show_id - data["show_name"] = timeslot.show_name + data["show_name"] = timeslot.show_name data = SU.clean_dictionary(data) # Any previous (fake) timeslots should get the proper end now - if not self.previous_timeslot: + if not self.previous_timeslot: self.current_timeslot["timeslot_end"] = timeslot.timeslot_start - self.previous_timeslot = self.current_timeslot + self.previous_timeslot = self.current_timeslot self.current_timeslot = data # Defaults for a not existing timeslot - else: + else: self.init_timeslot(next_timeslot) - + # A valid following timeslot is available self.next_timeslot = None if next_timeslot: @@ -353,7 +353,7 @@ class Playlog: playlist = None if timeslot: - fallback_type, playlist = self.engine.scheduler.fallback.resolve_playlist(timeslot) + fallback_type, playlist = self.engine.scheduler.fallback.resolve_playlist(timeslot) if playlist: data["playlist_id"] = playlist.playlist_id @@ -380,7 +380,7 @@ class Playlog: """ Saves the currently preloaded [`Entry`] to the local cache. """ - self.history.append(entry) + self.history.append(entry) def get_recent_entries(self): @@ -392,7 +392,7 @@ class Playlog: def resolve_entry(self, uri): """ - Retrieves the playlog matching the provied file URI. + Retrieves the playlog matching the provided file URI. Args: path (String): The URI of the resource @@ -407,7 +407,7 @@ class Playlog: entry_source = entry.source if entry.get_content_type() in ResourceClass.FILE.types: - base_dir = self.config.get("audio_source_folder") + base_dir = self.config.abs_audio_store_path() extension = self.config.get("audio_source_extension") entry_source = ResourceUtil.source_to_filepath(base_dir, entry.source, extension) if entry_source == uri: diff --git a/src/scheduling/utils.py b/src/scheduling/utils.py index f406442092b44ab56b9368facfc78cad94c0b698..80c8c1e06609bc6103a166eb9aa10d93719d01f6 100644 --- a/src/scheduling/utils.py +++ b/src/scheduling/utils.py @@ -47,9 +47,9 @@ class TimeslotFilter(): @staticmethod def filter_24h(timeslots): - """ + """ Removes entries 24h in the future and 12 hours in the past. - Note: This might influence resuming (in case of a crash) + Note: This might influence resuming (in case of a crash) single timeslots which are longer than 12 hours long. Think e.g. live broadcasts. """ @@ -71,7 +71,7 @@ class TimeslotFilter(): @staticmethod def filter_past(timeslots): """ - Removes all timeslot dictionaries from the past, except the one which is + Removes all timeslot dictionaries from the past, except the one which is currently playing. """ items = [] @@ -108,9 +108,7 @@ class M3UPlaylistProcessor(): """ self.config = AuraConfig.config() self.logger = logging.getLogger("AuraEngine") - self.playlist_folder = self.config.get("audio_playlist_folder") - if not self.playlist_folder.endswith("/"): - self.playlist_folder += "/" + self.playlist_folder = self.config.abs_playlist_path() @@ -127,7 +125,7 @@ class M3UPlaylistProcessor(): for entry in entries: # It's a M3U Playlist which needs to be spread if "uri" in entry and entry["uri"].startswith("playlist://"): - + playlist_name = entry["uri"].split("playlist://")[1] self.logger.info(f"Spreading entries of M3U playlist '{playlist_name}'") m3u_entries = self.read_m3u_file(self.playlist_folder + playlist_name) @@ -189,7 +187,7 @@ class TimeslotRenderer: def get_ascii_timeslots(self): """ - Creates a printable version of the current programme (playlists and entries as per timeslot) + Creates a printable version of the current programme (playlists and entries as per timeslot) Returns: (String): An ASCII representation of the current and next timeslots @@ -206,13 +204,13 @@ class TimeslotRenderer: (fallback_type, resolved_playlist) = self.scheduler.fallback.resolve_playlist(active_timeslot) s += "\n│ Playing timeslot %s " % active_timeslot - if planned_playlist: + if planned_playlist: if resolved_playlist and resolved_playlist.playlist_id != planned_playlist.playlist_id: s += "\n│ └── Playlist %s " % planned_playlist s += "\n│ " s += SU.red("↑↑↑ That's the originally planned playlist.") + ("Instead playing the fallback playlist below ↓↓↓") - if resolved_playlist: + if resolved_playlist: if not planned_playlist: s += "\n│ " s += SU.red("No playlist assigned to timeslot. Instead playing the `%s` playlist below ↓↓↓" % SU.cyan(str(fallback_type))) @@ -237,7 +235,7 @@ class TimeslotRenderer: rest_of_playlist = active_entry.get_next_entries(False) entries = self.preprocess_entries(rest_of_playlist, False) s += self.build_playlist_string(entries) - + else: s += "\n│ └── %s" % (SU.red("No active playlist. There should be at least some fallback playlist running...")) else: @@ -251,7 +249,7 @@ class TimeslotRenderer: if not next_timeslots: s += "\n│ Nothing. " else: - for timeslot in next_timeslots: + for timeslot in next_timeslots: (fallback_type, resolved_playlist) = self.scheduler.fallback.resolve_playlist(timeslot) if resolved_playlist: @@ -260,7 +258,7 @@ class TimeslotRenderer: if resolved_playlist.end_unix > timeslot.end_unix: s += "\n│ %s! " % \ (SU.red("↑↑↑ Playlist #%s ends after timeslot #%s!" % (resolved_playlist.playlist_id, timeslot.timeslot_id))) - + entries = self.preprocess_entries(resolved_playlist.entries, False) s += self.build_playlist_string(entries) @@ -268,7 +266,7 @@ class TimeslotRenderer: return s - + def build_playlist_string(self, entries): """ Returns a stringified list of entries @@ -329,7 +327,7 @@ class TimeslotRenderer: entry.queue_state = EntryQueueState.CUT else: entry.queue_state = EntryQueueState.OKAY - + if not entry.queue_state == EntryQueueState.OUT_OF_SCHEDULE or not cut_oos: clean_entries.append(entry)