From 90256a99885c3391914645a0b31d144090c49dec Mon Sep 17 00:00:00 2001 From: Lars Kruse <devel@sumpfralle.de> Date: Thu, 17 Feb 2022 00:37:42 +0100 Subject: [PATCH] style: apply "black" formatting --- meta.py | 2 +- src/base/config.py | 25 ++-- src/base/exceptions.py | 11 +- src/base/logger.py | 18 +-- src/base/utils.py | 15 +-- src/client/client.py | 74 +++++++---- src/client/connector.py | 132 ++++++++++++++------ src/client/playerclient.py | 55 +++------ src/engine.py | 1 - src/mixer.py | 136 +++++++++++--------- src/plugins/mailer.py | 57 +++++---- src/scheduling/api.py | 133 ++++++++++++-------- src/scheduling/models.py | 1 - src/scheduling/programme.py | 226 ++++++++++++++++++++++------------ src/scheduling/scheduler.py | 11 +- src/scheduling/utils.py | 103 +++++++++------- tests/test_config.py | 15 +-- tests/test_engine_executor.py | 84 +++++++------ tests/test_logger.py | 10 +- 19 files changed, 654 insertions(+), 455 deletions(-) diff --git a/meta.py b/meta.py index 80d8f610..30999f8a 100644 --- a/meta.py +++ b/meta.py @@ -7,4 +7,4 @@ __version__ = "0.9.9" __version_info__ = (0, 9, 9) __maintainer__ = "David Trattnig" __email__ = "david.trattnig@subsquare.at" -__status__ = "Development" \ No newline at end of file +__status__ = "Development" diff --git a/src/base/config.py b/src/base/config.py index 00dcd70f..3336b659 100644 --- a/src/base/config.py +++ b/src/base/config.py @@ -32,11 +32,11 @@ class AuraConfig: Holds the Engine Configuration as in the file `engine.ini`. """ + instance = None ini_path = "" logger = None - def __init__(self, ini_path="/etc/aura/engine.ini"): """ Initializes the configuration, defaults to `/etc/aura/engine.ini`. @@ -49,7 +49,9 @@ class AuraConfig: 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() + ini_path = ( + "%s/config/engine.ini" % Path(__file__).parent.parent.parent.absolute() + ) self.ini_path = ini_path self.load_config() @@ -59,8 +61,6 @@ class AuraConfig: self.set("config_dir", os.path.dirname(ini_path)) self.set("install_dir", os.path.realpath(__file__ + "../../../..")) - - @staticmethod def config(): """ @@ -68,8 +68,6 @@ class AuraConfig: """ return AuraConfig.instance - - def set(self, key, value): """ Setter for some specific config property. @@ -83,8 +81,6 @@ class AuraConfig: except: self.__dict__[key] = str(value) - - def get(self, key, default=None): """ Getter for some specific config property. @@ -97,7 +93,9 @@ class AuraConfig: if default: self.set(key, default) else: - self.logger.warning("Key " + key + " not found in configfile " + self.ini_path + "!") + self.logger.warning( + "Key " + key + " not found in configfile " + self.ini_path + "!" + ) return None value = self.__dict__[key] @@ -105,7 +103,6 @@ class AuraConfig: value = os.path.expandvars(value) return value - def get_database_uri(self): """ Retrieves the database connection string. @@ -127,7 +124,6 @@ class AuraConfig: else: return f"Error: invalid database type '{db_type}'" - def load_config(self): """ Set config defaults and load settings from file @@ -137,7 +133,7 @@ class AuraConfig: sys.exit(1) # Read the file - f = open(self.ini_path, 'r') + f = open(self.ini_path, "r") ini_str = f.read() f.close() @@ -151,10 +147,9 @@ class AuraConfig: for section in config_parser.sections(): for key, value in config_parser.items(section): - v = config_parser.get(section, key).replace('"', '').strip() + v = config_parser.get(section, key).replace('"', "").strip() self.set(key, v) - def to_abs_path(self, path): """ Transforms any given (relative) path to an absolute paths @@ -165,14 +160,12 @@ class AuraConfig: else: 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 diff --git a/src/base/exceptions.py b/src/base/exceptions.py index da55ab28..fca60ad9 100644 --- a/src/base/exceptions.py +++ b/src/base/exceptions.py @@ -17,37 +17,42 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. - # Scheduler Exceptions + class NoProgrammeLoadedException(Exception): pass + class NoActiveTimeslotException(Exception): pass # Soundsystem and Mixer Exceptions + class LoadSourceException(Exception): pass + class InvalidChannelException(Exception): pass + class PlaylistException(Exception): pass + class NoActiveEntryException(Exception): pass - # Liquidsoap Exceptions + class LQConnectionError(Exception): pass + class LQStreamException(Exception): pass - diff --git a/src/base/logger.py b/src/base/logger.py index d5f9e107..bfe58615 100644 --- a/src/base/logger.py +++ b/src/base/logger.py @@ -22,24 +22,24 @@ import logging from src.base.config import AuraConfig -class AuraLogger(): +class AuraLogger: """ AuraLogger Class Logger for all Aura Engine components. The default logger is `AuraEngine`. Other loggers are defined - by passing a custom name on instantiation. - + by passing a custom name on instantiation. + The logger respects the log-level as defined in the engine's configuration file. """ + config = None logger = None - def __init__(self, config, name="AuraEngine"): """ - Constructor to create a new logger defined by + Constructor to create a new logger defined by the passed name. Args: @@ -49,7 +49,6 @@ class AuraLogger(): lvl = self.get_log_level() self.create_logger(name, lvl) - def get_log_level(self): """ Retrieves the configured log level (default=INFO). @@ -67,7 +66,6 @@ class AuraLogger(): else: return logging.CRITICAL - def create_logger(self, name, lvl): """ Creates the logger instance for the given name. @@ -81,7 +79,9 @@ class AuraLogger(): if not self.logger.hasHandlers(): # create file handler for logger - file_handler = logging.FileHandler(self.config.get("log_dir") + "/"+name+".log") + file_handler = logging.FileHandler( + self.config.get("log_dir") + "/" + name + ".log" + ) file_handler.setLevel(lvl) # create stream handler for logger @@ -104,4 +104,4 @@ class AuraLogger(): self.logger.debug("ADDED HANDLERS") else: - self.logger.debug("REUSED LOGGER") \ No newline at end of file + self.logger.debug("REUSED LOGGER") diff --git a/src/base/utils.py b/src/base/utils.py index 3e9c0179..14074337 100644 --- a/src/base/utils.py +++ b/src/base/utils.py @@ -23,13 +23,11 @@ import time from enum import Enum - class SimpleUtil: """ A container class for simple utility methods. """ - @staticmethod def clean_dictionary(data): """ @@ -49,9 +47,8 @@ class SimpleUtil: SimpleUtil.clean_dictionary(value) return data - @staticmethod - def to_datetime(datetime_str:str): + def to_datetime(datetime_str: str): """ Converts a timezone aware date-time string into `datetime`. """ @@ -59,7 +56,6 @@ class SimpleUtil: return datetime.datetime.fromisoformat(datetime_str) return None - @staticmethod def fmt_time(timestamp): """ @@ -71,8 +67,7 @@ class SimpleUtil: Returns: (String): Displaying the time """ - return datetime.datetime.fromtimestamp(timestamp).strftime('%H:%M:%S') - + return datetime.datetime.fromtimestamp(timestamp).strftime("%H:%M:%S") @staticmethod def nano_to_seconds(nanoseconds): @@ -87,7 +82,6 @@ class SimpleUtil: """ return float(nanoseconds / 1000000000) - @staticmethod def seconds_to_nano(seconds): """ @@ -101,7 +95,6 @@ class SimpleUtil: """ return int(seconds * 1000000000) - @staticmethod def timestamp(date_and_time=None): """ @@ -118,7 +111,6 @@ class SimpleUtil: date_and_time = datetime.datetime.now() return time.mktime(date_and_time.timetuple()) - @staticmethod def strike(text): """ @@ -192,7 +184,6 @@ class SimpleUtil: return TerminalColors.CYAN.value + text + TerminalColors.ENDC.value - class TerminalColors(Enum): """ Colors for formatting terminal output. @@ -214,4 +205,4 @@ class TerminalColors(Enum): UNDERLINE = "\033[4m" STRIKE = "\u0336" - ENDC = "\033[0m" \ No newline at end of file + ENDC = "\033[0m" diff --git a/src/client/client.py b/src/client/client.py index 1ecd030f..9d499d8f 100644 --- a/src/client/client.py +++ b/src/client/client.py @@ -22,12 +22,10 @@ import urllib.parse import configparser import logging -from multiprocessing import Lock - -from src.base.exceptions import LQConnectionError -from src.base.utils import TerminalColors - +from multiprocessing import Lock +from src.base.exceptions import LQConnectionError +from src.base.utils import TerminalColors class LiquidSoapClient: @@ -38,6 +36,7 @@ class LiquidSoapClient: #TODO Refactor class: https://gitlab.servus.at/aura/engine/-/issues/65 """ + mutex = None logger = None debug = False @@ -60,7 +59,7 @@ class LiquidSoapClient: self.mutex = Lock() self.connected = False self.can_connect = True - self.message = '' + self.message = "" self.socket = None self.metareader = configparser.ConfigParser() @@ -74,18 +73,24 @@ class LiquidSoapClient: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.connect(self.socket_path) except socket.error as e: - msg = "Cannot connect to socketpath " + self.socket_path + ". Reason: "+str(e) - self.logger.critical(TerminalColors.RED.value+msg+TerminalColors.ENDC.value) + msg = ( + "Cannot connect to socketpath " + + self.socket_path + + ". Reason: " + + str(e) + ) + self.logger.critical( + TerminalColors.RED.value + msg + TerminalColors.ENDC.value + ) self.can_connect = False self.connected = False -# raise e + # raise e else: self.can_connect = True self.connected = True return True - -# AttributeError('characters_written') + # AttributeError('characters_written') # ------------------------------------------------------------------------------------------ # def is_connected(self): @@ -113,7 +118,7 @@ class LiquidSoapClient: # make socket non blocking # self.client.setblocking(0) - data = '' + data = "" try: # set timeout @@ -134,7 +139,9 @@ class LiquidSoapClient: self.mutex.release() except Exception as e: - self.logger.error(TerminalColors.RED.value+str(e)+TerminalColors.ENDC.value) + self.logger.error( + TerminalColors.RED.value + str(e) + TerminalColors.ENDC.value + ) self.mutex.release() return data @@ -150,7 +157,7 @@ class LiquidSoapClient: ret = self.read_all().splitlines() try: - last = ret.pop() # pop out end + last = ret.pop() # pop out end if len(ret) > 1: self.message = str.join(" - ", ret) @@ -190,23 +197,34 @@ class LiquidSoapClient: @return: Die Antwort des Liquidsoap-Servers """ - param = (param.strip() if param.strip() == "" else " " + urllib.parse.unquote(param.strip())) + param = ( + param.strip() + if param.strip() == "" + else " " + urllib.parse.unquote(param.strip()) + ) if self.connected: # print namespace + '.' + command + param + "\n" if namespace == "": message = str(command) + str(param) + str("\n") else: - message = str(namespace) + str(".") + str(command) + str(param) + str("\n") + message = ( + str(namespace) + str(".") + str(command) + str(param) + str("\n") + ) try: if not self.disable_logging: - self.logger.debug("LiquidSoapClient sending to LiquidSoap Server: " + message[0:len(message)-1]) + self.logger.debug( + "LiquidSoapClient sending to LiquidSoap Server: " + + message[0 : len(message) - 1] + ) # send all the stuff over the socket to liquidsoap server self.socket.sendall(message.encode()) if not self.disable_logging: - self.logger.debug("LiquidSoapClient waiting for reply from LiquidSoap Server") + self.logger.debug( + "LiquidSoapClient waiting for reply from LiquidSoap Server" + ) # wait for reply self.read() @@ -214,7 +232,15 @@ class LiquidSoapClient: if not self.disable_logging: self.logger.debug("LiquidSoapClient got reply: " + self.message) except BrokenPipeError as e: - self.logger.error(TerminalColors.RED.value+"Detected a problem with liquidsoap connection while sending: " + message + ". Reason: " + str(e) + "! Trying to reconnect."+TerminalColors.RED.value) + self.logger.error( + TerminalColors.RED.value + + "Detected a problem with liquidsoap connection while sending: " + + message + + ". Reason: " + + str(e) + + "! Trying to reconnect." + + TerminalColors.RED.value + ) self.connect() raise @@ -236,7 +262,7 @@ class LiquidSoapClient: @return: the response of the liquidsoap server """ if self.connected: - self.command('help', '') + self.command("help", "") return self.message # ------------------------------------------------------------------------------------------ # @@ -247,8 +273,8 @@ class LiquidSoapClient: @return: the response of the liquidsoap server """ if self.connected: - message = 'version' - self.command(message, '') + message = "version" + self.command(message, "") return self.message # ------------------------------------------------------------------------------------------ # @@ -260,7 +286,7 @@ class LiquidSoapClient: """ if self.connected: - self.command('uptime', '') + self.command("uptime", "") return self.message # ------------------------------------------------------------------------------------------ # @@ -273,4 +299,4 @@ class LiquidSoapClient: if self.connected: self.command("", "quit") - return self.message \ No newline at end of file + return self.message diff --git a/src/client/connector.py b/src/client/connector.py index b8387ff7..9345538e 100644 --- a/src/client/connector.py +++ b/src/client/connector.py @@ -20,19 +20,19 @@ import logging import time -from src.base.config import AuraConfig -from src.base.utils import TerminalColors, SimpleUtil as SU -from src.base.exceptions import LQConnectionError -from src.client.playerclient import LiquidSoapPlayerClient +from src.base.config import AuraConfig +from src.base.utils import TerminalColors, SimpleUtil as SU +from src.base.exceptions import LQConnectionError +from src.client.playerclient import LiquidSoapPlayerClient - -class PlayerConnector(): +class PlayerConnector: """ Establishes a Socket connection to Liquidsoap. #TODO Refactor class: https://gitlab.servus.at/aura/engine/-/issues/65 """ + client = None logger = None transaction = 0 @@ -41,7 +41,6 @@ class PlayerConnector(): event_dispatcher = None has_connection = None - def __init__(self, event_dispatcher): """ Constructor @@ -55,7 +54,6 @@ class PlayerConnector(): self.event_dispatcher = event_dispatcher self.has_connection = False - def send_lqc_command(self, namespace, command, *args): """ Ein Kommando an Liquidsoap senden @@ -74,36 +72,45 @@ class PlayerConnector(): try: if not self.disable_logging: if command == "": - self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + str(args)) + self.logger.debug( + "LiquidSoapCommunicator is calling " + + str(namespace) + + str(args) + ) else: - self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + "." + str(command) + str(args)) + self.logger.debug( + "LiquidSoapCommunicator is calling " + + str(namespace) + + "." + + str(command) + + str(args) + ) # call wanted function ... # FIXME REFACTOR all calls in a common way - if command in [ - "queue_push", - "queue_seek", - "queue_clear", - "playlist_uri_set", - "playlist_uri_clear", - "stream_set_url", - "stream_start", - "stream_stop", - "stream_status", - ]: + if command in [ + "queue_push", + "queue_seek", + "queue_clear", + "playlist_uri_set", + "playlist_uri_clear", + "stream_set_url", + "stream_start", + "stream_stop", + "stream_status", + ]: func = getattr(lqs_instance, command) result = func(str(namespace), *args) - elif namespace == "mixer": # or namespace == "mixer_fallback": + elif namespace == "mixer": # or namespace == "mixer_fallback": func = getattr(lqs_instance, command) result = func(str(namespace), *args) else: func = getattr(lqs_instance, namespace) result = func(command, *args) - if not self.disable_logging: self.logger.debug("LiquidSoapCommunicator got response " + str(result)) @@ -112,14 +119,26 @@ class PlayerConnector(): return result except LQConnectionError as e: - self.logger.error("Connection Error when sending " + str(namespace) + "." + str(command) + str(args)) + self.logger.error( + "Connection Error when sending " + + str(namespace) + + "." + + str(command) + + str(args) + ) if self.try_to_reconnect(): time.sleep(0.2) self.connection_attempts += 1 if self.connection_attempts < 5: # reconnect self.__open_conn(self.client) - self.logger.info("Trying to resend " + str(namespace) + "." + str(command) + str(args)) + self.logger.info( + "Trying to resend " + + str(namespace) + + "." + + str(command) + + str(args) + ) # grab return value retval = self.send_lqc_command(namespace, command, *args) # disconnect @@ -128,19 +147,31 @@ class PlayerConnector(): return retval else: if command == "": - msg = "Rethrowing Exception while trying to send " + str(namespace) + str(args) + msg = ( + "Rethrowing Exception while trying to send " + + str(namespace) + + str(args) + ) else: - msg = "Rethrowing Exception while trying to send " + str(namespace) + "." + str(command) + str(args) + msg = ( + "Rethrowing Exception while trying to send " + + str(namespace) + + "." + + str(command) + + str(args) + ) self.logger.info(msg) self.disable_transaction(socket=self.client, force=True) raise e else: - self.event_dispatcher.on_critical("Critical Liquidsoap connection issue", \ - "Could not connect to Liquidsoap after multiple attempts", e) + self.event_dispatcher.on_critical( + "Critical Liquidsoap connection issue", + "Could not connect to Liquidsoap after multiple attempts", + e, + ) raise e - # ------------------------------------------------------------------------------------------ # def try_to_reconnect(self): self.enable_transaction() @@ -154,7 +185,12 @@ class PlayerConnector(): self.transaction = self.transaction + 1 - self.logger.debug(TerminalColors.WARNING.value + "Enabling transaction! cnt: " + str(self.transaction) + TerminalColors.ENDC.value) + self.logger.debug( + TerminalColors.WARNING.value + + "Enabling transaction! cnt: " + + str(self.transaction) + + TerminalColors.ENDC.value + ) if self.transaction > 1: return @@ -164,11 +200,14 @@ class PlayerConnector(): except FileNotFoundError: self.disable_transaction(socket=socket, force=True) subject = "CRITICAL Exception when connecting to Liquidsoap" - msg = "socket file " + socket.socket_path + " not found. Is liquidsoap running?" + msg = ( + "socket file " + + socket.socket_path + + " not found. Is liquidsoap running?" + ) self.logger.critical(SU.red(msg)) # Not using this for now, as it should be triggered by "on_sick(..)" as well - #self.event_dispatcher.on_critical(subject, msg, None) - + # self.event_dispatcher.on_critical(subject, msg, None) # ------------------------------------------------------------------------------------------ # def disable_transaction(self, socket=None, force=False): @@ -181,13 +220,22 @@ class PlayerConnector(): self.transaction = self.transaction - 1 # debug msg - self.logger.debug(TerminalColors.WARNING.value + "DISabling transaction! cnt: " + str(self.transaction) + TerminalColors.ENDC.value) + self.logger.debug( + TerminalColors.WARNING.value + + "DISabling transaction! cnt: " + + str(self.transaction) + + TerminalColors.ENDC.value + ) # return if connection is still needed if self.transaction > 0: return else: - self.logger.debug(TerminalColors.WARNING.value + "Forcefully DISabling transaction! " + TerminalColors.ENDC.value) + self.logger.debug( + TerminalColors.WARNING.value + + "Forcefully DISabling transaction! " + + TerminalColors.ENDC.value + ) # close conn and set transactioncounter to 0 self.__close_conn(socket) @@ -202,7 +250,11 @@ class PlayerConnector(): if self.has_connection: return - self.logger.debug(TerminalColors.GREEN.value + "LiquidSoapCommunicator opening conn" + TerminalColors.ENDC.value) + self.logger.debug( + TerminalColors.GREEN.value + + "LiquidSoapCommunicator opening conn" + + TerminalColors.ENDC.value + ) # try to connect socket.connect() @@ -222,4 +274,8 @@ class PlayerConnector(): # socket.byebye() # debug msg - self.logger.debug(TerminalColors.BLUE.value + "LiquidSoapCommunicator closed conn" + TerminalColors.ENDC.value) + self.logger.debug( + TerminalColors.BLUE.value + + "LiquidSoapCommunicator closed conn" + + TerminalColors.ENDC.value + ) diff --git a/src/client/playerclient.py b/src/client/playerclient.py index 7d96f43c..3aa2fb87 100644 --- a/src/client/playerclient.py +++ b/src/client/playerclient.py @@ -22,7 +22,7 @@ from src.client.client import LiquidSoapClient class LiquidSoapPlayerClient(LiquidSoapClient): - #TODO Refactor class: https://gitlab.servus.at/aura/engine/-/issues/65 + # TODO Refactor class: https://gitlab.servus.at/aura/engine/-/issues/65 # # Mixer @@ -48,14 +48,13 @@ class LiquidSoapPlayerClient(LiquidSoapClient): # return "LiquidSoapPlayerClient does not understand mixer."+command+str(args) - # ------------------------------------------------------------------------------------------ # def mixer_inputs(self, mixer_id): # send command self.command(mixer_id, "inputs") # convert to list and return it - return self.message.strip().split(' ') + return self.message.strip().split(" ") # ------------------------------------------------------------------------------------------ # def mixer_status(self, mixer_id, pos=""): @@ -69,7 +68,6 @@ class LiquidSoapPlayerClient(LiquidSoapClient): self.command(mixer_id, "status", str(pos)) return self.message - def mixer_volume(self, mixer_id, pos, volume): """ Sets some mixer channel to the given volume @@ -84,7 +82,6 @@ class LiquidSoapPlayerClient(LiquidSoapClient): self.command(mixer_id, "volume", str(pos) + " " + str(volume)) return self.message - def mixer_select(self, mixer_id, pos, select): """ Selects some mixer channel or vice versa. @@ -99,7 +96,6 @@ class LiquidSoapPlayerClient(LiquidSoapClient): self.command(mixer_id, "select", str(pos) + " " + str(select).lower()) return self.message - def mixer_activate(self, mixer_id, pos, activate): """ Selects some mixer channel and increases the volume to 100 or vice versa. @@ -114,7 +110,6 @@ class LiquidSoapPlayerClient(LiquidSoapClient): self.command(mixer_id, "activate", str(pos) + " " + str(activate).lower()) return self.message - # # Queues # @@ -127,10 +122,9 @@ class LiquidSoapPlayerClient(LiquidSoapClient): channel (String): Liquidsoap Source ID uri (String): Path to the file """ - self.command(channel, 'push', uri) + self.command(channel, "push", uri) return self.message - def queue_seek(self, channel, duration): """ Forward the playing `equeue` track/playlist of the given channel. @@ -142,10 +136,9 @@ class LiquidSoapPlayerClient(LiquidSoapClient): Returns: Liquidsoap server response """ - self.command(channel, 'seek', str(duration)) + self.command(channel, "seek", str(duration)) return self.message - def queue_clear(self, channel): """ Clears all `equeue` playlist entries of the given channel. @@ -157,10 +150,9 @@ class LiquidSoapPlayerClient(LiquidSoapClient): Returns: Liquidsoap server response """ - self.command(channel, 'clear') + self.command(channel, "clear") return self.message - # # Playlist # @@ -176,10 +168,9 @@ class LiquidSoapPlayerClient(LiquidSoapClient): Returns: Liquidsoap server response """ - self.command(channel, 'uri', uri) + self.command(channel, "uri", uri) return self.message - def playlist_uri_clear(self, channel): """ Clears the URI of a playlist source. @@ -191,10 +182,9 @@ class LiquidSoapPlayerClient(LiquidSoapClient): Returns: Liquidsoap server response """ - self.command(channel, 'clear') + self.command(channel, "clear") return self.message - # # Stream # @@ -203,34 +193,30 @@ class LiquidSoapPlayerClient(LiquidSoapClient): """ Sets the URL on the given HTTP channel. """ - self.command(channel, 'url', url) + self.command(channel, "url", url) return self.message - def stream_start(self, channel): """ Starts the HTTP stream set with `stream_set_url` on the given channel. """ - self.command(channel, 'start') + self.command(channel, "start") return self.message - def stream_stop(self, channel): """ Stops the HTTP stream on the given channel. """ - self.command(channel, 'stop') + self.command(channel, "stop") return self.message - def stream_status(self, channel): """ Returns the status of the HTTP stream on the given channel. """ - self.command(channel, 'status') + self.command(channel, "status") return self.message - # # General Entries # @@ -248,25 +234,22 @@ class LiquidSoapPlayerClient(LiquidSoapClient): self.command("request", "status", str(rid)) return self.message - # # Other # - def uptime(self, command=""): # no command will come + def uptime(self, command=""): # no command will come """ Retrieves how long the engine is running already. """ return self.command("", "uptime") - - def version(self, command=""): # no command will come + def version(self, command=""): # no command will come """ Retrieves the Liquidsoap version. """ return self.command("", "version") - def engine(self, command, *args): """ Retrieves the state of all input and outputs. @@ -274,17 +257,17 @@ class LiquidSoapPlayerClient(LiquidSoapClient): if command == "state": return self.engine_state() - return "LiquidSoapPlayerClient does not understand engine." + command + str(args) - + return ( + "LiquidSoapPlayerClient does not understand engine." + command + str(args) + ) def engine_state(self): """ Retrieves the state of all input and outputs. """ - self.command('auraengine', 'state') + self.command("auraengine", "state") return self.message - # ------------------------------------------------------------------------------------------ # # def skip(self, namespace="playlist", pos=""): # """ @@ -446,7 +429,7 @@ class LiquidSoapPlayerClient(LiquidSoapClient): @rtype: string @return: Die Antwort des Liquidsoap-Servers """ - self.command('volume', namespace, str(pos) + ' ' + str(volume)) + self.command("volume", namespace, str(pos) + " " + str(volume)) return self.message # ------------------------------------------------------------------------------------------ # @@ -475,4 +458,4 @@ class LiquidSoapPlayerClient(LiquidSoapClient): # else: # self.success('00', channels) - # self.notifyClient() \ No newline at end of file + # self.notifyClient() diff --git a/src/engine.py b/src/engine.py index 392b8982..2f62578a 100644 --- a/src/engine.py +++ b/src/engine.py @@ -583,7 +583,6 @@ class Player: self.mixer.channel_activate(channel.value, True) self.mixer.channel_activate(channel.value, False) - Thread(target=clean_up).start() # diff --git a/src/mixer.py b/src/mixer.py index a1ed2a67..9a3efa9c 100644 --- a/src/mixer.py +++ b/src/mixer.py @@ -1,4 +1,3 @@ - # # Aura Engine (https://gitlab.servus.at/aura/engine) # @@ -18,27 +17,24 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. - import logging import time -from enum import Enum - -from src.base.exceptions import LQConnectionError -from src.base.utils import SimpleUtil as SU +from enum import Enum +from src.base.exceptions import LQConnectionError +from src.base.utils import SimpleUtil as SU class MixerType(Enum): """ Types of mixers mapped to the Liquidsoap mixer ids. """ + MAIN = "mixer" # FALLBACK = "mixer_fallback" - - class MixerUtil: """ Little helpers for the mixer. @@ -57,11 +53,11 @@ class MixerUtil: return s - -class Mixer(): +class Mixer: """ A virtual mixer. """ + config = None logger = None connector = None @@ -70,7 +66,6 @@ class Mixer(): fade_in_active = None fade_out_active = None - def __init__(self, config, mixer_id, connector): """ Constructor @@ -86,25 +81,22 @@ class Mixer(): self.connector = connector self.mixer_initialize() - # # Mixer # - def mixer_initialize(self): """ - - Pull all faders down to volume 0. - - Initialize default channels per type + - Pull all faders down to volume 0. + - Initialize default channels per type """ self.connector.enable_transaction() - time.sleep(1) # TODO Check is this is still required + time.sleep(1) # TODO Check is this is still required channels = self.mixer_channels_reload() for channel in channels: self.channel_volume(channel, "0") self.connector.disable_transaction() - def mixer_status(self): """ Returns the state of all mixer channels @@ -122,16 +114,16 @@ class Mixer(): self.connector.disable_transaction() return inputstate - def mixer_channels(self): """ Retrieves all mixer channels """ if self.channels is None or len(self.channels) == 0: - self.channels = self.connector.send_lqc_command(self.mixer_id.value, "mixer_inputs") + self.channels = self.connector.send_lqc_command( + self.mixer_id.value, "mixer_inputs" + ) return self.channels - def mixer_channels_selected(self): """ Retrieves all selected channels of the mixer. @@ -151,7 +143,6 @@ class Mixer(): self.connector.disable_transaction() return activeinputs - def mixer_channels_except(self, input_type): """ Retrieves all mixer channels except the ones of the given type. @@ -160,13 +151,15 @@ class Mixer(): activemixer_copy = self.mixer_channels().copy() activemixer_copy.remove(input_type) except ValueError as e: - self.logger.error("Requested channel (%s) not in channel-list. Reason: %s" % (input_type, str(e))) + self.logger.error( + "Requested channel (%s) not in channel-list. Reason: %s" + % (input_type, str(e)) + ) except AttributeError: self.logger.critical("Empty channel list") return activemixer_copy - def mixer_channels_reload(self): """ Reloads all mixer channels. @@ -174,7 +167,6 @@ class Mixer(): self.channels = None return self.mixer_channels() - # # Channel # @@ -192,11 +184,12 @@ class Mixer(): channels = self.mixer_channels() index = channels.index(channel) if index < 0: - self.logger.critical(f"There's no valid channel number for channel ID '{channel.value}'") + self.logger.critical( + f"There's no valid channel number for channel ID '{channel.value}'" + ) return None return index - def channel_status(self, channel_number): """ Retrieves the status of a channel identified by the channel number. @@ -207,8 +200,9 @@ class Mixer(): Returns: (String): Channel status info as a String """ - return self.connector.send_lqc_command(self.mixer_id.value, "mixer_status", channel_number) - + return self.connector.send_lqc_command( + self.mixer_id.value, "mixer_status", channel_number + ) def channel_select(self, channel, select): """ @@ -228,12 +222,14 @@ class Mixer(): if len(channel) < 1: self.logger.critical("Cannot select channel. There are no channels!") else: - message = self.connector.send_lqc_command(self.mixer_id.value, "mixer_select", index, select) + message = self.connector.send_lqc_command( + self.mixer_id.value, "mixer_select", index, select + ) return message except Exception as e: - self.logger.critical("Ran into exception when selecting channel. Reason: " + str(e)) - - + self.logger.critical( + "Ran into exception when selecting channel. Reason: " + str(e) + ) def channel_activate(self, channel, activate): """ @@ -255,11 +251,14 @@ class Mixer(): if len(channel) < 1: self.logger.critical("Cannot activate channel. There are no channels!") else: - message = self.connector.send_lqc_command(self.mixer_id.value, "mixer_activate", index, activate) + message = self.connector.send_lqc_command( + self.mixer_id.value, "mixer_activate", index, activate + ) return message except Exception as e: - self.logger.critical("Ran into exception when activating channel. Reason: " + str(e)) - + self.logger.critical( + "Ran into exception when activating channel. Reason: " + str(e) + ) def channel_current_volume(self, channel): """ @@ -272,10 +271,11 @@ class Mixer(): if volume: return int(volume.split("%")[0]) else: - self.logger.error(f"Invalid volume for channel {channel.value} (status: '{status}'") + self.logger.error( + f"Invalid volume for channel {channel.value} (status: '{status}'" + ) return 0 - def channel_volume(self, channel, volume): """ Set volume of a channel @@ -299,31 +299,51 @@ class Mixer(): try: if len(channel) < 1: - msg = SU.red("Cannot set volume of channel " + channel + " to " + str(volume) + "! There are no channels.") + msg = SU.red( + "Cannot set volume of channel " + + channel + + " to " + + str(volume) + + "! There are no channels." + ) self.logger.warning(msg) else: - message = self.connector.send_lqc_command(self.mixer_id.value, "mixer_volume", str(index), str(int(volume))) + message = self.connector.send_lqc_command( + self.mixer_id.value, "mixer_volume", str(index), str(int(volume)) + ) if not self.connector.disable_logging: - if message.find('volume=' + str(volume) + '%'): - self.logger.info(SU.pink("Set volume of channel '%s' to %s" % (channel, str(volume)))) + if message.find("volume=" + str(volume) + "%"): + self.logger.info( + SU.pink( + "Set volume of channel '%s' to %s" + % (channel, str(volume)) + ) + ) else: - msg = SU.red("Setting volume of channel " + channel + " has gone wrong! Liquidsoap message: " + message) + msg = SU.red( + "Setting volume of channel " + + channel + + " has gone wrong! Liquidsoap message: " + + message + ) self.logger.warning(msg) return message - except AttributeError as e: #(LQConnectionError, AttributeError): + except AttributeError as e: # (LQConnectionError, AttributeError): self.connector.disable_transaction(force=True) - msg = SU.red("Ran into exception when setting volume of channel " + channel + ". Reason: " + str(e)) + msg = SU.red( + "Ran into exception when setting volume of channel " + + channel + + ". Reason: " + + str(e) + ) self.logger.error(msg) - - # # Fading # - def fade_in(self, channel, volume): """ Performs a fade-in for the given channel. @@ -339,10 +359,14 @@ class Mixer(): current_volume = self.channel_current_volume(channel) if current_volume == volume: - self.logger.warning(f"Current volume for channel {channel.value} is already at target volume of {volume}% SKIPPING...") + self.logger.warning( + f"Current volume for channel {channel.value} is already at target volume of {volume}% SKIPPING..." + ) return elif current_volume > volume: - self.logger.warning(f"Current volume {current_volume}% of channel {channel.value} exceeds target volume of {volume}% SKIPPING...") + self.logger.warning( + f"Current volume {current_volume}% of channel {channel.value} exceeds target volume of {volume}% SKIPPING..." + ) return fade_in_time = float(self.config.get("fade_in_time")) @@ -353,8 +377,10 @@ class Mixer(): step = fade_in_time / target_volume - msg = "Starting to fading-in '%s'. Step is %ss and target volume is %s." % \ - (channel, str(step), str(target_volume)) + msg = ( + "Starting to fading-in '%s'. Step is %ss and target volume is %s." + % (channel, str(step), str(target_volume)) + ) self.logger.info(SU.pink(msg)) # Enable logging, which might have been disabled in a previous fade-out @@ -378,8 +404,6 @@ class Mixer(): return False return True - - def fade_out(self, channel, volume=None): """ Performs a fade-out for the given channel starting at its current volume. @@ -397,7 +421,9 @@ class Mixer(): volume = current_volume if current_volume == 0: - self.logger.warning(f"Current volume for channel {channel.value} is already at target volume of 0%. SKIPPING...") + self.logger.warning( + f"Current volume for channel {channel.value} is already at target volume of 0%. SKIPPING..." + ) return fade_out_time = float(self.config.get("fade_out_time")) @@ -413,7 +439,7 @@ class Mixer(): self.connector.client.disable_logging = True for i in range(volume): - self.channel_volume(channel.value, volume-i-1) + self.channel_volume(channel.value, volume - i - 1) time.sleep(step) msg = "Finished with fading-out '%s'" % channel diff --git a/src/plugins/mailer.py b/src/plugins/mailer.py index b08825b6..c407467a 100644 --- a/src/plugins/mailer.py +++ b/src/plugins/mailer.py @@ -26,19 +26,17 @@ from src.base.config import AuraConfig from src.base.utils import SimpleUtil as SU - class MailingException(Exception): """ Thrown when some mail cannot be sent. """ - - -class AuraMailer(): +class AuraMailer: """ Event handler to send emails to Aura administrators and programme coordinators. """ + logger = None engine = None mail = None @@ -55,12 +53,10 @@ class AuraMailer(): self.engine = engine self.mail = MailService() - # # METHODS # - def on_fallback_active(self, timeslot, fallback_type): """ Called when a fallback is activated for the given timeslot, @@ -69,11 +65,15 @@ class AuraMailer(): show = "EMPTY TIMESLOT" show_id = "" timeframe = "" - + if timeslot: show = timeslot.show_name show_id = "The ID of the show is: " + str(timeslot.show_id) - timeframe = SU.fmt_time(timeslot.start_unix) + " - " + SU.fmt_time(timeslot.end_unix) + timeframe = ( + SU.fmt_time(timeslot.start_unix) + + " - " + + SU.fmt_time(timeslot.end_unix) + ) subject = f"Fallback for show '{show}' activated" message = "Dear programme coordinator, \n\n" @@ -82,42 +82,42 @@ class AuraMailer(): self.logger.debug(message) self.mail.notify_coordinator(subject, message) - - def on_sick(self, data): """ Called when the engine is in some unhealthy state. """ subject = "ERROR - Engine turned into some INVALID STATE!" - message = "There's an issue with your AURA Engine '%s':\n\n%s" % (data.get("engine_id"), data.get("status")) + message = "There's an issue with your AURA Engine '%s':\n\n%s" % ( + data.get("engine_id"), + data.get("status"), + ) self.mail.notify_admin(subject, message) - - def on_resurrect(self, data): """ Called when the engine turned healthy again after being sick. """ subject = "OK - Engine became healthy again" - message = "Good news, things seem fine again with your AURA Engine '%s':\n\n%s" % (data.get("engine_id"), data.get("status")) + message = ( + "Good news, things seem fine again with your AURA Engine '%s':\n\n%s" + % (data.get("engine_id"), data.get("status")) + ) self.mail.notify_admin(subject, message) - - def on_critical(self, subject, message, data=None): """ Callend when some critical event occurs """ - if not data: data = "" + if not data: + data = "" self.mail.notify_admin(subject, message + "\n\n" + str(data)) - - -class MailService(): +class MailService: """ Service to send emails to Aura administrators. """ + config = None logger = None admin_mails = None @@ -140,7 +140,6 @@ class MailService(): # METHODS # - def notify_admin(self, subject, body): """ Sends an email to the administrator(s) as defined in the configuration. @@ -150,7 +149,11 @@ class MailService(): body (String): The email body text """ if self.admin_mails_enabled == "false": - self.logger.warning(SU.red("No admin mail sent, because doing so is disabled in engine.ini!")) + self.logger.warning( + SU.red( + "No admin mail sent, because doing so is disabled in engine.ini!" + ) + ) return False admin_mails = self.admin_mails.split() @@ -158,8 +161,6 @@ class MailService(): for mail_to in admin_mails: self.send(mail_to, subject, body) - - def notify_coordinator(self, subject, body): """ Sends an email to the programme coordinator(s) as defined in the configuration. @@ -169,7 +170,11 @@ class MailService(): body (String): The email body text """ if self.coordinator_mails_enabled == "false": - self.logger.warning(SU.yellow("No programme coordinator mail sent, because doing so is disabled in engine.ini!")) + self.logger.warning( + SU.yellow( + "No programme coordinator mail sent, because doing so is disabled in engine.ini!" + ) + ) return False coordinator_mails = self.coordinator_mails.split() @@ -177,8 +182,6 @@ class MailService(): for mail_to in coordinator_mails: self.send(mail_to, subject, body) - - def send(self, mail_to, subject, body): """ Sends an email to the given address. diff --git a/src/scheduling/api.py b/src/scheduling/api.py index bbed1038..c2efed4d 100644 --- a/src/scheduling/api.py +++ b/src/scheduling/api.py @@ -1,4 +1,3 @@ - # # Aura Engine (https://gitlab.servus.at/aura/engine) # @@ -18,7 +17,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. - import logging import requests import queue @@ -29,12 +27,12 @@ from src.base.utils import SimpleUtil as SU from src.scheduling.utils import TimeslotFilter - class ApiFetcher(threading.Thread): """ Fetches the timeslots, playlists and playlist entries as JSON via the API endpoints of Steering and Tank. """ + config = None logging = None queue = None @@ -48,8 +46,6 @@ class ApiFetcher(threading.Thread): tank_session = None tank_secret = None - - def __init__(self, config): """ Constructor @@ -64,8 +60,6 @@ class ApiFetcher(threading.Thread): self.stop_event = threading.Event() threading.Thread.__init__(self) - - def run(self): """ Fetch timeslot data from the API. @@ -75,7 +69,9 @@ class ApiFetcher(threading.Thread): """ try: fetched_timeslots = self.fetch() - self.logger.debug("Timeslot data fetched from API: " + str(fetched_timeslots)) + self.logger.debug( + "Timeslot data fetched from API: " + str(fetched_timeslots) + ) # If nothing is fetched, return if not fetched_timeslots: @@ -92,21 +88,16 @@ class ApiFetcher(threading.Thread): # Terminate the thread return - - # # METHODS # - def get_fetched_data(self): """ Retrieves the fetched data from the queue. """ return self.queue.get() - - def fetch(self): """ Retrieve all required data from the API. @@ -122,10 +113,14 @@ class ApiFetcher(threading.Thread): for timeslot in self.fetched_timeslot_data: if "schedule_default_playlist_id" in timeslot: - timeslot["default_schedule_playlist_id"] = timeslot["schedule_default_playlist_id"] + timeslot["default_schedule_playlist_id"] = timeslot[ + "schedule_default_playlist_id" + ] timeslot["schedule_fallback_id"] = None if "show_default_playlist_id" in timeslot: - timeslot["default_show_playlist_id"] = timeslot["show_default_playlist_id"] + timeslot["default_show_playlist_id"] = timeslot[ + "show_default_playlist_id" + ] timeslot["show_fallback_id"] = None self.logger.debug("Fetching playlists from TANK") @@ -136,10 +131,15 @@ class ApiFetcher(threading.Thread): # Skip timeslot if no start or end is given if "start" not in timeslot: - self.logger.warning("No start of timeslot given. Skipping timeslot: " + str(timeslot)) + self.logger.warning( + "No start of timeslot given. Skipping timeslot: " + + str(timeslot) + ) timeslot = None if "end" not in timeslot: - self.logger.warning("No end of timeslot given. Skipping timeslot: " + str(timeslot)) + self.logger.warning( + "No end of timeslot given. Skipping timeslot: " + str(timeslot) + ) timeslot = None if timeslot: @@ -151,8 +151,6 @@ class ApiFetcher(threading.Thread): return return_data - - def fetch_timeslot_data(self): """ Fetches timeslot data from Steering. @@ -161,18 +159,26 @@ class ApiFetcher(threading.Thread): ([Timeslot]): An array of timeslots """ timeslots = None - headers = { "content-type": "application/json" } + headers = {"content-type": "application/json"} try: self.logger.debug("Fetch timeslots from Steering API...") - response = requests.get(self.steering_calendar_url, data=None, headers=headers) + 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))) + 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() except Exception as e: - self.logger.critical(SU.red("Error while requesting timeslots from Steering!"), e) + self.logger.critical( + SU.red("Error while requesting timeslots from Steering!"), e + ) if not timeslots: self.logger.error(SU.red("Got no timeslots via Playout API (Steering)!")) @@ -180,8 +186,6 @@ class ApiFetcher(threading.Thread): return self.polish_timeslots(timeslots) - - def fetch_playlists(self): """ Fetches all playlists including fallback playlists for every timeslot. @@ -189,32 +193,50 @@ class ApiFetcher(threading.Thread): over and extend timeslot data. """ # store fetched entries => do not have to fetch playlist_id more than once - fetched_entries=[] + fetched_entries = [] try: for timeslot in self.fetched_timeslot_data: # 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_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" + ) show_fallback_id = self.get_playlist_id(timeslot, "show_fallback_id") - station_fallback_id = self.get_playlist_id(timeslot, "station_fallback_id") + station_fallback_id = self.get_playlist_id( + timeslot, "station_fallback_id" + ) # Retrieve playlist, default and the fallback playlists for every timeslot. # If a playlist (like station_fallback) is already fetched, it is not fetched again but reused 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["show_fallback"] = self.fetch_playlist(show_fallback_id, fetched_entries) - timeslot["station_fallback"] = self.fetch_playlist(station_fallback_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["show_fallback"] = self.fetch_playlist( + show_fallback_id, fetched_entries + ) + timeslot["station_fallback"] = self.fetch_playlist( + station_fallback_id, fetched_entries + ) except Exception as e: - self.logger.error("Error while fetching playlists from API endpoints: " + str(e), e) - - + self.logger.error( + "Error while fetching playlists from API endpoints: " + str(e), e + ) def fetch_playlist(self, playlist_id, fetched_playlists): """ @@ -234,7 +256,7 @@ class ApiFetcher(threading.Thread): url = self.tank_playlist_url.replace("${ID}", playlist_id) headers = { "Authorization": "Bearer %s:%s" % (self.tank_session, self.tank_secret), - "content-type": "application/json" + "content-type": "application/json", } # If playlist is already fetched in this round, use the existing one @@ -247,19 +269,26 @@ class ApiFetcher(threading.Thread): 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))) + 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() except Exception as e: - self.logger.critical(SU.red("Error while requesting playlist #%s from Tank" % str(playlist_id)), 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): """ Extracts the playlist ID for a given playlist (fallback) type. @@ -276,13 +305,14 @@ class ApiFetcher(threading.Thread): playlist_id = str(timeslot[id_name]) if not playlist_id or playlist_id == "None": - self.logger.debug("No value defined for '%s' in timeslot '#%s'" % (id_name, timeslot["id"])) + self.logger.debug( + "No value defined for '%s' in timeslot '#%s'" + % (id_name, timeslot["id"]) + ) return None return playlist_id - - def polish_timeslots(self, timeslots): """ Removes all timeslots which are not relevant for further processing, @@ -292,14 +322,15 @@ class ApiFetcher(threading.Thread): timeslots = TimeslotFilter.filter_24h(timeslots) timeslots = TimeslotFilter.filter_past(timeslots) count_after = len(timeslots) - self.logger.debug("Removed %d unnecessary timeslots from response. Timeslots left: %d" % ((count_before - count_after), count_after)) + self.logger.debug( + "Removed %d unnecessary timeslots from response. Timeslots left: %d" + % ((count_before - count_after), count_after) + ) for t in timeslots: t["timeslot_id"] = t["id"] return timeslots - - def terminate(self): """ Terminates the thread. diff --git a/src/scheduling/models.py b/src/scheduling/models.py index 9b3baf67..adf87160 100644 --- a/src/scheduling/models.py +++ b/src/scheduling/models.py @@ -289,7 +289,6 @@ class Timeslot(DB.Model, AuraDatabaseModel): ) return timeslots - @hybrid_property def start_unix(self): """ diff --git a/src/scheduling/programme.py b/src/scheduling/programme.py index 32614a9b..c44ed3cd 100644 --- a/src/scheduling/programme.py +++ b/src/scheduling/programme.py @@ -1,5 +1,3 @@ - - # # Aura Engine (https://gitlab.servus.at/aura/engine) # @@ -19,7 +17,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. - import logging from datetime import datetime @@ -29,15 +26,20 @@ from src.base.config import AuraConfig from src.base.utils import SimpleUtil as SU from src.engine import Engine from src.scheduling.utils import M3UPlaylistProcessor -from src.scheduling.models import Timeslot, Playlist, PlaylistEntry, PlaylistEntryMetaData +from src.scheduling.models import ( + Timeslot, + Playlist, + PlaylistEntry, + PlaylistEntryMetaData, +) from src.scheduling.api import ApiFetcher - -class ProgrammeService(): +class ProgrammeService: """ The current programme of the calendar. The programme is a set of timeslots for the current day. """ + config = None logger = None timeslots = None @@ -45,7 +47,6 @@ class ProgrammeService(): last_successful_fetch = None programme_store = None - def __init__(self): """ Constructor @@ -54,8 +55,6 @@ class ProgrammeService(): self.logger = logging.getLogger("AuraEngine") self.programme_store = ProgrammeStore() - - def refresh(self): """ Fetch the latest programme from `ProgrammeStore` which stores it to the database. @@ -71,31 +70,51 @@ class ProgrammeService(): response = self.api_fetcher.get_fetched_data() if response is None: - msg = SU.red("Trying to load programme from Engine Database, because ApiFetcher returned an empty response.") + msg = SU.red( + "Trying to load programme from Engine Database, because ApiFetcher returned an empty response." + ) self.logger.warning(msg) elif type(response) is list: if len(response) > 0: self.last_successful_fetch = datetime.now() self.timeslots = self.programme_store.store_timeslots(response) - self.logger.info(SU.green(f"Finished fetching current programme from API ({len(response)} timeslots)")) + self.logger.info( + SU.green( + f"Finished fetching current programme from API ({len(response)} timeslots)" + ) + ) else: - self.logger.critical("Programme fetched from Steering/Tank has no entries!") + self.logger.critical( + "Programme fetched from Steering/Tank has no entries!" + ) elif response.startswith("fetching_aborted"): - msg = SU.red("Load programme from DB, because fetching was aborted by ApiFetcher! Reason: " + response[17:]) + msg = SU.red( + "Load programme from DB, because fetching was aborted by ApiFetcher! Reason: " + + response[17:] + ) self.logger.warning(msg) else: - msg = SU.red("Load programme from DB, because of an unknown response from ApiFetcher: " + response) + msg = SU.red( + "Load programme from DB, because of an unknown response from ApiFetcher: " + + response + ) self.logger.warning(msg) # Load latest programme from the database if not self.timeslots: self.timeslots = self.programme_store.load_timeslots() - self.logger.info(SU.green("Finished loading current programme from database (%s timeslots)" % str(len(self.timeslots)))) + self.logger.info( + SU.green( + "Finished loading current programme from database (%s timeslots)" + % str(len(self.timeslots)) + ) + ) for timeslot in self.timeslots: - self.logger.debug("\tTimeslot %s with Playlist %s" % (str(timeslot), str(timeslot.playlist))) - - + self.logger.debug( + "\tTimeslot %s with Playlist %s" + % (str(timeslot), str(timeslot.playlist)) + ) def get_current_entry(self): """ @@ -138,8 +157,6 @@ class ProgrammeService(): return current_entry - - def get_current_timeslot(self): """ Retrieves the timeslot currently to be played. @@ -159,8 +176,6 @@ class ProgrammeService(): return current_timeslot - - def get_current_playlist(self, timeslot): """ Retrieves the playlist to be scheduled. If no specific playlist is assigned, @@ -181,8 +196,6 @@ class ProgrammeService(): return (playlist_type, playlist) - - def get_next_timeslots(self, max_count=0): """ Retrieves the timeslots to be played after the current one. @@ -208,8 +221,6 @@ class ProgrammeService(): return next_timeslots - - def is_timeslot_in_window(self, timeslot): """ Checks if the timeslot is within the scheduling window. @@ -218,14 +229,14 @@ class ProgrammeService(): 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: + if ( + timeslot.start_unix - window_start < now_unix + and timeslot.start_unix - window_end > now_unix + ): return True return False - - def terminate(self): """ Called when thread is stopped or a signal to terminate is received. @@ -235,9 +246,7 @@ class ProgrammeService(): self.api_fetcher.terminate() - - -class ProgrammeStore(): +class ProgrammeStore: """ The `ProgrammeStore` service retrieves all current schedules and related playlists including audio files from the configured API endpoints and stores @@ -245,6 +254,7 @@ class ProgrammeStore(): To perform the API queries it utilizes the ApiFetcher class. """ + config = None logger = None m3u_processor = None @@ -257,7 +267,6 @@ class ProgrammeStore(): self.logger = logging.getLogger("AuraEngine") self.m3u_processor = M3UPlaylistProcessor() - def load_timeslots(self): """ Loads the programme from the database. @@ -266,11 +275,14 @@ class ProgrammeStore(): try: timeslots = Timeslot.get_timeslots(datetime.now()) except Exception as e: - self.logger.critical(SU.red("Could not load programme from database. We are in big trouble my friend!"), e) + self.logger.critical( + SU.red( + "Could not load programme from database. We are in big trouble my friend!" + ), + e, + ) return timeslots - - def store_timeslots(self, fetched_timeslots): """ Stores the fetched timeslots to the database. @@ -285,10 +297,16 @@ class ProgrammeStore(): # Check timeslot for validity if "start" not in timeslot: - self.logger.warning("No 'start' of timeslot given. Skipping the timeslot: %s " % str(timeslot)) + self.logger.warning( + "No 'start' of timeslot given. Skipping the timeslot: %s " + % str(timeslot) + ) continue if "end" not in timeslot: - self.logger.warning("No 'end' of timeslot given. Skipping the timeslot: %s " % str(timeslot)) + self.logger.warning( + "No 'end' of timeslot given. Skipping the timeslot: %s " + % str(timeslot) + ) continue # Store the timeslot @@ -296,22 +314,40 @@ class ProgrammeStore(): timeslots.append(timeslot_db) # Store assigned playlists - self.store_playlist(timeslot_db, timeslot_db.playlist_id, timeslot["playlist"]) + self.store_playlist( + timeslot_db, timeslot_db.playlist_id, timeslot["playlist"] + ) if timeslot_db.default_schedule_playlist_id: - self.store_playlist(timeslot_db, timeslot_db.default_schedule_playlist_id, timeslot["default_schedule_playlist"]) + self.store_playlist( + timeslot_db, + timeslot_db.default_schedule_playlist_id, + timeslot["default_schedule_playlist"], + ) if timeslot_db.default_show_playlist_id: - self.store_playlist(timeslot_db, timeslot_db.default_show_playlist_id, timeslot["default_show_playlist"]) + self.store_playlist( + timeslot_db, + timeslot_db.default_show_playlist_id, + timeslot["default_show_playlist"], + ) if timeslot_db.schedule_fallback_id: - self.store_playlist(timeslot_db, timeslot_db.schedule_fallback_id, timeslot["schedule_fallback"]) + self.store_playlist( + timeslot_db, + timeslot_db.schedule_fallback_id, + timeslot["schedule_fallback"], + ) if timeslot_db.show_fallback_id: - self.store_playlist(timeslot_db, timeslot_db.show_fallback_id, timeslot["show_fallback"]) + self.store_playlist( + timeslot_db, timeslot_db.show_fallback_id, timeslot["show_fallback"] + ) if timeslot_db.station_fallback_id: - self.store_playlist(timeslot_db, timeslot_db.station_fallback_id, timeslot["station_fallback"]) + self.store_playlist( + timeslot_db, + timeslot_db.station_fallback_id, + timeslot["station_fallback"], + ) return timeslots - - def update_deleted_timeslots(self, fetched_timeslots): """ Checks if some timeslot has been deleted remotely, so delete it locally too. @@ -335,22 +371,33 @@ class ProgrammeStore(): if local_timeslot.start_unix > now_unix: # Filter the local timeslot from the fetched ones - existing_remotely = list(filter(lambda new_timeslot: \ - new_timeslot["timeslot_id"] == local_timeslot.timeslot_id, fetched_timeslots)) + existing_remotely = list( + filter( + lambda new_timeslot: new_timeslot["timeslot_id"] + == local_timeslot.timeslot_id, + fetched_timeslots, + ) + ) if not existing_remotely: # Only allow deletion of timeslots which are deleted before the start of the scheduling window if (local_timeslot.start_unix - scheduling_window_start) > now_unix: - self.logger.info("Timeslot #%s has been deleted remotely, hence also delete it locally too [%s]" % \ - (local_timeslot.timeslot_id, str(local_timeslot))) + self.logger.info( + "Timeslot #%s has been deleted remotely, hence also delete it locally too [%s]" + % (local_timeslot.timeslot_id, str(local_timeslot)) + ) local_timeslot.delete(commit=True) - self.logger.info("Remotely deleted timeslot #%s from local database" % local_timeslot.timeslot_id) + self.logger.info( + "Remotely deleted timeslot #%s from local database" + % local_timeslot.timeslot_id + ) else: - msg = "Timeslot #%s has been deleted remotely. Since the scheduling window has already started, it won't be deleted locally." % local_timeslot.timeslot_id + msg = ( + "Timeslot #%s has been deleted remotely. Since the scheduling window has already started, it won't be deleted locally." + % local_timeslot.timeslot_id + ) self.logger.error(SU.red(msg)) - - def store_timeslot(self, timeslot): """ Stores the given timeslot to the database. @@ -363,7 +410,9 @@ class ProgrammeStore(): havetoadd = False if not timeslot_db: - self.logger.debug("no timeslot with given timeslot id in database => create new") + self.logger.debug( + "no timeslot with given timeslot id in database => create new" + ) timeslot_db = Timeslot() havetoadd = True @@ -384,7 +433,9 @@ class ProgrammeStore(): # Optional API properties if "default_schedule_playlist_id" in timeslot: - timeslot_db.default_schedule_playlist_id = timeslot["default_schedule_playlist_id"] + timeslot_db.default_schedule_playlist_id = timeslot[ + "default_schedule_playlist_id" + ] if "default_show_playlist_id" in timeslot: timeslot_db.default_show_playlist_id = timeslot["default_show_playlist_id"] if "schedule_fallback_id" in timeslot: @@ -394,12 +445,14 @@ class ProgrammeStore(): if "station_fallback_id" in timeslot: timeslot_db.station_fallback_id = timeslot["station_fallback_id"] - self.logger.debug(SU.pink(f"Store/Update TIMESLOT havetoadd={havetoadd} - data: "+str(timeslot))) + self.logger.debug( + SU.pink( + f"Store/Update TIMESLOT havetoadd={havetoadd} - data: " + str(timeslot) + ) + ) timeslot_db.store(add=havetoadd, commit=True) return timeslot_db - - def store_playlist(self, timeslot_db, playlist_id, fetched_playlist): """ Stores the Playlist to the database. @@ -408,14 +461,18 @@ class ProgrammeStore(): self.logger.debug(f"Playlist ID#{playlist_id} is not available!") return - playlist_db = Playlist.select_playlist_for_timeslot(timeslot_db.timeslot_start, playlist_id) + playlist_db = Playlist.select_playlist_for_timeslot( + timeslot_db.timeslot_start, playlist_id + ) havetoadd = False if not playlist_db: playlist_db = Playlist() havetoadd = True - self.logger.debug("Storing playlist %d for timeslot (%s)" % (playlist_id, str(timeslot_db))) + self.logger.debug( + "Storing playlist %d for timeslot (%s)" % (playlist_id, str(timeslot_db)) + ) playlist_db.playlist_id = playlist_id playlist_db.timeslot_start = timeslot_db.timeslot_start playlist_db.show_name = timeslot_db.show_name @@ -431,8 +488,6 @@ class ProgrammeStore(): return playlist_db - - def store_playlist_entries(self, timeslot_db, playlist_db, fetched_playlist): """ Stores the playlist entries to the database. @@ -450,7 +505,9 @@ class ProgrammeStore(): self.delete_orphaned_entries(playlist_db, entries) for entry in entries: - entry_db = PlaylistEntry.select_playlistentry_for_playlist(playlist_db.artificial_id, entry_num) + entry_db = PlaylistEntry.select_playlistentry_for_playlist( + playlist_db.artificial_id, entry_num + ) havetoadd = False if not entry_db: entry_db = PlaylistEntry() @@ -474,8 +531,6 @@ class ProgrammeStore(): entry_num = entry_num + 1 time_marker += entry_db.duration - - def delete_orphaned_entries(self, playlist_db, entries): """ Deletes all playlist entries which are beyond the current playlist's `entry_count`. @@ -483,25 +538,30 @@ class ProgrammeStore(): less entries than before. """ new_last_idx = len(entries) - existing_last_idx = PlaylistEntry.count_entries(playlist_db.artificial_id)-1 + existing_last_idx = PlaylistEntry.count_entries(playlist_db.artificial_id) - 1 if existing_last_idx < new_last_idx: return - for entry_num in range(new_last_idx, existing_last_idx+1, 1): + for entry_num in range(new_last_idx, existing_last_idx + 1, 1): PlaylistEntry.delete_entry(playlist_db.artificial_id, entry_num) - self.logger.info(SU.yellow("Deleted playlist entry %s:%s" % (playlist_db.artificial_id, entry_num))) + self.logger.info( + SU.yellow( + "Deleted playlist entry %s:%s" + % (playlist_db.artificial_id, entry_num) + ) + ) entry_num += 1 - - def expand_entry_duration(self, timeslot_db, entries): """ If some playlist entry doesn't have a duration assigned, its duration is expanded to the remaining duration of the playlist (= timeslot duration minus playlist entries with duration). If there's more than one entry without duration, such entries are removed from the playlist. """ - total_seconds = (timeslot_db.timeslot_end - timeslot_db.timeslot_start).total_seconds() + total_seconds = ( + timeslot_db.timeslot_end - timeslot_db.timeslot_start + ).total_seconds() total_duration = SU.seconds_to_nano(total_seconds) actual_duration = 0 missing_duration = [] @@ -516,22 +576,27 @@ class ProgrammeStore(): if len(missing_duration) == 1: entries[missing_duration[0]]["duration"] = total_duration - actual_duration - self.logger.info(f"Expanded duration of playlist entry #{missing_duration[0]}") + self.logger.info( + f"Expanded duration of playlist entry #{missing_duration[0]}" + ) elif len(missing_duration) > 1: # This case should actually never happen, as TANK doesn't allow more than one entry w/o duration anymore for i in reversed(missing_duration[1:-1]): - self.logger.error(SU.red("Deleted Playlist Entry without duration: %s" % \ - str(entries[i]))) + self.logger.error( + SU.red( + "Deleted Playlist Entry without duration: %s" % str(entries[i]) + ) + ) del entries[i] - - def store_playlist_entry_metadata(self, entry_db, metadata): """ Stores the meta-data for a PlaylistEntry. """ - metadata_db = PlaylistEntryMetaData.select_metadata_for_entry(entry_db.artificial_id) + metadata_db = PlaylistEntryMetaData.select_metadata_for_entry( + entry_db.artificial_id + ) havetoadd = False if not metadata_db: metadata_db = PlaylistEntryMetaData() @@ -555,6 +620,3 @@ class ProgrammeStore(): metadata_db.title = "" metadata_db.store(havetoadd, commit=True) - - - diff --git a/src/scheduling/scheduler.py b/src/scheduling/scheduler.py index 0fae8bad..966441e9 100644 --- a/src/scheduling/scheduler.py +++ b/src/scheduling/scheduler.py @@ -147,7 +147,6 @@ class AuraScheduler(threading.Thread): # Nothing to do atm - # # METHODS # @@ -447,7 +446,11 @@ class TimeslotCommand(EngineExecutor): # Initialize the "fade in" EngineExecuter and instantiate a connected child EngineExecuter for "fade out" when the parent is ready timeslot_id = timeslot.timeslot_id super().__init__( - f"TIMESLOT#{timeslot_id}", None, timeslot.start_unix, self.do_start_timeslot, timeslot + f"TIMESLOT#{timeslot_id}", + None, + timeslot.start_unix, + self.do_start_timeslot, + timeslot, ) EngineExecutor( f"TIMESLOT#{timeslot_id}", @@ -505,7 +508,9 @@ class PlayCommand(EngineExecutor): self.logger.info(msg) # Initialize the "preload" EngineExecuter and attach a child `PlayCommand` to the "on_ready" event handler timeslot_id = entries[0].playlist.timeslot.timeslot_id - super().__init__(f"PRELOAD#{timeslot_id}", None, start_preload, self.do_preload, entries) + super().__init__( + f"PRELOAD#{timeslot_id}", None, start_preload, self.do_preload, entries + ) EngineExecutor(f"PLAY#{timeslot_id}", self, start_play, self.do_play, entries) def do_preload(self, entries): diff --git a/src/scheduling/utils.py b/src/scheduling/utils.py index 8f11c918..210150ed 100644 --- a/src/scheduling/utils.py +++ b/src/scheduling/utils.py @@ -1,4 +1,3 @@ - # # Aura Engine (https://gitlab.servus.at/aura/engine) # @@ -27,20 +26,17 @@ from src.base.utils import SimpleUtil as SU from src.base.config import AuraConfig - class EntryQueueState(Enum): """ Types of playlist entry behaviours. """ + OKAY = "ok" CUT = "cut" OUT_OF_SCHEDULE = "oos" - - - -class TimeslotFilter(): +class TimeslotFilter: """ Filters timeslot dictionaries with various criteria. """ @@ -55,8 +51,8 @@ class TimeslotFilter(): """ items = [] now = SU.timestamp() - now_plus_24hours = now + (12*60*60) - now_minus_12hours = now - (12*60*60) + now_plus_24hours = now + (12 * 60 * 60) + now_minus_12hours = now - (12 * 60 * 60) for s in timeslots: start_time = datetime.strptime(s["start"], "%Y-%m-%dT%H:%M:%S") @@ -67,7 +63,6 @@ class TimeslotFilter(): return items - @staticmethod def filter_past(timeslots): """ @@ -85,23 +80,22 @@ class TimeslotFilter(): # Append all elements in the future if start_time >= now: items.append(s) - # Append the one which is playing now + # Append the one which is playing now elif start_time < now < end_time: items.append(s) return items - -class M3UPlaylistProcessor(): +class M3UPlaylistProcessor: """ Renders a M3U Playlist as a engine compatible playlist dictionary. """ + config = None logging = None playlist_folder = None - def __init__(self): """ Constructor @@ -110,8 +104,6 @@ class M3UPlaylistProcessor(): self.logger = logging.getLogger("AuraEngine") self.playlist_folder = self.config.abs_playlist_path() - - def spread(self, entries): """ Converts a playlist with M3U entries and renders them as individual playlist entries. @@ -136,8 +128,6 @@ class M3UPlaylistProcessor(): return result - - def read_m3u_file(self, source_file): """ Read entries from an M3U file. @@ -154,11 +144,11 @@ class M3UPlaylistProcessor(): "metadata": { "artist": metadata[1].split(" - ")[0], "album": "", - "title": metadata[1].split(" - ")[1] + "title": metadata[1].split(" - ")[1], } }, "duration": SU.seconds_to_nano(int(metadata[0])), - "uri": "file://" + lines[i+1] + "uri": "file://" + lines[i + 1], } entries.append(entry) @@ -166,16 +156,15 @@ class M3UPlaylistProcessor(): return entries - class TimeslotRenderer: """ Displays current and next timeslots in ASCII for maintenance and debugging. """ + logger = None scheduler = None programme = None - def __init__(self, scheduler): """ Constructor @@ -184,7 +173,6 @@ class TimeslotRenderer: self.scheduler = scheduler self.programme = scheduler.get_programme() - def get_ascii_timeslots(self): """ Creates a printable version of the current programme (playlists and entries as per timeslot) @@ -201,19 +189,28 @@ class TimeslotRenderer: if active_timeslot.playlist: planned_playlist = active_timeslot.playlist - (playlist_type, resolved_playlist) = self.scheduler.resolve_playlist(active_timeslot) + (playlist_type, resolved_playlist) = self.scheduler.resolve_playlist( + active_timeslot + ) s += "\n│ Playing timeslot %s " % active_timeslot if planned_playlist: - if resolved_playlist and resolved_playlist.playlist_id != planned_playlist.playlist_id: + 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 default playlist below ↓↓↓") + s += SU.red("↑↑↑ That's the originally planned playlist.") + ( + "Instead playing the default playlist below ↓↓↓" + ) if resolved_playlist: if not planned_playlist: s += "\n│ " - s += SU.red(f"No playlist assigned to timeslot. Instead playing the '{playlist_type.get('name')}' playlist below ↓↓↓") + s += SU.red( + f"No playlist assigned to timeslot. Instead playing the '{playlist_type.get('name')}' playlist below ↓↓↓" + ) s += "\n│ └── Playlist %s " % resolved_playlist @@ -228,8 +225,10 @@ class TimeslotRenderer: # Entry currently being played if active_entry: - s += "\n│ └── Entry %s | %s " % \ - (str(active_entry.entry_num+1), SU.green("PLAYING > "+str(active_entry))) + s += "\n│ └── Entry %s | %s " % ( + str(active_entry.entry_num + 1), + SU.green("PLAYING > " + str(active_entry)), + ) # Open entries for current playlist rest_of_playlist = active_entry.get_next_entries(False) @@ -237,7 +236,11 @@ class TimeslotRenderer: s += self.build_playlist_string(entries) else: - s += "\n│ └── %s" % (SU.red("No active playlist. There should be at least some fallback playlist running...")) + s += "\n│ └── %s" % ( + SU.red( + "No active playlist. There should be at least some fallback playlist running..." + ) + ) else: s += "\n│ Nothing. " s += "\n└──────────────────────────────────────────────────────────────────────────────────────────────────────" @@ -250,14 +253,23 @@ class TimeslotRenderer: s += "\n│ Nothing. " else: for timeslot in next_timeslots: - (playlist_type, resolved_playlist) = self.scheduler.resolve_playlist(timeslot) + (playlist_type, resolved_playlist) = self.scheduler.resolve_playlist( + timeslot + ) if resolved_playlist: s += "\n│ Queued timeslot %s " % timeslot - s += "\n│ └── Playlist %s (Type: %s)" % (resolved_playlist, SU.cyan(str(playlist_type))) + s += "\n│ └── Playlist %s (Type: %s)" % ( + resolved_playlist, + SU.cyan(str(playlist_type)), + ) 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))) + 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) @@ -265,8 +277,6 @@ class TimeslotRenderer: s += "\n└──────────────────────────────────────────────────────────────────────────────────────────────────────\n\n" return s - - def build_playlist_string(self, entries): """ Returns a stringified list of entries @@ -275,17 +285,19 @@ class TimeslotRenderer: is_out_of_timeslot = False for entry in entries: - if entry.queue_state == EntryQueueState.OUT_OF_SCHEDULE and not is_out_of_timeslot: - s += "\n│ %s" % \ - SU.red("↓↓↓ These entries won't be played because they are out of timeslot.") + if ( + entry.queue_state == EntryQueueState.OUT_OF_SCHEDULE + and not is_out_of_timeslot + ): + s += "\n│ %s" % SU.red( + "↓↓↓ These entries won't be played because they are out of timeslot." + ) is_out_of_timeslot = True s += self.build_entry_string("\n│ └── ", entry, is_out_of_timeslot) return s - - def build_entry_string(self, prefix, entry, strike): """ Returns an stringified entry. @@ -299,11 +311,9 @@ class TimeslotRenderer: else: entry_str = str(entry) - entry_line = "%sEntry %s | %s" % (prefix, str(entry.entry_num+1), entry_str) + entry_line = "%sEntry %s | %s" % (prefix, str(entry.entry_num + 1), entry_str) return s + entry_line - - def preprocess_entries(self, entries, cut_oos): """ Analyses and marks entries which are going to be cut or excluded. @@ -320,7 +330,10 @@ class TimeslotRenderer: for entry in entries: if entry.entry_start >= entry.playlist.timeslot.timeslot_end: - msg = "Filtered entry (%s) after end-of timeslot (%s) ... SKIPPED" % (entry, entry.playlist.timeslot) + msg = "Filtered entry (%s) after end-of timeslot (%s) ... SKIPPED" % ( + entry, + entry.playlist.timeslot, + ) self.logger.debug(msg) entry.queue_state = EntryQueueState.OUT_OF_SCHEDULE elif entry.end_unix > entry.playlist.timeslot.end_unix: @@ -331,4 +344,4 @@ class TimeslotRenderer: if not entry.queue_state == EntryQueueState.OUT_OF_SCHEDULE or not cut_oos: clean_entries.append(entry) - return clean_entries \ No newline at end of file + return clean_entries diff --git a/tests/test_config.py b/tests/test_config.py index 392ebc46..a1dfcc16 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,4 +1,3 @@ - # # Aura Engine (https://gitlab.servus.at/aura/engine) # @@ -26,18 +25,16 @@ from src.base.utils import SimpleUtil as SU from src.base.config import AuraConfig - - class TestConfig(unittest.TestCase): """ Testing the Configuration. """ + config = None def setUp(self): self.config = AuraConfig() - def test_config(self): # Check if config is available self.assertIsNotNone(self.config.ini_path) @@ -54,7 +51,9 @@ class TestConfig(unittest.TestCase): self.assertTrue(validators.url(self.config.get("api_engine_status"))) self.assertTrue(validators.url(self.config.get("api_engine_store_playlog"))) self.assertTrue(validators.url(self.config.get("api_engine_store_clock"))) - engine_health_url = self.config.get("api_engine_store_health").replace("${ENGINE_NUMBER}", "1") + engine_health_url = self.config.get("api_engine_store_health").replace( + "${ENGINE_NUMBER}", "1" + ) self.assertTrue(validators.url(engine_health_url)) # Check if Liquidsoap "socket_dir" is set and a directory @@ -67,7 +66,5 @@ class TestConfig(unittest.TestCase): self.assertIsNotNone(self.config.get("db_host")) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_engine_executor.py b/tests/test_engine_executor.py index e5034a2a..841d2436 100644 --- a/tests/test_engine_executor.py +++ b/tests/test_engine_executor.py @@ -1,4 +1,3 @@ - # # Aura Engine (https://gitlab.servus.at/aura/engine) # @@ -26,8 +25,6 @@ from src.base.utils import SimpleUtil as SU from src.control import EngineExecutor - - class TestEngineExecutor(unittest.TestCase): """ Testing the EngineExecutor. @@ -36,13 +33,12 @@ class TestEngineExecutor(unittest.TestCase): def setUp(self): None - - def test_single_executor(self): # Initialize state and executor params EngineExecutor.timer_store = {} global_state = ["none"] due_time = SU.timestamp() + 0.3 + def f(param): global_state[0] = param @@ -55,24 +51,27 @@ class TestEngineExecutor(unittest.TestCase): time.sleep(0.5) self.assertEqual("hello world", global_state[0]) - - def test_two_executors(self): # Initialize state and executor params EngineExecutor.timer_store = {} global_state = ["none"] + def f(param): global_state[0] = param # Before the executor 1 is done there should be the initial value due_time1 = SU.timestamp() + 1 - e1 = EngineExecutor("EXECUTOR_1", None, due_time1, f, "hello world from executor 1") + e1 = EngineExecutor( + "EXECUTOR_1", None, due_time1, f, "hello world from executor 1" + ) self.assertEqual("none", global_state[0]) self.assertNotEqual("hello world from executor 1", global_state[0]) # Before the executor 2 is done there should be still the initial value due_time2 = SU.timestamp() + 0.5 - e2 = EngineExecutor("EXECUTOR_2", None, due_time2, f, "hello world from executor 2") + e2 = EngineExecutor( + "EXECUTOR_2", None, due_time2, f, "hello world from executor 2" + ) self.assertEqual("none", global_state[0]) self.assertNotEqual("hello world from executor 2", global_state[0]) @@ -88,23 +87,26 @@ class TestEngineExecutor(unittest.TestCase): time.sleep(0.5) self.assertEqual("hello world from executor 1", global_state[0]) - - def test_parent_child_executors_in_order(self): # Initialize state and executor params EngineExecutor.timer_store = {} global_state = ["none"] + def f(param): global_state[0] = param # Before the the parent is done there should be the initial value due_time1 = SU.timestamp() + 0.5 - parent = EngineExecutor("EXECUTOR_PARENT", None, due_time1, f, "hello world from parent") + parent = EngineExecutor( + "EXECUTOR_PARENT", None, due_time1, f, "hello world from parent" + ) self.assertEqual("none", global_state[0]) # Before the the child is done there should be the initial value due_time2 = SU.timestamp() + 1 - child = EngineExecutor("EXECUTOR_CHILD", parent, due_time2, f, "hello world from child") + child = EngineExecutor( + "EXECUTOR_CHILD", parent, due_time2, f, "hello world from child" + ) self.assertEqual("none", global_state[0]) # After 0.3 seconds there still should be the initial value @@ -119,26 +121,28 @@ class TestEngineExecutor(unittest.TestCase): time.sleep(1.2) self.assertEqual("hello world from child", global_state[0]) - - def test_parent_child_executors_with_child_before(self): # Initialize state and executor params EngineExecutor.timer_store = {} global_state = ["none", "never called by parent"] + def f(param): global_state[0] = param if param == "hello world from parent": global_state[1] = param - # Before the the parent is done there should be the initial value due_time1 = SU.timestamp() + 0.5 - parent = EngineExecutor("EXECUTOR_PARENT", None, due_time1, f, "hello world from parent") + parent = EngineExecutor( + "EXECUTOR_PARENT", None, due_time1, f, "hello world from parent" + ) self.assertEqual("none", global_state[0]) # Before the the child is done there should be the initial value due_time2 = SU.timestamp() + 1.5 - child = EngineExecutor("EXECUTOR_CHILD", parent, due_time2, f, "hello world from child") + child = EngineExecutor( + "EXECUTOR_CHILD", parent, due_time2, f, "hello world from child" + ) self.assertEqual("none", global_state[0]) # After 0.2 seconds there still should be the initial value @@ -166,13 +170,11 @@ class TestEngineExecutor(unittest.TestCase): # But we do not just believe what we expect, but check if it really has ever been called by a parent self.assertEqual("hello world from parent", global_state[1]) - - - def test_timer_store_replacement(self): # Initialize state and executor params EngineExecutor.timer_store = {} global_state = ["none"] + def f(param): global_state[0] = param @@ -182,12 +184,16 @@ class TestEngineExecutor(unittest.TestCase): # Before the the parent is done there should be the initial value due_time1 = SU.timestamp() + 0.5 - parent = EngineExecutor("EXECUTOR_PARENT", None, due_time1, f, "hello world from parent") + parent = EngineExecutor( + "EXECUTOR_PARENT", None, due_time1, f, "hello world from parent" + ) self.assertEqual("none", global_state[0]) # Before the the child is done there should be the initial value due_time2 = SU.timestamp() + 1.5 - child = EngineExecutor("EXECUTOR_CHILD", parent, due_time2, f, "hello world from child") + child = EngineExecutor( + "EXECUTOR_CHILD", parent, due_time2, f, "hello world from child" + ) self.assertEqual("none", global_state[0]) # There should be a total of 2 timers @@ -195,11 +201,15 @@ class TestEngineExecutor(unittest.TestCase): self.assertEqual(2, len(timers)) # Replacing the parent with a new instance - parent = EngineExecutor("EXECUTOR_PARENT", None, due_time1, f, "hello world from alternative parent") + parent = EngineExecutor( + "EXECUTOR_PARENT", None, due_time1, f, "hello world from alternative parent" + ) self.assertEqual("none", global_state[0]) # Before the the child is done there should be the initial value - child = EngineExecutor("EXECUTOR_CHILD", parent, due_time2, f, "hello world from alternative child") + child = EngineExecutor( + "EXECUTOR_CHILD", parent, due_time2, f, "hello world from alternative child" + ) self.assertEqual("none", global_state[0]) # After 1 seconds max there should be the updated value from the alternative parent @@ -214,24 +224,26 @@ class TestEngineExecutor(unittest.TestCase): timers = EngineExecutor.command_history() self.assertEqual(2, len(timers)) - - - def test_dead_parent_with_lively_child(self): # Initialize state and executor params EngineExecutor.timer_store = {} global_state = ["none"] + def f(param): global_state[0] = param # Before the the parent is done there should be the initial value due_time1 = SU.timestamp() + 0.5 - parent = EngineExecutor("EXECUTOR_PARENT", None, due_time1, f, "hello world from parent") + parent = EngineExecutor( + "EXECUTOR_PARENT", None, due_time1, f, "hello world from parent" + ) self.assertEqual("none", global_state[0]) # Before the the child is done there should be the initial value due_time2 = SU.timestamp() + 1.5 - child = EngineExecutor("EXECUTOR_CHILD", parent, due_time2, f, "hello world from child") + child = EngineExecutor( + "EXECUTOR_CHILD", parent, due_time2, f, "hello world from child" + ) self.assertEqual("none", global_state[0]) # Wait until the parent timer got executed @@ -243,8 +255,12 @@ class TestEngineExecutor(unittest.TestCase): self.assertEqual(True, child.is_alive()) # Replacing the parent & child with a new instance - parent = EngineExecutor("EXECUTOR_PARENT", None, due_time1, f, "hello world from late parent") - child = EngineExecutor("EXECUTOR_PARENT", None, due_time2, f, "hello world from alternative child") + parent = EngineExecutor( + "EXECUTOR_PARENT", None, due_time1, f, "hello world from late parent" + ) + child = EngineExecutor( + "EXECUTOR_PARENT", None, due_time2, f, "hello world from alternative child" + ) # New parent = dead before finished initialization already, so actually never born self.assertEqual(False, parent.is_alive()) @@ -258,7 +274,5 @@ class TestEngineExecutor(unittest.TestCase): self.assertEqual("hello world from alternative child", global_state[0]) - - -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_logger.py b/tests/test_logger.py index dbbdcf6c..81ecf8f0 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -1,4 +1,3 @@ - # # Aura Engine (https://gitlab.servus.at/aura/engine) # @@ -25,12 +24,11 @@ from src.base.logger import AuraLogger from src.base.config import AuraConfig - - class TestLogger(unittest.TestCase): """ Testing the Logger. """ + aura_logger = None def setUp(self): @@ -41,7 +39,5 @@ class TestLogger(unittest.TestCase): self.assertTrue(self.aura_logger.logger.hasHandlers()) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() -- GitLab