Commit a449c907 authored by david's avatar david
Browse files

Merge branch 'master' into 'lars-documentation'

# Conflicts:
#   src/engine.py
#   src/mixer.py
parents df321119 cc39a7e0
Pipeline #1159 passed with stage
in 1 minute and 19 seconds
...@@ -11,4 +11,4 @@ env.list ...@@ -11,4 +11,4 @@ env.list
audio audio
python python
__pycache__ __pycache__
config/engine.docker.ini config/docker.engine.ini
# Contributing
When contributing to this repository, please first read about [contributing to AURA](https://gitlab.servus.at/aura/meta/-/blob/master/docs/development/contributions.md). Then discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change.
Please note we have a [code of conduct](/aura/meta/-/blob/master/docs/development/code_of_conduct.md), please follow it in all your interactions with the project.
\ No newline at end of file
...@@ -31,4 +31,4 @@ VOLUME ["/srv/socket", "/srv/logs", "/var/audio/source", "/var/audio/playlist"] ...@@ -31,4 +31,4 @@ VOLUME ["/srv/socket", "/srv/logs", "/var/audio/source", "/var/audio/playlist"]
# Start the Engine # Start the Engine
EXPOSE 1337/tcp EXPOSE 1337/tcp
ENTRYPOINT ["./run.sh"] ENTRYPOINT ["./run.sh", "prod"]
...@@ -10,22 +10,22 @@ the requirements of community radio stations. ...@@ -10,22 +10,22 @@ the requirements of community radio stations.
<!-- TOC --> <!-- TOC -->
1. [Aura Engine](#aura-engine) 1. [Aura Engine](#aura-engine)
1. [Functionality](#functionality) 1. [Functionality](#functionality)
1. [Scheduler](#scheduler) 1. [Scheduler](#scheduler)
1. [Versatile playlists](#versatile-playlists) 1. [Versatile playlists](#versatile-playlists)
2. [Default playlists](#default-playlists) 2. [Default playlists](#default-playlists)
2. [Heartbeat Monitoring](#heartbeat-monitoring) 2. [Heartbeat Monitoring](#heartbeat-monitoring)
3. [Logging](#logging) 3. [Logging](#logging)
2. [Getting started](#getting-started) 2. [Getting started](#getting-started)
3. [Using Docker](#using-docker) 3. [Using Docker](#using-docker)
4. [Read more](#read-more) 4. [Read more](#read-more)
5. [About](#about) 5. [About](#about)
<!-- /TOC --> <!-- /TOC -->
## Functionality ## Functionality
In conjuction with other AURA components Engine provides several features: In conjunction with other AURA components Engine provides several features:
- **Scheduler** to automatically broadcast your radio programme (see [AURA Dashboard](https://gitlab.servus.at/aura/dashboard) for a user interface to do scheduling) - **Scheduler** to automatically broadcast your radio programme (see [AURA Dashboard](https://gitlab.servus.at/aura/dashboard) for a user interface to do scheduling)
- **Analog input and outputs** provided by [Engine Core](https://gitlab.servus.at/aura/engine-core) - **Analog input and outputs** provided by [Engine Core](https://gitlab.servus.at/aura/engine-core)
...@@ -53,7 +53,7 @@ It is possible to schedules playlists with music or pre-recorded shows stored on ...@@ -53,7 +53,7 @@ It is possible to schedules playlists with music or pre-recorded shows stored on
The switching between types of audio source is handled automatically, with configured fadings applied. The switching between types of audio source is handled automatically, with configured fadings applied.
> Note: Any live sources or streams not specifing a length property, are automatically expanded to the left duration of the timeslot. > Note: Any live sources or streams not specifying a length property, are automatically expanded to the left duration of the timeslot.
#### Default playlists #### Default playlists
...@@ -64,13 +64,13 @@ for schedules and shows: ...@@ -64,13 +64,13 @@ for schedules and shows:
In case the timeslot doesn't have any specific playlist assigned, this playlist is broadcasted. In case the timeslot doesn't have any specific playlist assigned, this playlist is broadcasted.
- **Default Show Playlist**: This playlist can be assigned to some show. If neither the specific timeslot - **Default Show Playlist**: This playlist can be assigned to some show. If neither the specific timeslot
playlist nor the default schedule playlist is specificed the *default show playlist* is broadcasted. playlist nor the default schedule playlist is specified the *default show playlist* is broadcasted.
If none of these playlists have been specified the *Auto DJ* feature of [Engine Core](https://gitlab.servus.at/aura/engine-core) takes over (optional). If none of these playlists have been specified the *Auto DJ* feature of [Engine Core](https://gitlab.servus.at/aura/engine-core) takes over (optional).
### Heartbeat Monitoring ### Heartbeat Monitoring
Instead of checking all status properties, the Heartbeat only validates the vital ones required to run the engine. If all of those are valid, a network socket request is sent to a defined server. This heartbeat is sent continiously based on the configured `heartbeat_frequency`. The service receiving this heartbeat ticks can decide what to do with that information. One scenario could be switching to another Engine instance or any other custom failover scenario. Under `contrib/heartbeat-monitor` you'll find some sample application digesting these heartbeat signals. Instead of checking all status properties, the Heartbeat only validates the vital ones required to run the engine. If all of those are valid, a network socket request is sent to a defined server. This heartbeat is sent continuously based on the configured `heartbeat_frequency`. The service receiving this heartbeat ticks can decide what to do with that information. One scenario could be switching to another Engine instance or any other custom failover scenario. Under `contrib/heartbeat-monitor` you'll find some sample application digesting these heartbeat signals.
### Logging ### Logging
...@@ -84,21 +84,34 @@ For production we recommend running Engine using Docker Compose. If you want to ...@@ -84,21 +84,34 @@ For production we recommend running Engine using Docker Compose. If you want to
## Using Docker ## Using Docker
If you only want to run the single Engine Docker container, you can do this in a few, simple steps. Before getting started copy the default configuration file to `config/engine.docker.ini`: If you only want to run a single Engine Docker container, you can do this in a few, simple steps.
Before getting started copy the default configuration file to `config/engine.docker.ini`:
```shell
cp config/sample-docker.engine.ini config/docker.engine.ini
```
You'll need update a few settings:
- The password `db_pass` for the local database holding scheduling information
- The app secret `api_tank_secret` for connecting to [AURA Tank](https://gitlab.servus.at/aura/tank)
- Also check the `ENV` variables defined in the `run.sh` script.
At the moment production deployment using Docker and Docker Compose is [*work in progress*](https://gitlab.servus.at/aura/meta/-/issues/56).
If you would like to run the local codebase, starting Engine in Docker requires you to do a build first:
```shell ```shell
cp config/sample-docker.engine.ini config/engine.docker.ini ./run.sh docker:build
``` ```
You'll need to do a few configurations which are required: After your build has finished start the Engine with following command.
- The password `db_pass` for the local database holding scheduling information
- The app secret `api_tank_secret` for connecting to [AURA Tank](https://gitlab.servus.at/aura/tank)
- Also check the `ENV` variables defined in the `run.sh` script.
Now start the engine with: If no build is available it pulls the latest image from [Docker Hub](https://hub.docker.com/r/autoradio/engine).
```shell ```shell
./run.sh docker:engine ./run.sh docker:dev
``` ```
## Read more ## Read more
...@@ -112,7 +125,7 @@ Now start the engine with: ...@@ -112,7 +125,7 @@ Now start the engine with:
[<img src="https://gitlab.servus.at/autoradio/meta/-/raw/master/assets/images/aura-logo.png" width="150" />](https://gitlab.servus.at/aura/meta) [<img src="https://gitlab.servus.at/autoradio/meta/-/raw/master/assets/images/aura-logo.png" width="150" />](https://gitlab.servus.at/aura/meta)
AURA stands for Automated Radio and is a swiss army knife for community radio stations. Beside the Engine it provides Steering (Admin Interface for the radio station), Dashboard (Collaborative scheduling and programme coordination), Tank (Audio uploading, pre-processing and delivery). Read more in the [Aura Meta](https://gitlab.servus.at/aura/meta) repository or on the specific project pages. Automated Radio (AURA) is a open source software suite for community radio stations. Learn more about AURA in the [Meta repository](https://gitlab.servus.at/aura/meta).
| [<img src="https://gitlab.servus.at/aura/meta/-/raw/master/assets/images/aura-steering.png" width="150" align="left" />](https://gitlab.servus.at/aura/steering) | [<img src="https://gitlab.servus.at/aura/meta/-/raw/master/assets/images/aura-dashboard.png" width="150" align="left" />](https://gitlab.servus.at/aura/dashboard) | [<img src="https://gitlab.servus.at/aura/meta/-/raw/master/assets/images/aura-tank.png" width="150" align="left" />](https://gitlab.servus.at/aura/tank) | [<img src="https://gitlab.servus.at/aura/meta/-/raw/master/assets/images/aura-engine.png" width="150" align="left" />](https://gitlab.servus.at/aura/engine) | | [<img src="https://gitlab.servus.at/aura/meta/-/raw/master/assets/images/aura-steering.png" width="150" align="left" />](https://gitlab.servus.at/aura/steering) | [<img src="https://gitlab.servus.at/aura/meta/-/raw/master/assets/images/aura-dashboard.png" width="150" align="left" />](https://gitlab.servus.at/aura/dashboard) | [<img src="https://gitlab.servus.at/aura/meta/-/raw/master/assets/images/aura-tank.png" width="150" align="left" />](https://gitlab.servus.at/aura/tank) | [<img src="https://gitlab.servus.at/aura/meta/-/raw/master/assets/images/aura-engine.png" width="150" align="left" />](https://gitlab.servus.at/aura/engine) |
|---|---|---|---| |---|---|---|---|
......
[program:aura-engine] [program:aura-engine]
user = engineuser user = engineuser
directory = /opt/aura/engine directory = /opt/aura/engine
command = /opt/aura/engine/run.sh engine command = /opt/aura/engine/run.sh prod
priority = 666 priority = 666
autostart = true autostart = true
......
...@@ -8,7 +8,7 @@ Requires=aura-engine-core.service ...@@ -8,7 +8,7 @@ Requires=aura-engine-core.service
Type=simple Type=simple
User=engineuser User=engineuser
WorkingDirectory=/opt/aura/engine WorkingDirectory=/opt/aura/engine
ExecStart=/opt/aura/engine/run.sh ExecStart=/opt/aura/engine/run.sh prod
Restart=always Restart=always
[Install] [Install]
......
#!/usr/bin/env python2.7 #!/usr/bin/env python3
# Copyright (c) 2001, Nicola Larosa # Copyright (c) 2001, Nicola Larosa
# All rights reserved. # All rights reserved.
# Redistribution and use in source and binary forms, with or without # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions # modification, are permitted provided that the following conditions
# are met: # are met:
# * Redistributions of source code must retain the above copyright # * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer. # notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above # * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following # copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided # disclaimer in the documentation and/or other materials provided
# with the distribution. # with the distribution.
# * Neither the name of the <ORGANIZATION> nor the names of its # * Neither the name of the <ORGANIZATION> nor the names of its
# contributors may be used to endorse or promote products derived # contributors may be used to endorse or promote products derived
# from this software without specific prior written permission. # from this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
...@@ -34,54 +34,72 @@ clients that sent at least one packet during the run, but have ...@@ -34,54 +34,72 @@ clients that sent at least one packet during the run, but have
not sent any packet since a time longer than the definition of the timeout. not sent any packet since a time longer than the definition of the timeout.
Adjust the constant parameters as needed, or call as: Adjust the constant parameters as needed, or call as:
PyHBServer.py [timeout [udpport]]
PyHeartBeat.py [udpport [timeout]]
Set the environment variable "DEBUG" to "1" in order to emit more detailed
debug messages.
In addition "127.0.0.1" is marked as a previously active peer.
Manual heartbeat messages can be easily sent via "netcat":
echo foo | nc -q 1 -u localhost 43334
https://www.oreilly.com/library/view/python-cookbook/0596001673/ch10s13.html https://www.oreilly.com/library/view/python-cookbook/0596001673/ch10s13.html
""" """
HBPORT = 43334 import os
CHECKWAIT = 10 import socket
import sys
from socket import socket, gethostbyname, AF_INET, SOCK_DGRAM
from threading import Lock, Thread, Event from threading import Lock, Thread, Event
from time import time, ctime, sleep from time import time, ctime, sleep
import sys
DEFAULT_HEARTBEAT_PORT = 43334
DEFAULT_WAIT_PERIOD = 10
DEBUG_ENABLED = os.getenv("DEBUG", "0") == "1"
class BeatDict: class BeatDict:
"Manage heartbeat dictionary" "Manage heartbeat dictionary"
def __init__(self): def __init__(self):
self.beatDict = {} self.beatDict = {}
if __debug__: if DEBUG_ENABLED:
self.beatDict['127.0.0.1'] = time( ) self.beatDict["127.0.0.1"] = time()
self.dictLock = Lock( ) self.dictLock = Lock()
def __repr__(self): def __repr__(self):
list = '' result = ""
self.dictLock.acquire( ) self.dictLock.acquire()
for key in self.beatDict.keys( ): for key in self.beatDict.keys():
list = "%sIP address: %s - Last time: %s\n" % ( result += "IP address: %s - Last time: %s\n" % (
list, key, ctime(self.beatDict[key])) key,
self.dictLock.release( ) ctime(self.beatDict[key]),
return list )
self.dictLock.release()
return result
def update(self, entry): def update(self, entry):
"Create or update a dictionary entry" "Create or update a dictionary entry"
self.dictLock.acquire( ) self.dictLock.acquire()
self.beatDict[entry] = time( ) self.beatDict[entry] = time()
self.dictLock.release( ) self.dictLock.release()
def extractSilent(self, howPast): def extractSilent(self, howPast):
"Returns a list of entries older than howPast" "Returns a list of entries older than howPast"
silent = [] silent = []
when = time( ) - howPast when = time() - howPast
self.dictLock.acquire( ) self.dictLock.acquire()
for key in self.beatDict.keys( ): for key in self.beatDict.keys():
if self.beatDict[key] < when: if self.beatDict[key] < when:
silent.append(key) silent.append(key)
self.dictLock.release( ) self.dictLock.release()
return silent return silent
class BeatRec(Thread): class BeatRec(Thread):
"Receive UDP packets, log them in heartbeat dictionary" "Receive UDP packets, log them in heartbeat dictionary"
...@@ -90,52 +108,62 @@ class BeatRec(Thread): ...@@ -90,52 +108,62 @@ class BeatRec(Thread):
self.goOnEvent = goOnEvent self.goOnEvent = goOnEvent
self.updateDictFunc = updateDictFunc self.updateDictFunc = updateDictFunc
self.port = port self.port = port
self.recSocket = socket(AF_INET, SOCK_DGRAM) self.recSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.recSocket.bind(('', port)) self.recSocket.settimeout(0.2)
self.recSocket.bind(("", port))
def __repr__(self): def __repr__(self):
return "Heartbeat Server on port: %d\n" % self.port return f"Heartbeat Server on port: {self.port}"
def run(self): def run(self):
while self.goOnEvent.isSet( ): while self.goOnEvent.isSet():
if __debug__: if DEBUG_ENABLED:
print "Waiting to receive..." print("Waiting to receive...")
data, addr = self.recSocket.recvfrom(6) try:
if __debug__: data, addr = self.recSocket.recvfrom(6)
print "Received packet from " + `addr` except socket.timeout:
self.updateDictFunc(addr[0]) # no incoming message -> no timestamp update -> check again
pass
def main( ): else:
if DEBUG_ENABLED:
print(f"Received packet from {addr}")
self.updateDictFunc(addr[0])
def main():
"Listen to the heartbeats and detect inactive clients" "Listen to the heartbeats and detect inactive clients"
global HBPORT, CHECKWAIT if len(sys.argv) > 1:
if len(sys.argv)>1: heartbeat_port = int(sys.argv[1])
HBPORT=sys.argv[1] else:
if len(sys.argv)>2: heartbeat_port = DEFAULT_HEARTBEAT_PORT
CHECKWAIT=sys.argv[2] if len(sys.argv) > 2:
wait_period = float(sys.argv[2])
beatRecGoOnEvent = Event( ) else:
beatRecGoOnEvent.set( ) wait_period = DEFAULT_WAIT_PERIOD
beatDictObject = BeatDict( )
beatRecThread = BeatRec(beatRecGoOnEvent, beatDictObject.update, HBPORT) beatRecGoOnEvent = Event()
if __debug__: beatRecGoOnEvent.set()
print beatRecThread beatDictObject = BeatDict()
beatRecThread.start( ) beatRecThread = BeatRec(beatRecGoOnEvent, beatDictObject.update, heartbeat_port)
print "PyHeartBeat server listening on port %d" % HBPORT if DEBUG_ENABLED:
print "\n*** Press Ctrl-C to stop ***\n" print(beatRecThread)
while 1: beatRecThread.start()
print(f"PyHeartBeat server listening on port {heartbeat_port}")
print("\n*** Press Ctrl-C to stop ***\n")
while True:
try: try:
if __debug__: if DEBUG_ENABLED:
print "Beat Dictionary" print(f"Beat Dictionary: {beatDictObject}")
print `beatDictObject` silent = beatDictObject.extractSilent(wait_period)
silent = beatDictObject.extractSilent(CHECKWAIT)
if silent: if silent:
print "Silent clients" print(f"Silent clients: {' '.join(silent)}")
print `silent` sleep(wait_period)
sleep(CHECKWAIT)
except KeyboardInterrupt: except KeyboardInterrupt:
print "Exiting." print("Exiting.")
beatRecGoOnEvent.clear( ) beatRecGoOnEvent.clear()
beatRecThread.join( ) beatRecThread.join()
break
if __name__ == '__main__': if __name__ == "__main__":
main( ) main()
\ No newline at end of file
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
- [Setting up the database](#setting-up-the-database) - [Setting up the database](#setting-up-the-database)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Running Engine](#running-engine) - [Running Engine](#running-engine)
- [Starting dependencies](#starting-dependencies)
- [Daemonized Engine](#daemonized-engine) - [Daemonized Engine](#daemonized-engine)
- [Running with Systemd](#running-with-systemd) - [Running with Systemd](#running-with-systemd)
- [Running with Supervisor](#running-with-supervisor) - [Running with Supervisor](#running-with-supervisor)
...@@ -23,8 +24,9 @@ Aura Engine runs on any modern Debian-based OS. It requires at least ...@@ -23,8 +24,9 @@ Aura Engine runs on any modern Debian-based OS. It requires at least
- [Python 3.8+](https://www.python.org/downloads/release/python-380/) - [Python 3.8+](https://www.python.org/downloads/release/python-380/)
- [`pip`](https://pip.pypa.io/en/stable/) - [`pip`](https://pip.pypa.io/en/stable/)
- [`git`](https://git-scm.com/) - [`git`](https://git-scm.com/)
- [`virtualenv`](https://pypi.org/project/virtualenv/) - 'python3.8-venv' or [`virtualenv`](https://pypi.org/project/virtualenv/)
- [PostgreSQL 13+](https://www.postgresql.org/) - `python3-wheel`
- [PostgreSQL 12+](https://www.postgresql.org/)
**Setting up the project structure** **Setting up the project structure**
...@@ -94,9 +96,9 @@ In your development environment edit following file to configure the engine: ...@@ -94,9 +96,9 @@ In your development environment edit following file to configure the engine:
./config/engine.ini ./config/engine.ini
``` ```
> Please note, if some configuration exists under `/etc/aura/engine.ini` the configuration by default is drawn from there. > Please note, if some configuration exists under `/etc/aura/engine.ini` the configuration by default is drawn from there. This overrides any configuration located in `./engine/config`.
While the configuration has plenty of configuration options, you only need to set a few mandatory ones, given you are running the other components (such as 'engine-core', "engine-api" etc.) at the default settings too. While the configuration file has plenty of options, you only need to set a few mandatory ones, given you are running the other components (such as 'engine-core', "engine-api" etc.) at their default settings too.
Required modifications are: Required modifications are:
- The password `db_pass` for the local database holding scheduling information - The password `db_pass` for the local database holding scheduling information
...@@ -110,19 +112,45 @@ If you have defined a virtual env during the installation step you'll need to ac ...@@ -110,19 +112,45 @@ If you have defined a virtual env during the installation step you'll need to ac
source python/bin/activate source python/bin/activate
``` ```
There's a convencience script `run.sh` to get engine started There's a convenience script `run.sh` to get Engine components started.
```shell ```shell
engine$ ./run.sh engine$ ./run.sh
``` ```
Keep in mind you'll also need to start Engine Core separately The script executes the *default target*, which is usually `dev` for development environments.
You can call this target explicitely too:
```shell
engine$ ./run.sh dev
```
Or run Engine in production mode:
```shell
engine$ ./run.sh prod
```
For details on the run script, consult the [AURA CLI documentation](https://gitlab.servus.at/aura/meta/-/blob/master/docs/administration/cli.md).
### Starting dependencies
You'll also need to start Engine Core separately.
> Note it should not matter in which order you start Engine and Engine Core.
```shell ```shell
engine-core$ ./run.sh engine-core$ ./run.sh
``` ```
In order to have a full engine experience also the other AURA Components are required to be running. For convencience in starting the full environment checkout how to run Aura Web using Docker Compose within the [Meta Repository](https://gitlab.servus.at/aura/tank) Last but not least, Engine API is the target service to store playlogs, health information and details for the [Studio Clock](https://gitlab.servus.at/aura/dashboard-clock).
```shell
engine-api$ ./run.sh
```
In order to have the complete Engine experience, other AURA Components are required to be running too. Checkout the [Meta Repository](https://gitlab.servus.at/aura/meta) on how to run for example AURA Web using Docker Compose.
## Daemonized Engine ## Daemonized Engine
...@@ -141,7 +169,7 @@ systemctl daemon-reload ...@@ -141,7 +169,7 @@ systemctl daemon-reload
### Running with Supervisor ### Running with Supervisor
Now, given you are in the engine's home directory `/opt/aura/engine/`, simply type following to start the services: Now, given you are in the engine's home directory like `/opt/aura/engine/`, simply type following to start the services:
```shell ```shell
supervisord supervisord
...@@ -155,8 +183,6 @@ Then you'll need to reload the supervisor configuration using `sudo`: ...@@ -155,8 +183,6 @@ Then you'll need to reload the supervisor configuration using `sudo`:
sudo supervisorctl reload sudo supervisorctl reload
``` ```
## Logging ## Logging
All Engine logs can be found under `./logs`. All Engine logs can be found under `./logs`.
......
...@@ -5,7 +5,7 @@ This page gives insights on extending Aura Engine internals or through the API. ...@@ -5,7 +5,7 @@ This page gives insights on extending Aura Engine internals or through the API.
<!-- TOC --> <!-- TOC -->
1. [Aura Engine Development Guide](#aura-engine-development-guide) 1. [Aura Engine Development Guide](#aura-engine-development-guide)
1. [AURA Componentes](#aura-componentes) 1. [AURA Components](#aura-componentes)
2. [Engine Components](#engine-components) 2. [Engine Components](#engine-components)
3. [Running for Development](#running-for-development) 3. [Running for Development](#running-for-development)
4. [Testing](#testing) 4. [Testing](#testing)
...@@ -16,7 +16,7 @@ This page gives insights on extending Aura Engine internals or through the API. ...@@ -16,7 +16,7 @@ This page gives insights on extending Aura Engine internals or through the API.
<!-- /TOC --> <!-- /TOC -->
## AURA Componentes ## AURA Components
AURA Engine as part of the AURA Radio Suite uses an modulear architecture based on a REST API. All external information is retrieved using JSON data-structures. AURA Engine as part of the AURA Radio Suite uses an modulear architecture based on a REST API. All external information is retrieved using JSON data-structures.
...@@ -28,7 +28,7 @@ For example: ...@@ -28,7 +28,7 @@ For example: