diff --git a/.gitignore b/.gitignore index 6cb3be02904fa80bd49e9f6c99135abfb1e7d012..4c72169115408ad41e98232601df575d39fc6ca4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ env.list audio python __pycache__ -config/engine.docker.ini +config/docker.engine.ini diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..c543aa139972f05b67786ef3658464e0d2d064cd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,5 @@ +# Contributing + +When contributing to this repository, please first read about [contributing to AURA](https://gitlab.servus.at/aura/meta/-/blob/master/docs/development/contributions.md). Then discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. + +Please note we have a [code of conduct](/aura/meta/-/blob/master/docs/development/code_of_conduct.md), please follow it in all your interactions with the project. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 11050e39776b16c4dd42b5924c012fbfd3388e16..85f1a7addc00d46dd7a554d0940d94adee0dc249 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,4 +31,4 @@ VOLUME ["/srv/socket", "/srv/logs", "/var/audio/source", "/var/audio/playlist"] # Start the Engine EXPOSE 1337/tcp -ENTRYPOINT ["./run.sh"] +ENTRYPOINT ["./run.sh", "prod"] diff --git a/README.md b/README.md index 07219f42933b76240a68c370e750ad490e2f3013..f52be2aaaee84b5a6bce6a982df112b7459b3046 100644 --- a/README.md +++ b/README.md @@ -4,30 +4,30 @@ <img src="https://gitlab.servus.at/autoradio/meta/-/raw/master/assets/images/aura-engine.png" width="250" align="right" /> -Aura Engine is a scheduling and play-out engine as part of [Aura Radio Software Suite](#About), specifically build for +Aura Engine is a scheduling and play-out engine as part of [Aura Radio Software Suite](#About), specifically built for the requirements of community radio stations. <!-- TOC --> 1. [Aura Engine](#aura-engine) - 1. [Functionality](#functionality) - 1. [Scheduler](#scheduler) - 1. [Versatile playlists](#versatile-playlists) - 2. [Default playlists](#default-playlists) - 2. [Heartbeat Monitoring](#heartbeat-monitoring) - 3. [Logging](#logging) - 2. [Getting started](#getting-started) - 3. [Using Docker](#using-docker) - 4. [Read more](#read-more) - 5. [About](#about) + 1. [Functionality](#functionality) + 1. [Scheduler](#scheduler) + 1. [Versatile playlists](#versatile-playlists) + 2. [Default playlists](#default-playlists) + 2. [Heartbeat Monitoring](#heartbeat-monitoring) + 3. [Logging](#logging) + 2. [Getting started](#getting-started) + 3. [Using Docker](#using-docker) + 4. [Read more](#read-more) + 5. [About](#about) <!-- /TOC --> ## Functionality -In conjuction with other AURA components Engine provides several features: +In conjunction with other AURA components Engine provides several features: -- **Scheduler** to automatically broadcast your radio programme (see [AURA Dashboard](https://gitlab.servus.at/aura/dashboard) for an user interface to do scheduling) +- **Scheduler** to automatically broadcast your radio programme (see [AURA Dashboard](https://gitlab.servus.at/aura/dashboard) for a user interface to do scheduling) - **Analog input and outputs** provided by [Engine Core](https://gitlab.servus.at/aura/engine-core) - **Streaming to an [Icecast](https://icecast.org/) Server including [Icy Metadata](https://cast.readme.io/docs/icy)** provided by [Engine Core](https://gitlab.servus.at/aura/engine-core) - **Autonomous playout** by caching the schedule information pulled from [Steering](https://gitlab.servus.at/aura/steering) in a local database. This allows Engine be keep running, independently from any network or service outages. This enables the application of (*High Availability* infrastructure scenarios)[https://gitlab.servus.at/aura/meta/-/blob/master/docs/administration/installation-guide.md#high-availability]. @@ -41,7 +41,7 @@ In conjuction with other AURA components Engine provides several features: Engine provide a scheduling functionality by polling external API endpoints frequently. Those API endpoints are provided by [Steering](https://gitlab.servus.at/aura/steering) to retrieve schedule information and [Tank](https://gitlab.servus.at/aura/tank) to retrieve playlist information. To define your schedule you'll also need [AURA Dashboard](https://gitlab.servus.at/aura/dashboard) which is an elegent web user interface to manage your shows, playlists and schedules. -Ideally any audio is scheduled some time before the actual, planned playout to avoid timing issues with buffering and preloading. Nonetheless, playlists can also be scheduled after a given calendar timeslot has started already. In such case the playout starts as soon it's preloaded. +Ideally any audio is scheduled some time before the actual, planned playout to avoid timing issues with buffering and preloading. Nonetheless, playlists can also be scheduled after a given calendar timeslot has started already. In such case the playout starts as soon it is preloaded. If for some reason the playout is corrupted, stopped or too silent to make any sense, then this <u>triggers a fallback using the silence detector</u> (see chapter below). @@ -49,28 +49,28 @@ If for some reason the playout is corrupted, stopped or too silent to make any s #### Versatile playlists -It's possible to schedules playlists with music or pre-recorded shows stored on the **file system**, via external **streams** or live from an **analog input** in the studio. All types of sources can be mixed in a single playlist. +It is possible to schedules playlists with music or pre-recorded shows stored on the **file system**, via external **streams** or live from an **analog input** in the studio. All types of sources can be mixed in a single playlist. The switching between types of audio source is handled automatically, with configured fadings applied. -> Note: Any live sources or streams not specifing a length property, are automatically expanded to the left duration of the timeslot. +> Note: Any live sources or streams not specifying a length property, are automatically expanded to the left duration of the timeslot. #### Default playlists -While a timeslot can have a specific playlist assigned, it's also possible to define default playlists +While a timeslot can have a specific playlist assigned, it is also possible to define default playlists for schedules and shows: - **Default Schedule Playlist**: This playlist is defined on the level of some recurrence rules (*Schedule*). In case the timeslot doesn't have any specific playlist assigned, this playlist is broadcasted. - **Default Show Playlist**: This playlist can be assigned to some show. If neither the specific timeslot - playlist nor the default schedule playlist is specificed the *default show playlist* is broadcasted. + playlist nor the default schedule playlist is specified the *default show playlist* is broadcasted. If none of these playlists have been specified the *Auto DJ* feature of [Engine Core](https://gitlab.servus.at/aura/engine-core) takes over (optional). ### Heartbeat Monitoring -Instead of checking all status properties, the Heartbeat only validates the vital ones required to run the engine. If all of those are valid, a network socket request is sent to a defined server. This heartbeat is sent continiously based on the configured `heartbeat_frequency`. The service receiving this heartbeat ticks can decide what to do with that information. One scenario could be switching to another Engine instance or any other custom failover scenario. Under `contrib/heartbeat-monitor` you'll find some sample application digesting these heartbeat signals. +Instead of checking all status properties, the Heartbeat only validates the vital ones required to run the engine. If all of those are valid, a network socket request is sent to a defined server. This heartbeat is sent continuously based on the configured `heartbeat_frequency`. The service receiving this heartbeat ticks can decide what to do with that information. One scenario could be switching to another Engine instance or any other custom failover scenario. Under `contrib/heartbeat-monitor` you'll find some sample application digesting these heartbeat signals. ### Logging @@ -84,21 +84,34 @@ For production we recommend running Engine using Docker Compose. If you want to ## Using Docker -If you only want to run the single Engine Docker container, you can do this in a few, simple steps. Before getting started copy the default configuration file to `config/engine.docker.ini`: +If you only want to run a single Engine Docker container, you can do this in a few, simple steps. + +Before getting started copy the default configuration file to `config/engine.docker.ini`: + +```shell + cp config/sample-docker.engine.ini config/docker.engine.ini +``` + +You'll need update a few settings: + +- The password `db_pass` for the local database holding scheduling information +- The app secret `api_tank_secret` for connecting to [AURA Tank](https://gitlab.servus.at/aura/tank) +- Also check the `ENV` variables defined in the `run.sh` script. + +At the moment production deployment using Docker and Docker Compose is [*work in progress*](https://gitlab.servus.at/aura/meta/-/issues/56). + +If you would like to run the local codebase, starting Engine in Docker requires you to do a build first: ```shell - cp config/sample-docker.engine.ini config/engine.docker.ini + ./run.sh docker:build ``` -You'll need to do a few configurations which are required: - - The password `db_pass` for the local database holding scheduling information - - The app secret `api_tank_secret` for connecting to [AURA Tank](https://gitlab.servus.at/aura/tank) - - Also check the `ENV` variables defined in the `run.sh` script. +After your build has finished start the Engine with following command. -Now start the engine with: +If no build is available it pulls the latest image from [Docker Hub](https://hub.docker.com/r/autoradio/engine). ```shell - ./run.sh docker:engine + ./run.sh docker:dev ``` ## Read more @@ -112,7 +125,7 @@ Now start the engine with: [<img src="https://gitlab.servus.at/autoradio/meta/-/raw/master/assets/images/aura-logo.png" width="150" />](https://gitlab.servus.at/aura/meta) -AURA stands for Automated Radio and is a swiss army knife for community radio stations. Beside the Engine it provides Steering (Admin Interface for the radio station), Dashboard (Collaborative scheduling and programme coordination), Tank (Audio uploading, pre-processing and delivery). Read more in the [Aura Meta](https://gitlab.servus.at/aura/meta) repository or on the specific project pages. +Automated Radio (AURA) is a open source software suite for community radio stations. Learn more about AURA in the [Meta repository](https://gitlab.servus.at/aura/meta). | [<img src="https://gitlab.servus.at/aura/meta/-/raw/master/assets/images/aura-steering.png" width="150" align="left" />](https://gitlab.servus.at/aura/steering) | [<img src="https://gitlab.servus.at/aura/meta/-/raw/master/assets/images/aura-dashboard.png" width="150" align="left" />](https://gitlab.servus.at/aura/dashboard) | [<img src="https://gitlab.servus.at/aura/meta/-/raw/master/assets/images/aura-tank.png" width="150" align="left" />](https://gitlab.servus.at/aura/tank) | [<img src="https://gitlab.servus.at/aura/meta/-/raw/master/assets/images/aura-engine.png" width="150" align="left" />](https://gitlab.servus.at/aura/engine) | |---|---|---|---| diff --git a/config/sample-development.engine.ini b/config/sample-development.engine.ini index c1bb95fa0547525d25f281f11b90c4c72b468283..61472191048e2f6bb1d729fbceb4cb682f4a9326 100644 --- a/config/sample-development.engine.ini +++ b/config/sample-development.engine.ini @@ -93,7 +93,7 @@ scheduling_window_end=60 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 -# overall playout, it's delays and possible fallbacks +# overall playout, its delays and possible fallbacks input_stream_retry_delay=1 input_stream_max_retries=10 input_stream_buffer=3.0 diff --git a/config/sample-docker.engine.ini b/config/sample-docker.engine.ini index 283c1f56d519073ff0749e38f643942b04eaee6a..23e4aae4140f04c68fb179b8fb11b42d8e64129f 100644 --- a/config/sample-docker.engine.ini +++ b/config/sample-docker.engine.ini @@ -93,7 +93,7 @@ scheduling_window_end=60 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 -# overall playout, it's delays and possible fallbacks +# overall playout, its delays and possible fallbacks input_stream_retry_delay=1 input_stream_max_retries=10 input_stream_buffer=3.0 diff --git a/config/sample-production.engine.ini b/config/sample-production.engine.ini index ff0053a4fb49d89424e84ab78245079a0bb4377a..bf7653b0694c708efd5162266b22c6ec5cd83f7b 100644 --- a/config/sample-production.engine.ini +++ b/config/sample-production.engine.ini @@ -93,7 +93,7 @@ scheduling_window_end=60 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 -# overall playout, it's delays and possible fallbacks +# overall playout, its delays and possible fallbacks input_stream_retry_delay=1 input_stream_max_retries=10 input_stream_buffer=3.0 diff --git a/config/supervisor/aura-engine.conf b/config/supervisor/aura-engine.conf index df4e33c683b5355f80bf08697e1656a473f90f4c..bada2f8d90c3728d004f0459921a979d96c6e2c4 100644 --- a/config/supervisor/aura-engine.conf +++ b/config/supervisor/aura-engine.conf @@ -1,7 +1,7 @@ [program:aura-engine] user = engineuser directory = /opt/aura/engine -command = /opt/aura/engine/run.sh engine +command = /opt/aura/engine/run.sh prod priority = 666 autostart = true diff --git a/config/systemd/aura-engine.service b/config/systemd/aura-engine.service index 004b652a49ba8b7a3a2b8e3d95c0ba4a86d05e22..02076fceac47108ea6eed23770a6592b9156c2ac 100644 --- a/config/systemd/aura-engine.service +++ b/config/systemd/aura-engine.service @@ -8,7 +8,7 @@ Requires=aura-engine-core.service Type=simple User=engineuser WorkingDirectory=/opt/aura/engine -ExecStart=/opt/aura/engine/run.sh +ExecStart=/opt/aura/engine/run.sh prod Restart=always [Install] diff --git a/contrib/heartbeat-monitor/PyHeartBeat.py b/contrib/heartbeat-monitor/PyHeartBeat.py old mode 100644 new mode 100755 index 5b3e495dbdcbc4797170fa1d1f4e1dd0c7470473..0115d99bcf4986c52ca651fe56b945b4b6268bbb --- a/contrib/heartbeat-monitor/PyHeartBeat.py +++ b/contrib/heartbeat-monitor/PyHeartBeat.py @@ -1,19 +1,19 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # Copyright (c) 2001, Nicola Larosa # All rights reserved. -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions # are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided -# with the distribution. -# * Neither the name of the <ORGANIZATION> nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. +# with the distribution. +# * Neither the name of the <ORGANIZATION> nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS @@ -34,54 +34,72 @@ clients that sent at least one packet during the run, but have not sent any packet since a time longer than the definition of the timeout. Adjust the constant parameters as needed, or call as: - PyHBServer.py [timeout [udpport]] + + PyHeartBeat.py [udpport [timeout]] + +Set the environment variable "DEBUG" to "1" in order to emit more detailed +debug messages. +In addition "127.0.0.1" is marked as a previously active peer. + + +Manual heartbeat messages can be easily sent via "netcat": + + echo foo | nc -q 1 -u localhost 43334 + https://www.oreilly.com/library/view/python-cookbook/0596001673/ch10s13.html """ -HBPORT = 43334 -CHECKWAIT = 10 - -from socket import socket, gethostbyname, AF_INET, SOCK_DGRAM +import os +import socket +import sys from threading import Lock, Thread, Event from time import time, ctime, sleep -import sys + + +DEFAULT_HEARTBEAT_PORT = 43334 +DEFAULT_WAIT_PERIOD = 10 +DEBUG_ENABLED = os.getenv("DEBUG", "0") == "1" + class BeatDict: "Manage heartbeat dictionary" def __init__(self): self.beatDict = {} - if __debug__: - self.beatDict['127.0.0.1'] = time( ) - self.dictLock = Lock( ) + if DEBUG_ENABLED: + self.beatDict["127.0.0.1"] = time() + self.dictLock = Lock() def __repr__(self): - list = '' - self.dictLock.acquire( ) - for key in self.beatDict.keys( ): - list = "%sIP address: %s - Last time: %s\n" % ( - list, key, ctime(self.beatDict[key])) - self.dictLock.release( ) - return list + result = "" + self.dictLock.acquire() + for key in self.beatDict.keys(): + result += "IP address: %s - Last time: %s\n" % ( + key, + ctime(self.beatDict[key]), + ) + self.dictLock.release() + return result def update(self, entry): "Create or update a dictionary entry" - self.dictLock.acquire( ) - self.beatDict[entry] = time( ) - self.dictLock.release( ) + self.dictLock.acquire() + self.beatDict[entry] = time() + self.dictLock.release() def extractSilent(self, howPast): "Returns a list of entries older than howPast" silent = [] - when = time( ) - howPast - self.dictLock.acquire( ) - for key in self.beatDict.keys( ): + when = time() - howPast + self.dictLock.acquire() + for key in self.beatDict.keys(): if self.beatDict[key] < when: silent.append(key) - self.dictLock.release( ) + self.dictLock.release() return silent + class BeatRec(Thread): "Receive UDP packets, log them in heartbeat dictionary" @@ -90,52 +108,62 @@ class BeatRec(Thread): self.goOnEvent = goOnEvent self.updateDictFunc = updateDictFunc self.port = port - self.recSocket = socket(AF_INET, SOCK_DGRAM) - self.recSocket.bind(('', port)) + self.recSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.recSocket.settimeout(0.2) + self.recSocket.bind(("", port)) def __repr__(self): - return "Heartbeat Server on port: %d\n" % self.port + return f"Heartbeat Server on port: {self.port}" def run(self): - while self.goOnEvent.isSet( ): - if __debug__: - print "Waiting to receive..." - data, addr = self.recSocket.recvfrom(6) - if __debug__: - print "Received packet from " + `addr` - self.updateDictFunc(addr[0]) - -def main( ): + while self.goOnEvent.isSet(): + if DEBUG_ENABLED: + print("Waiting to receive...") + try: + data, addr = self.recSocket.recvfrom(6) + except socket.timeout: + # no incoming message -> no timestamp update -> check again + pass + else: + if DEBUG_ENABLED: + print(f"Received packet from {addr}") + self.updateDictFunc(addr[0]) + + +def main(): "Listen to the heartbeats and detect inactive clients" - global HBPORT, CHECKWAIT - if len(sys.argv)>1: - HBPORT=sys.argv[1] - if len(sys.argv)>2: - CHECKWAIT=sys.argv[2] - - beatRecGoOnEvent = Event( ) - beatRecGoOnEvent.set( ) - beatDictObject = BeatDict( ) - beatRecThread = BeatRec(beatRecGoOnEvent, beatDictObject.update, HBPORT) - if __debug__: - print beatRecThread - beatRecThread.start( ) - print "PyHeartBeat server listening on port %d" % HBPORT - print "\n*** Press Ctrl-C to stop ***\n" - while 1: + if len(sys.argv) > 1: + heartbeat_port = int(sys.argv[1]) + else: + heartbeat_port = DEFAULT_HEARTBEAT_PORT + if len(sys.argv) > 2: + wait_period = float(sys.argv[2]) + else: + wait_period = DEFAULT_WAIT_PERIOD + + beatRecGoOnEvent = Event() + beatRecGoOnEvent.set() + beatDictObject = BeatDict() + beatRecThread = BeatRec(beatRecGoOnEvent, beatDictObject.update, heartbeat_port) + if DEBUG_ENABLED: + print(beatRecThread) + beatRecThread.start() + print(f"PyHeartBeat server listening on port {heartbeat_port}") + print("\n*** Press Ctrl-C to stop ***\n") + while True: try: - if __debug__: - print "Beat Dictionary" - print `beatDictObject` - silent = beatDictObject.extractSilent(CHECKWAIT) + if DEBUG_ENABLED: + print(f"Beat Dictionary: {beatDictObject}") + silent = beatDictObject.extractSilent(wait_period) if silent: - print "Silent clients" - print `silent` - sleep(CHECKWAIT) + print(f"Silent clients: {' '.join(silent)}") + sleep(wait_period) except KeyboardInterrupt: - print "Exiting." - beatRecGoOnEvent.clear( ) - beatRecThread.join( ) + print("Exiting.") + beatRecGoOnEvent.clear() + beatRecThread.join() + break + -if __name__ == '__main__': - main( ) \ No newline at end of file +if __name__ == "__main__": + main() diff --git a/docs/bare-metal-installation.md b/docs/bare-metal-installation.md index 4d2e5e160bef0be464bfae08e6694b301d0aae6d..911d52d7e5719652c4e13846e138ebb0213eb167 100644 --- a/docs/bare-metal-installation.md +++ b/docs/bare-metal-installation.md @@ -8,6 +8,7 @@ - [Setting up the database](#setting-up-the-database) - [Configuration](#configuration) - [Running Engine](#running-engine) + - [Starting dependencies](#starting-dependencies) - [Daemonized Engine](#daemonized-engine) - [Running with Systemd](#running-with-systemd) - [Running with Supervisor](#running-with-supervisor) @@ -59,9 +60,9 @@ Create your base configuration from the sample configuration ```shell # Development -cp config/sample.development.engine.ini config/engine.ini +cp config/sample-development.engine.ini config/engine.ini # Production -cp config/sample.production.engine.ini config/engine.ini +cp config/sample-production.engine.ini config/engine.ini ``` ### Setting up the database @@ -83,9 +84,9 @@ In your development environment edit following file to configure the engine: ./config/engine.ini ``` -> Please note, if some configuration exists under `/etc/aura/engine.ini` the configuration by default is drawn from there. +> Please note, if some configuration exists under `/etc/aura/engine.ini` the configuration by default is drawn from there. This overrides any configuration located in `./engine/config`. -While the configuration has plenty of configuration options, you only need to set a few mandatory ones, given you are running the other components (such as 'engine-core', "engine-api" etc.) at the default settings too. +While the configuration file has plenty of options, you only need to set a few mandatory ones, given you are running the other components (such as 'engine-core', "engine-api" etc.) at their default settings too. Required modifications are: - The password `db_pass` for the local database holding scheduling information @@ -96,16 +97,42 @@ Required modifications are: There's a convencience script `run.sh` to get engine started ```shell - engine$ ./run.sh +engine$ ./run.sh ``` -Keep in mind you'll also need to start Engine Core separately +The script executes the *default target*, which is usually `dev` for development environments. + +You can call this target explicitely too: + +```shell +engine$ ./run.sh dev +``` + +Or run Engine in production mode: + +```shell +engine$ ./run.sh prod +``` + +For details on the run script, consult the [AURA CLI documentation](https://gitlab.servus.at/aura/meta/-/blob/master/docs/administration/cli.md). + +### Starting dependencies + +You'll also need to start Engine Core separately. + +> Note it should not matter in which order you start Engine and Engine Core. ```shell - engine-core$ ./run.sh +engine-core$ ./run.sh ``` -In order to have a full engine experience also the other AURA Components are required to be running. For convencience in starting the full environment checkout how to run Aura Web using Docker Compose within the [Meta Repository](https://gitlab.servus.at/aura/tank) +Last but not least, Engine API is the target service to store playlogs, health information and details for the [Studio Clock](https://gitlab.servus.at/aura/dashboard-clock). + +```shell +engine-api$ ./run.sh +``` + +In order to have the complete Engine experience, other AURA Components are required to be running too. Checkout the [Meta Repository](https://gitlab.servus.at/aura/meta) on how to run for example AURA Web using Docker Compose. ## Daemonized Engine @@ -124,7 +151,7 @@ systemctl daemon-reload ### Running with Supervisor -Now, given you are in the engine's home directory `/opt/aura/engine/`, simply type following to start the services: +Now, given you are in the engine's home directory like `/opt/aura/engine/`, simply type following to start the services: ```shell supervisord @@ -138,8 +165,6 @@ Then you'll need to reload the supervisor configuration using `sudo`: sudo supervisorctl reload ``` - - ## Logging All Engine logs can be found under `./logs`. diff --git a/docs/developer-guide.md b/docs/developer-guide.md index a2cb20f9c68b1c382123533cd337bafc48becb4a..00c5441e0df53b04acc897e5d5a16a440d88e8e8 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -5,7 +5,7 @@ This page gives insights on extending Aura Engine internals or through the API. <!-- TOC --> 1. [Aura Engine Development Guide](#aura-engine-development-guide) - 1. [AURA Componentes](#aura-componentes) + 1. [AURA Components](#aura-componentes) 2. [Engine Components](#engine-components) 3. [Running for Development](#running-for-development) 4. [Testing](#testing) @@ -16,7 +16,7 @@ This page gives insights on extending Aura Engine internals or through the API. <!-- /TOC --> -## AURA Componentes +## AURA Components AURA Engine as part of the AURA Radio Suite uses an modulear architecture based on a REST API. All external information is retrieved using JSON data-structures. @@ -28,25 +28,19 @@ For example: - Steering, to get the main incredient of an play-out engine: schedules (or "timeslots" in Steering terms), which hold the actual information on playlists and their entries. -- Dashboard, to have a neat interface, being able to programm the timeslots +- Dashboard, to have a neat interface, being able to programme the timeslots - Tank, to get the references to audio files and other audio sources. Plus the actual files. If you need to test and develop against the Engine's API you'll also need to get the `engine-api` project running. -For a start it's recommended to create a general `aura` project folder. In there you start cloning all the sub-projects. After having all the sub-projects configured, and verified that they are working, take a look at the AURA `meta` project. +For a start it is recommended to create a general `aura` project folder. In there you start cloning all the sub-projects. After having all the sub-projects configured, and verified that they are working, take a look at the AURA `meta` project. There's a convenience script to start all of the three main dependencies (Steering, Dashboard, Tank) all at once: -```bash - ~/code/aura/meta$ ./run.sh aura local -``` - ## Engine Components - *...TBD...* - ## Running for Development Ensure you have following other projects up and running: @@ -57,7 +51,7 @@ Ensure you have following other projects up and running: - engine-api - dashboard-clock (optional) -The following steps espect you having done the bases configuration and set up a database as outlined in the [Native Installation](https://gitlab.servus.at/aura/engine/-/blob/master/docs/bare-metal-installation.md) document. +The following steps expect you having done the bases configuration and set up a database as outlined in the [Native Installation](https://gitlab.servus.at/aura/engine/-/blob/master/docs/bare-metal-installation.md) document. If you don't have already, you'll need to create an virtual environment: @@ -122,11 +116,11 @@ point in time and the involved phase before: in `engine.ini`). The actual start of the window is calcuated by (timeslot start - window start) and the end by (timeslot end - window end) - During the scheduling window, the external API Endpoints are pulled continiously, to + During the scheduling window, the external API Endpoints are pulled continuously, to check for updated timeslots and related playlists. Also, any changes to playlists and its entries are respected within that window (see `fetching_frequency` in `engine.ini`). - > Important: It's vital that the the scheduling window is wider than the fetching frequency. + > Important: It is vital that the the scheduling window is wider than the fetching frequency. Otherwise one fetch might never hit a scheduling window, hence not being able to schedule stuff. > Note: If you delete any existing timeslot in Dashboard/Steering this is only reflected in Engine until the start @@ -148,7 +142,7 @@ point in time and the involved phase before: or due to some severe connectivity issues to some external stream. - **Play-out**: Finally the actual play-out is happening. The faders of the virtual mixers are pushed - all the way up, as soon it's "time to play" for one of the pre-loaded entries. + all the way up, as soon it is "time to play" for one of the pre-loaded entries. Transitions between playlist entries with different types of sources (file, stream and analog inputs) are performed automatically. At the end of each timeslot the channel is faded-out, no matter if the total length of the playlist entries would require a longer timeslot. @@ -165,6 +159,12 @@ Build your own, local Docker image ./run.sh docker:build ``` +Run the locally build image + +```shell +./run.sh docker:dev +``` + Releasing a new version to DockerHub ```shell diff --git a/requirements.txt b/requirements.txt index e4712a60a901e6996bbbb52a3f894854b0c1648c..daa7520de4ca7a57aae92c5bef2d1e43009201e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ sqlalchemy==1.3.17 Flask==1.1.2 Flask_SQLAlchemy==2.4.3 validators==0.12.1 -http-parser==0.9.0 \ No newline at end of file +http-parser==0.9.0 +wheel==0.37.0 \ No newline at end of file diff --git a/run.py b/run.py index f56e0569d4653c56e5110abca8ffcdb3df8b3ed4..d2f036f903b2ecd456fa227ad6bbb22de072dd68 100755 --- a/run.py +++ b/run.py @@ -26,6 +26,7 @@ import os import sys import signal import logging +import threading from flask import Flask from flask_sqlalchemy import SQLAlchemy @@ -41,7 +42,7 @@ def configure_flask(): app.config['BABEL_DEFAULT_LOCALE'] = 'de' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -# FIXME Instatiate SQLAlchemy without the need for Flask +# FIXME Instantiate SQLAlchemy without the need for Flask app = Flask(__name__) configure_flask() DB = SQLAlchemy(app) @@ -54,7 +55,6 @@ class EngineRunner: logger = None config = None engine = None - lqs = None def __init__(self): @@ -64,12 +64,12 @@ class EngineRunner: self.config = config AuraLogger(self.config) self.logger = logging.getLogger("AuraEngine") - + def run(self): """ Starts Engine Core. - """ + """ from src.engine import Engine self.engine = Engine() @@ -86,15 +86,14 @@ class EngineRunner: """ Shutdown of the engine. Also terminates the Liquidsoap thread. """ + for thread in threading.enumerate(): + self.logger.info(thread.name) + if self.engine: self.engine.terminate() - if self.lqs: - self.lqs.terminate() - self.logger.info("Terminated Liquidsoap") - - self.logger.info("Gracefully terminated Aura Engine! (signum:%s, frame:%s)" % (signum, frame)) - sys.exit(0) + self.logger.info(f"Gracefully terminated Aura Engine! (signum:{signum}, frame:{frame})") + sys.exit(0) @@ -103,7 +102,7 @@ class EngineRunner: # -if __name__ == "__main__": +if __name__ == "__main__": runner = EngineRunner() signal.signal(signal.SIGINT, runner.exit_gracefully) signal.signal(signal.SIGTERM, runner.exit_gracefully) @@ -112,5 +111,5 @@ if __name__ == "__main__": if "--recreate-database" in sys.argv: runner.recreate_db() sys.exit(0) - + runner.run() diff --git a/run.sh b/run.sh index 2bd3e6f5d1c7602056d833b7913b7e0e9b275fe1..b5323c6fee99f7acdec066da17caeff68eeb973a 100755 --- a/run.sh +++ b/run.sh @@ -1,5 +1,5 @@ #!/bin/bash -mode="engine" +mode="dev" docker="false" # @@ -8,16 +8,17 @@ docker="false" # Call with one of these parameters: # # - dev -# - engine +# - prod # - test # - recreate-database -# - docker:engine +# - docker:dev +# - docker:prod # - docker:build # - docker:push # -if [[ $* =~ ^(dev|engine|test|recreate-database)$ ]]; then +if [[ $* =~ ^(dev|prod|test|recreate-database)$ ]]; then mode=$1 fi @@ -50,7 +51,7 @@ if [[ $docker == "false" ]]; then ### Runs Engine ### - if [[ $mode == "engine" ]]; then + if [[ $mode == "prod" ]]; then /usr/bin/env $PYTHON_EXEC run.py fi @@ -77,12 +78,12 @@ if [[ $docker == "true" ]]; then BASE_DIR=$(readlink -f .) echo "Absolute base dir: " $BASE_DIR - ### Runs Engine Docker Container ### + ### Runs Engine Docker Container (Local build for development, if n/a pull from Docker Hub) ### - if [[ $mode == "engine" ]]; then + if [[ $mode == "dev" ]]; then if [[ -z "${AURA_ENGINE_CONFIG_PATH}" ]]; then - AURA_ENGINE_CONFIG_PATH=$(readlink -f "${BASE_DIR}/config/engine.docker.ini") + AURA_ENGINE_CONFIG_PATH=$(readlink -f "${BASE_DIR}/config/docker.engine.ini") fi if [[ -z "${AURA_ENGINE_SOCKET_DIR}" ]]; then diff --git a/src/base/exceptions.py b/src/base/exceptions.py index 9e14ae38dc209b04d80c766a37f77cfc76279ea1..da55ab28ecc0f8b9920148ac94722f7723159912 100644 --- a/src/base/exceptions.py +++ b/src/base/exceptions.py @@ -43,7 +43,7 @@ class NoActiveEntryException(Exception): -# Liquidsoap Execeptions +# Liquidsoap Exceptions class LQConnectionError(Exception): pass diff --git a/src/base/utils.py b/src/base/utils.py index 1239e04baf488a47e535f9d17b33b7995fc891e2..8bba00d75bb811aae7f0b1c47d7a0e4725134e8a 100644 --- a/src/base/utils.py +++ b/src/base/utils.py @@ -37,7 +37,7 @@ class SimpleUtil: This alters the input so you may wish to `copy` the dict first. Args: - data (dict): The dicationary + data (dict): The dictionary Returns: (dict): @@ -67,7 +67,7 @@ class SimpleUtil: @staticmethod def nano_to_seconds(nanoseconds): """ - Converts nano-seconds to senconds + Converts nano-seconds to seconds Args: (Integer) nanoseconds @@ -81,7 +81,7 @@ class SimpleUtil: @staticmethod def seconds_to_nano(seconds): """ - Converts senconds to nano-seconds + Converts seconds to nano-seconds Args: (Integer) seconds diff --git a/src/client/connector.py b/src/client/connector.py index 1f5811f6f87c7ad5d83416a3f92e86eaefe18837..0662aa68ace4bbb510aa5be16543754e6cf1a7a2 100644 --- a/src/client/connector.py +++ b/src/client/connector.py @@ -96,7 +96,7 @@ class PlayerConnector(): 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: @@ -136,7 +136,7 @@ class PlayerConnector(): self.disable_transaction(socket=self.client, force=True) raise e else: - self.event_dispatcher.on_critical("Criticial Liquidsoap connection issue", \ + self.event_dispatcher.on_critical("Critical Liquidsoap connection issue", \ "Could not connect to Liquidsoap after multiple attempts", e) raise e diff --git a/src/control.py b/src/control.py index bdcd260d3c3ced1223376168b0d03cc3491476cc..c9a9dd48b21823124e40162fb84356dafcce23cc 100644 --- a/src/control.py +++ b/src/control.py @@ -73,7 +73,7 @@ class SocketControlInterface: Network socket server to control a running Engine from Liquidsoap. Note this server only allows a single connection at once. This - service is primarly utilized to store new playlogs. + service is primarily utilized to store new playlogs. """ PORT = 1337 ACTION_ON_METADATA = "on_metadata" @@ -237,7 +237,7 @@ class EngineExecutor(Timer): is_stored = self.update_store() if not is_stored: - self.logger.info(SU.yellow(f"Timer '{self.timer_id}' omitted because it's already existing but dead")) + self.logger.info(SU.yellow(f"Timer '{self.timer_id}' omitted because it already exists but is dead")) self.is_aborted = True else: if diff < 0: @@ -256,7 +256,7 @@ class EngineExecutor(Timer): """ @private - Child timers are dependend on their parents. So let's wait until parents are done with their stuff => finished execution. + Child timers are dependent on their parents. So let's wait until parents are done with their stuff => finished execution. Checks the parent state to be finished every 0.2 seconds. """ if self.parent_timer: @@ -269,7 +269,7 @@ class EngineExecutor(Timer): """ @private - Immediate execution within a thread. It's not stored in the timer store. + Immediate execution within a thread. It is not stored in the timer store. It also assigns the `timer_id` as the thread name. """ diff --git a/src/engine.py b/src/engine.py index 11a0722270d7c2ef40b6e002a1cdf31420b2b761..70dd94de37dc8a5ef00e915f48cee893efc77a39 100644 --- a/src/engine.py +++ b/src/engine.py @@ -167,10 +167,12 @@ class Engine(): """ Terminates the engine and all related processes. """ + if self.scheduler: self.scheduler.terminate() if self.eci: self.eci.terminate() + # # PLAYER # @@ -187,7 +189,7 @@ class Player: channel_router = None event_dispatcher = None mixer = None - mixer_fallback = None + # mixer_fallback = None @@ -204,7 +206,7 @@ class Player: self.connector = connector self.channel_router = ChannelRouter(self.config, self.logger) self.mixer = Mixer(self.config, MixerType.MAIN, self.connector) - self.mixer_fallback = Mixer(self.config, MixerType.FALLBACK, self.connector) + # self.mixer_fallback = Mixer(self.config, MixerType.FALLBACK, self.connector) @@ -213,7 +215,7 @@ class Player: Pre-Load the entry. This is required before the actual `play(..)` can happen. Be aware when using this method to queue a very short entry (shorter than ``) this may - result in sitations with incorrect timing. In this case bundle multiple short entries as + result in situations 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 @@ -295,7 +297,7 @@ class Player: def play(self, entry, transition): """ - Plays a new `Entry`. In case of a new timeslot (or some intented, immediate transition), + Plays a new `Entry`. In case of a new timeslot (or some intended, immediate transition), 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)` @@ -313,8 +315,8 @@ class Player: channel_type = self.channel_router.type_of_channel(entry.channel) mixer = self.mixer - if channel_type == ChannelType.FALLBACK_QUEUE: - mixer = self.mixer_fallback + # if channel_type == ChannelType.FALLBACK_QUEUE: + # mixer = self.mixer_fallback # Instant activation or fade-in self.connector.enable_transaction() @@ -425,7 +427,7 @@ class Player: entry (Entry): The entry to be pre-loaded Returns: - (Boolean): `True` if successfull + (Boolean): `True` if successful """ self.stream_load(entry.channel, entry.source) time.sleep(1) @@ -448,7 +450,7 @@ class Player: def stream_load(self, channel, url): """ Preloads the stream URL on the given channel. Note this method is blocking - some serious amount of time; hence it's worth being called asynchroneously. + some serious amount of time; hence it is worth being called asynchronously. Args: channel (Channel): The stream channel @@ -486,7 +488,7 @@ class Player: def stream_is_ready(self, channel, url): """ Checks if the stream on the given channel is ready to play. Note this method is blocking - some serious amount of time even when successfull; hence it's worth being called asynchroneously. + some serious amount of time even when successful; hence it's worth being called asynchronously. Args: channel (Channel): The stream channel diff --git a/src/events.py b/src/events.py index c73ea27e5872a53f5faf82e47796e364d63a1a1c..826fd48f0aeadbe303230ef0c803e7273808154b 100644 --- a/src/events.py +++ b/src/events.py @@ -221,7 +221,7 @@ class EngineEventDispatcher(): def on_play(self, entry): """ Event Handler which is called by the engine when some play command to Liquidsoap is issued. - This does not indiciate that Liquidsoap started playing actually, only that the command has + This does not indicate that Liquidsoap started playing actually, only that the command has been issued. To get the metadata update issued by Liquidsoap use `on_metadata` instead. Args: diff --git a/src/mixer.py b/src/mixer.py index d383840b0dd7a96d4e55e514535982563538ef9f..a1ed2a67e8a69e2ec3690eb631e7718f43cabb4d 100644 --- a/src/mixer.py +++ b/src/mixer.py @@ -34,7 +34,7 @@ class MixerType(Enum): Types of mixers mapped to the Liquidsoap mixer ids. """ MAIN = "mixer" - FALLBACK = "mixer_fallback" + # FALLBACK = "mixer_fallback" @@ -169,7 +169,7 @@ class Mixer(): def mixer_channels_reload(self): """ - Reloads all mixer channels. + Reloads all mixer channels. """ self.channels = None return self.mixer_channels() @@ -185,7 +185,7 @@ class Mixer(): Args: channel (Channel): The channel - + Returns: (Integer): The channel number """ @@ -203,7 +203,7 @@ class Mixer(): Args: channel_number (Integer): The channel number - + Returns: (String): Channel status info as a String """ @@ -239,7 +239,7 @@ class Mixer(): """ Combined call of following to save execution time: - Select some mixer channel - - Increase the volume to 100, + - Increase the volume to 100, Args: pos (Integer): The channel number @@ -331,7 +331,7 @@ class Mixer(): Args: channel (Channel): The channel to fade volume (Integer): The target volume - + Returns: (Boolean): `True` if successful """ @@ -382,12 +382,12 @@ class Mixer(): def fade_out(self, channel, volume=None): """ - Performs a fade-out for the given channel starting at it's current volume. - + Performs a fade-out for the given channel starting at its current volume. + Args: channel (Channel): The channel to fade volume (Integer): The start volume - + Returns: (Boolean): `True` if successful """ diff --git a/src/plugins/monitor.py b/src/plugins/monitor.py index 3ae57fbf71865499f1f6c1571a6dc9307787b0d2..87ecdcf2820e376bec6485c19f31df11ea162ec1 100644 --- a/src/plugins/monitor.py +++ b/src/plugins/monitor.py @@ -220,7 +220,7 @@ class AuraMonitor: self.status["lqs"]["uptime"] = self.engine.uptime() self.status["lqs"]["io"] = self.get_io_state() self.status["lqs"]["mixer"] = self.engine.player.mixer.mixer_status() - self.status["lqs"]["mixer_fallback"] = self.engine.player.mixer_fallback.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") @@ -254,7 +254,7 @@ class AuraMonitor: def heartbeat(self): """ - Every `heartbeat_frequency` seconds the current vitality status is checked. If it's okay, + Every `heartbeat_frequency` seconds the current vitality status is checked. If it is okay, a heartbeat is sent to the configured server. """ if self.has_valid_status(True): @@ -366,4 +366,4 @@ class AuraMonitor: return s.getsockname()[0] except: self.logger.critical(SU.red("Error while accessing network via <broadcast>!")) - return "<UNKNOWN NETWORK>" \ No newline at end of file + return "<UNKNOWN NETWORK>" diff --git a/src/plugins/trackservice.py b/src/plugins/trackservice.py index 44d41504b091a067404261843e621d7f263a01b2..df8140e2eca97721508bacd5c7f9292837fc134b 100644 --- a/src/plugins/trackservice.py +++ b/src/plugins/trackservice.py @@ -288,7 +288,7 @@ class Playlog: Any previous timeslot is stored to `self.previous_timeslot` and the following one to `self.next_timeslot`. - This method is protect by overwritting by multiple calls with the same timeslot. + This method is protect by overwriting by multiple calls with the same timeslot. Args: timeslot (Timeslot): The current timeslot diff --git a/src/resources.py b/src/resources.py index d2126019428b35964ca5e5a1eaae417fa9c0b162..fdb4431f408f32d65d753e2ca09eb0ba697e31b3 100644 --- a/src/resources.py +++ b/src/resources.py @@ -152,7 +152,7 @@ class ResourceUtil(Enum): absolute path to the file, appending the extension as provided in "source_extension". - If the path starts with an "/" it indicates that it's already an + If the path starts with an "/", it indicates that it is already an absolute path including a valid extension. Args: @@ -192,11 +192,11 @@ class ResourceUtil(Enum): Args: uri (String): The path to the audio source - cue_in (Float): The value in seconds wher the cue in should start + cue_in (Float): The value in seconds where the cue in should start Returns: (String): The annotated URI """ if cue_in > 0.0: uri = "annotate:liq_cue_in=\"%s\":%s" % (str(cue_in), uri) - return uri \ No newline at end of file + return uri diff --git a/src/scheduling/api.py b/src/scheduling/api.py index c7e976579c53117dcf2599ba624a0ced9820a067..bbed103821f72e502c4f3bc706bd70a94a0d67e3 100644 --- a/src/scheduling/api.py +++ b/src/scheduling/api.py @@ -286,7 +286,7 @@ class ApiFetcher(threading.Thread): def polish_timeslots(self, timeslots): """ Removes all timeslots which are not relevant for further processing, - and transparent timeslot ID assigment for more expressive use. + and transparent timeslot ID assignment for more expressive use. """ count_before = len(timeslots) timeslots = TimeslotFilter.filter_24h(timeslots) @@ -304,5 +304,5 @@ class ApiFetcher(threading.Thread): """ Terminates the thread. """ - self.logger.info("Shutting down API fetcher...") + self.logger.info(SU.yellow("[ApiFetcher] Shutting down...")) self.stop_event.set() diff --git a/src/scheduling/models.py b/src/scheduling/models.py index f5a6b487e61aca67124071155197955511ef49b0..70921228375572c2218661a165e793919d38a3a7 100644 --- a/src/scheduling/models.py +++ b/src/scheduling/models.py @@ -88,7 +88,7 @@ class AuraDatabaseModel(): def refresh(self): """ - Refreshes the currect record + Refreshes the correct record """ DB.session.expire(self) DB.session.refresh(self) @@ -152,8 +152,8 @@ class Timeslot(DB.Model, AuraDatabaseModel): playlist (Playlist): The specific playlist for this timeslot schedule_default (Playlist): Some playlist played by default, when no specific playlist is assigned show_default (Playlist): Some playlist played by default, when no default schedule playlist is assigned - schedule_fallback (Playlist): Some playlist played as fallback, when no specific playlist is assigned or if it is errorneous (includes silence detection) - show_fallback (Playlist): Some playlist played as fallback, when no schedule fallback playlist is assigned or if some specific playlist is errorneous (includes silence detection) + schedule_fallback (Playlist): Some playlist played as fallback, when no specific playlist is assigned or if it is erroneous (includes silence detection) + show_fallback (Playlist): Some playlist played as fallback, when no schedule fallback playlist is assigned or if some specific playlist is erroneous (includes silence detection) station_fallback (Playlist): Defined in the original AURA API but not implemented, as station fallbacks are handled locally """ __tablename__ = 'timeslot' diff --git a/src/scheduling/programme.py b/src/scheduling/programme.py index 3c29ca1396f0900feb408dc35465e6ffba97a4cb..a7a6172182560b25ff9d79b9e4ae246d189c72dc 100644 --- a/src/scheduling/programme.py +++ b/src/scheduling/programme.py @@ -65,7 +65,7 @@ class ProgrammeService(): # Fetch programme from API endpoints self.logger.debug("Trying to fetch new programme from API endpoints...") - # Create a fetching thread and wait until it's done + # Create a fetching thread and wait until it is done self.api_fetcher = ApiFetcher(self.config) self.api_fetcher.start() response = self.api_fetcher.get_fetched_data() @@ -230,7 +230,7 @@ class ProgrammeService(): """ Called when thread is stopped or a signal to terminate is received. """ - self.logger.info("Shutting down programme service ...") + self.logger.info(SU.yellow("[ProgrammeService] Shutting down...")) if self.api_fetcher: self.api_fetcher.terminate() diff --git a/src/scheduling/scheduler.py b/src/scheduling/scheduler.py index 62cbfd5df3551aae055a9746b68cd2976fa16e79..a9728f72135cd25dab146d399b5f215ec098500b 100644 --- a/src/scheduling/scheduler.py +++ b/src/scheduling/scheduler.py @@ -407,7 +407,7 @@ class AuraScheduler(threading.Thread): """ Called when thread is stopped or a signal to terminate is received. """ - self.logger.info("Shutting down scheduler ...") + self.logger.info(SU.yellow("[Scheduler] Shutting down...")) self.programme.terminate() self.exit_event.set() diff --git a/src/scheduling/utils.py b/src/scheduling/utils.py index ed94f7e0497c6cf5b9e563e5ce69c9af3209930e..8f11c9187b5032a42f7104b43cec9eae6b481d3e 100644 --- a/src/scheduling/utils.py +++ b/src/scheduling/utils.py @@ -169,7 +169,7 @@ class M3UPlaylistProcessor(): class TimeslotRenderer: """ - Displays current and next timeslots in ASCII for maintainence and debugging. + Displays current and next timeslots in ASCII for maintenance and debugging. """ logger = None scheduler = None diff --git a/tests/test_engine_executor.py b/tests/test_engine_executor.py index 9977dd5a912782516ed3011afcfe3a685a666840..e5034a2a45cb57070c02e29fd99fcd7500a45452 100644 --- a/tests/test_engine_executor.py +++ b/tests/test_engine_executor.py @@ -145,11 +145,11 @@ class TestEngineExecutor(unittest.TestCase): time.sleep(0.2) self.assertEqual("none", global_state[0]) - # After 0.4 seconds max there isn't a setting from the child yet, because it's waiting for the parent + # After 0.4 seconds max there isn't a setting from the child yet, because it is waiting for the parent time.sleep(0.2) self.assertNotEqual("hello world from child", global_state[0]) - # But the parent didn't set anything either, because it's scheduled for later + # But the parent didn't set anything either, because it is scheduled for later self.assertNotEqual("hello world from parent", global_state[0]) self.assertEqual("none", global_state[0]) @@ -261,4 +261,4 @@ class TestEngineExecutor(unittest.TestCase): if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main()