Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • dev-old
  • dev-old-david
  • develop
  • lars-tests
  • master
  • master-old
  • topic/filesystem-fallbacks
  • topic/tank_connection
  • topic/tank_connection_david
  • user/equinox/docker
10 results

Target

Select target project
  • aura/engine
  • hermannschwaerzler/engine
  • sumpfralle/aura-engine
3 results
Select Git revision
  • 122-synchronized-ci
  • feat-use-docker-main-tag
  • fix-aura-sysuser
  • fix-broken-pipe-153
  • fix-docker-release
  • fix-push-latest-with-tag
  • fix-streamchannel-retries
  • gitlab-templates
  • improve-test-coverage-137
  • improve-test-coverage-143
  • main
  • orm-less-scheduling
  • remove-mailer
  • update-changelog-alpha3
  • virtual-timeslots-131
  • 1.0.0-alpha1
  • 1.0.0-alpha2
  • 1.0.0-alpha3
  • 1.0.0-alpha4
  • 1.0.0-alpha5
20 results
Show changes
Showing
with 3790 additions and 0 deletions
# Aura Engine Features
This page gives a more detailed overview of the Aura Engine features and how to configure them.
<!-- TOC -->
- [Aura Engine Features](#aura-engine-features)
- [Input](#input)
- [Play audio from multiple sources](#play-audio-from-multiple-sources)
- [Dynamic switching of sources](#dynamic-switching-of-sources)
- [Output](#output)
- [Record output to filesystem](#record-output-to-filesystem)
- [Stream output to an Icecast Server](#stream-output-to-an-icecast-server)
- [Multichannel Line-out](#multichannel-line-out)
- [Blank Detenction / Silence Detecter](#blank-detenction--silence-detecter)
- [Auto Pilot a.k.a. Fallback Handling](#auto-pilot-aka-fallback-handling)
- [API Endpoints](#api-endpoints)
- [Web Applications](#web-applications)
- [Monitoring](#monitoring)
- [Send mails on errors and warnings](#send-mails-on-errors-and-warnings)
- [Engine Status Information](#engine-status-information)
- [Engine Heartbeat](#engine-heartbeat)
- [Logging](#logging)
- [Read more](#read-more)
<!-- /TOC -->
## Input
### Play audio from multiple sources
It's possible to air playlists with music or recordings stored on the **filessystem**,
via external **streams** or live from a **studio**.
### Dynamic switching of sources
TODO extend: * switch the soundserver at the correct time to a given source for a specific show
## Output
### Record output to filesystem
TODO extend: * record what is broadcasted
### Stream output to an Icecast Server
TODO extend: * stream to an icecast server
### Multichannel Line-out
TODO extend: * play to line-out
## Blank Detenction / Silence Detecter
The engine offers a simple way to detect scenarios where no music is on air.
It possible to configure the sensitivity of the Silence Detector and automatically
transition play-out to a Fallback Playlist (see Auto Pilot).
## Auto Pilot a.k.a. Fallback Handling
In case there is no schedule delivered by Steering, engine provides multiple
fallback handling scenarios. The available fallbacks are evaluated in following order:
1. **Timeslot Fallback**:
2. **Show Fallback**:
3. **Station Fallback**:
3.1 Station Fallback via remote playlist
3.2 Station Fallback via local music folder
Note: All filenames in the music folder should be ASCII encoded, otherwise this may cause engine failures.
## API Endpoints
**Track Service API**: These endpoints provide information on the played tracks, their schedules and shows.
* `/api/v1/trackservice` .............. Returns all Track Service entries for the current day
* `/api/v1/trackservice/$ID` .......... Returns a Track Service entry by ID
* `/api/v1/trackservice/current` ...... Returns the track currently playing
* `/api/v1/trackservice/day/$DAY` ..... Returns all tracks for a given day formated as `YYYY-MM-DD`
The Swagger Specification of a Track Service entry as YAML looks like this:
```yaml
components:
schemas:
TrackService:
properties:
album: {}
artist: {}
duration: {}
fallback: {}
id: {}
schedule_start: {}
title: {}
track_start: {}
type: object
info:
title: Swagger API Specification for Aura Engine
version: 1.0.0
openapi: 3.0.2
paths: {}
```
**Reporting API**: Create monthly reports using this endpoint.
* `/api/v1/report/$MONTH` ...... Returns all playout details for the given month in the format `YYYY-MM`
```yaml
schemas:
Report:
properties:
fallback_type: {}
id: {}
playlist_id: {}
schedule.category: {}
schedule.is_repetition: {}
schedule.languages: {}
schedule.musicfocus: {}
schedule.schedule_end: {}
schedule.schedule_id: {}
schedule.schedule_start: {}
schedule.show_funding_category: {}
schedule.show_hosts: {}
schedule.show_id: {}
schedule.show_name: {}
schedule.show_type: {}
schedule.topic: {}
schedule.type: {}
schedule_fallback_id: {}
show_fallback_id: {}
station_fallback_id: {}
track: {}
track_start: {}
type: object
info:
title: Swagger API Specification for Aura Engine
version: 1.0.0
openapi: 3.0.2
```
**Schedule API**: Retrieves information on the programme.
* `/api/v1/schedule/upcoming` .. Returns the next three schedules, after the one currently playing.
```yaml
schemas:
Schedule:
properties:
id: {}
schedule: {}
schedule_id: {}
schedule_start: {}
show_hosts: {}
show_id: {}
show_name: {}
show_type: {}
type: object
info:
title: Swagger API Specification for Aura Engine
version: 1.0.0
openapi: 3.0.2
```
**Clock API**: Retrieve all data relevant for a studio clock.
* `/api/v1/clock` .............. Returns the current show, next show, playlist and time left until the next show.
```yaml
schemas:
Clock:
properties:
current: {}
next: {}
track: {}
track_id: {}
track_start: {}
type: object
info:
title: Swagger API Specification for Aura Engine
version: 1.0.0
openapi: 3.0.2
```
## Web Applications
* `/app/trackservice` ................. Web Application for displaying the Track-Service
* `/app/clock` ........................ Web Application for displaying the studio clock
## Monitoring
You have following options to monitor the Engine:
* Send mails on errors and warnings
* Engine Status Information
* Engine Heartbeat
* Logging
### Send mails on errors and warnings
To activate you'll need to set some mail account within the `[monitoring]` section of your configuration.
```ini
[monitoring]
mail_server="mail.o94.at"
mail_server_port="587"
mail_user="aura@o94.at"
mail_pass="---SECRET--PASSWORD---"
# If you want to send multiple adminmails, make them space separated
admin_mail="admin-email@your.domain"
# Which from mailadress should be used
from_mail="monitoring@aura.engine"
# The beginning of the subject. With that you can easily apply filter rules using a mail client
mailsubject_prefix="[Aura Engine]"
```
### Engine Status Information
You can get various status fields & flags using the `/status` endpoint.
> Please note this is a rather expensive call. If you need to call this on a regular basis
for continious monitoring, the *Heartbeat* option might be preferred.
### Engine Heartbeat
Instead of checking all status properties, the Heartbeat only validates the vital ones
required to run the engine. If all of those are valid, as network socket request is sent
to a defined server.
This heartbeat is sent continiously based on the configured `heartbeat_frequency`.
```ini
# Server where heartbeat info is sent to
heartbeat_server = "127.0.0.1"
# Some UDP port
heartbeat_port = 43334
# Seconds how often the vitality of the Engine should be checked (0 = disabled)
heartbeat_frequency = 1
```
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 failover scenario.
Under `contrib/heartbeat-monitor` you'll find some sample application digesting these
heartbeat signals.
### Logging
In development all Engine logs can be found under `./log`, and for production they can
are located in `/var/log/aura`. Adapt the log-level within your configuration to get
more or less verbose log output:
```ini
logdir="/var/log/aura"
# Possible values: debug, info, warning, error, critical
loglevel="info"
```
The log directory holds individual logs from Engine Core, Liquidsoap and the API.
But also `stout` outputs from supervisor services are written there.
Additionally you'll finde Supervisor specific logs under`/var/log/supervisor`.
## Read more
- [Overview](/README.md)
- [Installation for Development](installation-development.md)
- [Installation for Production](installation-production.md)
- [Running with Docker](running-docker.md)
- [Setup the Audio Store](docs/setup-audio-store.md)
- [Configuration Guide](configuration-guide.md)
- [Developer Guide](developer-guide.md)
- [Engine Features](engine-features.md)
- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md)
# Frequently Asked Questions
<!-- TOC -->
- [Frequently Asked Questions](#frequently-asked-questions)
- [I have issues with starting the Engine](#i-have-issues-with-starting-the-engine)
- [Read More](#read-more)
<!-- /TOC -->
## I have issues with starting the Engine
**Cannot connect to socketpath /opt/aura/engine/modules/liquidsoap/engine.sock. Reason: [Errno 111] Connection refused**
- This indicates some issue with the proper startup of Liquidsoap or any related audio device. Check the Liquidsoap logs for details.
**[clock.wallclock_alsa:2] Error when starting output lineout: Failure("Error while setting open_pcm: No such file or directory")!**
- This might indicate some issue with your ALSA device. Check if `aplay -l` and `aplay -L` returns some valid device(s). You can also try `alsamixer`.
- It might be also helpful to set your default audio device in `/etc/asound.conf`.
- Also check if your user (è.g. `engineuser`) belongs to the group `audio`.
- Check the audio interface configuration section in `engine.ini`. Verify if the default settings `input_device_0="hw:0"` and `output_device_0="hw:0"` are valid device IDs.
## Read More
- [Overview](/README.md)
- [Installation for Development](installation-development.md)
- [Installation for Production](installation-production.md)
- [Running with Docker](running-docker.md)
- [Setup the Audio Store](docs/setup-audio-store.md)
- [Configuration Guide](configuration-guide.md)
- [Developer Guide](developer-guide.md)
- [Engine Features](engine-features.md)
- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md)
# Install for Development
<!-- TOC -->
- [Install for Development](#install-for-development)
- [Prerequisites](#prerequisites)
- [Setup Database](#setup-database)
- [Installation](#installation)
- [Configuration](#configuration)
- [Running Engine](#running-engine)
- [Logging](#logging)
- [Read more](#read-more)
<!-- /TOC -->
## Prerequisites
Aura Engine runs on any modern Debian-based OS. It requires at least
- `Node 13`
- `Python 3.7`
- `git`
Additionally you'll need these system packages:
```bash
sudo apt-get install \
opam \
redis-server \
libsndfile1 \
ffmpeg \
quelcom \
python3-pip \
virtualenv \
libssl-dev
```
Depending on the database management system you gonna use, you'll also need to install those packages.
In case of MariaDB this is:
```shell
sudo apt-get install \
python-dev \
default-libmysqlclient-dev \
mariadb-server \
libmariadbclient-dev
```
**Liquidsoap Repository**
Engine requires at least `Liquidsoap 1.4.1` or newer, installed using [OPAM (OCaml Package Manager)](https://opam.ocaml.org/).
Add the current Liquidsoap repository from [this installation guide](https://www.liquidsoap.info/doc-1.4.1/install.html).
The other steps required for the Liquidsoap installation are handled by the `install.sh` script. If you experience any
errors, carefully review them and consult the official documentation for installing Liquidsoap.
## Setup Database
The following installation script sets up the database.
```bash
bash script/setup-db.sh
```
By default Aura Engine uses MariaDB for persistence. When starting this script, please
ensure you have root access to your database instance. The installation script automatically
creates a database plus an associated user with password. If you want to use your own database
system, select "Other / Manually" during the database installation step.
In case of MariaDB the script also installs these system dependencies:
apt-get install mariadb-server libmariadbclient-dev
When this is completed, carefully check if any error occured. In case your database has been setup
automatically, note the relevant credentials for later use in your `engine.ini` configuration.
**Cloning the project**
Then clone the project to some development folder:
```bash
git clone https://gitlab.servus.at/autoradio/engine
```
## Installation
```shell
./install.sh
```
This script does the following:
- Install Liquidsoap components using OPAM (`script/install-opam-packages`)
- NodeJS Modules (`script/install-web.sh`)
- Python Packages (`requirements.txt`)
- Creates a default configuration file in `configuration/engine.ini`
When this is completed, carefully check if any error occured.
## Configuration
In your development environment edit following file to configure the engine:
```shell
./configuration/engine.ini
```
Please note if some configuration exists under `/etc/aura/engine.ini` the configuration is
read from there by default.
Now, specify at least following settings to get started:
```ini
[database]
db_user="aura"
db_name="aura_engine"
db_pass="---SECRET--PASSWORD---"
```
Set the URLs to the *Steering* and *Tank* API:
```ini
[api]
# STEERING
api_steering_status = "http://localhost:8000/api/v1/"
# The URL to get the Calendar via Steering
api_steering_calendar="http://localhost:8000/api/v1/playout"
# The URL to get show details via Steering
api_steering_show="http://localhost:8000/api/v1/shows/${ID}/"
# TANK
api_tank_status = "http://localhost:8040/healthz"
# The URL to get playlist details via Tank
api_tank_playlist="http://localhost:8040/api/v1/shows/${SLUG}/playlists"
```
Ensure that the Liquidsoap installation path is valid:
```ini
[lqs]
liquidsoap_path="/home/david/.opam/4.08.0/bin/liquidsoap"
```
Finally Engine needs to be able to access the audio folder, where all the tracks of the playlists
are stored via *Tank*:
```ini
[audiofolder]
audiofolder="/var/audio"
```
If the audio device desired for playback is set as `default`, the Engine now should be ready to play
sound. You can check the default audio hardware by executing `aplay -L` on the command line. If that's
not the case you can set the default device in `/etc/asound.conf`. More advanced audio device settings
can be looked up in the [Configuration Guide](docs/configuration-guide.md).
Read about all other available settings in the [Configuration Guide](docs/configuration-guide.md).
## Running Engine
Use the convencience script `run.sh` to get engine started in different ways:
**Run the Engine**
This includes the Liquidsoap audio engine, but does not start the API server.
```shell
./run.sh
```
**Run the Engine Core and Liquidsoap separately**
When developing and debugging engine it is helpful to start the core and the Liquidsoap
component separately. In such case it is important to start both in the correct order.
First start the core of the engine:
```shell
./run.sh core
```
When engine-core completed its boot phase, indicated by a log that it is waiting for
Liquidsoap, you can run following:
```shell
./run.sh lqs
```
**Run the Engine API**
This requires to start the core component in another terminal.
```shell
./run.sh api-dev
```
In development mode Engine uses the default [Flask](https://palletsprojects.com/p/flask/) web server.
Please be careful not to use this type of server in your production environment.
Check out more ways of running the engine in the [Developer Guide](docs/developer-guide.md).
## Logging
All Engine logs for development can be found under `./logs`.
## Read more
- [Overview](/README.md)
- [Installation for Development](installation-development.md)
- [Installation for Production](installation-production.md)
- [Running with Docker](running-docker.md)
- [Setup the Audio Store](docs/setup-audio-store.md)
- [Configuration Guide](configuration-guide.md)
- [Developer Guide](developer-guide.md)
- [Engine Features](engine-features.md)
- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md)
# Install for Production
<!-- TOC -->
- [Install for Production](#install-for-production)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Configuration](#configuration)
- [Running Engine](#running-engine)
- [The API Server](#the-api-server)
- [Maintanence using Supervisor](#maintanence-using-supervisor)
- [Logging](#logging)
- [Read more](#read-more)
<!-- /TOC -->
## Prerequisites
Aura Engine runs on any modern Debian-based OS. It requires at least
- `Python 3.7`
- `git`
Additionally you'll need these system packages below.
```shell
sudo apt-get update
sudo apt-get install \
supervisor \
opam \
redis-server \
libsndfile1 \
ffmpeg \
quelcom \
python3-pip \
virtualenv \
libssl-dev
```
Depending on the database management system you gonna use, you'll also need to install those packages.
In case of MariaDB this is:
```shell
sudo apt-get install \
python-dev \
default-libmysqlclient-dev \
mariadb-server \
libmariadbclient-dev
```
**Create an user**
While previous packages need superuser rights to be installed, the following ones are installed for the user which is
executing the engine. In your development environment you can skip this step. In production you first need to create
a user called `engineuser`.
```shell
sudo adduser engineuser
sudo adduser engineuser audio sudo
```
And switch to that user
```shell
su engineuser
```
**Liquidsoap Repository**
Engine requires at least `Liquidsoap 1.4.1` or newer, installed using [OPAM (OCaml Package Manager)](https://opam.ocaml.org/).
Add the current Liquidsoap repository from [this installation guide](https://www.liquidsoap.info/doc-1.4.2/install.html).
The other steps required for the Liquidsoap installation are handled by the `install.sh` script. If you experience any
errors, carefully review them and consult the official documentation for installing Liquidsoap.
**Cloning the project**
Create the folder `/opt/aura` and clone the engine project from there:
```shell
engineuser:/opt/aura/$ git clone https://gitlab.servus.at/autoradio/engine
```
Now you should have `/opt/aura/engine/`.
Let's move inside the home of engine:
```shell
engineuser:/opt/aura/$ cd engine
```
**Setup the database**
The following installation script sets up the database. You either need to be logged in as root
or have sudo rights.
```shell
root:/opt/aura/engine/$ bash script/setup-db.sh
```
By default Aura Engine uses MariaDB for persistence. When starting this script, please
ensure you have root access to your database instance. The installation script automatically
creates a database plus an associated user with password. If you want to use your own database
system, select "Other / Manually" during the database installation step.
If you have chosen to setup your database automatically, note the relevant credentials.
**Initialize folders and permissions**
Call this script to create the required log folders and update all permissions.
```bash
root:/opt/aura/engine$ bash script/initialize.sh
```
## Installation
The following installation script also sets up the database.
By default Aura Engine uses MariaDB for persistence. When starting the installation, please
ensure you have root access to your database instance. The installation script automatically
creates a database plus an associated user with password. If you want to use your own database
system, select "Other / Manually" during the database installation step.
```shell
engineuser:/opt/aura/engine$ ./install.sh prod
```
This script does the following:
- Install Liquidsoap components using OPAM (`script/install-opam-packages`)
- Python Packages (`requirements.txt`)
- Creates a default Engine configuration file in `/etc/aura/engine.ini`
- Creates a default Gunicorn configuration file in `gunicorn.conf.py`
When this is completed, carefully check if any error occured. In case your database has been setup
automatically, note the relevant credentials for later use in your `engine.ini` configuration.
## Configuration
In your production environment edit following file to configure the engine:
```shell
engineuser:/opt/aura/engine$ nano /etc/aura/engine.ini
```
Now, specify at least following settings to get started:
```ini
[database]
db_user="aura"
db_name="aura_engine"
db_pass="---SECRET--PASSWORD---"
```
Set the URLs to the *Steering* and *Tank* API:
```ini
[api]
# STEERING
api_steering_status = "http://localhost:8000/api/v1/"
# The URL to get the Calendar via Steering
api_steering_calendar="http://localhost:8000/api/v1/playout"
# The URL to get show details via Steering
api_steering_show="http://localhost:8000/api/v1/shows/${ID}/"
# TANK
api_tank_status = "http://localhost:8040/healthz"
# The URL to get playlist details via Tank
api_tank_playlist="http://localhost:8040/api/v1/shows/${SLUG}/playlists"
```
Ensure that the Liquidsoap installation path is valid:
```ini
[lqs]
liquidsoap_path="/home/engineuser/.opam/4.08.0/bin/liquidsoap"
```
Finally Engine needs to be able to access the audio folder, where all the tracks of the playlists
are stored via *Tank*:
```ini
[audiofolder]
audiofolder="/var/audio"
```
There is some document on how to [Setup the Audio Store](docs/setup-audio-store.md).
If the audio device desired for playback is set as `default`, the Engine now should be ready to play
sound. You can check the default audio hardware by executing `aplay -L` on the command line. If that's
not the case you can set the default device in `/etc/asound.conf`. More advanced audio device settings
can be looked up in the [Configuration Guide](docs/configuration-guide.md).
Read about all other available settings in the [Configuration Guide](docs/configuration-guide.md).
## Running Engine
In production the process of starting the engine is slightly different compared to some development environment.
This is due to the need of ensuring the engine's components are always running i.e. letting them to restart
automatically after some system restart or crash has happened.
For this we utilize [Supervisor](http://supervisord.org/).
Also note, while running the engine might also work using a `systemd` service, the
recommened option to use in combination with Gunicorn ([API server](Running the API Server), see below),
is Supervisor. Beside others pros, Supervisor has the advantage that you are able to run services without
having superuser rights.
Now, given you are in the engine's home directory `/opt/aura/engine/`, simply type following to start
the services:
```shell
supervisord
```
This picks up the supervisor configuration provided in the local `supervisord.conf` and the service configurations
located in `configuration/supervisor/*.conf`.
Experience has shown it might be helpful to reload the supervisor configuration using `sudo`:
```shell
sudo supervisorctl reload
```
Note that the supervisor daemon starts all (both) services at once. If you want more fine-grained control for
starting services individually, please check-out the next section.
**Listing available Services**
```shell
engineuser:/opt/aura/engine$ supervisorctl avail
```
You should get these two services with their actual state listed:
```c++
aura-engine in use auto 666:666
aura-engine-api in use auto 999:999
```
## The API Server
For production Engine API uses the WSGI HTTP Server [`Gunicorn`](https://gunicorn.org/).
In production Gunicorn is used in combination with some proxy server, such as Nginx.
> Although there are many HTTP proxies available, we strongly advise that you use Nginx. If you choose another proxy
server you need to make sure that it buffers slow clients when you use default Gunicorn workers. Without this buffering
Gunicorn will be easily susceptible to denial-of-service attacks. You can use Hey to check if your proxy is behaving properly.
[**Gunicorn Docs**](http://docs.gunicorn.org/en/latest/deploy.html).
## Maintanence using Supervisor
Please remember to call all `supervisorctl` commands from within your engine home directory (`/opt/aura/engine/`),
to pickup the correct `supervisord.conf`.
**Starting Services**
```shell
supervisorctl start <service-name>
```
**Stopping Services**
```shell
supervisorctl stop <service-name>
```
**Restarting Services**
```shell
supervisorctl restart <service-name>
```
**Refresh after changing configurations**
```shell
supervisorctl restart <service-name>
```
**Start the API service with Supervisor**
```shell
sudo supervisorctl update
sudo supervisorctl restart engine-api
```
In case you want to reload whole supervisor service
```shell
sudo service supervisor restart
```
## Logging
All Engine logs for production can be found under:
```shell
`/var/log/aura`
```
This includes individual logs from Engine Core, Liquidsoap and the API.
But also `stdout` outputs from supervisor services are written there.
Additionally you'll finde Supervisor specific logs under:
```
`/var/log/supervisor`
```
## Read more
- [Overview](/README.md)
- [Installation for Development](installation-development.md)
- [Installation for Production](installation-production.md)
- [Running with Docker](running-docker.md)
- [Setup the Audio Store](docs/setup-audio-store.md)
- [Configuration Guide](configuration-guide.md)
- [Developer Guide](developer-guide.md)
- [Engine Features](engine-features.md)
- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md)
# Running Engine with Docker
Docker provides a simple way to get your engine with all dependencies running.
Here you can find the official AURA Engine Docker images:
https://hub.docker.com/repository/docker/autoradio/engine
> Note: The Engine Docker image is in *POC* state and waiting to be fully implemented.
It's not yet ready to be offically used. If you want to try AURA Engine meanwhile
try the [Standard Installation](docs/installation-development).
<!-- TOC -->
- [Running Engine with Docker](#running-engine-with-docker)
- [Start an image](#start-an-image)
- [Configure an image](#configure-an-image)
- [Making Docker Releases](#making-docker-releases)
- [Build your own, local Docker image](#build-your-own-local-docker-image)
- [Releasing a new version to DockerHub](#releasing-a-new-version-to-dockerhub)
- [Read more](#read-more)
<!-- /TOC -->
## Start an image
*To be extended ...*
## Configure an image
*To be extended ...*
## Making Docker Releases
This section is only relevant if you are an Engine Developer.
### Build your own, local Docker image
```shell
./run.sh docker:build
```
### Releasing a new version to DockerHub
```shell
./run.sh docker:push
```
## Read more
- [Overview](/README.md)
- [Installation for Development](installation-development.md)
- [Installation for Production](installation-production.md)
- [Running with Docker](running-docker.md)
- [Setup the Audio Store](docs/setup-audio-store.md)
- [Configuration Guide](configuration-guide.md)
- [Developer Guide](developer-guide.md)
- [Engine Features](engine-features.md)
- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md)
# Setting up the Audio Store
The *Audio Store* is a folder which is utilized by AURA Tank and Engine to exchange audio files.
Assuming AURA Engine and Tank are hosted on different machines, the `audiofolder` must by shared
using some network share.
In case you are hosting Engine and Tank on the same machine (e.g. in development), you can skip
this documentation. Just think about pointing them to the same directory.
<!-- TOC -->
- [Setting up the Audio Store](#setting-up-the-audio-store)
- [Share Location](#share-location)
- [Share Type](#share-type)
- [Setting up SSHFS](#setting-up-sshfs)
- [Configuring Engine](#configuring-engine)
- [Configuring Tank](#configuring-tank)
- [Read more](#read-more)
<!-- /TOC -->
By default Engine expects audio files shared by Tank in `/var/audio`.
This can be configurated in `engine.ini`:
```ini
[audiofolder]
audiofolder="/var/audio"
```
Now, this folder must be somehow writable by Tank.
## Share Location
You have following options where your share can be located:
1. **Engine and all other AURA components (Tank, Dashboard, Steering) are running on the same instance.** This is the most simple solution,
as Engine and Tank can share the same directory locally. But this scenario requires some more sophisticated tuning of the system resources
to avoid e.g. some overload of multiple Uploads in Tank may affect the performance of engine. You can eliminate this risk by setting CPU and
memory limits for Steering, Dashboard and Tank using Docker or `systemd-cgroups`. A disadvantage here is the case of maintainence of system
reboot. This would mean that all components are offline at once.
2. **Physical directory where the Engine lives, mounted to Tank**. This may cause an issue with the mount, when no network connection to Engine
is unavailable or the instance is rebooting.
3. **Physical directory where the Tank lives, mounted to Engine.** This may cause an issue with the mount, when no network connection to Tank
is unavailable or the instance is rebooting.
4. **Central Data Store or *Storage Box*** which is mountet to Engine and Tank. In this case a downtime of the store make both, Engine and Tank
dysfunctional.
5. **Replicated storage solution using [Gluster](https://www.gluster.org/), both Engine and Tank have their virtual audio directory mounted.**
That's the ideal approach, because if any of the instances is down, the other has all the data available.
In any case, you should think about some backup solution involving this directory.
## Share Type
Then, there's the question how the share is managed. Beside other you have the options
to use [NFS](https://en.wikipedia.org/wiki/Network_File_System), [SSHFS](https://en.wikipedia.org/wiki/SSHFS) or even something like [Gluster](https://www.gluster.org/).
For our initial setup we have chosen to use *SSHFS*.
Please share your experience with other share types, and we will include it in further releases of
this documentation.
## Setting up SSHFS
SSHFS allows you to access the filesystem on a remote computer via SSH. Interaction with files and folders behaves similar to any local data.
This example is setting up the `audiofolder` on the Engine instance.
### Configuring Engine
First, you'll need to create an user which enables Tank to access the `audiofolder` on Engine:
```shell
adduser tankuser
chown tankuser:engineuser /var/audio
```
Ensure that `engineuser` has no permissions to write the directory:
```shell
chmod u=+rwx,go=+rx-w /var/audio
```
### Configuring Tank
On the Tank side you need to install `sshfs`:
```shell
sudo apt-get install sshfs
```
Then create an `audio-store` folder inside the AURA home:
```shell
:/opt/aura/$ mkdir audio-store
```
Try if you can connect to the engine over SSH using your `tankuser`:
```shell
ssh tankuser@192.168.0.111 -p22
```
Replace `-p22` with the actual port number your SSH service is running with. For security reasons it's recommended
to run SSH not over the default port 22.
Uncomment following setting in `/etc/fuse.conf` to allow the tank-user access the share with write permissions:
```conf
# Allow non-root users to specify the allow_other or allow_root mount options.
user_allow_other
```
Now create the mount:
```shell
sudo sshfs -o allow_other -o IdentityFile=~/.ssh/id_rsa tankuser@192.168.0.111:/var/audio /opt/aura/audio-store -p22
```
Replace `192.168.0.111` with the actual IP for your Engine and `-p22` with the actual port number your Engine's SSH service
is running with.
To make this mount persistent i.e. keep it alive even after a system reboot, you'll need to add a configuration
in the `/etc/fstab` file by adding this at the end:
```yaml
# Audio Store @ AURA Engine
sshfs#tankuser@192.168.0.111:/var/audio /opt/aura/audio-store fuse auto,port=22,identityfile=~/.ssh/id_rsa,allow_other 0 0
```
Again, check for the correct port number in the line above.
To take this into effect you'll need to remount the filesystem with `sudo mount -a` or reboot the machine. When mounting
you'll need to authenticate with the `tankuser` password once.
Then review if your Tank's Docker configuration mounts the exact same volume (`/opt/aura/audio-store`).
If not edit the tank configuration `/etc/aura/tank.env` and set following property:
```shell
TANK_STORE_PATH=/opt/aura/audio-store
```
Finally, do some testing if the directory is writable from Tank's system (`touch some-file`) and if the Engine's side can read
this file. Then restart your Tank Docker container and you should be good to go.
## Read more
- [Overview](/README.md)
- [Installation for Development](installation-development.md)
- [Installation for Production](installation-production.md)
- [Running with Docker](running-docker.md)
- [Setup the Audio Store](docs/setup-audio-store.md)
- [Configuration Guide](configuration-guide.md)
- [Developer Guide](developer-guide.md)
- [Engine Features](engine-features.md)
- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md)
#!/usr/bin/env python3.7
#
# Aura Engine
#
# Copyright (C) 2020 David Trattnig <david.trattnig@subsquare.at>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import os, os.path
import subprocess
import json
from datetime import datetime, date, timedelta
from flask import Flask, Response
from flask_caching import Cache
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from marshmallow import Schema, fields, post_dump
from flask_restful import Api, Resource, abort
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec_webframeworks.flask import FlaskPlugin
# import werkzeug
from werkzeug.exceptions import HTTPException, default_exceptions, Aborter
from modules.base.logger import AuraLogger
from modules.base.config import AuraConfig
from modules.database.model import AuraDatabaseModel, Schedule, Playlist, PlaylistEntry, PlaylistEntryMetaData, TrackService
#
# Initialize the Aura Web App and API.
#
config = AuraConfig()
app = Flask(__name__,
static_url_path='',
static_folder='web/')
# static_folder='contrib/aura-player/public/')
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SQLALCHEMY_DATABASE_URI"] = config.get_database_uri()
app.config["CACHE_TYPE"] = "simple"
app.config["CACHE_DEFAULT_TIMEOUT"] = 0
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
cache = Cache(app)
cors = CORS(app, resources={r"/*": {"origins": "*"}}) # FIXME Update CORS for production use
db = SQLAlchemy(app)
ma = Marshmallow(app)
api = Api(app)
#
# Werkzeug HTTP code mappings
#
class NoDataAvailable(HTTPException):
code = 204
description = "There is currently no content available."
default_exceptions[204] = NoDataAvailable
abort = Aborter()
class EngineApi:
"""
Provides the Aura Engine API services.
"""
config = None
api = None
logger = None
# trackservice_schema = None
def __init__(self, config, api):
"""
Initializes the API.
Args:
config (AuraConfig): The Engine configuration.
api (Api): The Flask restful API object.
"""
self.config = config
self.logger = AuraLogger(self.config, "engine-api")
self.logger = logging.getLogger("engine-api")
self.api = api
# Generate HTML files
self.generate_html("web/templates/clock.html", "web/clock.html")
self.generate_html("web/templates/trackservice.html", "web/trackservice.html")
# API Spec
spec.components.schema("TrackService", schema=TrackServiceSchema)
spec.components.schema("Report", schema=ReportSchema)
spec.components.schema("Schedule", schema=ScheduleSchema)
spec.components.schema("Clock", schema=ClockDataSchema)
spec.components.schema("Status", schema=StatusSchema)
# TODO Generates HTML for specification
#self.logger.info(spec.to_yaml())
# Schema instances
EngineApi.trackservice_schema = TrackServiceSchema(many=True)
EngineApi.track_schema = TrackServiceSchema()
EngineApi.report_schema = ReportSchema(many=True)
EngineApi.schedule_schema = ScheduleSchema(many=True)
EngineApi.clockdata_schema = ClockDataSchema()
EngineApi.status_schema = StatusSchema()
# Define API routes
self.api.add_resource(TrackServiceResource, config.api_prefix + "/trackservice/")
self.api.add_resource(TrackResource, config.api_prefix + "/trackservice/<int:track_id>")
self.api.add_resource(CurrentTrackResource, config.api_prefix + "/trackservice/current")
self.api.add_resource(TracksByDayResource, config.api_prefix + "/trackservice/date/<string:date_string>")
self.api.add_resource(ReportResource, config.api_prefix + "/report/<string:year_month>")
self.api.add_resource(UpcomingSchedulesResource, config.api_prefix + "/schedule/upcoming")
self.api.add_resource(ClockDataResource, config.api_prefix + "/clock")
self.api.add_resource(StatusResource, "/status")
self.logger.info("Engine API routes successfully set!")
# Static resources
@app.route('/app/trackservice', methods=['GET'])
def trackservice():
content = open(os.path.join("web/", "trackservice.html"))
return Response(content, mimetype="text/html")
# Static resources
@app.route('/app/clock', methods=['GET'])
def clock():
content = open(os.path.join("web/", "clock.html"))
return Response(content, mimetype="text/html")
def generate_html(self, src_file, target_file):
"""
Generates HTML based on the configuration options and templates.
Args:
src_file (String): The template file
target_file (String): The HTML file to be generated
"""
src_file = open(src_file, "r")
target_file = open(target_file, "w")
content = src_file.read()
config_options = {
"CONFIG-STATION-NAME": config.station_name,
"CONFIG-STATION-LOGO-URL": config.station_logo_url,
"CONFIG-STATION-LOGO-SIZE": config.station_logo_size,
"CONFIG-API-URL": config.exposed_api_url
}
for key, value in config_options.items():
content = content.replace(":::"+key+":::", value)
target_file.write(content)
src_file.close()
target_file.close()
def run(self):
"""
Starts the API server.
"""
# Set debug=False if you want to use your native IDE debugger
self.api.app.run(port=self.config.api_port, debug=False)
#
# API SPEC
#
spec = APISpec(
title="Swagger API Specification for Aura Engine",
version="1.0.0",
openapi_version="3.0.2",
plugins=[FlaskPlugin(), MarshmallowPlugin()],
)
#
# API SCHEMA
#
class TrackServiceSchema(ma.Schema):
class Meta:
fields = (
"id",
"schedule.schedule_id",
"schedule.schedule_start",
"schedule.schedule_end",
"schedule.languages",
"schedule.type",
"schedule.category",
"schedule.topic",
"schedule.musicfocus",
"schedule.is_repetition",
"track",
"track_start",
"show"
)
class ClockDataSchema(ma.Schema):
class Meta:
fields = (
"current",
"next",
"track_id",
"track_start",
"track"
)
class ScheduleSchema(ma.Schema):
class Meta:
fields = (
"id",
"schedule_id",
"schedule_start",
"schedule",
"show_id",
"show_name",
"show_hosts",
"show_type"
)
class ReportSchema(ma.Schema):
class Meta:
fields = (
"id",
"schedule.schedule_id",
"schedule.schedule_start",
"schedule.schedule_end",
"schedule.languages",
"schedule.type",
"schedule.category",
"schedule.topic",
"schedule.musicfocus",
"schedule.is_repetition",
"schedule.show_id",
"schedule.show_name",
"schedule.show_hosts",
"schedule.show_type",
"schedule.show_funding_category",
"track",
"track_start",
"playlist_id",
"fallback_type",
"schedule_fallback_id",
"show_fallback_id",
"station_fallback_id"
)
class StatusSchema(ma.Schema):
class Meta:
fields = (
"engine",
"soundsystem",
"api",
"redis_ready",
"audio_store"
)
#
# API RESOURCES
#
class TrackServiceResource(Resource):
logger = None
def __init__(self):
self.logger = logging.getLogger("engine-api")
def get(self):
today = date.today()
today = datetime(today.year, today.month, today.day)
tracks = TrackService.select_by_day(today)
return EngineApi.trackservice_schema.dump(tracks)
class TrackResource(Resource):
logger = None
def __init__(self):
self.logger = logging.getLogger("engine-api")
def get(self, track_id):
track = TrackService.select_one(track_id)
return EngineApi.track_schema.dump(track)
class ClockDataResource(Resource):
logger = None
def __init__(self):
self.logger = logging.getLogger("engine-api")
def get(self):
item = TrackService.select_current()
next_schedule = Schedule.select_upcoming(1)
if next_schedule:
next_schedule = next_schedule[0].as_dict()
next_schedule["playlist"] = None
else:
next_schedule = {}
clockdata = {
"track_id": item.id,
"track_start": item.track_start,
"track": item.track,
"current": {},
"next": next_schedule
}
if item.schedule:
clockdata["current"] = item.schedule.as_dict()
if item.schedule.playlist:
clockdata["current"]["playlist"] = item.schedule.playlist[0].as_dict()
clockdata["current"]["show"] = item.show
return EngineApi.clockdata_schema.dump(clockdata)
class CurrentTrackResource(Resource):
logger = None
def __init__(self):
self.logger = logging.getLogger("engine-api")
def get(self):
track = TrackService.select_current()
if not track:
return abort(204) # No content available
return EngineApi.track_schema.dump(track)
class TracksByDayResource(Resource):
logger = None
def __init__(self):
self.logger = logging.getLogger("engine-api")
def get(self, date_string):
date = datetime.strptime(date_string, "%Y-%m-%d")
self.logger.debug("Query track-service by day: %s" % str(date))
tracks = TrackService.select_by_day(date)
if not tracks:
return abort(204) # No content available
return EngineApi.trackservice_schema.dump(tracks)
class UpcomingSchedulesResource(Resource):
logger = None
def __init__(self):
self.logger = logging.getLogger("engine-api")
def get(self):
now = datetime.now()
self.logger.debug("Query upcoming schedules after %s" % str(now))
schedules = Schedule.select_upcoming(3)
if not schedules:
return abort(204) # No content available
return EngineApi.schedule_schema.dump(schedules)
class ReportResource(Resource):
logger = None
def __init__(self):
self.logger = logging.getLogger("engine-api")
def get(self, year_month):
year = int(year_month.split("-")[0])
month = int(year_month.split("-")[1])
first_day = datetime(year, month, 1)
next_month = first_day.replace(day=28) + timedelta(days=4)
next_month - timedelta(days=next_month.day)
self.logger.debug("Query report for month: %s - %s" % (str(first_day), str(next_month)))
report = TrackService.select_by_range(first_day, next_month)
if not report:
return abort(204) # No content available
return EngineApi.report_schema.dump(report)
class StatusResource(Resource):
logger = None
def __init__(self):
self.logger = logging.getLogger("engine-api")
def get(self):
status = subprocess.check_output(["python3", "guru.py", "-s", "-q"])
status = status.decode("utf-8").replace("'", '"')
status = json.loads(status, strict=False)
if not status:
return abort(204) # No content available
return EngineApi.status_schema.dump(status)
#
# Initialization calls
#
engine_api = EngineApi(config, api)
if __name__ == "__main__":
engine_api.run()
#!/usr/bin/env python3.7
#
# Aura Engine
#
# Copyright (C) 2017-2020
# David Trattnig <david.trattnig@subsquare.at>
# Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import meta
import signal
import logging
import unittest
import subprocess
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from modules.base.logger import AuraLogger
from modules.base.config import AuraConfig
from modules.base.utils import SimpleUtil
config = AuraConfig()
def configure_flask():
app.config["SQLALCHEMY_DATABASE_URI"] = config.get_database_uri()
app.config['BABEL_DEFAULT_LOCALE'] = 'de'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# FIXME Instatiate SQLAlchemy without the need for Flask
app = Flask(__name__)
configure_flask()
DB = SQLAlchemy(app)
class AuraEngine:
"""
AuraEngine does the following:
1. Initialize the soundsystem and scheduler
2. Initialize Redis
3. Start Liquidsoap in a separate thread which connects to the engine
"""
logger = None
config = None
server = None
messenger = None
controller = None
scheduler = None
lqs = None
lqs_startup = None
def __init__(self):
"""
Initializes Engine Core.
"""
self.config = config
def startup(self, lqs_startup):
"""
Starts Engine Core.
"""
AuraLogger(self.config)
self.logger = logging.getLogger("AuraEngine")
from modules.scheduling.scheduler import AuraScheduler
from modules.core.engine import SoundSystem
from modules.communication.redis.adapter import ServerRedisAdapter
# If Liquidsoap should be started automatically
self.lqs_startup = lqs_startup
# Check if the database has to be re-created
if self.config.get("recreate_db") is not None:
AuraScheduler(self.config, None, None)
# Create scheduler and Liquidsoap communicator
self.soundsystem = SoundSystem(self.config)
self.scheduler = AuraScheduler(self.config, self.soundsystem, self.on_initialized)
# Create the Redis adapter
self.messenger = ServerRedisAdapter(self.config)
self.messenger.scheduler = self.scheduler
self.messenger.soundsystem = self.soundsystem
# And finally wait for redis message / start listener thread
self.messenger.start()
def on_initialized(self):
"""
Called when the engine is initialized, before the Liquidsoap connection is established."
"""
self.logger.info(SimpleUtil.green("Engine Core initialized - Waiting for Liquidsoap connection ..."))
if self.lqs_startup:
self.start_lqs(False, False)
else:
self.logger.info(SimpleUtil.yellow("Please note, Liquidsoap needs to be started manually."))
def start_lqs(self, debug_output, verbose_output):
"""
Starts Liquidsoap.
"""
lqs_path = self.config.get("liquidsoap_path")
lqs_cwd = os.getcwd() + "/" + self.config.get("liquidsoap_working_dir")
lqs_output = ""
lqs_output = self.get_debug_flags(debug_output, verbose_output)
self.lqs = subprocess.Popen([lqs_path, lqs_output, "engine.liq"], \
cwd=lqs_cwd, \
stdout=subprocess.PIPE, \
shell=False)
def get_lqs_cmd(self, debug_output, verbose_output):
"""
Returns a shell command string to start Liquidsoap
"""
lqs_path = self.config.get("liquidsoap_path")
lqs_cwd = os.getcwd() + "/" + self.config.get("liquidsoap_working_dir")
lqs_output = self.get_debug_flags(debug_output, verbose_output)
return "(cd %s && %s %s ./engine.liq)" % (lqs_cwd, lqs_path, lqs_output)
def get_debug_flags(self, debug_output, verbose_output):
"""
Build Liquidssoap debug parameters.
"""
output = ""
if debug_output:
output += "--debug "
if verbose_output:
output += "--verbose "
return output
def exit_gracefully(self, signum, frame):
"""
Shutdown of the engine. Also terminates the Liquidsoap thread.
"""
if self.lqs:
self.lqs.terminate()
self.logger.info("Terminated Liquidsoap")
if self.messenger:
self.messenger.terminate()
self.logger.info("Gracefully terminated Aura Engine!" + str(self.lqs))
sys.exit(0)
#
# START THE ENGINE
#
if __name__ == "__main__":
engine = AuraEngine()
lqs_startup = True
lqs_cmd = False
signal.signal(signal.SIGINT, engine.exit_gracefully)
signal.signal(signal.SIGTERM, engine.exit_gracefully)
if len(sys.argv) >= 2:
if "--without-lqs" in sys.argv:
lqs_startup = False
if "--get-lqs-command" in sys.argv:
lqs_cmd = True
if "--use-test-data" in sys.argv:
engine.config.set("use_test_data", True)
if "--recreate-database" in sys.argv:
engine.config.set("recreate_db", True)
if lqs_cmd:
print(engine.get_lqs_cmd(True, True))
else:
engine.startup(lqs_startup)
#!/usr/bin/env python3.7
#
# engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
# This file is part of engine.
#
# engine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# engine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
import time
import sys
import redis
from pathlib import Path
from argparse import ArgumentParser
# own libs
from modules.cli_tool.padavan import Padavan
from modules.base.exceptions import PlaylistException
from modules.base.config import AuraConfig
class Guru():
"""
Command Line Interface (CLI) for Aura Engine.
"""
# config_path = "%s/configuration/engine.ini" % Path(__file__).parent.absolute()
config = AuraConfig()
parser = None
args = None
# ------------------------------------------------------------------------------------------ #
def __init__(self):
self.init_argument_parser()
self.handle_arguments()
def handle_arguments(self):
if self.args.stoptime:
start = time.time()
if not self.args.quiet:
print("Guru thinking...")
try:
p = Padavan(self.args, self.config)
p.meditate()
except PlaylistException as pe:
# typically there is no next file found
if not self.args.quiet:
print(pe)
else:
print("")
exit(4)
except redis.exceptions.TimeoutError:
print("Timeout when waiting for redis message. Is AURA daemon running? Exiting...")
exit(3)
if not self.args.quiet:
print("...result: ")
if p.stringreply != "":
#print(p.stringreply)
if p.stringreply[len(p.stringreply)-1] == "\n":
print(p.stringreply[0:len(p.stringreply) - 1])
else:
print(p.stringreply[0:len(p.stringreply)])
if self.args.stoptime:
end = time.time()
exectime = end-start
print("execution time: "+str(exectime)+"s")
def init_argument_parser(self):
try:
self.create_parser()
self.args = self.parser.parse_args()
except (ValueError, TypeError) as e:
if self.parser is not None:
self.parser.print_help()
print()
print(e)
exit(1)
def create_parser(self):
self.parser = ArgumentParser()
# options
self.parser.add_argument("-sep", "--stop-execution-time", action="store_true", dest="stoptime", default=False, help="Prints the execution time at the end of the skript")
self.parser.add_argument("-q", "--quiet", action="store_true", dest="quiet", default=False, help="Just the result will outputed to stout")
self.parser.add_argument("-rd", "--recreate-database", action="store_true", dest="recreatedb", default=False, help="Do you want to recreate the database?")
# getter
self.parser.add_argument("-pcs", "--print-connection-status", action="store_true", dest="get_connection_status", default=False, help="Prints the status of the connection to liquidsoap, pv and tank")
self.parser.add_argument("-gam", "--get-active-mixer", action="store_true", dest="get_active_mixer", default=False, help="Which mixer is activated?")
self.parser.add_argument("-pms", "--print-mixer-status", action="store_true", dest="get_mixer_status", default=False, help="Prints all mixer sources and their states")
self.parser.add_argument("-pap", "--print-act-programme", action="store_true", dest="get_act_programme", default=False, help="Prints the actual Programme, the controller holds")
self.parser.add_argument("-s", "--status", action="store_true", dest="get_status", default=False, help="Returns the Engine Status as JSON")
# liquid manipulation
self.parser.add_argument("-am", "--select-mixer", action="store", dest="select_mixer", default=-1, metavar="MIXERNAME", help="Which mixer should be activated?")
self.parser.add_argument("-dm", "--de-select-mixer", action="store", dest="deselect_mixer", default=-1, metavar="MIXERNAME", help="Which mixer should be activated?")
self.parser.add_argument("-vm", "--volume", action="store", dest="set_volume", default=0, metavar=("MIXERNUM", "VOLUME"), nargs=2, help="Set volume of a mixer source", type=int)
# shutdown server
self.parser.add_argument("-sd", "--shutdown", action="store_true", dest="shutdown", default=False, help="Shutting down aura server")
# playlist in/output
self.parser.add_argument("-fnp", "--fetch-new-programmes", action="store_true", dest="fetch_new_programme", default=False, help="Fetch new programmes from api_steering_calendar in engine.ini")
self.parser.add_argument("-pmq", "--print-message-queue", action="store_true", dest="print_message_queue", default=False, help="Prints message queue")
# send a redis message
self.parser.add_argument("-rm", "--redis-message", action="store", dest="redis_message", default=False, metavar=("CHANNEL", "MESSAGE"), nargs=2, help="Send a redis message to the Listeners")
# calls from liquidsoap
self.parser.add_argument("-gnf", "--get-next-file-for", action="store", dest="get_file_for", default=False, metavar="PLAYLISTTYPE", help="For which type you wanna GET a next audio file?")
self.parser.add_argument("-snf", "--set-next-file-for", action="store", dest="set_file_for", default=False, metavar=("PLAYLISTTYPE", "FILE"), nargs=2, help="For which type you wanna SET a next audio file?")
self.parser.add_argument("-np", "--now-playing", action="store_true", dest="now_playing", default=False, help="Which source is now playing")
self.parser.add_argument("-ip", "--init-player", action="store_true", dest="init_player", default=False, help="Reset liquidsoap volume and mixer activations?")
self.parser.add_argument("-ts", "--on_play", action="store", dest="on_play", default=False, metavar="INFO", help="Event handling when some entry started playing")
if len(sys.argv) == 1:
raise ValueError("No Argument passed!")
def valid_playlist_entry(argument):
from datetime import datetime
try:
index = int(argument[0])
fromtime = datetime.strptime(argument[1], "%Y-%m-%d")
source = argument[2]
return index, fromtime, source
except:
msg = "Not a valid date: '{0}'.".format(argument[0])
print(msg)
raise
# # ## ## ## ## ## # #
# # ENTRY FUNCTION # #
# # ## ## ## ## ## # #
def main():
Guru()
# # ## ## ## ## ## ## # #
# # End ENTRY FUNCTION # #
# # ## ## ## ## ## ## # #
if __name__ == "__main__":
main()
#!/bin/bash
mode="dev"
if [[ $* =~ ^(prod)$ ]]; then
mode="prod"
fi
if [ $mode == "dev" ]; then
echo "[Installing AURA ENGINE for Development]"
fi
if [ $mode == "prod" ]; then
echo "[Installing AURA ENGINE for Production]"
fi
# Development and Production
echo "Installing OPAM Packages ..."
bash script/install-opam-packages.sh
echo "Installing Python Requirements ..."
python3.7 $(which pip3) install -r requirements.txt
# Development
if [ $mode == "dev" ]; then
echo "Create local 'logs' Folder ..."
mkdir -p logs
echo "Copy configuration to './configuration/engine.ini'"
cp -n configuration/sample-development.engine.ini configuration/engine.ini
echo "Installing Web Application Packages ..."
bash script/install-web.sh
fi
# Production
if [ $mode == "prod" ]; then
echo "Create local 'tmp' Folder ..."
mkdir -p tmp
echo "Copy default Engine configuration to '/etc/aura/engine.ini'"
cp -n configuration/sample-production.engine.ini /etc/aura/engine.ini
echo "Copy default Gunicorn configuration to '/etc/aura/engine.ini'"
cp -n configuration/sample-production.gunicorn.conf.py configuration/gunicorn.conf.py
echo "Create Virtual Env for Gunicorn"
virtualenv -p /usr/bin/python3.7 ../python-env
source ../python-env/bin/activate
echo "Install Requirements to Virtual Env"
pip3 install -r requirements.txt
fi
echo
echo "+++ Installation of AURA Engine finished! +++"
echo
\ No newline at end of file
# Meta
__author__ = "David Trattnig and Gottfried Gaisbauer"
__copyright__ = "Copyright 2017-2020, Aura Engine Team"
__credits__ = ["David Trattnig", "Gottfried Gaisbauer", "Michael Liebler"]
__license__ = "GNU Affero General Public License (AGPL) Version 3"
__version__ = "0.6.5"
__version_info__ = (0, 6, 5)
__maintainer__ = "David Trattnig"
__email__ = "david.trattnig@subsquare.at"
__status__ = "Development"
\ No newline at end of file
#
# Aura Engine
#
# Copyright (C) 2017-2020
# David Trattnig <david.trattnig@subsquare.at>
# Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import os.path
import sys
import logging
from pathlib import Path
from configparser import ConfigParser
class AuraConfig:
"""
AuraConfig Class
Holds the Aura Configuration as in the file `engine.ini`.
"""
ini_path = ""
logger = None
def __init__(self, ini_path="/etc/aura/engine.ini"):
"""
Initializes the configuration, defaults to `/etc/aura/engine.ini`.
If this file doesn't exist it uses `./configuration/engine.ini` from
the project directory.
Args:
ini_path(String): The path to the configuration file `engine.ini`
"""
config_file = Path(ini_path)
if not config_file.is_file():
ini_path = "%s/configuration/engine.ini" % Path(__file__).parent.parent.parent.absolute()
self.ini_path = ini_path
self.logger = logging.getLogger("AuraEngine")
self.load_config()
def set(self, key, value):
"""
Setter for some specific config property.
Args:
key (String): key
default (*): value
"""
try:
self.__dict__[key] = int(value)
except:
self.__dict__[key] = str(value)
def get(self, key, default=None):
"""
Getter for some specific config property.
Args:
key (String): key
default (*): value
"""
if key not in self.__dict__:
if default:
self.set(key, default)
else:
self.logger.warning("Key " + key + " not found in configfile " + self.ini_path + "!")
return None
if key == "loglevel":
loglvl = self.__dict__[key]
if loglvl == "debug":
return logging.DEBUG
elif loglvl == "info":
return logging.INFO
elif loglvl == "warning":
return logging.WARNING
elif loglvl == "error":
return logging.ERROR
else:
return logging.CRITICAL
if key == "debug":
return self.__dict__[key].count("y")
return self.__dict__[key]
def load_config(self):
"""
Set config defaults and load settings from file
"""
if not os.path.isfile(self.ini_path):
self.logger.critical(self.ini_path + " not found :(")
sys.exit(1)
# Read the file
f = open(self.ini_path, 'r')
ini_str = f.read()
f.close()
# Parse the values
config_parser = ConfigParser()
try:
config_parser.read_string(ini_str)
except Exception as e:
self.logger.critical("Cannot read " + self.ini_path + "! Reason: " + str(e))
sys.exit(0)
for section in config_parser.sections():
for key, value in config_parser.items(section):
v = config_parser.get(section, key).replace('"', '').strip()
self.set(key, v)
# Custom overrides and defaults
self.set("install_dir", os.path.realpath(__file__ + "../../../.."))
self.set("use_test_data", False)
self.set("api_prefix", "/api/v1")
def get_database_uri(self):
"""
Retrieves the database connection string.
"""
db_name = self.get("db_name")
db_user = self.get("db_user")
db_pass = self.get("db_pass")
db_host = self.get("db_host")
db_charset = self.get("db_charset", "utf8")
return "mysql://" + db_user + ":" + db_pass + "@" + db_host + "/" + db_name + "?charset=" + db_charset
\ No newline at end of file
#
# engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
# This file is part of engine.
#
# engine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# engine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
from enum import Enum
class RedisChannel(Enum):
STANDARD = "aura"
DPE_REPLY = "delete_playlist_entry_reply"
FNP_REPLY = "fetch_new_programme_reply"
GAP_REPLY = "get_act_programme_reply"
GS_REPLY = "get_status_reply"
GCS_REPLY = "get_connection_status_reply"
GNF_REPLY = "get_next_file_reply"
IPE_REPLY = "insert_playlist_entry_reply"
IP_REPLY = "init_player_reply"
TS_REPLY = "track_service_reply"
MPE_REPLY = "move_playlist_entry_reply"
PMQ_REPLY = "print_message_queue_reply"
RDB_REPLY = "recreate_database_reply"
SNF_REPLY = "get_next_file_reply"
class Channel(Enum):
"""
Channel name mappings to the Liqidsoap channel IDs
"""
FILESYSTEM_A = "in_filesystem_0"
FILESYSTEM_B = "in_filesystem_1"
STREAM_A = "http_1"
STREAM_B = "http_2"
LIVE_0 = "aura_linein_0"
LIVE_1 = "aura_linein_1"
LIVE_2 = "aura_linein_2"
LIVE_3 = "aura_linein_3"
LIVE_4 = "aura_linein_4"
def __str__(self):
return str(self.value)
class ChannelType(Enum):
"""
Engine channel types mapped to `Entry` source types.
"""
FILESYSTEM = {
"id": "fs",
"channels": [Channel.FILESYSTEM_A, Channel.FILESYSTEM_B]
}
STREAM = {
"id": "http",
"channels": [Channel.STREAM_A, Channel.STREAM_B]
}
LIVE = {
"id": "live",
"channels": [
Channel.LIVE_0,
Channel.LIVE_1,
Channel.LIVE_2,
Channel.LIVE_3,
Channel.LIVE_4
]
}
@property
def channels(self):
return self.value["channels"]
def __str__(self):
return str(self.value["id"])
class TransitionType(Enum):
"""
Types of fade-in and fade-out transition.
"""
INSTANT = "instant"
FADE = "fade"
class FallbackType(Enum):
SHOW = "show" # the first played when the show playlist fails
TIMESLOT = "timeslot" # the second played when timeslot fallback fails
STATION = "station" # the last played when everything else fails
class TimerType(Enum):
SWITCH = "switch"
FADEIN = "fadein"
FADEOUT = "fadeout"
class EntryQueueState(Enum):
OKAY = "ok"
CUT = "cut"
OUT_OF_SCHEDULE = "oos"
#
# engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
# This file is part of engine.
#
# engine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# engine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
# Scheduler Exceptions
class NoProgrammeLoadedException(Exception):
pass
class NoActiveScheduleException(Exception):
pass
# Mixer Exceptions
class InvalidChannelException(Exception):
pass
class PlaylistException(Exception):
pass
class NoActiveEntryException(Exception):
pass
# Monitoring Exceptions
class EngineMalfunctionException(Exception):
pass
class MailingException(Exception):
pass
# Liquidsoap Execeptions
class LQConnectionError(Exception):
pass
class RedisConnectionException(Exception):
pass
\ No newline at end of file
#
# Aura Engine
#
# Copyright (C) 2017-2020
# David Trattnig <david.trattnig@subsquare.at>
# Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from modules.base.config import AuraConfig
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.
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
the passed name.
Args:
name (String): The name of the logger
"""
self.config = config
self.__create_logger(name)
def __create_logger(self, name):
"""
Creates the logger instance for the given name.
Args:
name (String): The name of the logger
"""
lvl = self.config.get("loglevel")
# create logger
self.logger = logging.getLogger(name)
self.logger.setLevel(lvl)
if not self.logger.hasHandlers():
# create file handler for logger
file_handler = logging.FileHandler(self.config.get("logdir") + "/"+name+".log")
file_handler.setLevel(lvl)
# create stream handler for logger
stream_handler = logging.StreamHandler()
stream_handler.setLevel(lvl)
# set format of log
datepart = "%(asctime)s:%(name)s:%(levelname)s"
message = " - %(message)s - "
filepart = "[%(filename)s:%(lineno)s-%(funcName)s()]"
formatter = logging.Formatter(datepart + message + filepart)
# set log of handlers
file_handler.setFormatter(formatter)
stream_handler.setFormatter(formatter)
# add handlers to the logger
self.logger.addHandler(file_handler)
self.logger.addHandler(stream_handler)
self.logger.critical("ADDED HANDLERS")
else:
self.logger.critical("REUSED LOGGER")
\ No newline at end of file
#
# Aura Engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2020 David Trattnig <david.trattnig@subsquare.at>
#
# This file is part of engine.
#
# engine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# engine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
import datetime
import time
from enum import Enum
from modules.base.enum import Channel, ChannelType
class EngineUtil:
"""
A class for Engine utilities.
"""
@staticmethod
def get_channel_type(uri):
"""
Returns the channel type, depending on the passed URI and source.
Args:
uri (String): The URI of the source
"""
if uri.startswith("http"):
return ChannelType.STREAM
if uri.startswith("pool") or uri.startswith("playlist") or uri.startswith("file"):
return ChannelType.FILESYSTEM
if uri.startswith("live") or uri.startswith("linein"):
return ChannelType.LIVE
@staticmethod
def lqs_annotate_cuein(uri, cue_in):
"""
Wraps the given URI with a Liquidsoap Cue In annotation.
Args:
uri (String): The path to the audio source
cue_in (Float): The value in seconds wher 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
@staticmethod
def engine_info(component, version):
"""
Prints the engine logo and version info.
"""
return """\n
█████╗ ██╗ ██╗██████╗ █████╗ ███████╗███╗ ██╗ ██████╗ ██╗███╗ ██╗███████╗
██╔══██╗██║ ██║██╔══██╗██╔══██╗ ██╔════╝████╗ ██║██╔════╝ ██║████╗ ██║██╔════╝
███████║██║ ██║██████╔╝███████║ █████╗ ██╔██╗ ██║██║ ███╗██║██╔██╗ ██║█████╗
██╔══██║██║ ██║██╔══██╗██╔══██║ ██╔══╝ ██║╚██╗██║██║ ██║██║██║╚██╗██║██╔══╝
██║ ██║╚██████╔╝██║ ██║██║ ██║ ███████╗██║ ╚████║╚██████╔╝██║██║ ╚████║███████╗
╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝╚══════╝
%s v%s - Ready to play!
\n""" % (component, version)
class SimpleUtil:
"""
A container class for simple utility methods.
"""
@staticmethod
def fmt_time(timestamp):
"""
Formats a UNIX timestamp to a String displaying time in the format '%H:%M:%S'.
Args:
(Integer) timestamp: Unix epoch
Returns:
(String): Displaying the time
"""
return datetime.datetime.fromtimestamp(timestamp).strftime('%H:%M:%S')
@staticmethod
def timestamp(date_and_time=None):
"""
Transforms the given `datetime` into a UNIX epoch timestamp.
If no parameter is passed, the current timestamp is returned.
Args:
(Datetime) date_and_time: the date and time to transform.
Returns:
(Integer): timestamp in seconds.
"""
if not date_and_time:
date_and_time = datetime.datetime.now()
return time.mktime(date_and_time.timetuple())
@staticmethod
def strike(text):
"""
Creates a strikethrough version of the given text.
Args:
(String) text: the text to strike.
Returns:
(String): the striked text.
"""
result = ""
for c in str(text):
result += c + TerminalColors.STRIKE.value
return result
@staticmethod
def bold(text):
"""
Creates a bold version of the given text.
"""
return TerminalColors.BOLD.value + text + TerminalColors.ENDC.value
@staticmethod
def underline(text):
"""
Creates a underlined version of the given text.
"""
return TerminalColors.UNDERLINE.value + text + TerminalColors.ENDC.value
@staticmethod
def blue(text):
"""
Creates a blue version of the given text.
"""
return TerminalColors.BLUE.value + text + TerminalColors.ENDC.value
@staticmethod
def red(text):
"""
Creates a red version of the given text.
"""
return TerminalColors.RED.value + text + TerminalColors.ENDC.value
@staticmethod
def pink(text):
"""
Creates a red version of the given text.
"""
return TerminalColors.PINK.value + text + TerminalColors.ENDC.value
@staticmethod
def yellow(text):
"""
Creates a yellow version of the given text.
"""
return TerminalColors.YELLOW.value + text + TerminalColors.ENDC.value
@staticmethod
def green(text):
"""
Creates a red version of the given text.
"""
return TerminalColors.GREEN.value + text + TerminalColors.ENDC.value
@staticmethod
def cyan(text):
"""
Creates a cyan version of the given text.
"""
return TerminalColors.CYAN.value + text + TerminalColors.ENDC.value
class TerminalColors(Enum):
"""
Colors for formatting terminal output.
"""
HEADER = "\033[95m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
PINK = "\033[35m"
CYAN = "\033[36m"
WARNING = "\033[31m"
FAIL = "\033[41m"
BOLD = "\033[1m"
UNDERLINE = "\033[4m"
STRIKE = "\u0336"
ENDC = "\033[0m"
\ No newline at end of file
#
# engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
# This file is part of engine.
#
# engine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# engine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
import json
from modules.base.enum import RedisChannel
from modules.base.utils import TerminalColors
from modules.communication.redis.adapter import ClientRedisAdapter, ServerRedisAdapter
from modules.communication.redis.messenger import RedisMessenger
from modules.database.model import AuraDatabaseModel
class Padavan:
args = None
config = None
ss = None
zmqclient = None
redisclient = None
stringreply = ""
# ------------------------------------------------------------------------------------------ #
def __init__(self, args, config):
self.args = args
self.config = config
# ------------------------------------------------------------------------------------------ #
def meditate(self):
if self.args.fetch_new_programme:
self.fetch_new_programme()
elif self.args.get_active_mixer:
self.get_active_mixer()
elif self.args.get_mixer_status:
self.get_mixer_status()
elif self.args.get_act_programme:
self.get_act_programme()
elif self.args.get_status:
self.get_status()
elif self.args.get_connection_status:
self.get_connection_status()
elif self.args.shutdown:
self.shutdown()
elif self.args.redis_message:
self.redis_message(self.args.redis_message[0], self.args.redis_message[1])
elif self.args.select_mixer != -1:
self.select_mixer(self.args.select_mixer)
elif self.args.deselect_mixer != -1:
self.select_mixer(self.args.deselect_mixer, False)
elif self.args.set_volume:
self.set_volume(self.args.set_volume[0], self.args.set_volume[1])
elif self.args.print_message_queue:
self.print_message_queue()
elif self.args.get_file_for:
self.get_next_file(self.args.get_file_for)
elif self.args.set_file_for:
self.set_next_file(self.args.set_file_for[0], self.args.set_file_for[1])
elif self.args.now_playing:
print("")
elif self.args.init_player:
self.init_player()
elif self.args.on_play:
self.on_play(self.args.on_play)
elif self.args.recreatedb:
self.recreatedb()
# else:
# raise Exception("")
# init liquid => faster exec time, when loading at runtime just what is needed
# ------------------------------------------------------------------------------------------ #
def init_liquidsoap_communication(self):
# import
from modules.core.engine import SoundSystem
# init liquidsoap communication
self.ss = SoundSystem(self.config)
# enable connection
self.ss.enable_transaction()
# ------------------------------------------------------------------------------------------ #
def destroy_liquidsoap_communication(self):
# enable connection
self.ss.disable_transaction()
# ------------------------------------------------------------------------------------------ #
def init_redis_communication(self, with_server=False):
self.redisclient = ClientRedisAdapter(self.config)
if with_server:
self.redisserver = ServerRedisAdapter(self.config)
# ------------------------------------------------------------------------------------------ #
def send_redis(self, channel, message):
self.init_redis_communication()
self.redisclient.publish(channel, message)
# ------------------------------------------------------------------------------------------ #
def send_and_wait_redis(self, channel, message, reply_channel):
self.init_redis_communication(True)
self.redisclient.publish(channel, message)
return self.redisserver.listen_for_one_message(reply_channel.value)
# ------------------------------------------------------------------------------------------ #
def shutdown(self):
self.send_redis("aura", "shutdown")
self.stringreply = "Shutdown message sent!"
# ------------------------------------------------------------------------------------------ #
def fetch_new_programme(self):
json_reply = self.send_and_wait_redis("aura", "fetch_new_programme", RedisChannel.FNP_REPLY)
if json_reply != "":
actprogramme = json.loads(json_reply)
self.print_programme(actprogramme)
else:
print("No programme fetched")
# ------------------------------------------------------------------------------------------ #
def get_act_programme(self):
json_reply = self.send_and_wait_redis("aura", "get_act_programme", RedisChannel.GAP_REPLY)
actprogramme = json.loads(json_reply)
self.print_programme(actprogramme)
def get_status(self):
"""
Retrieves the Engine's status information.
"""
json_reply = self.send_and_wait_redis("aura", "get_status", RedisChannel.GS_REPLY)
# status = json.loads(json_reply)
self.stringreply = json_reply
# ------------------------------------------------------------------------------------------ #
def get_connection_status(self):
json_reply = self.send_and_wait_redis("aura", "get_connection_status", RedisChannel.GCS_REPLY)
connection_status = json.loads(json_reply)
self.print_connection_status(connection_status)
# ------------------------------------------------------------------------------------------ #
def print_programme(self, programme):
cnt = 1
for show in programme:
for entry in show["playlist"]:
self.stringreply += str(cnt) + \
" --- schedule id #" + str(show["schedule_id"]) + "." + str(entry["entry_num"]) + \
" - show: " + show["show_name"] + \
" - starts @ " + entry["entry_start"] + \
" - plays " + str(entry["source"]) + "\n"
cnt = cnt + 1
# ------------------------------------------------------------------------------------------ #
def print_connection_status(self, connection_status):
if connection_status["pv"]:
self.stringreply = "Connection to pv: " + TerminalColors.GREEN.value + " " + str(connection_status["pv"]) + TerminalColors.ENDC.value
else:
self.stringreply = "Connection to pv: " + TerminalColors.RED.value + " " + str(connection_status["pv"]) + TerminalColors.ENDC.value
if connection_status["db"]:
self.stringreply += "\nConnection to db: " + TerminalColors.GREEN.value + " " + str(connection_status["db"]) + TerminalColors.ENDC.value
else:
self.stringreply += "\nConnection to db: " + TerminalColors.RED.value + " " + str(connection_status["db"]) + TerminalColors.ENDC.value
if connection_status["lqs"]:
self.stringreply += "\nConnection to lqs: " + TerminalColors.GREEN.value + " " + str(connection_status["lqs"]) + TerminalColors.ENDC.value
else:
self.stringreply += "\nConnection to lqs: " + TerminalColors.RED.value + " " + str(connection_status["lqs"]) + TerminalColors.ENDC.value
if connection_status["lqsr"]:
self.stringreply += "\nConnection to lqsr: " + TerminalColors.GREEN.value + " " + str(connection_status["lqsr"]) + TerminalColors.ENDC.value
else:
self.stringreply += "\nConnection to lqsr: " + TerminalColors.RED.value + " " + str(connection_status["lqsr"]) + TerminalColors.ENDC.value
if connection_status["tank"]:
self.stringreply += "\nConnection to tank: " + TerminalColors.GREEN.value + " " + str(connection_status["tank"]) + TerminalColors.ENDC.value
else:
self.stringreply += "\nConnection to tank: " + TerminalColors.RED.value + " " + str(connection_status["tank"]) + TerminalColors.ENDC.value
if connection_status["redis"]:
self.stringreply += "\nConnection to redis: " + TerminalColors.GREEN.value + " " + str(connection_status["redis"]) + TerminalColors.ENDC.value
else:
self.stringreply += "\nConnection to redis: " + TerminalColors.RED.value + " " + str(connection_status["redis"]) + TerminalColors.ENDC.value
# ------------------------------------------------------------------------------------------ #
def init_player(self):
"""
Initializes the player on Liquidsaop startup.
"""
self.stringreply = self.send_and_wait_redis("aura", "init_player", RedisChannel.IP_REPLY)
def on_play(self, info):
"""
Event handler to be called when some entry started playing.
"""
self.stringreply = self.send_and_wait_redis("aura", "on_play " + info, RedisChannel.GNF_REPLY)
# ------------------------------------------------------------------------------------------ #
def recreatedb(self):
print("YOU WILL GET PROBLEMS DUE TO DATABASE BLOCKING IF aura.py IS RUNNING! NO CHECKS IMPLEMENTED SO FAR!")
x = AuraDatabaseModel()
x.recreate_db()
self.stringreply = "Database recreated!"
# ------------------------------------------------------------------------------------------ #
def redis_message(self, channel, message):
self.send_redis(channel, message)
self.stringreply = "Message '"+message+"' sent to channel '"+channel+"'"
# ------------------------------------------------------------------------------------------ #
def print_message_queue(self):
self.stringreply = self.send_and_wait_redis("aura", "print_message_queue", RedisChannel.PMQ_REPLY)
# LIQUIDSOAP #
# ------------------------------------------------------------------------------------------ #
def select_mixer(self, mixername, activate=True):
# init lqs
self.init_liquidsoap_communication()
# select mixer and return the feedback
self.stringreply = self.ss.channel_activate(mixername, activate)
# disable connection
self.destroy_liquidsoap_communication()
# ------------------------------------------------------------------------------------------ #
def set_volume(self, mixernumber, volume):
# init lqs and enable comm
self.init_liquidsoap_communication()
self.stringreply = self.ss.channel_volume(mixernumber, volume)
# disable connection
self.destroy_liquidsoap_communication()
# ------------------------------------------------------------------------------------------ #
def get_active_mixer(self):
self.init_liquidsoap_communication()
am = self.ss.get_active_mixer()
if len(am) == 0:
self.destroy_liquidsoap_communication()
raise Exception("Guru recognized a problem: No active source!!!")
self.stringreply = str(am)
# disable connection
self.destroy_liquidsoap_communication()
# ------------------------------------------------------------------------------------------ #
def get_mixer_status(self):
self.init_liquidsoap_communication()
status = self.ss.get_mixer_status()
for k, v in status.items():
self.stringreply += "source: " + k + "\t status: " + v + "\n"
# disable connection
self.destroy_liquidsoap_communication()
# REDIS #
# ------------------------------------------------------------------------------------------ #
def get_next_file(self, type):
# redis = RedisMessenger()
# next_file = redis.get_next_file_for(type)
# if next_file == "":
# next_file = "/var/audio/blank.flac"
# self.stringreply = next_file
#self.send_redis("aura", "set_next_file " + type)
next_file = self.send_and_wait_redis("aura", "get_next_file " + type, RedisChannel.GNF_REPLY)
self.stringreply = next_file
# ------------------------------------------------------------------------------------------ #
def set_next_file(self, type, file):
#from modules.communication.redis.messenger import RedisMessenger
#redis = RedisMessenger()
#redis.set_next_file_for(type, file)
self.send_redis("aura", "set_next_file " + type + " " + file)
self.stringreply = "Set "+file+" for fallback '"+type+"'"
#
# engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
# This file is part of engine.
#
# engine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# engine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
import socket
import urllib.parse
import configparser
import logging
from multiprocessing import Lock
from modules.base.exceptions import LQConnectionError
from modules.base.utils import TerminalColors
"""
LiquidSoapClient Class
Connects to a LiquidSoap instance over a socket and sends commands to it
"""
class LiquidSoapClient:
mutex = None
logger = None
debug = False
socket_path = ""
disable_logging = True
def __init__(self, config, socket_filename):
"""
Constructor
@type socket_path: string
@param socket_path: Der Pfad zum Socket des Liquidsoap-Scripts
"""
self.logger = logging.getLogger("AuraEngine")
self.socket_path = config.get('socketdir') + '/' + socket_filename
self.logger.debug("LiquidSoapClient using socketpath: " + self.socket_path)
# init
self.mutex = Lock()
self.connected = False
self.can_connect = True
self.message = ''
self.socket = None
self.metareader = configparser.ConfigParser()
# ------------------------------------------------------------------------------------------ #
def connect(self):
"""
Verbindung herstellen
"""
try:
self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
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)
self.can_connect = False
self.connected = False
# raise e
else:
self.can_connect = True
self.connected = True
return True
# AttributeError('characters_written')
# ------------------------------------------------------------------------------------------ #
def is_connected(self):
return self.connected
# ------------------------------------------------------------------------------------------ #
def write(self, data):
"""
Auf den Socket schreiben
@type data: string
@param data: Der String der gesendet wird
"""
if self.connected:
self.socket.sendall(data.decode("UTF-8"))
# ------------------------------------------------------------------------------------------ #
def read_all(self, timeout=2):
"""
Vom Socket lesen, bis dieser "END" sendet
@type timeout: int
@param timeout: Ein optionales Timeout
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
# make socket non blocking
# self.client.setblocking(0)
data = ''
try:
# set timeout
self.socket.settimeout(timeout)
# acquire the lock
self.mutex.acquire()
while True:
data += self.socket.recv(1).decode("utf-8")
# receive as long as we are not at the END or recv a Bye! from liquidsoap
if data.find("END\r\n") != -1 or data.find("Bye!\r\n") != -1:
data.replace("END\r\n", "")
break
# release the lock
self.mutex.release()
except Exception as e:
self.logger.error(TerminalColors.RED.value+str(e)+TerminalColors.ENDC.value)
self.mutex.release()
return data
# ------------------------------------------------------------------------------------------ #
def read(self):
"""
read from socket and store return value in self.message
@rtype: string
@return: The answer of liquidsoap server
"""
if self.connected:
ret = self.read_all().splitlines()
try:
last = ret.pop() # pop out end
if len(ret) > 1:
self.message = str.join(" - ", ret)
elif len(ret) == 1:
self.message = ret[0]
if last == "Bye!":
self.message = last
except Exception as e:
self.logger.error(str(e))
return self.message
# ------------------------------------------------------------------------------------------ #
def close(self):
"""
Quit senden und Verbindung schließen
"""
if self.connected:
message = "quit\r"
self.socket.sendall(message.decode("UTF-8"))
self.socket.close()
self.connected = False
# ------------------------------------------------------------------------------------------ #
def command(self, namespace, command, param=""):
"""
Kommando an Liquidosap senden
@type command: string
@param command: Kommando
@type namespace: string
@param namespace: Namespace/Kanal der angesprochen wird
@type param: mixed
@param param: ein optionaler Parameter
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
param = (param.strip() if param.strip() == "" else " " + urllib.parse.unquote(param.strip()))
if self.connected:
# print namespace + '.' + command + param + "\n"
if namespace is "":
message = str(command) + str(param) + str("\n")
else:
message = str(namespace) + str(".") + str(command) + str(param) + str("\n")
try:
if not self.disable_logging:
self.logger.info("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")
# wait for reply
self.read()
if not self.disable_logging:
self.logger.info("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.connect()
raise
except Exception as e:
self.logger.error("Unexpected error: " + str(e))
raise
return self.message
else:
msg = "LiquidsoapClient not connected to LiquidSoap Server"
self.logger.error(msg)
raise LQConnectionError(msg)
# ------------------------------------------------------------------------------------------ #
def help(self):
"""
get liquidsoap server help
@rtype: string
@return: the response of the liquidsoap server
"""
if self.connected:
self.command('help', '')
return self.message
# ------------------------------------------------------------------------------------------ #
def version(self):
"""
Liquidsoap get version
@rtype: string
@return: the response of the liquidsoap server
"""
if self.connected:
message = 'version'
self.command(message, '')
return self.message
# ------------------------------------------------------------------------------------------ #
def uptime(self):
"""
Liquidsoap get uptime
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
if self.connected:
self.command('uptime', '')
return self.message
# ------------------------------------------------------------------------------------------ #
def byebye(self):
"""
Liquidsoap say byebye
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
if self.connected:
self.command("", "quit")
return self.message
\ No newline at end of file
#
# engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
# This file is part of engine.
#
# engine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# engine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
from modules.base.enum import Channel, ChannelType
from modules.communication.liquidsoap.client import LiquidSoapClient
class LiquidSoapPlayerClient(LiquidSoapClient):
# ------------------------------------------------------------------------------------------ #
def mixer(self, command, *args):
if command == "status":
return self.mixerstatus(*args)
if command == "inputs":
return self.mixerinputs()
if command == "volume":
return self.mixervolume(*args)
if command == "select":
if len(args) == 2:
return self.mixerselect(args[0], args[1])
return "LiquidSoapPlayerClient does not understand mixer."+command+str(args)
# ------------------------------------------------------------------------------------------ #
def recorder(self, num, command, *args):
if command == "status":
return self.recorderstatus(num)
if command == "start":
return self.recorderstart(num)
if command == "stop":
return self.recorderstop(num)
return "LiquidSoapPlayerClient does not understand mixer." + command + str(args)
# ------------------------------------------------------------------------------------------ #
def http(self, command, *args):
if command == "url":
return self.set_http_url(*args)
return "LiquidSoapPlayerClient does not understand http." + command + str(args)
# ------------------------------------------------------------------------------------------ #
def playlist_push(self, channel, uri):
"""
Pushs the passed file URI to the playlist channel.
Args:
channel (String): Liquidsoap Channel ID
"""
self.command(channel, 'push', uri)
return self.message
def playlist_seek(self, channel, duration):
"""
Forward the playing track/playlist of the given channel.
Args:
channel (String): Liquidsoap Channel ID
duration (Integer): Seek duration ins seconds
Returns:
Liquidsoap server response
"""
self.command(channel, 'seek', str(duration))
return self.message
def playlist_clear(self, channel):
"""
Clears all playlist entries of the given channel.
Args:
channel (String): Liquidsoap Channel ID
duration (Integer): Seek duration ins seconds
Returns:
Liquidsoap server response
"""
if channel == Channel.FILESYSTEM_A.value:
self.command(channel, 'clear_filesystem_0')
elif channel == Channel.FILESYSTEM_B.value:
self.command(channel, 'clear_filesystem_1')
else:
return "Invalid filesystem channel '%s'" % channel
return self.message
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
"""
Retrieves the Liquidsoap version.
"""
return self.command("", "version")
def engine(self, command, *args):
"""
Retrieves the state of all input and outputs.
"""
if command == "state":
return self.engine_state()
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')
return self.message
# ------------------------------------------------------------------------------------------ #
def set_http_url(self, uri):
self.command('http', 'url', uri)
return self.message
# ------------------------------------------------------------------------------------------ #
def mixerinputs(self):
# send command
self.command("mixer", "inputs")
# convert to list and return it
return self.message.strip().split(' ')
# ------------------------------------------------------------------------------------------ #
def mixerstatus(self, pos=""):
"""
Get state of a source in the mixer
@type pos: string
@param pos: Mixerposition
@rtype: string
@return: Response from LiquidSoap
"""
self.command("mixer", "status", str(pos))
return self.message
# ------------------------------------------------------------------------------------------ #
def mixerselect(self, pos, activate):
"""
Kanal/Source aktivieren
@type pos: string
@param pos: Die Position
@type namespace: string
@param namespace: Namespace der Source
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
self.command("mixer", "select", str(pos) + " " + str(activate).lower())
return self.message
# ------------------------------------------------------------------------------------------ #
def mixervolume(self, pos, volume):
"""
set channel volume
:param pos:
:param volume:
:return:
"""
self.command("mixer", "volume", str(pos) + " " + str(volume))
return self.message
# ------------------------------------------------------------------------------------------ #
# def recorderstatus(self, num):
# """
# get status of a recorder
# :return:
# """
# self.command("recorder_" + str(num), "status")
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def recorderstart(self, num):
# """
# get status of a recorder
# :return:
# """
# self.command("recorder_" + str(num), "start")
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def recorderstop(self, num):
# """
# get status of a recorder
# :return:
# """
# self.command("recorder_" + str(num), "stop")
# return self.message
# ------------------------------------------------------------------------------------------ #
# def skip(self, namespace="playlist", pos=""):
# """
# Source skippen
# @type namespace: string
# @param namespace: Namespace der Source
# @type pos: string
# @param pos: Die Position - optional - Position des Channels vom Mixer benötigt
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('skip', namespace, pos)
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def remove(self, pos, namespace="playlist"):
# """
# Track aus der secondary_queue oder der Playlist entfernen
# @type pos: string
# @param pos: Die Position
# @type namespace: string
# @param namespace: Namespace der Source
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('remove', namespace, str(pos))
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def insert(self, uri, pos='0', namespace="playlist"):
# """
# Track einfügen
# @type uri: string
# @param uri: Uri einer Audiodatei
# @type pos: string
# @param pos: Die Position
# @type namespace: string
# @param namespace: Namespace der Source
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('insert', namespace, str(pos) + ' ' + uri)
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def move(self, fromPos, toPos, namespace="playlist"):
# """
# Track von Position fromPos nach Position toPos verschieben
# @type fromPos: string/int
# @param fromPos: Position des zu verschiebenden Tracks
# @type toPos: string
# @param toPos: Die Position zu der verschoben werden soll
# @type namespace: string
# @param namespace: Namespace der Source
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('move', namespace, str(fromPos) + ' ' + str(toPos))
# return self.message
# ------------------------------------------------------------------------------------------ #
# def play(self, namespace="playlist"):
# """
# Source abspielen - funktioniert nur bei Playlist
# @type namespace: string
# @param namespace: Namespace der Source
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('play', namespace)
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def pause(self, namespace="playlist"):
# """
# Source pausieren/stoppen - funktioniert nur bei Playlist
# @type namespace: string
# @param namespace: Namespace der Source
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('pause', namespace)
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def flush(self, namespace="playlist"):
# """
# Playlist leeren
# @type namespace: string
# @param namespace: Namespace der Source
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('flush', namespace)
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def playlistData(self):
# """
# Metadaten der Playlist ausgeben
# @rtype: string
# @return: Ein Json-String
# """
# self.command('data', 'playlist')
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def get_queue(self, namespace="ch1", queue='queue'):
# """
# Queue eines Kanals ausgeben
# @type namespace: string
# @param namespace: Namespace der Source
# @type queue: string
# @param queue: Name des queues (queue, primary_queue, secondary_queue)
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command(queue, namespace)
# return self.message
# ------------------------------------------------------------------------------------------ #
# def loadPlaylist(self, uri, params="", namespace="playlist"):
# """
# Playlist laden
# @type uri: string
# @param uri: Uri einer Playlist im XSPF-Format
# @type params: string
# @param params: obsolete
# @type namespace: string
# @param namespace: Namespace der Source - hier nur playlist
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('load', namespace, uri + params)
# return self.message
# ------------------------------------------------------------------------------------------ #
# def currentTrack(self, namespace="request"):
# """
# Das oder die ID(s) der gerade abgespielten requests erhalten
# @type namespace: string
# @param namespace: Namespace der Source
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers (als String)
# """
# self.command('on_air', namespace)
# return self.message
# ------------------------------------------------------------------------------------------ #
def volume(self, pos, volume, namespace="mixer"):
"""
Lautstärke eines Kanals setzen
@type pos: int/string
@param pos: Die Position/ Nummer des Kanals (playlist=0)
@type volume: int/string
@param volume: Zahl von 1 -100
@type namespace: string
@param namespace: Namespace der Source (immer mixer)
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
self.command('volume', namespace, str(pos) + ' ' + str(volume))
return self.message
# ------------------------------------------------------------------------------------------ #
# def playlist_remaining(self):
# """
# Wie lange läuft der aktuelle Track der Playlist noch
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('remaining', 'playlist')
# return self.message
# ------------------------------------------------------------------------------------------ #
# def list_channels(self):
# """
# Channels auflisten (Simple JSON)
# """
# # Liquidsoap Kommando
# channels = self.sendLqcCommand(self.lqc, 'mixer', 'inputs')
# if not isinstance(channels, list):
# self.error('02')
# elif len(channels) < 1:
# self.warning('01')
# else:
# self.success('00', channels)
# self.notifyClient()
\ No newline at end of file
#
# engine
#
# Playout Daemon for autoradio project
#
#
# Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
# This file is part of engine.
#
# engine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# engine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with engine. If not, see <http://www.gnu.org/licenses/>.
#
import smtplib
from email.message import EmailMessage
from modules.base.exceptions import MailingException
class AuraMailer():
"""
Service to send emails to Aura administrators.
"""
config = None
def __init__(self, config):
"""
Constructor to initialize service with Aura `config`.
Args:
config (AuraConfig): The configuration with the mail server details
"""
self.config = config
self.admin_mails = config.get("admin_mail")
#
# PUBLIC METHODS
#
def send_admin_mail(self, subject, body):
"""
Sends an email to the administrator as defined in the configuration.
Args:
subject (String): The email's subject
body (String): The email's body text
"""
admin_mails = self.admin_mails.split()
for mail_to in admin_mails:
self.__send(mail_to, subject, body)
#
# PRIVATE METHODS
#
def __send(self, mail_to, subject, body):
"""
Sends an email to the given address.
Args:
subject (String): The email's subject
body (String): The email's body text
"""
# read config
mail_server = self.config.get("mail_server")
mail_port = self.config.get("mail_server_port")
mail_user = self.config.get("mail_user")
mail_pass = self.config.get("mail_pass")
from_mail = self.config.get("from_mail")
# check settings
if mail_server == "":
raise MailingException("Mail Server not set")
if mail_port == "":
raise MailingException("Mailserver Port not set")
if mail_user == "":
raise MailingException("Mail user not set")
if mail_pass == "":
raise MailingException("No Password for mailing set")
if from_mail == "":
raise MailingException("From Mail not set")
# stuff the message together and ...
msg = EmailMessage()
msg.set_content(body)
mailsubject_prefix = self.config.get("mailsubject_prefix")
if mailsubject_prefix == "":
msg["Subject"] = subject
else:
msg["Subject"] = mailsubject_prefix + " " + subject
msg["From"] = from_mail
msg["To"] = mail_to
# ... send the mail
try:
server = smtplib.SMTP(mail_server, int(mail_port))
server.starttls()
server.login(mail_user, mail_pass)
server.send_message(msg)
server.quit()
except Exception as e:
raise MailingException(str(e))