Commit 3bf43f00 authored by Gottfried Gaisbauer's avatar Gottfried Gaisbauer
Browse files

Fixed several issues. Started to create README.md

parent fe69527d
This diff is collapsed.
# AURA Engine
This piece of Software is part of 'AURA - AUtomated RAdio'.
AURA Engine does:
* requesting the programme from an external Source
* switches the soundserver at the correct time to a given source for a specific show
* records what is broadcasted
## Installation
### Software
#### Operating System
Any sound supporting linux system should work. It is tested and coded on a **debian stretch**
#### Packages
On a debian machine:
```bash
sudo apt install python3 python3-pip \
liquidsoap liquidsoap-plugin-alsa liquidsoap-plugin-ao liquidsoap-plugin-faad \
liquidsoap-plugin-flac liquidsoap-plugin-icecast liquidsoap-plugin-lame \
liquidsoap-plugin-mad liquidsoap-plugin-ogg liquidsoap-plugin-pulseaudio \
liquidsoap-plugin-samplerate liquidsoap-plugin-taglib liquidsoap-plugin-voaacenc \
liquidsoap-plugin-vorbis
```
#### Python Packages
```
sudo pip3 install Flask Flask-Babel flask-babel-utclocal-utils \
flask-mongoengine Flask-RESTful Flask-SQLAlchemy Flask-WTF \
mysqlclient redis simplejson
```
#### aura.py
It is the server which is connected to the external programme source, to liquidsoap and is listening for redis pubsub messages.
#### Guru
The commandline tool for interacting with the server.
#### Liquidsoap
The heart of AURA Engine. It uses the built in mixer, to switch between different sources. A source can be a stream, the filesystem or linein
### Hardware
AURA Engine ist tested with an ASUS Xonar DGX. It should work with every by ALSA supported soundcard. PulseAudio support is planned.
......@@ -6,7 +6,7 @@ from libraries.base.config import ConfigReader
#from libraries.reporting.messenger import RedisListener
from modules.controller.controller import AuraController
#from modules.communication.zmq.zmqadapter import ServerZMQAdapter
from modules.communication.redis.redisadapter import ServerRedisAdapter
from modules.communication.redis.adapter import ServerRedisAdapter
class Aura():
......
......@@ -24,9 +24,9 @@ db_host="localhost"
# if you have no idea what to do here => set use_alsa to "n", then pulseaudio is used
use_alsa="y"
# alsa_buffer => int
alsa_buffer="8192"
alsa_buffer="16000"
# alsa_buffer_length => int
alsa_buffer_length="15"
alsa_buffer_length="5"
# alsa_periods => int
alsa_periods="0"
# frame_duration => double
......@@ -100,3 +100,5 @@ webservice_mode="apache"
#serviceport=""
install_dir="/home/gg/PycharmProjects/aura"
debug="y"
#!/usr/bin/python3
# python libs
import time
import sys
......@@ -9,6 +7,8 @@ from argparse import ArgumentParser
# own libs
from libraries.base.config import ConfigReader
from modules.tools.padavan import Padavan
from libraries.exceptions.auraexceptions import FallbackException
class Guru:
config = ConfigReader()
......@@ -21,8 +21,6 @@ class Guru:
# commands
parser.add_argument("-fnp", "--fetch-new-programmes", action="store_true", dest="fetch_new_programme",
default=False, help="Fetch new programmes from calendarurl in comba.ini")
parser.add_argument("-ip", "--init-player", action="store_true", dest="init_player", default=False,
help="Checks what is the active source and stops everything else")
# options
parser.add_argument("-sep", "--stop-execution-time", action="store_true", dest="stoptime", default=False,
......@@ -31,9 +29,9 @@ class Guru:
# getter
parser.add_argument("-gam", "--get-active-mixer", action="store_true", dest="get_active_mixer", default=False,
help="Which mixer is activated?")
parser.add_argument("-pms", "--print-mixer-status", action="store_true", dest="print_mixer_status", default=False,
parser.add_argument("-pms", "--print-mixer-status", action="store_true", dest="get_mixer_status", default=False,
help="Prints all mixer sources and their states")
parser.add_argument("-pap", "--print-act-programme", action="store_true", dest="print_act_prog", default=False,
parser.add_argument("-pap", "--print-act-programme", action="store_true", dest="get_act_programme", default=False,
help="Prints the actual Programme, the controller holds")
# manipulation
......@@ -54,10 +52,10 @@ class Guru:
help="Send a redis message to the Listeners")
# calls from liquidsoap
parser.add_argument("-gnft", "--get-next-for-fallback-file", action="store", dest="type", default=False,
help="For which type you wanna have a next audio file?")
parser.add_argument("-snft", "--select-next-for-fallback-file", action="store", dest="type", default=False,
help="For which type you wanna select a next audio file?")
parser.add_argument("-gnf", "--get-next-fallback-file-for", action="store", dest="get_fallback_for", default=False, metavar=("FALLBACKTYPE"),
help="For which type you wanna GET a next audio file?")
parser.add_argument("-snf", "--set-next-fallback-file-for", action="store", dest="set_fallback_for", default=False, metavar=("FALLBACKTYPE", "FILE"), nargs=2,
help="For which type you wanna SET a next audio file?")
parser.add_argument("-ls", "--liquid-startup", action="store_true", dest="liquid_startup", default=False,
help="Reset liquidsoap volume and mixer activations?")
......@@ -67,17 +65,24 @@ class Guru:
raise ValueError("No Argument passed!")
except ValueError:
parser.print_help()
exit(1)
except TypeError:
parser.print_help()
exit(2)
except:
exit(3)
if args.stoptime:
start = time.time()
p = Padavan(args, self.config)
p.meditate()
print("Guru thinking...")
try:
p = Padavan(args, self.config)
p.meditate()
except FallbackException as fe:
print(fe)
exit(4)
print("...result: \n" + p.stringreply)
if args.stoptime:
end = time.time()
......
......@@ -27,7 +27,7 @@ import queue
from datetime import datetime, timedelta
from libraries.database.broadcasts import Schedule, ScheduleEntry
from libraries.reporting.messenger import RedisMessenger
from modules.communication.redis.messenger import RedisMessenger
class AuraCalendarService(threading.Thread):
......
......@@ -72,6 +72,10 @@ class ConfigReader(object):
else:
print("WARNING: Key "+key+" not found!")
return None
if key == "debug":
return self.__dict__[key].count("y")
return self.__dict__[key]
# ------------------------------------------------------------------------------------------ #
......
# -*- coding: utf-8 -*-
import sys
import simplejson
import urllib
"""
Die AuraClient Klasse stellt die Tasks zur Verfügung,
die dem Playlout Controller übertragen werden können
Dies ist im Wesentlichen ein Wrapper
"""
class AuraClient():
def __init__(self, sender):
"""
Constructor
@type sender: object
@param sender: Der Communicator Adapter - z-B. zmq
"""
self.sender = sender
# ------------------------------------------------------------------------------------------ #
def command(self, command):
"""
Kommando an den Controller absetzen
und Antwort entgegennehmen
@type command: string
@param command: Kommando
@rtype: string
@return: Antwort des Controllers
"""
self.sender.send(command)
message = self.sender.receive()
return message
# ------------------------------------------------------------------------------------------ #
def channel_skip(self, channel):
"""
Skipt einen Kanal oder die Playlist
@type channel: string
@param channel: Kanal
@rtype: string
@return: Antwort des Controllers
"""
return self.command('channel_skip ' + channel)
# ------------------------------------------------------------------------------------------ #
def channel_is_active(self, channel='playlist'):
"""
Ist der Kanal aktiv?
@type channel: string
@param channel: Kanal
@rtype: boolean
@return: True/False
"""
state = self._get_channel_state(channel)
is_active = True if state['selected'] == 'true' else False
return is_active
# ------------------------------------------------------------------------------------------ #
def channel_on(self, channel):
"""
Kanal einschalten
@type channel: string
@param channel: Kanal
@rtype: string
@return: Antwort des Controllers
"""
return self.command('channel_on ' + channel)
# ------------------------------------------------------------------------------------------ #
def channel_off(self, channel):
"""
Kanal ausschalten
@type channel: string
@param channel: Kanal
@rtype: string
@return: Antwort des Controllers
"""
return self.command('channel_off ' + channel)
# ------------------------------------------------------------------------------------------ #
def get_channellist(self):
"""
Channels als Liste ausgeben
@rtype: list
@return: Antwort des Controllers
"""
return simplejson.loads(self.command('listChannels'))
# ------------------------------------------------------------------------------------------ #
def get_channelqueue(self, channel):
"""
Channel Queue ausgeben
@type channel: string
@param channel: Kanal
@rtype: dict
@return: Antwort des Controllers
"""
return simplejson.loads(self.command('channel_queue ' + channel))
# ------------------------------------------------------------------------------------------ #
def get_channel_volume(self, channel='playlist'):
"""
Lautstärke des Kanals ausgeben
@type channel: string
@param channel: Kanal
@rtype: string/boolean
@return: Volumen von 1-100/False
"""
state = self._get_channel_state(channel)
channels = simplejson.loads(self.command('allData'))
try:
volume = state['volume']
except KeyError:
return False
else:
return volume
return False
# ------------------------------------------------------------------------------------------ #
def channel_remove_track(self, channel, track_pos):
"""
Löscht einen Track aus dem secondary_queue
@type channel: string
@param channel: Kanal
@type track_pos: string/int
@param track_pos: Position des zu entfernenden Eintrags
@rtype: string
@return: Antwort des Controllers
"""
return self.command('channel_remove ' + channel + ' ' + str(track_pos))
# ------------------------------------------------------------------------------------------ #
def channel_seek(self, channel, duration):
"""
Spult den laufenen Track des Kanals <duration> Sekunden weiter (falls möglich)
Beispiel: channel_seek('ch1',60) - 60 Sekunden nach vorne
@type channel: string
@param channel: Kanal
@type duration: string/int
@param duration: Dauer in Sekunden
@rtype: string
@return: Antwort des Controllers
"""
return self.command('channel_seek ' + channel + ' ' + str(duration))
# ------------------------------------------------------------------------------------------ #
def channel_track_up(self, channel, track_pos):
"""
Einen Track um eine Position nach oben schieben
@type channel: string
@param channel: Kanal
@type track_pos: string
@param track_pos: Position des zu verschiebenden Eintrags
@rtype: string
@return: Antwort des Controllers
"""
return self.command('channel_move ' + channel + ' ' + str(track_pos) + ' ' + str(track_pos - 1))
# ------------------------------------------------------------------------------------------ #
def channel_track_down(self, channel, track_pos):
"""
Einen Track um eine Position nach unten schieben
@type channel: string
@param channel: Kanal
@type track_pos: string
@param track_pos: Position des zu verschiebenden Eintrags
@rtype: string
@return: Antwort des Controllers
"""
return self.command('channel_move ' + channel + ' ' + str(track_pos) + ' ' + str(track_pos + 1))
# ------------------------------------------------------------------------------------------ #
def channel_track_insert(self, channel, uri, pos=0):
"""
Uri eines Audios in den (secondary) queue einfügen
@type channel: string
@param channel: Kanal
@type uri: string
@param uri: uri, z.b. file:///my/audio/song.mp3
@rtype: string
@return: Antwort des Controllers
"""
return self.command('channel_insert ' + channel + ' ' + urllib.quote(uri) + ' ' + str(pos))
# ------------------------------------------------------------------------------------------ #
def channel_set_volume(self, channel, volume):
"""
Lautstärke setzen (Prozentual von 1 - 100
@type channel: string
@param channel: Kanal
@type volume: string/int
@param volume: Zahl von 1 bis 100
@rtype: string
@return: Antwort des Controllers
"""
return self.command('channel_volume ' + channel + ' ' + str(volume))
# ------------------------------------------------------------------------------------------ #
def _get_channel_state(self, channel):
"""
Private: Status eines Kanals abfragen
Ausgabe Beispiel: {'ready':'true', 'selected:'false', 'single':'false','volume':'100%','remaining:'0.00'}
@type channel: string
@param channel: Kanal
@rtype: dict/boolen
@return: Antwort des Controllers/ False
"""
data = simplejson.loads(self.command('allData'))
if not isinstance(data, dict):
# es wurde kein assoz. Array/dict zurückgegeben
return False
if data['success'] != 'success':
# die Abfrage war nicht erfolgreich
return False
if not data.has_key('value'):
# es wurden keine Werte geliefert
return False
channels = data['value']
if not isinstance(channels, dict):
# es wurde kein assoz. Array/dict zurückgegeben
return False
if channels.has_key(channel):
chan = channels[channel]
try:
state = chan['state']
except KeyError:
return False
else:
return state
else:
return False
# ------------------------------------------------------------------------------------------ #
def playlist_load(self, uri):
"""
Playlist im XSPF-Format laden
@type uri: string
@param uri: Uri einer Playlist - z.B. /my/playlists/2014-12-22-12-00.xspf
@rtype: string
@return: Antwort des Controllers
"""
return self.command('playlist_load ' + str(uri))
# ------------------------------------------------------------------------------------------ #
def playlist_data(self):
"""
Gibt die aktuelle Playlist als dict zurück
@rtype: dict
@return: Antwort des Controllers
"""
return simplejson.loads(self.command('playlist_data'))
# ------------------------------------------------------------------------------------------ #
def playlist_flush(self):
"""
Leert die Playlist
@rtype: dict
@return: Antwort des Controllers
"""
return self.command('playlist_flush')
# ------------------------------------------------------------------------------------------ #
def playlist_insert(self, uri, pos):
"""
Audio in Playlist einfügen
@type uri: string
@param uri: Uri einer Audiodatei - z.B. /my/audio/song.mp3
@rtype: string
@return: Antwort des Controllers
"""
return self.command('playlist_insert ' + uri + ' ' + pos)
# ------------------------------------------------------------------------------------------ #
def playlist_track_up(self, track_pos):
"""
Einen Track der Playlist um eine Position nach oben schieben
@type track_pos: string
@param track_pos: Position des zu verschiebenden Eintrags
@rtype: string
@return: Antwort des Controllers
"""
return self.command('playlist_move ' + str(track_pos) + ' ' + str(track_pos - 1))
# ------------------------------------------------------------------------------------------ #
def playlist_track_down(self, track_pos):
"""
Einen Track der Playlist um eine Position nach unten schieben
@type track_pos: string
@param track_pos: Position des zu verschiebenden Eintrags
@rtype: string
@return: Antwort des Controllers
"""
self.command('playlist_move ' + str(track_pos) + ' ' + str(track_pos + 1))
# ------------------------------------------------------------------------------------------ #
def playlist_pause(self):
"""
Playlist anhalten
@rtype: string
@return: Antwort des Controllers
"""
return self.command('playlist_pause')
# ------------------------------------------------------------------------------------------ #
def playlist_stop(self):
"""
Playlist stoppen
@rtype: string
@return: Antwort des Controllers
"""
return self.command('playlist_stop')
# ------------------------------------------------------------------------------------------ #
def playlist_play(self):
"""
Playlist starten/abspielen
@rtype: string
@return: Antwort des Controllers
"""
return self.command('playlist_play')
# ------------------------------------------------------------------------------------------ #
def playlist_remove_track(self, track_pos):
"""
Löscht einen Track der Playlist
Hinweis: Der laufende Track wird nicht berücksichtigt
@type track_pos: string/int
@param track_pos: Position des zu entfernenden Eintrags
@rtype: string
@return: Antwort des Controllers
"""
return self.command('playlist_remove ' + str(track_pos))
# ------------------------------------------------------------------------------------------ #
def playlist_seek(self, duration):
"""
Spult den laufenden Track der Playlist <duration> Sekunden weiter (falls möglich)
Beispiel: playlist_seek('ch1',60) - 60 Sekunden nach vorne
@type duration: string/int
@param duration: Dauer in Sekunden
@rtype: string
@return: Antwort des Controllers
"""
return self.command('playlist_seek ' + str(duration))
# ------------------------------------------------------------------------------------------ #
def playlist_skip(self):
"""
Skipt den laufenden Track der Playlist
@rtype: string
@return: Antwort des Controllers
"""
return self.command('playlist_skip')
# ------------------------------------------------------------------------------------------ #
def recorder_start(self):
"""
Recorder starten
@rtype: string
@return: Antwort des Controllers
"""
return self.command('recorder_start')
# ------------------------------------------------------------------------------------------ #
def recorder_stop(self):
"""
Recorder stoppen
@rtype: string
@return: Antwort des Controllers
"""
return self.command('recorder_stop')
def recorder_data(self):
"""
Daten der aktuellen Aufnahme abrufen
Beispiel: {'file':'/my/audio/2014-12-22-12-00.wav', 'recorded': '25'} - Die Aufnahme der Datei /my/audio/2014-12-22-12-00.wav ist bei 25%
@rtype: dict
@return: Antwort des Controllers
"""
return simplejson.loads(self.command('recorder_data'))
\ No newline at end of file
class TerminalColors:
HEADER = '\033[95m'
OK_GREEN = '\033[32m'
OK_ORANGE = '\033[33m'
OK_BLUE = '\033[34m'
OK_PINK = '\033[35m'
WARNING = '\033[31m'
FAIL = '\033[41m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
ENDC = '\033[0m'
\ No newline at end of file
......@@ -24,12 +24,12 @@ class Model:
# return None
# return [value.strftime("%Y-%m-%d"), value.strftime("%H:%M:%S")]
def alchemy_encoder(obj):
"""JSON encoder function for SQLAlchemy special classes."""
if isinstance(obj, datetime.date):
return obj.isoformat()
elif isinstance(obj, decimal.Decimal):
return float(obj)
# def alchemy_encoder(obj):
# """JSON encoder function for SQLAlchemy special classes."""
# if isinstance(obj, datetime.date):
# return obj.isoformat()
# elif isinstance(obj, decimal.Decimal):
# return float(obj)
def _asdict(self):
return self.__dict__
......
......@@ -303,7 +303,10 @@ class RedisStateStore(object):
subscriber_count = self.db.execute_command('PUBSUB', 'NUMSUB', channel)
if subscriber_count[1] == 0:
raise Exception("No subscriber! Is Aura daemon running?")