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

Target

Select target project
  • aura/engine
  • hermannschwaerzler/engine
  • sumpfralle/aura-engine
3 results
Show changes
Showing
with 3276 additions and 0 deletions
#
# 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/>.
#
class NoProgrammeLoadedException(Exception):
pass
class LQConnectionError(Exception):
pass
class RedisConnectionException(Exception):
pass
class PlaylistException(Exception):
pass
class MailingException(Exception):
pass
class DiskSpaceException(Exception):
pass
#
# 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
import logging
class ExceptionLogger:
logger = None
error_data = None
job_result = None
def __init__(self):
self.logger = logging.getLogger("AuraEngine")
# ------------------------------------------------------------------------------------------ #
def __get_error__(self, job, errornumber, data):
"""
Privat: Ermittelt Fehlermeldung, Job-Name (Klassenmethode) und Fehlercode für den Job aus error/controller_error.js
@type errornumber: string
@param errornumber: Die interne Fehlernummer der aufrufenden Methode
"""
if data is None:
data = {}
if type(data) == type(str()):
data = json.loads(data)
has_data = isinstance(data, (dict)) and len(data) > 0
if job in self.error_data:
err_msg = self.error_data[job][errornumber]
err_id = self.error_data[job]['id'] + str(errornumber)
if has_data:
for key in data.keys():
err_msg = err_msg.replace('::' + key + '::', str(data[key]))
data['message'] = err_msg
data['job'] = job
data['code'] = err_id
return data
# ------------------------------------------------------------------------------------------ #
def success(self, job, data=None, errnum='00', value=''):
"""
Erfolgsmeldung loggen
@type errnum: string
@param errnum: Errornummer der aufrufenden Funktion
@type value: string
@param value: Optionaler Wert
@type section: string
@param section: Gültigkeitsbereich
"""
error = self.__get_error__(job, errnum, data)
self.job_result = {'message': error['message'],
'code': error['code'],
'success': 'success',
'job': error['job'],
'value': value}
self.logger.debug(job + " successfully done " + str(self.job_result))
# ------------------------------------------------------------------------------------------ #
def info(self, job, data=None, errnum='01', value=''):
"""
Info loggen
@type errnum: string
@param errnum: Errornummer der aufrufenden Funktion
@type value: string
@param value: Optionaler Wert
@type section: string
@param section: Gültigkeitsbereich
"""
error = self.__get_error__(job, errnum, data)
self.job_result = {'message': error['message'],
'code': error['code'],
'success': 'info',
'job': error['job'],
'value': value}
self.logger.info(str(self.job_result))
# ------------------------------------------------------------------------------------------ #
def warning(self, job, data=None, errnum='01', value=''):
"""
Warnung loggen
@type errnum: string
@param errnum: Errornummer der aufrufenden Funktion
@type value: string
@param value: Optionaler Wert
"""
error = self.__get_error__(job, errnum, data)
self.job_result = {'message': error['message'],
'code': error['code'],
'success': 'warning',
'job': error['job'],
'value': value}
self.logger.warning(str(self.job_result))
# ------------------------------------------------------------------------------------------ #
def error(self, job, data=None, errnum='01', value=''):
"""
Error loggen
@type errnum: string
@param errnum: Errornummer der aufrufenden Funktion
@type value: string
@param value: Optionaler Wert
"""
error = self.__get_error__(job, errnum, data)
self.job_result = {'message': error['message'],
'code': error['code'],
'success': 'error',
'job': error['job'],
'value': value}
self.logger.error(str(self.job_result))
# ------------------------------------------------------------------------------------------ #
def fatal(self, job, data=None, errnum='01', value='', section='execjob'):
"""
Fatal error loggen
@type errnum: string
@param errnum: Errornummer der aufrufenden Funktion
@type value: string
@param value: Optionaler Wert
@type section: string
@param section: Gültigkeitsbereich
"""
error = self.__get_error__(job, errnum, data)
self.job_result = {'message': error['message'],
'code': error['code'],
'success': 'fatal',
'job': error['job'],
'value': value,
'section': section}
self.logger.critical(str(self.job_result))
\ 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 libraries.enum.auraenumerations import RedisChannel, TerminalColors
from modules.communication.redis.adapter import ClientRedisAdapter, ServerRedisAdapter
from modules.communication.redis.messenger import RedisMessenger
from libraries.database.broadcasts import AuraDatabaseModel
class Padavan:
args = None
config = None
lsc = 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_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.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.communication.liquidsoap.communicator import LiquidSoapCommunicator
# init liquidsoap communication
self.lsc = LiquidSoapCommunicator(self.config)
# enable connection
self.lsc.enable_transaction()
# ------------------------------------------------------------------------------------------ #
def destroy_liquidsoap_communication(self):
# enable connection
self.lsc.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_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):
self.stringreply = self.send_and_wait_redis("aura", "init_player", RedisChannel.IP_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.lsc.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.lsc.set_volume(mixernumber, volume)
# disable connection
self.destroy_liquidsoap_communication()
# ------------------------------------------------------------------------------------------ #
def get_active_mixer(self):
self.init_liquidsoap_communication()
am = self.lsc.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.lsc.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 urllib
import logging
import json
from modules.communication.liquidsoap.communicator import LiquidSoapCommunicator
#from libraries.database.broadcasts import ScheduleEntry
from libraries.base.config import AuraConfig
# ------------------------------------------------------------------------------------------ #
class ConnectionTester(AuraConfig):
# ------------------------------------------------------------------------------------------ #
def __init__(self):
super(ConnectionTester, self).__init__()
# ------------------------------------------------------------------------------------------ #
def get_connection_status(self):
status = dict()
status["db"] = False # self.test_db_conn()
status["pv"] = self.test_pv_conn()
status["lqs"] = self.test_lqs_conn()
status["lqsr"] = False # self.test_lqsr_conn()
status["tank"] = self.test_tank_conn()
status["redis"] = self.test_redis_conn()
return json.dumps(status)
# ------------------------------------------------------------------------------------------ #
# def test_db_conn(self):
# try:
# ScheduleEntry.select_all()
# except:
# return False
#
# return True
# ------------------------------------------------------------------------------------------ #
def test_lqs_conn(self):
try:
lsc = LiquidSoapCommunicator(self.config)
lsc.get_mixer_status()
return True
except Exception as e:
return False
# ------------------------------------------------------------------------------------------ #
def test_lqsr_conn(self):
try:
lsc = LiquidSoapCommunicator(self.config)
lsc.get_recorder_status()
return True
except Exception as e:
return False
# ------------------------------------------------------------------------------------------ #
def test_pv_conn(self):
return self.test_url_connection(self.config.get("calendarurl"))
# ------------------------------------------------------------------------------------------ #
def test_tank_conn(self):
# test load of playlist 1
return self.test_url_connection(self.config.get("importerurl")+"1")
# ------------------------------------------------------------------------------------------ #
def test_redis_conn(self):
from modules.communication.redis.adapter import ClientRedisAdapter
try:
cra = ClientRedisAdapter()
cra.publish("aura", "status")
except:
return False
return True
def test_url_connection(self, url):
try:
request = urllib.request.Request(url)
response = urllib.request.urlopen(request)
response.read()
except Exception as e:
return False
return True
\ 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 socket
import urllib.parse
import configparser
import logging
from multiprocessing import Lock
from libraries.exceptions.auraexceptions import LQConnectionError
"""
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(msg)
self.can_connect = False
self.connected = False
# raise e
else:
self.can_connect = True
self.connected = True
return True
# ------------------------------------------------------------------------------------------ #
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(str(e))
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("Detected a problem with liquidsoap connection while sending: " + message + ". Reason: " + str(e) + "! Trying to reconnect.")
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/>.
#
import time
import logging
import json
from modules.communication.liquidsoap.playerclient import LiquidSoapPlayerClient
# from modules.communication.liquidsoap.recorderclient import LiquidSoapRecorderClient
from modules.communication.liquidsoap.initthread import LiquidSoapInitThread
from modules.communication.mail.mail import AuraMailer
from libraries.enum.auraenumerations import TerminalColors, ScheduleEntryType
from libraries.exceptions.auraexceptions import LQConnectionError
#from libraries.database.broadcasts import TrackService
from libraries.exceptions.exception_logger import ExceptionLogger
"""
LiquidSoapCommunicator Class
Uses LiquidSoapClient, but introduces more complex commands, transactions and error handling
"""
class LiquidSoapCommunicator(ExceptionLogger):
client = None
logger = None
transaction = 0
channels = None
scheduler = None
error_data = None
auramailer = None
is_liquidsoap_running = False
connection_attempts = 0
active_channel = None
disable_logging = False
fade_in_active = False
fade_out_active = False
# ------------------------------------------------------------------------------------------ #
def __init__(self, config):
"""
Constructor
"""
self.config = config
self.logger = logging.getLogger("AuraEngine")
self.client = LiquidSoapPlayerClient(config, "engine.sock")
# self.lqcr = LiquidSoapRecorderClient(config, "record.sock")
errors_file = self.config.get("install_dir") + "/errormessages/controller_error.js"
f = open(errors_file)
self.error_data = json.load(f)
f.close()
self.auramailer = AuraMailer(self.config)
self.is_liquidsoap_up_and_running()
# ------------------------------------------------------------------------------------------ #
def is_liquidsoap_up_and_running(self):
try:
self.uptime()
self.is_liquidsoap_running = True
except LQConnectionError as e:
self.logger.info("Liquidsoap is not running so far")
self.is_liquidsoap_running = False
except Exception as e:
self.logger.error("Cannot check if Liquidsoap is running. Reason: " + str(e))
self.is_liquidsoap_running = False
# ------------------------------------------------------------------------------------------ #
def set_volume(self, mixernumber, volume):
return self.__send_lqc_command__(self.client, "mixer", "volume", mixernumber, volume)
# ------------------------------------------------------------------------------------------ #
def get_active_mixer(self):
"""
get active mixer in liquidsoap server
:return:
"""
activeinputs = []
# enable more control over the connection
self.enable_transaction()
inputs = self.get_all_channels()
cnt = 0
for input in inputs:
status = self.__get_mixer_status__(cnt)
if "selected=true" in status:
activeinputs.append(input)
cnt = cnt + 1
self.disable_transaction()
return activeinputs
# ------------------------------------------------------------------------------------------ #
def get_active_channel(self):
"""
gets active channel from programme
:return:
"""
(show, active_entry) = self.scheduler.get_active_entry()
if active_entry is None:
return ""
return active_entry.type
# ------------------------------------------------------------------------------------------ #
def get_mixer_status(self):
inputstate = {}
self.enable_transaction()
inputs = self.get_all_channels()
cnt = 0
for input in inputs:
inputstate[input] = self.__get_mixer_status__(cnt)
cnt = cnt + 1
self.disable_transaction()
return inputstate
# ------------------------------------------------------------------------------------------ #
def get_mixer_volume(self, channel):
return False
# ------------------------------------------------------------------------------------------ #
def get_recorder_status(self):
self.enable_transaction(self.client)
recorder_state = self.__send_lqc_command__(self.client, "record", "status")
self.disable_transaction(self.client)
return recorder_state
# ------------------------------------------------------------------------------------------ #
def http_start_stop(self, start):
if start:
cmd = "start"
else:
cmd = "stop"
try:
self.enable_transaction()
self.__send_lqc_command__(self.client, "http", cmd)
self.disable_transaction()
except LQConnectionError:
# we already caught and handled this error in __send_lqc_command__, but we do not want to execute this function further
pass
# ------------------------------------------------------------------------------------------ #
def recorder_stop(self):
self.enable_transaction()
for i in range(5):
if self.config.get("rec_" + str(i)) == "y":
self.__send_lqc_command__(self.client, "recorder_" + str(i), "stop")
self.disable_transaction()
# ------------------------------------------------------------------------------------------ #
def recorder_start(self, num=-1):
if not self.is_liquidsoap_running:
if num==-1:
msg = "Want to start recorder, but LiquidSoap is not running"
else:
msg = "Want to start recorder " + str(num) + ", but LiquidSoap is not running"
self.logger.warning(msg)
return False
self.enable_transaction()
if num == -1:
self.recorder_start_all()
else:
self.recorder_start_one(num)
self.disable_transaction()
# ------------------------------------------------------------------------------------------ #
def recorder_start_all(self):
if not self.is_liquidsoap_running:
self.logger.warning("Want to start all recorder, but LiquidSoap is not running")
return False
self.enable_transaction()
for i in range(5):
self.recorder_start_one(i)
self.disable_transaction()
# ------------------------------------------------------------------------------------------ #
def recorder_start_one(self, num):
if not self.is_liquidsoap_running:
return False
if self.config.get("rec_" + str(num)) == "y":
returnvalue = self.__send_lqc_command__(self.client, "recorder", str(num), "status")
if returnvalue == "off":
self.__send_lqc_command__(self.client, "recorder", str(num), "start")
# ------------------------------------------------------------------------------------------ #
def fade_in(self, new_entry):
try:
fade_in_time = float(self.config.get("fade_in_time"))
if fade_in_time > 0:
self.fade_in_active = True
target_volume = new_entry.volume
step = fade_in_time / target_volume
self.logger.info("Starting to fading " + new_entry.type.value + " in. step is " + str(step) + "s. target volume is " + str(target_volume))
self.disable_logging = True
self.client.disable_logging = True
for i in range(target_volume):
self.channel_volume(new_entry.type.value, i + 1)
time.sleep(step)
self.logger.info("Finished with fading " + new_entry.type.value + " in.")
self.fade_in_active = False
if not self.fade_out_active:
self.disable_logging = False
self.client.disable_logging = False
except LQConnectionError as e:
self.logger.critical(str(e))
return True
# ------------------------------------------------------------------------------------------ #
def fade_out(self, old_entry):
try:
fade_out_time = float(self.config.get("fade_out_time"))
if fade_out_time > 0:
step = abs(fade_out_time) / old_entry.volume
self.logger.info("Starting to fading " + old_entry.type.value + " out. step is " + str(step) + "s")
# disable logging... it is going to be enabled again after fadein and -out is finished
self.disable_logging = True
self.client.disable_logging = True
for i in range(old_entry.volume):
self.channel_volume(old_entry.type.value, old_entry.volume-i-1)
time.sleep(step)
self.logger.info("Finished with fading " + old_entry.type.value + " out.")
# enable logging again
self.fade_out_active = False
if not self.fade_in_active:
self.disable_logging = False
self.client.disable_logging = False
except LQConnectionError as e:
self.logger.critical(str(e))
return True
# ------------------------------------------------------------------------------------------ #
def activate(self, new_entry):
# grab the actual active entry
(show, old_entry) = self.scheduler.get_active_entry()
# determine its type
# TODO Move to <get_active_entry>
# FIXME No need to switch if current type = new type
old_type = ScheduleEntryType.FILESYSTEM # Set default if no previous track is available
if old_entry:
old_type = old_entry.type
try:
# enable transaction
self.enable_transaction()
if old_type == new_entry.type:
# push something to active channel
self.activate_same_channel(new_entry)
else:
# switch to another channel
self.activate_different_channel(new_entry, old_type)
# disable conn
self.disable_transaction()
# insert playlist entry
self.logger.critical("Trackservice entry not written here anymore")
# self.insert_track_service_entry(new_entry)
except LQConnectionError:
# we already caught and handled this error in __send_lqc_command__, but we do not want to execute this function further and pass the exception
pass
# ------------------------------------------------------------------------------------------ #
def activate_same_channel(self, entry, activate_different_channel=False):
if not activate_different_channel:
self.logger.info(TerminalColors.PINK.value + entry.type.value + " already active!" + TerminalColors.ENDC.value)
# push to fs or stream
if entry.type == ScheduleEntryType.FILESYSTEM:
self.playlist_push(entry.filename)
self.active_channel = entry.type
elif entry.type == ScheduleEntryType.STREAM:
self.set_http_url(entry.source)
self.http_start_stop(True)
self.active_channel = entry.type
# else: # live
# nothing to do when we are live => just leave it as is
self.active_channel = entry.type
# set active channel to wanted volume
if not activate_different_channel:
self.channel_volume(entry.type.value, entry.volume)
# ------------------------------------------------------------------------------------------ #
def activate_different_channel(self, entry, active_type):
self.logger.info(TerminalColors.PINK.value + "LiquidSoapCommunicator is activating " + entry.type.value + " & deactivating " + active_type.value + "!" + TerminalColors.ENDC.value)
# reuse of this function, because activate_same_channel and activate_different_channel are doing pretty the same except setting of the volume to zero
self.activate_same_channel(entry, True)
# set other channels to zero volume
others = self.all_inputs_but(entry.getChannel())
for o in others:
self.channel_volume(o, 0)
# set active channel to wanted volume
self.channel_volume(entry.type.value, entry.volume)
# ------------------------------------------------------------------------------------------ #
def insert_track_service_entry(self, schedule_entry):
# create trackservice entry
trackservice_entry = TrackService()
# set foreign keys
trackservice_entry.playlist_id = schedule_entry.playlist_id
trackservice_entry.entry_num = schedule_entry.entry_num
trackservice_entry.source = schedule_entry.source
# store
trackservice_entry.store(add=True, commit=True)
# ------------------------------------------------------------------------------------------ #
def all_inputs_but(self, input_type):
try:
activemixer_copy = self.get_all_channels().copy()
activemixer_copy.remove(input_type)
except ValueError as e:
self.logger.error("Requested channel (" + input_type + ") not in channellist. Reason: " + str(e))
except AttributeError:
self.logger.critical("Channellist is None")
return activemixer_copy
# ------------------------------------------------------------------------------------------ #
def get_all_channels(self):
if self.channels is None or len(self.channels) == 0:
self.channels = self.__send_lqc_command__(self.client, "mixer", "inputs")
return self.channels
# ------------------------------------------------------------------------------------------ #
def reload_channels(self):
self.channels = None
return self.get_all_channels()
# ------------------------------------------------------------------------------------------ #
def __get_mixer_status__(self, mixernumber):
return self.__send_lqc_command__(self.client, "mixer", "status", mixernumber)
# ------------------------------------------------------------------------------------------ #
def init_player(self):
(_, active_entry) = self.scheduler.get_active_entry()
t = LiquidSoapInitThread(self, active_entry)
t.start()
return "LiquidSoapInitThread started!"
# ------------------------------------------------------------------------------------------ #
def channel_activate(self, channel, activate):
channels = self.get_all_channels()
try:
index = channels.index(channel)
if len(channel) < 1:
self.logger.critical("Cannot activate channel. There are no channels!")
else:
message = self.__send_lqc_command__(self.client, "mixer", "select", index, activate)
return message
except Exception as e:
self.logger.critical("Ran into exception when activating channel. Reason: " + str(e))
# ------------------------------------------------------------------------------------------ #
def channel_volume(self, channel, volume):
"""
set volume of a channel
@type channel: string
@param channel: Channel
@type volume: int
@param volume: Volume between 0 and 100
"""
try:
channels = self.get_all_channels()
index = channels.index(channel)
except ValueError as e:
self.logger.error("Cannot set volume of channel " + channel + " to " + str(volume) + "!. Reason: " + str(e))
return
try:
if len(channel) < 1:
self.logger.warning("Cannot set volume of channel " + channel + " to " + str(volume) + "! There are no channels.")
else:
message = self.__send_lqc_command__(self.client, "mixer", "volume", str(index), str(int(volume)))
if not self.disable_logging:
if message.find('volume=' + str(volume) + '%'):
self.logger.debug("Set volume of channel " + channel + " to " + str(volume))
else:
self.logger.warning("Setting volume of channel " + channel + " gone wrong! Liquidsoap message: " + message)
return message
except AttributeError as e: #(LQConnectionError, AttributeError):
self.disable_transaction(force=True)
self.logger.error("Ran into exception when setting volume of channel " + channel + ". Reason: " + str(e))
# ------------------------------------------------------------------------------------------ #
def auraengine_state(self):
state = self.__send_lqc_command__(self.client, "auraengine", "state")
return state
# ------------------------------------------------------------------------------------------ #
def liquidsoap_help(self):
data = self.__send_lqc_command__(self.client, 'help')
if not data:
self.logger.warning("Could not get Liquidsoap's help")
else:
self.logger.debug("Got Liquidsoap's help")
return data
# ------------------------------------------------------------------------------------------ #
def set_http_url(self, uri):
return self.__send_lqc_command__(self.client, "http", "url", uri)
# ------------------------------------------------------------------------------------------ #
def playlist_push(self, uri):
"""
Eine Uri in die Playlist einfügen
@type uri: str
@param uri: Die Uri
"""
return self.__send_lqc_command__(self.client, "fs", "push", uri)
# ------------------------------------------------------------------------------------------ #
def playlist_seek(self, seconds_to_seek):
return self.__send_lqc_command__(self.client, "fs", "seek", seconds_to_seek)
# ------------------------------------------------------------------------------------------ #
def version(self):
"""
get version
"""
data = self.__send_lqc_command__(self.client, "version", "")
self.logger.debug("Got Liquidsoap's version")
return data
# ------------------------------------------------------------------------------------------ #
def uptime(self):
"""
get uptime
"""
data = self.__send_lqc_command__(self.client, "uptime", "")
self.logger.debug("Got Liquidsoap's uptime")
return data
# ------------------------------------------------------------------------------------------ #
def __send_lqc_command__(self, lqs_instance, namespace, command, *args):
"""
Ein Kommando an Liquidsoap senden
@type lqs_instance: object
@param lqs_instance: Instance of LiquidSoap Client
@type namespace: string
@param namespace: Namespace of function
@type command: string
@param command: Function name
@type args: list
@param args: List of parameters
@rtype: string
@return: Response from LiquidSoap
"""
try:
if not self.disable_logging:
if namespace == "recorder":
self.logger.info("LiquidSoapCommunicator is calling " + str(namespace) + "_" + str(command) + "." + str(args))
else:
if command == "":
self.logger.info("LiquidSoapCommunicator is calling " + str(namespace) + str(args))
else:
self.logger.info("LiquidSoapCommunicator is calling " + str(namespace) + "." + str(command) + str(args))
# call wanted function ...
func = getattr(lqs_instance, namespace)
# ... and fetch the result
result = func(command, *args)
if not self.disable_logging:
self.logger.debug("LiquidSoapCommunicator got response " + str(result))
self.connection_attempts = 0
return result
except LQConnectionError as e:
self.logger.error("Connection Error when sending " + str(namespace) + "." + str(command) + str(args))
if self.try_to_reconnect():
time.sleep(0.2)
self.connection_attempts += 1
if self.connection_attempts < 5:
# reconnect
self.__open_conn(self.client)
self.logger.info("Trying to resend " + str(namespace) + "." + str(command) + str(args))
# grab return value
retval = self.__send_lqc_command__(lqs_instance, namespace, command, *args)
# disconnect
self.__close_conn(self.client)
# return the val
return retval
else:
if command == "":
msg = "Rethrowing Exception while trying to send " + str(namespace) + str(args)
else:
msg = "Rethrowing Exception while trying to send " + str(namespace) + "." + str(command) + str(args)
self.logger.info(msg)
self.disable_transaction(socket=self.client, force=True)
raise e
else:
# also store when was last admin mail sent with which content...
self.logger.critical("SEND ADMIN MAIL AT THIS POINT")
raise e
# ------------------------------------------------------------------------------------------ #
def try_to_reconnect(self):
self.enable_transaction()
return self.transaction > 0
# ------------------------------------------------------------------------------------------ #
def enable_transaction(self, socket=None):
# set socket to playout if nothing else is given
if socket is None:
socket = self.client
self.transaction = self.transaction + 1
self.logger.debug(TerminalColors.WARNING.value + "ENabling transaction! cnt: " + str(self.transaction) + TerminalColors.ENDC.value)
if self.transaction > 1:
return
try:
self.__open_conn(socket)
except FileNotFoundError:
self.disable_transaction(socket=socket, force=True)
msg = "socket file " + socket.socket_path + " not found. Is liquidsoap running?"
self.logger.critical(TerminalColors.RED.value + msg + TerminalColors.ENDC.value)
self.auramailer.send_admin_mail("CRITICAL Exception when connecting to Liquidsoap", msg)
# ------------------------------------------------------------------------------------------ #
def disable_transaction(self, socket=None, force=False):
if not force:
# nothing to disable
if self.transaction == 0:
return
# decrease transaction counter
self.transaction = self.transaction - 1
# debug msg
self.logger.debug(TerminalColors.WARNING.value + "DISabling transaction! cnt: " + str(self.transaction) + TerminalColors.ENDC.value)
# return if connection is still needed
if self.transaction > 0:
return
else:
self.logger.debug(TerminalColors.WARNING.value + "Forcefully DISabling transaction! " + TerminalColors.ENDC.value)
# close conn and set transactioncounter to 0
self.__close_conn(socket)
self.transaction = 0
# ------------------------------------------------------------------------------------------ #
def __open_conn(self, socket):
# already connected
if self.transaction > 1:
return
self.logger.debug(TerminalColors.GREEN.value + "LiquidSoapCommunicator opening conn" + TerminalColors.ENDC.value)
# try to connect
socket.connect()
# ------------------------------------------------------------------------------------------ #
def __close_conn(self, socket):
# set socket to playout
if socket is None:
socket = self.client
# do not disconnect if a transaction is going on
if self.transaction > 0:
return
# say bye
socket.byebye()
# debug msg
self.logger.debug(TerminalColors.BLUE.value + "LiquidSoapCommunicator closed conn" + TerminalColors.ENDC.value)
#
# 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 logging
import datetime
import threading
from libraries.enum.auraenumerations import ScheduleEntryType
class LiquidSoapInitThread(threading.Thread):
logger = None
active_entry = None
liquidsoapcommunicator = None
# ------------------------------------------------------------------------------------------ #
def __init__(self, liquidsoapcommunicator, active_entry):
threading.Thread.__init__(self)
self.logger = logging.getLogger("AuraEngine")
self.liquidsoapcommunicator = liquidsoapcommunicator
self.active_entry = active_entry
# ------------------------------------------------------------------------------------------ #
def run(self):
try:
# sleep needed, because the socket is created too slow by liquidsoap
time.sleep(1)
self.logger.info("Waited 1s for liquidsoap. Jez soit a si gspian")
# enable lqs transaction
self.liquidsoapcommunicator.enable_transaction()
# wait another second. lqs really starts slow.. be prepared you liquidsoap you!
time.sleep(1)
# set some parameters
self.set_start_parameters()
# set active
self.set_active_show()
# disable lqs transaction again
self.liquidsoapcommunicator.disable_transaction()
# the rest of the system now can use liquidsoap connection
self.liquidsoapcommunicator.is_liquidsoap_running = True
except Exception as e:
self.logger.critical("Liquidsoap connection ERROR! Restart LQ Server! Reason: "+str(e))
def set_active_show(self):
if self.active_entry is not None:
self.logger.info("LiquidSoapInitThread sets activechannel: " + str(self.active_entry))
channel = self.active_entry.type
# have to seek?
if channel == ScheduleEntryType.FILESYSTEM:
# calc how many seconds were missed
now_unix = time.mktime(datetime.datetime.now().timetuple())
seconds_to_seek = now_unix - self.active_entry.start_unix
# and seek these seconds forward
if seconds_to_seek > 0:
self.liquidsoapcommunicator.playlist_seek(seconds_to_seek)
# finally make something hearable :-)
if channel != "" and channel is not None:
# activate http stream if needed
self.liquidsoapcommunicator.http_start_stop(channel == ScheduleEntryType.STREAM)
# finally set the volume up
self.liquidsoapcommunicator.channel_volume(channel.value, self.active_entry.volume)
else:
self.logger.error("Channel is NULL or empty! Cannot set ")
else:
self.logger.warning("No active entry in the scheduler! Is a programme loaded?")
def set_start_parameters(self):
# reset channels and reload them
channels = self.liquidsoapcommunicator.reload_channels()
# for all available channels
for c in channels:
# set volume to zero
self.liquidsoapcommunicator.channel_volume(c, "0")
# and activate this channel
self.liquidsoapcommunicator.channel_activate(c, True)
# setting init params like a blank file..
install_dir = self.liquidsoapcommunicator.config.get("install_dir")
self.liquidsoapcommunicator.playlist_push(install_dir + "/configuration/blank.flac")
# .. or the radio fro stream (it is overwritten as soon as one http overtake is planned)
#self.liquidsoapcommunicator.set_http_url("http://stream.fro.at/fro-128.ogg")
#
# 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.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 fs(self, command, *args):
if command == "push":
return self.fs_push(*args)
if command == "seek":
return self.fs_seek(*args)
return "LiquidSoapPlayerClient does not understand fs." + command + str(args)
# ------------------------------------------------------------------------------------------ #
def uptime(self, command=""): # no command will come
return self.command("uptime", "")
# ------------------------------------------------------------------------------------------ #
def auraengine(self, command, *args):
if command == "state":
return self.auraengine_state()
return "LiquidSoapPlayerClient does not understand auraengine." + command + str(args)
# ------------------------------------------------------------------------------------------ #
def auraengine_state(self):
self.command('auraengine', 'state')
return self.message
# ------------------------------------------------------------------------------------------ #
def fs_push(self, uri):
self.command('fs', 'push', uri)
return self.message
# ------------------------------------------------------------------------------------------ #
def fs_seek(self, uri):
self.command('fs', 'seek', uri)
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 seek(self, duration, namespace="playlist"):
"""
Aktuell laufenen Track des Kanals vorspulen
@type duration: string/int
@param duration: Dauer in Sekunden
@type namespace: string
@param namespace: Namespace der Source
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
self.command('seek', namespace, str(duration))
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 libraries.exceptions.auraexceptions import MailingException
class AuraMailer():
config = None
# ------------------------------------------------------------------------------------------ #
def __init__(self, config):
self.config = config
self.admin_mails = config.get("admin_mail")
# ------------------------------------------------------------------------------------------ #
def send_admin_mail(self, subject, body):
admin_mails = self.admin_mails.split()
for mail_to in admin_mails:
self.__send(mail_to, subject, body)
# ------------------------------------------------------------------------------------------ #
def __send(self, mail_to, subject, body):
# 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))
#
# 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 sys
import time
import redis
import logging
import threading
from datetime import datetime
from threading import Event
from modules.communication.redis.messenger import RedisMessenger
# from modules.communication.connection_tester import ConnectionTester
from libraries.database.statestore import RedisStateStore
from libraries.exceptions.auraexceptions import RedisConnectionException
from libraries.enum.auraenumerations import RedisChannel, TerminalColors, FallbackType
# ------------------------------------------------------------------------------------------ #
class ServerRedisAdapter(threading.Thread, RedisMessenger):
debug = False
pubsub = None
config = None
redisdb = None
channel = ""
scheduler = None
redisclient = None
# connection_tester = None
liquidsoapcommunicator = None
socket = None
# ------------------------------------------------------------------------------------------ #
def __init__(self, config):
threading.Thread.__init__(self)
RedisMessenger.__init__(self, config)
# init
#threading.Thread.__init__ (self)
self.config = config
self.shutdown_event = Event()
self.channel = RedisChannel.STANDARD.value
self.section = ''
self.rstore = RedisStateStore(config)
self.errnr = '00'
self.components = {'controller':'01', 'scheduling':'02', 'playd':'03', 'recorder':'04', 'helpers':'09'}
self.fromMail = ''
self.adminMails = ''
self.can_send = None
self.redisclient = ClientRedisAdapter(config)
# self.connection_tester = ConnectionTester()
# ------------------------------------------------------------------------------------------ #
def run(self):
self.redisdb = redis.Redis(host=self.config.get("redis_host"), port=self.config.get("redis_port"), db=self.config.get("redis_db"))
self.pubsub = self.redisdb.pubsub()
self.pubsub.subscribe(self.channel)
self.logger.info(TerminalColors.ORANGE.value + "waiting for REDIS message on channel " + self.channel + TerminalColors.ENDC.value)
# listener loop
for item in self.pubsub.listen():
if item["type"] == "subscribe":
continue
self.logger.info(TerminalColors.ORANGE.value + "received REDIS message: " + TerminalColors.ENDC.value + str(item))
item["channel"] = self.decode_if_needed(item["channel"])
item["data"] = self.decode_if_needed(item["data"])
try:
self.work(item)
except RedisConnectionException as rce:
self.logger.error(str(rce))
if not self.shutdown_event.is_set():
self.logger.info(TerminalColors.ORANGE.value + "waiting for REDIS message on channel " + self.channel + TerminalColors.ENDC.value)
self.pubsub.unsubscribe()
if not self.shutdown_event.is_set():
self.logger.warning("unsubscribed from " + self.channel + " and finished")
# ------------------------------------------------------------------------------------------ #
def decode_if_needed(self, val):
if isinstance(val, bytes):
return val.decode("utf-8")
return val
# ------------------------------------------------------------------------------------------ #
def listen_for_one_message(self, channel, socket_timeout=2):
self.redisdb = redis.Redis(host=self.config.get("redis_host"), port=self.config.get("redis_port"), db=self.config.get("redis_db"), socket_timeout=socket_timeout)
self.pubsub = self.redisdb.pubsub()
self.pubsub.subscribe(channel)
try:
self.logger.info("I am listening on channel '"+channel+"' for "+str(socket_timeout)+" seconds")
for item in self.pubsub.listen():
it = self.receive_message(item)
if it is not None:
break
except redis.exceptions.TimeoutError as te:
raise te
return item["data"]
# ------------------------------------------------------------------------------------------ #
def receive_message(self, item):
if item["type"] == "subscribe":
self.logger.info("i am subscribed to channel " + item["channel"].decode("utf-8"))
return None
item["channel"] = item["channel"].decode("utf-8")
if isinstance(item["data"], bytes):
item["data"] = item["data"].decode("utf-8")
self.pubsub.unsubscribe()
return item
# ------------------------------------------------------------------------------------------ #
def work(self, item):
if item["data"] == "fetch_new_programme":
#self.execute(RedisChannel.FNP_REPLY.value, self.scheduler.fetch_new_programme)
self.execute(RedisChannel.FNP_REPLY.value, self.scheduler.get_act_programme_as_string)
elif item["data"] == "shutdown":
self.shutdown_event.set()
self.scheduler.stop()
self.pubsub.close()
self.logger.info("shutdown event received. Bye bye...")
elif item["data"] == "init_player":
self.execute(RedisChannel.IP_REPLY.value, self.liquidsoapcommunicator.init_player)
elif item["data"] == "get_act_programme":
self.execute(RedisChannel.GAP_REPLY.value, self.scheduler.get_act_programme_as_string)
# elif item["data"] == "get_connection_status":
# self.execute(RedisChannel.GCS_REPLY.value, self.connection_tester.get_connection_status)
elif item["data"] == "print_message_queue":
self.execute(RedisChannel.PMQ_REPLY.value, self.scheduler.print_message_queue)
elif item["data"].find("set_next_file") >= 0:
playlist = item["data"].split()[1]
playlist = playlist[0:len(playlist)-8]
self.execute(RedisChannel.SNF_REPLY.value, self.scheduler.set_next_file_for, playlist)
elif item["data"].find("get_next_file") >= 0:
playlist = item["data"].split()[1]
#playlist = playlist[0:len(playlist)-8]
self.execute(RedisChannel.GNF_REPLY.value, self.scheduler.get_next_file_for, playlist)
elif item["data"] == "recreate_db":
self.execute(RedisChannel.RDB_REPLY.value, self.scheduler.recreate_database)
elif item["data"] == "status":
return True
else:
raise RedisConnectionException("ServerRedisAdapter Cannot understand command: " + item["data"])
# ------------------------------------------------------------------------------------------ #
def execute(self, channel, f, param=None):
if param:
reply = f(param)
else:
reply = f()
if reply is None:
reply = ""
# sometimes the sender is faster than the receiver. redis messages would be lost
time.sleep(0.1)
self.logger.info(TerminalColors.ORANGE.value + "replying REDIS message " + TerminalColors.ENDC.value + reply + TerminalColors.ORANGE.value + " on channel " + channel + TerminalColors.ENDC.value)
# publish
self.redisclient.publish(channel, reply)
# ------------------------------------------------------------------------------------------ #
def join_comm(self):
try:
while self.is_alive():
self.logger.info(str(datetime.now())+" joining")
self.join()
self.logger.warning("join out")
except (KeyboardInterrupt, SystemExit):
# Dem Server den Shutdown event setzen
# server.shutdown_event.set()
# Der Server wartet auf Eingabe
# Daher einen Client initiieren, der eine Nachricht schickt
self.halt()
sys.exit('Terminated')
# ------------------------------------------------------------------------------------------ #
def halt(self):
"""
Stop the server
"""
if self.shutdown_event.is_set():
return
self.shutdown_event.set()
try:
self.socket.unbind("tcp://"+self.ip+":"+self.port)
except:
pass
# self.socket.close()
# ------------------------------------------------------------------------------------------ #
def send(self, message):
"""
Send a message to the client
:param message: string
"""
# FIXME Review logic
if not self.can_send:
self.logger.info("sending a "+str(len(message))+" long message via REDIS.")
self.socket.send(message.encode("utf-8"))
self.can_send = False
else:
self.logger.warning("cannot send message via REDIS: "+str(message))
# ------------------------------------------------------------------------------------------ #
class ClientRedisAdapter(RedisMessenger):
def __init__(self, config):
RedisMessenger.__init__(self, config)
# ------------------------------------------------------------------------------------------ #
def publish(self, channel, message):
if type(channel) == RedisChannel:
channel = channel.value
self.rstore.publish(channel, message)
#
# 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 logging
import datetime
from libraries.database.statestore import RedisStateStore
from modules.communication.mail.mail import AuraMailer
from libraries.exceptions.auraexceptions import PlaylistException
from libraries.enum.auraenumerations import RedisChannel
from libraries.base.logger import AuraLogger
"""
Send and receive redis messages
"""
# ------------------------------------------------------------------------------------------ #
class RedisMessenger():
logger = None
rstore = None
# ------------------------------------------------------------------------------------------ #
def __init__(self, config):
super(RedisMessenger, self).__init__()
"""
Constructor
"""
self.logger = logging.getLogger("AuraEngine")
self.channel = RedisChannel.STANDARD
self.section = ''
self.rstore = RedisStateStore(config)
self.errnr = '00'
self.components = {'controller':'01', 'scheduling':'02', 'playd':'03', 'recorder':'04', 'helpers':'09'}
self.fromMail = ''
self.adminMails = ''
# ------------------------------------------------------------------------------------------ #
def set_channel(self, channel):
"""
Einen "Kanal" setzen - zb scheduling
@type channel: string
@param channel: Kanal/Name der Komponente
"""
self.channel = channel
if channel in self.components:
self.errnr = self.components[channel]
self.rstore.set_channel(channel)
# ------------------------------------------------------------------------------------------ #
def set_section(self, section):
"""
Einen Sektion / Gültigkeitsbereich der Meldung setzen - zb internal
@type section: string
@param section: Gültigkeitsbereich
"""
self.section = section
# ------------------------------------------------------------------------------------------ #
def set_mail_addresses(self, fromMail, adminMails):
"""
Einen Sektion / Gültigkeitsbereich der Meldung setzen - zb internal
@type section: string
@param section: Gültigkeitsbereich
"""
self.fromMail = fromMail
self.adminMails = adminMails
# ------------------------------------------------------------------------------------------ #
def send(self, message, code, level, job, value='', section=''):
"""
Eine Message senden
@type message: string
@param message: menschenverständliche Nachricht
@type code: string
@param code: Fehlercode - endet mit 00 bei Erfolg
@type level: string
@param level: Error-Level - info, warning, error, fatal
@type job: string
@param job: Name der ausgeführten Funktion
@type value: string
@param value: Ein Wert
@type section: string
@param section: Globale Sektion überschreiben
"""
section = self.section if section == '' else section
self.time = str(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S:%f'))
self.utime = time.time()
state = {'message':message.strip().replace("'","\\'"), 'code':self.errnr + str(code),'job':job,'value':value}
self.rstore.set_section(section)
self.rstore.store(level, state)
if level == 'info' or level == 'success':
self.logger.info(message)
elif level == 'warning':
self.logger.warning(message)
elif level == 'error':
self.logger.error(message)
self.send_admin_mail(level, message, state)
elif level == 'fatal':
self.logger.critical(message)
self.send_admin_mail(level, message, state)
# ------------------------------------------------------------------------------------------ #
def say_alive(self):
"""
Soll alle 20 Sekunden von den Komponenten ausgeführt werden,
um zu melden, dass sie am Leben sind
"""
self.rstore.set_alive_state()
# ------------------------------------------------------------------------------------------ #
def get_alive_state(self, channel):
"""
Live State abfragen
@type channel: string
@param channel: Channel/Komponente
"""
return self.rstore.get_alive_state(channel)
# ------------------------------------------------------------------------------------------ #
def set_state(self, name, value, expires=None, channel=None):
"""
Kündigt einen Event an
@type name: string
@param name: Name des state
@type value: string
@param value: Wert
@type channel: string
@param channel: Kanal (optional)
"""
if not channel:
channel = self.channel
self.rstore.set_state(name, value, expires, channel)
# ------------------------------------------------------------------------------------------ #
def queue_add_event(self, name, eventtime, value, channel=None):
"""
Kündigt einen Event an
@type name: string
@param name: der Name des Events
@type eventtime: string|datetime.datetime
@param eventtime: Datum und Zeit des events
@type value: dict
@param value: Werte
@type channel: string
@param channel: Kanal (optional)
"""
if not channel:
channel = self.channel
if type(eventtime) == type(str()):
eventtime_str = datetime.datetime.strptime(eventtime[0:16].replace(' ','T'), "%Y-%m-%dT%H:%M").strftime("%Y-%m-%dT%H:%M")
elif type(eventtime) is datetime.datetime:
eventtime_str = eventtime.strftime("%Y-%m-%dT%H:%M")
else:
raise TypeError('eventtime must be a datetime.date or a string, not a %s' % type(eventtime))
self.rstore.queue_add_event(eventtime_str, name, value, channel)
# ------------------------------------------------------------------------------------------ #
def queue_remove_events(self, name, channel=None):
"""
Löscht Events
@type name: string
@param name: der Name des Events
@type channel: string
@param channel: Kanal (optional)
"""
if not channel:
channel = self.channel
self.rstore.queue_remove_events(name, channel)
# ------------------------------------------------------------------------------------------ #
def fire_event(self, name, value, channel=None):
"""
Feuert einen Event
@type name: string
@param name: der Name des Events
@type value: dict
@param value: Werte
@type channel: string
@param channel: Kanal (optional)
"""
if not channel:
channel = self.channel
self.rstore.fire_event(name, value, channel)
# ------------------------------------------------------------------------------------------ #
def get_event_queue(self, name=None, channel=None):
"""
Holt events eines Kanals
@type channel: string
@param channel: Kanal (optional)
@rtype: list
@return: Liste der Events
"""
queue = self.rstore.get_event_queue(name, channel)
return queue
# ------------------------------------------------------------------------------------------ #
def get_events(self, name=None, channel=None):
"""
Holt events eines Kanals
@type channel: string
@param channel: Kanal (optional)
@rtype: list
@return: Liste der Events
"""
events = self.rstore.get_events(name, channel)
return events
# ------------------------------------------------------------------------------------------ #
def get_event(self, name=None, channel=None):
"""
Holt event eines Kanals
@type channel: string
@param channel: Kanal (optional)
@rtype: dict
@return: Event
"""
events = self.rstore.get_events(name, channel)
result = False
if events:
result = events.pop(0)
return result
# ------------------------------------------------------------------------------------------ #
def send_admin_mail(self, level, message, state):
"""
Sendent mail an Admin(s),
@type message: string
@param message: Die Message
@type state: dict
@param state: Der State
@return result
"""
# FIXME Make Mailer functional: Invalid constructor
if self.fromMail and self.adminMails:
#subject = "Possible comba problem on job " + state['job'] + " - " + level
mailmessage = "Hi Admin,\n comba reports a possible problem\n\n"
mailmessage = mailmessage + level + "!\n"
mailmessage = mailmessage + message + "\n\n"
mailmessage = mailmessage + "Additional information:\n"
mailmessage = mailmessage + "##################################################\n"
mailmessage = mailmessage + "Job:\t" + state['job'] + "\n"
mailmessage = mailmessage + "Code:\t" + state['code'] + "\n"
mailmessage = mailmessage + "Value:\t" + str(state['value']) + "\n"
#mailer = AuraMailer(self.adminMails, self.fromMail)
#mailer.send_admin_mail(subject, mailmessage)
else:
return False
# ------------------------------------------------------------------------------------------ #
def receive(self):
"""
Bisher wird nichts empfangen
"""
return ""
# ------------------------------------------------------------------------------------------ #
def get_next_file_for(self, playlisttype):
next = self.rstore.db.get('next_'+playlisttype+'file')
if next is None:
next = b""
return next.decode('utf-8')
# ------------------------------------------------------------------------------------------ #
def set_next_file_for(self, playlisttype, file):
self.rstore.db.set("next_" + playlisttype + "file", file)
__author__ = 'gg'
#!/usr/bin/liquidsoap
#
# 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/>.
#
icecast_vorbis_metadata = false
inputs = ref []
# load settings from ini file
%include "settings.liq"
# include some functions
%include "library.liq"
# include fallback functions
%include "fallback.liq"
#################
# create inputs #
#################
# enable play from filesystem
%include "in_filesystem.liq"
# enable stream overtakes
%include "in_stream.liq"
# enabled line in from soundcard
%include "in_soundcard.liq"
# fill the mixer
mixer = mix(id="mixer", list.append([input_fs, input_http], !inputs))
# output source with fallbacks
stripped_stream = strip_blank(track_sensitive=false, max_blank=fallback_max_blank, min_noise=fallback_min_noise, threshold=fallback_threshold, mixer)
# enable fallback
output_source = fallback(id="fallback", track_sensitive=false, [stripped_stream, timeslot_fallback, show_fallback, mksafe(station_fallback)])
##################
# create outputs #
##################
# create soundcard output
%include "out_soundcard.liq"
# recording output
%include "out_filesystem.liq"
# stream output
%include "out_stream.liq"
# enable socket functions
%include "serverfunctions.liq"
########################
# start initialization #
########################
system('#{list.assoc("install_dir", ini)}/guru.py --init-player --quiet')
#
# 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/>.
#
# Custom crossfade to deal with jingles.
def smart_crossfade (~start_next=5.,~fade_in=3.,~fade_out=3.,
~default=(fun (a,b) -> sequence([a, b])),
~high=-15., ~medium=-32., ~margin=4.,
~width=2.,~conservative=false,s)
fade.out = fade.out(type="sin",duration=fade_out)
fade.in = fade.in(type="sin",duration=fade_in)
add = fun (a,b) -> add(normalize=false,[b, a])
log = log(label="smart_crossfade")
def transition(a,b,ma,mb,sa,sb)
list.iter(fun(x)-> log(level=4,"Before: #{x}"),ma)
list.iter(fun(x)-> log(level=4,"After : #{x}"),mb)
if ma["type"] == "jingles" or mb["type"] == "jingles" then
log("Old or new file is a jingle: sequenced transition.")
sequence([sa, sb])
elsif
# If A and B are not too loud and close, fully cross-fade them.
a <= medium and b <= medium and abs(a - b) <= margin
then
log("Old <= medium, new <= medium and |old-new| <= margin.")
log("Old and new source are not too loud and close.")
log("Transition: crossed, fade-in, fade-out.")
add(fade.out(sa),fade.in(sb))
elsif
# If B is significantly louder than A, only fade-out A.
# We don't want to fade almost silent things, ask for >medium.
b >= a + margin and a >= medium and b <= high
then
log("new >= old + margin, old >= medium and new <= high.")
log("New source is significantly louder than old one.")
log("Transition: crossed, fade-out.")
add(fade.out(sa),sb)
elsif
# Opposite as the previous one.
a >= b + margin and b >= medium and a <= high
then
log("old >= new + margin, new >= medium and old <= high")
log("Old source is significantly louder than new one.")
log("Transition: crossed, fade-in.")
add(sa,fade.in(sb))
elsif
# Do not fade if it's already very low.
b >= a + margin and a <= medium and b <= high
then
log("new >= old + margin, old <= medium and new <= high.")
log("Do not fade if it's already very low.")
log("Transition: crossed, no fade.")
add(sa,sb)
# What to do with a loud end and a quiet beginning ?
# A good idea is to use a jingle to separate the two tracks,
# but that's another story.
else
# Otherwise, A and B are just too loud to overlap nicely,
# or the difference between them is too large and overlapping would
# completely mask one of them.
log("No transition: using default.")
default(sa, sb)
end
end
smart_cross(width=width, duration=start_next, conservative=conservative, transition, s)
end
# create a pool
def fallback_create(~skip=true, name, requestor)
log("Creating channel #{name}")
# Create the request.dynamic source
# Set conservative to true to queue several songs in advance
#source = request.dynamic(conservative=true, length=50., id="pool_"^name, requestor, timeout=60.)
source = request.dynamic(length=50., id="pool_"^name, requestor, timeout=60.)
# Apply normalization using replaygain information
source = amplify(1., override="replay_gain", source)
# Skip blank when asked to
source =
if skip then
skip_blank(max_blank=fallback_max_blank, min_noise=fallback_min_noise, threshold=fallback_threshold, source)
else
source
end
# Tell the system when a new track
# is played
source = on_metadata(fun (meta) ->
# log("ON_METADATA_DISABLED"),
system('#{list.assoc("install_dir", ini)}/guru.py --adapt-trackservice-title'),
source)
log("channel created")
# Finally apply a smart crossfading
smart_crossfade(source)
end
def create_dynamic_playlist(next)
request.create(list.hd(next))
end
def create_playlist() =
log("requesting next song for PLAYLIST")
result = get_process_lines('#{list.assoc("install_dir", ini)}/guru.py --get-next-file-for "playlist" --quiet')
create_dynamic_playlist(result)
end
def create_station_fallback() =
result = get_process_lines('#{list.assoc("install_dir", ini)}/guru.py --get-next-file-for station --quiet')
log("next song for STATION fallback is: #{result}")
create_dynamic_playlist(result)
end
def create_show_fallback() =
result = get_process_lines('#{list.assoc("install_dir", ini)}/guru.py --get-next-file-for show --quiet')
log("next song for SHOW fallback is: #{result}")
create_dynamic_playlist(result)
end
def create_timeslot_fallback() =
result = get_process_lines('#{list.assoc("install_dir", ini)}/guru.py --get-next-file-for timeslot --quiet')
log("next song for TIMESLOT fallback is: #{result}")
create_dynamic_playlist(result)
end
# create fallbacks
timeslot_fallback = fallback_create(skip=true, "timeslot_fallback", create_timeslot_fallback)
station_fallback = fallback_create(skip=true, "station_fallback", create_station_fallback)
show_fallback = fallback_create(skip=true, "show_fallback", create_show_fallback)
\ No newline at end of file
__author__ = 'michel'
import os
import sys
#!/bin/bash
pack_int(){ printf "%08X\n" $1 | sed 's/\([0-9A-F]\{2\}\)\([0-9A-F]\{2\}\)\([0-9A-F]\{2\}\)\([0-9A-F]\{2\}\)/\\\\\\x\4\\\\\\x\3\\\\\\x\2\\\\\\x\1/I' | xargs printf; }
pack_short(){ printf "%04X\n" $1 | sed 's/\([0-9A-F]\{2\}\)\([0-9A-F]\{2\}\)/\\\\\\x\2\\\\\\x\1/I' | xargs printf; }
duration=1800
if [[ $# -eq 1 ]]; then
duration=$1
fi
channels=2
bps=16
sample=44100
Subchunk1Size=18
Subchunk2Size=$(echo "$duration*$sample*$channels*$bps/8" | bc)
ChunkSize=$((20 + $Subchunk1Size + $Subchunk2Size))
echo -n RIFF
pack_int $ChunkSize
echo -n "WAVEfmt "
pack_int $Subchunk1Size
pack_short 1
pack_short $channels
pack_int $sample
pack_int $((bps/8 * channels * sample))
pack_short $((bps/8 * channels))
pack_short $bps
pack_short 0
echo -n data
pack_int $Subchunk2Size
dd if=/dev/zero bs=1 count=$Subchunk2Size 2>/dev/null
#
# 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/>.
#
# a hard one
input_fs = request.equeue(id="fs")
\ 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/>.
#
if a0_in != "" then
# we can ignore the result, since it is stored in the list 'inputs'
set_input(a0_in, "aura_linein_0")
end
if a1_in != "" then
ignore(set_input(a1_in, "aura_linein_1"))
end
if a2_in != "" then
ignore(set_input(a2_in, "aura_linein_2"))
end
if a3_in != "" then
ignore(set_input(a3_in, "aura_linein_3"))
end
if a4_in != "" then
ignore(set_input(a4_in, "aura_linein_4"))
# input_4 = ref output.dummy(blank())
# set_input(input_4, a4_in, "aura_linein_4")
# inputs := list.append([!input_4], !inputs)
end
\ 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/>.
#
# this is overwritten as soon as a streamovertake is programmed, but liquidsoap needs it to initialize this input
#starturl = "http://stream.fro.at/fro-128.ogg"
#starturl = "http://trance.out.airtime.pro:8000/trance_a"
starturl = "http://chill.out.airtime.pro:8000/chill_a"
input_http = input.http(id="http", starturl)
\ 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/>.
#
#####################
# stream to icecast #
#####################
def stream_to_icecast(id, encoding, bitrate, host, port, pass, mount_point, url, description, genre, user, stream, streamnumber, connected, name, channels) =
source = ref stream
def on_error(msg)
connected := "false"
log(msg)
5.
end
def on_connect()
connected := "true"
log("Successfully connected to stream_#{streamnumber}")
end
stereo = (int_of_string(channels) >= 2)
user_ref = ref user
if user == "" then
user_ref := "source"
end
# dumbass liquidsoap cannot handle one output definition for mono and stereo
output_icecast_mono = output.icecast(id = id, host = host, port = port, password = pass, mount = mount_point, fallible = true, url = url, description = description, name = name, genre = genre, user = !user_ref, on_error = on_error, on_connect = on_connect)
output_icecast_stereo = output.icecast(id = id, host = host, port = port, password = pass, mount = mount_point, fallible = true, url = url, description = description, name = name, genre = genre, user = !user_ref, on_error = on_error, on_connect = on_connect)
%ifencoder %aac
if encoding == "aac" then
log("ENABLING AAC to ICECAST")
%include "outgoing_streams/aac.liq"
end
%endif
%ifencoder %flac
if encoding == "flac" then
log("ENABLING FLAC to ICECAST")
%include "outgoing_streams/flac.liq"
end
%endif
if encoding == "mp3" then
log("ENABLING Mp3 to ICECAST")
%include "outgoing_streams/mp3.liq"
end
if encoding == "ogg" then
log("ENABLING OGG to ICECAST")
%include "outgoing_streams/ogg.liq"
end
%ifencoder %opus
if encoding == "opus" then
log("ENABLING OPUS to ICECAST")
%include "outgoing_streams/opus.liq"
end
%endif
end
###########
# line in #
###########
def set_input(device, name) =
if use_alsa == true then
alsa_in = input.alsa(id=name, device=a0_in, clock_safe=false, bufferize = false)
inputs := list.append([alsa_in], !inputs)
elsif use_jack == true then
jack_in = input.jack(id=name, clock_safe=false)
inputs := list.append([jack_in], !inputs)
else
pulse_in = input.pulseaudio(id=name, client="AuraEngine Line IN")
inputs := list.append([pulse_in], !inputs)
end
end
############
# line out #
############
def get_output(source, device, name) =
if device != "" then
if use_alsa == true then
log("--- Set ALSA Output ---")
if device == "default" then
output.alsa(id="lineout", bufferize = false, source)
else
output.alsa(id=name, device=device, bufferize = false, source)
end
elsif use_jack == true then
log("--- Set JACK AUDIO Output ---")
output.jack(id=name, source)
else
log("--- Set PULSE AUDIO Output ---")
output.pulseaudio(id=name, client="AuraEngine Line OUT", source)
end
else
log("OUTPUT DUMMY")
output.dummy(id=name^"_DUMMY", blank())
end
end
########################
# record to filesystem #
########################
# shows current file and how many bytes were written so far
def currecording(recfile)
if recfile != "" then
bytes_written = list.hd(get_process_lines("echo $(($(stat -c%s "^recfile^")))"))
"#{recfile}, #{bytes_written}B"
else
""
end
end
def start_recorder(folder, duration, encoding, bitrate, channels, filenamepattern, is_recording, stream, recorder_number) =
source = ref stream
stereo = (int_of_string(channels) >= 2)
# define on_start, on_close (good case) and on_stop (error case)
recfile = ref ''
def on_start()
is_recording := true
recfile := list.hd(get_process_lines("date +#{filenamepattern}"))
end
def on_close(filename)
is_recording := false
recfile := list.hd(get_process_lines("date +#{filenamepattern}"))
end
def on_stop()
is_recording := false
end
# register server function
server.register(namespace="recorder_"^recorder_number, description="Show current file.", usage="curfile", "curfile", fun (s) -> currecording(!recfile) )
# dumbass liquidsoap cannot handle one output definition for mono and stereo
output_filesystem_mono = output.file(id="recorder_"^recorder_number, perm = 0o664, on_start=on_start, on_close=on_close, on_stop=on_stop, reopen_when={ int_of_float(gettimeofday()/60.) mod duration == 0 })
output_filesystem_stereo = output.file(id="recorder_"^recorder_number, perm = 0o664, on_start=on_start, on_close=on_close, on_stop=on_stop, reopen_when={ int_of_float(gettimeofday()/60.) mod duration == 0 })
%ifencoder %aac
if encoding == "aac" then
log("ENABLING aac recorder to filesystem")
%include "outgoing_recordings/aac.liq"
end
%endif
%ifencoder %flac
if encoding == "flac" then
log("ENABLING flac recorder to filesystem")
%include "outgoing_recordings/flac.liq"
end
%endif
%ifencoder %mp3
if encoding == "mp3" then
log("ENABLING mp3 recorder to filesystem")
%include "outgoing_recordings/mp3.liq"
end
%endif
%ifencoder %vorbis
if encoding == "ogg" then
log("ENABLING ogg recorder to filesystem")
%include "outgoing_recordings/ogg.liq"
end
%endif
%ifencoder %opus
if encoding == "opus" then
log("ENABLING opus recorder to filesystem")
%include "outgoing_recordings/opus.liq"
end
%endif
%ifencoder %wav
if encoding == "wav" then
log("ENABLING wav recorder to filesystem")
%include "outgoing_recordings/wav.liq"
end
%endif
end