Skip to content
Snippets Groups Projects

Compare revisions

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

Source

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

Target

Select target project
  • aura/engine
  • hermannschwaerzler/engine
  • sumpfralle/aura-engine
3 results
Select Git revision
  • 122-synchronized-ci
  • feat-use-docker-main-tag
  • fix-aura-sysuser
  • fix-broken-pipe-153
  • fix-docker-release
  • fix-push-latest-with-tag
  • fix-streamchannel-retries
  • gitlab-templates
  • improve-test-coverage-137
  • improve-test-coverage-143
  • main
  • orm-less-scheduling
  • remove-mailer
  • update-changelog-alpha3
  • virtual-timeslots-131
  • 1.0.0-alpha1
  • 1.0.0-alpha2
  • 1.0.0-alpha3
  • 1.0.0-alpha4
  • 1.0.0-alpha5
20 results
Show changes
Showing
with 3775 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/>.
#
import simplejson
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 = simplejson.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 2014 BFR <info@freie-radios.de>
# Copyright (C) 2017-2018 Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; Version 3 of the License
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, the license can be downloaded here:
#
# http://www.gnu.org/licenses/gpl.html
# Meta
__version__ = '0.1.1'
__license__ = "GNU General Public License (GPL) Version 3"
__version_info__ = (0, 1, 1)
__author__ = 'Michael Liebler <michael-liebler@radio-z.net>'
# massively enhanced by Gottfried Gaisbauer <gottfried.gaisbauer@servus.at>
"""
Comba Base Class - lade Config
"""
import os
import sys
import socket
import logging
from configparser import ConfigParser
class ConfigReader(object):
ini_path = ""
logger = None
def __init__(self):
self.logger = logging.getLogger("AuraEngine")
def set(self, key, value):
"""
Set a property
@type key: string
@param key: The Key
@type value: mixed
@param value: Beliebiger Wert
"""
try:
self.__dict__[key] = int(value)
except:
self.__dict__[key] = str(value)
# ------------------------------------------------------------------------------------------ #
def get(self, key, default=None):
"""
get a loaded property
@type key: string
@param key: Der Key
@type default: mixed
@param default: Beliebiger Wert
"""
if key not in self.__dict__:
if default:
self.set(key, default)
else:
self.logger.warning("Key " + key + " not found in configfile " + self.ini_path + "!")
return None
if key == "loglevel":
loglvl = self.__dict__[key]
if loglvl == "debug":
return logging.DEBUG
elif loglvl == "info":
return logging.INFO
elif loglvl == "warning":
return logging.WARNING
elif loglvl == "error":
return logging.ERROR
else:
return logging.CRITICAL
if key == "debug":
return self.__dict__[key].count("y")
return self.__dict__[key]
# ------------------------------------------------------------------------------------------ #
def load_config(self):
"""
Set config defaults and load settings from file
:return:
"""
self.ini_path = self.get('configpath', '/etc/aura/engine.ini')
if not os.path.isfile(self.ini_path):
self.logger.critical(self.ini_path + " not found :(")
sys.exit(1)
# INI einlesen
f = open(self.ini_path, 'r')
ini_str = f.read()
f.close()
config_parser = ConfigParser()
try:
config_parser.read_string(ini_str)
except Exception as e:
self.logger.critical("Cannot read " + self.ini_path + "! Reason: " + str(e))
sys.exit(0)
for section in config_parser.sections():
for key, value in config_parser.items(section):
v = config_parser.get(section, key).replace('"', '').strip()
self.set(key, v)
self.set("install_dir", os.path.realpath(__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 simplejson
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.swap_playlist_entries:
self.swap_playlist_entries(self.args.swap_playlist_entries[0], self.args.swap_playlist_entries[1])
elif self.args.delete_playlist_entry:
self.delete_playlist_entry(self.args.delete_playlist_entry[0])
elif self.args.insert_playlist_entry:
self.insert_playlist_entry(self.args.insert_playlist_entry[0], self.args.insert_playlist_entry[1]) #, self.args.insert_playlist_entry[2])
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()
if with_server:
self.redisserver = ServerRedisAdapter()
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 = simplejson.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 = simplejson.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 = simplejson.loads(json_reply)
self.print_connection_status(connection_status)
def print_programme(self, programme):
for entry in programme:
self.stringreply += "idx: " + str(entry["programme_index"]) + \
" --- schedule id #" + str(entry["schedule_id"]) + "." + str(entry["entry_num"]) + \
" - show: " + entry["schedule"]["show_name"] + \
" - starts @ " + entry["entry_start"] + \
" - plays " + str(entry["source"]) + "\n"
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 swap_playlist_entries(self, from_index, to_index):
json_reply = self.send_and_wait_redis("aura", "swap_playlist_entries " + str(from_index) + " " + str(to_index), RedisChannel.MPE_REPLY)
actprogramme = simplejson.loads(json_reply)
self.print_programme(actprogramme)
def delete_playlist_entry(self, index):
json_reply = self.send_and_wait_redis("aura", "delete_playlist_entry " + str(index), RedisChannel.DPE_REPLY)
actprogramme = simplejson.loads(json_reply)
self.print_programme(actprogramme)
def insert_playlist_entry(self, fromtime, source):
json_reply = self.send_and_wait_redis("aura", "insert_playlist_entry " + fromtime + " " + source, RedisChannel.IPE_REPLY)
actprogramme = simplejson.loads(json_reply)
self.print_programme(actprogramme)
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)
# "annotate:file_id='3541',length='400.0',title='Titel',artist='Artist',album='Album',canal='reggae':" +
#print(next_file)
if next_file == "":
next_file = "/var/audio/blank.flac"
#print("stringreply: "+next_file)
self.stringreply = next_file
self.send_redis("aura", "set_next_file " + type)
def set_next_file(self, type, file):
from modules.communication.redis.messenger import RedisMessenger
redis = RedisMessenger()
redis.set_next_file_for(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 simplejson
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"] = 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 simplejson.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 = ""
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:
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())
self.logger.debug("LiquidSoapClient waiting for reply from LiquidSoap Server")
# wait for reply
self.read()
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.debug(msg)
raise LQConnectionError(msg)
# ------------------------------------------------------------------------------------------ #
def simplecommand(self, command):
"""
Parameterloses Kommando ohne Namespace senden
@type command: string
@param command: Kommando
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
if self.connected:
message = str(command) + '\n'
self.socket.sendall(message.decode("UTF-8"))
self.read()
# self.client.close()
return self.message
# ------------------------------------------------------------------------------------------ #
def get_metadata(self, rid):
"""
Parameterloses Kommando ohne Namespace senden
@type rid: string/int
@param rid: Die ID eines Requests
@rtype: dict
@return: Die Metadaten als dict
"""
meta = self.command('metadata ' + str(rid), 'request')
meta = '[root]\n' + meta
try:
self.metareader.read_string(meta)
except configparser.ParsingError as e:
self.logger.error("ParsingError. Reason: " + str(e))
return False
return self.metareader
# ------------------------------------------------------------------------------------------ #
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 os
import time
import codecs
import urllib
import logging
import tempfile
import simplejson
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
lqcr = None
logger = None
transaction = 0
channels = None
scheduler = None
error_data = None
auramailer = None
aborttransaction = False
connection_attempts = 0
# ------------------------------------------------------------------------------------------ #
def __init__(self, config, logger=True):
"""
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 = simplejson.load(f)
f.close()
self.auramailer = AuraMailer(self.config)
# ------------------------------------------------------------------------------------------ #
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:
"""
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_recorder_status(self):
self.enable_transaction(self.lqcr)
recorder_state = self.__send_lqc_command__(self.lqcr, "record", "status")
self.disable_transaction(self.lqcr)
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 as e:
# 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):
self.enable_transaction()
if num == -1:
self.recorder_start_all()
else:
self.recorder_start_one(num)
# ------------------------------------------------------------------------------------------ #
def recorder_start_all(self):
self.enable_transaction()
for i in range(5):
self.recorder_start_one(i)
self.disable_transaction()
# ------------------------------------------------------------------------------------------ #
def recorder_start_one(self, num):
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 activate(self, entry):
active_type = self.scheduler.get_active_entry().type
try:
# enable transaction
self.enable_transaction()
if active_type == entry.type:
# push something to active channel
self.activate_same_channel(entry)
else:
# switch to another channel
self.activate_different_channel(entry, active_type)
# disable conn
self.disable_transaction()
# insert playlist entry
self.insert_track_service_entry(entry)
except LQConnectionError as e:
# we already caught and handled this error in __send_lqc_command__, but we do not want to execute this function further
pass
# ------------------------------------------------------------------------------------------ #
def activate_same_channel(self, entry, silent=False):
if not silent:
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.source)
if entry.type == ScheduleEntryType.STREAM:
self.http_start_stop(True)
self.set_http_url(entry.source)
# nothing to do when we are live => just leave it as is
# ------------------------------------------------------------------------------------------ #
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)
self.activate_same_channel(entry, True)
# set others 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):
trackservice_entry = TrackService()
trackservice_entry.playlist_id = schedule_entry.playlist_id
trackservice_entry.entry_num = schedule_entry.entry_num
trackservice_entry.source = schedule_entry.source
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):
t = LiquidSoapInitThread()
t.liquidsoapcommunicator = self
t.active_entry = self.scheduler.get_active_entry()
t.start()
return "LiquidSoapInitThread started!"
# ------------------------------------------------------------------------------------------ #
def all_data(self):
"""
Gibt Metadaten aller Kanäle als JSON-String an den Client zurück
@rtype: string/None
@return: Die Antwort des Liquidsoap-Servers
"""
channels = self.__send_lqc_command__(self.client, 'list_channels')
if not isinstance(channels, list):
self.warning('01')
self.notifyClient()
return
data = {}
pdata = {}
try:
self.is_intern = True
playlist_data = simplejson.loads(self.playlist_data(True))
self.is_intern = False
except:
self.warning('01')
self.notifyClient()
return
# Status des Playlistkanals abfragen
status = self.__send_lqc_command__(self.client, 'status', 'mixer', '0')
states = status.split(' ')
state_data = {}
# Die Stati in python dicts einlesen
for state in states:
item = state.split('=')
try:
state_data[item[0]] = item[1]
except:
self.warning('01')
self.notifyClient()
return
remaining = self.__send_lqc_command__(self.client, 'playlist_remaining')
state_data['remaining'] = remaining
# Die Metadaten der Playlist
pdata['state'] = state_data
pdata['tracks'] = playlist_data
data['playlist'] = pdata
# Servermeldungen abschalten
self.is_intern = True
# die channel queues einlesen
for channel in channels:
data[channel] = self.channel_queue(channel, True)
# Servermeldungen einschalten
self.is_intern = False
self.success('00', data)
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def ping(self):
"""
dem Client antworten
"""
return self.message('OK')
# ------------------------------------------------------------------------------------------ #
def channel_insert(self, channel, uri, pos):
"""
Track in einen Channel einfuegen
@type channel: string
@param channel: Kanal
@type uri: string
@param uri: Uri - z.B. file:///my/audio/mp3
@type pos: int
@param pos: Die Position an der eingefügt werden soll
@rtype: string/None
@return: Die Antwort des Liquidsoap-Servers
"""
message = self.__send_lqc_command__(self.client, 'insert', uri, pos, channel)
message = message.strip()
try:
if int(message) > -1:
self.success()
return self.message(message)
except:
self.warning('01')
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def channel_move(self, channel, fromPos, toPos):
"""
Channel-Eintrag von Position fromPos nach Position toPos verschieben
@type channel: string
@param channel: Kanal
@type fromPos: int
@param fromPos: die Position des Eintrags, der verschoben wird
@type toPos: int
@param toPos: Zielposition
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
message = self.__send_lqc_command__(self.client, 'get_queue', channel, 'secondary_queue')
rids = message.strip().split(' ')
try:
rid = rids[int(fromPos) - 1]
except:
self.warning('01')
self.notifyClient()
return
try:
target = rids[int(toPos) - 1]
except:
self.warning('01')
self.notifyClient()
return
if rids[int(fromPos) - 1] == rids[int(toPos) - 1]:
self.warning('02')
self.notifyClient()
return
message = self.__send_lqc_command__(self.client, 'move', rid, str(int(toPos) - 1), channel)
message = message.strip()
if message.strip().find('OK') > -1:
self.success()
self.notifyClient()
return
else:
self.warning('03')
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def channel_off(self, channel):
"""
Channel deaktivieren
@type channel: string
@param channel: Kanal
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
# internal channel name for playlist is 'common'
if channel == 'playlist':
channel = 'common'
channels = self.__send_lqc_command__(self.client, 'list_channels', False)
index = channels.index(channel)
message = self.__send_lqc_command__(self.client, 'deactivate', str(index))
if message.find('selected=false'):
self.success()
else:
self.warning('01')
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def channel_on(self, channel):
"""
Channel aktivieren
@type channel: string
@param channel: Kanal
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
# Find channels
if channel == 'playlist':
channel = 'common'
channels = self.__send_lqc_command__(self.client, 'list_channels', False)
index = channels.index(channel)
# a activate channel
message = self.__send_lqc_command__(self.client, 'activate', str(index))
if message.find('selected=true'):
self.success()
else:
self.warning('01')
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def channel_queue(self, channel, raw=False):
"""
Channel Queue abrufen
@type channel: string
@param channel: Kanal
@type raw: boolean
@param raw: Wenn true, Rückgabe als Python dict Object, andernfalls als JSON-String
@rtype: string/dict
@return: Der Channel Queue
"""
data = {}
# queue will return request id's (rids)
message = self.__send_lqc_command__(self.client, 'get_queue', channel)
rids = message.strip().split(' ')
data['tracks'] = []
for rid in rids:
if rid != '':
# get each rids metadata
metadata = self.__send_lqc_command__(self.client, 'getMetadata', rid)
track = self._metadata_format(metadata)
if not 'title' in track:
if 'location' in track:
track['title'] = os.path.basename(track['location'])
elif 'filename' in track:
track['title'] = os.path.basename(track['filename'])
else:
track['title'] = 'unknown'
data['tracks'].extend([track])
channels = self.__send_lqc_command__(self.client, 'list_channels')
"""
now get channels state
self.lqc.status: ready=false volume=100% single=false selected=false remaining=0.00
"""
try:
index = channels.index(channel)
status = self.__send_lqc_command__(self.client, 'status', 'mixer', str(index + 1))
states = status.split(' ')
state_data = {}
for state in states:
item = state.split('=')
if len(item) > 1:
state_data[item[0]] = item[1]
except:
state_data = {}
self.error('01')
self.notifyClient()
return
data['state'] = state_data
if raw:
# return the list internal
data['state'] = state_data
return data
else:
self.success('00', data)
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def channel_remove(self, channel, pos):
"""
Channel-Eintrag löschen
@type channel: string
@param channel: Kanal
@type pos: int
@param pos: Position des Eintrags
"""
# Es kann nur vom Secondary Queue gelöscht werden
# Falls der Track im Primary Queue gelöscht werden soll, ist ein skip nötg
message = self.__send_lqc_command__(self.client, 'get_queue', channel, 'secondary_queue')
rids = message.strip().split(' ')
try:
rid = rids[int(pos) - 1]
except:
self.warning('02')
self.notifyClient()
return
message = self.__send_lqc_command__(self.client, 'remove', rid, channel)
if message.find('OK') > -1:
self.success()
else:
self.warning('01')
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def channel_seek(self, channel, duration):
"""
Im aktuell spielenden Track auf dem Kanal <channel> <duration> Sekunden "vorspulen"
@type channel: string
@param channel: Kanal
@type duration: int
@param duration: Dauer in Sekunden
"""
# Liquidsoap Kommando
data = self.__send_lqc_command__(self.client, 'seek', duration, channel)
# Resultate prüfen
if self._check_result(data):
self.success('00', self.lq_error['value'])
else:
self.warning('01')
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def channel_skip(self, channel):
"""
Kanal skippen
@type channel: string
@param channel: Kanal
"""
# Liquidsoap Kommando
channels = self.__send_lqc_command__(self.client, 'list_channels')
foundChannel = ''
if not isinstance(channels, list):
self.error('02')
else:
for index, item in enumerate(channels):
if item == channel:
foundChannel = self.__send_lqc_command__(self.client, 'skip', 'mixer', str(index + 1))
break
if foundChannel.strip().find('OK') > -1:
self.success()
elif len(channels) < 1:
self.warning('01')
else:
self.error('03')
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def channel_activate(self, channel, activate):
channels = self.get_all_channels()
try:
index = channels.index(channel)
if len(channel) < 1:
self.warning('02')
except:
import traceback
traceback.print_exc()
self.error('03')
else:
message = self.__send_lqc_command__(self.client, "mixer", "select", index, activate)
return message
# ------------------------------------------------------------------------------------------ #
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)
if len(channel) < 1:
self.warning(job="channel_volume", errnum="02")
else:
message = self.__send_lqc_command__(self.client, "mixer", "volume", str(index), str(int(volume)))
if message.find('volume=' + str(volume) + '%'):
self.success("channel_volume", errnum="00", value=str(volume))
else:
self.warning("channel_volume", errnum="01")
return message
except (AttributeError, ValueError): #(LQConnectionError, AttributeError):
self.disable_transaction(force=True)
self.error("channel_volume", errnum="03")
# ------------------------------------------------------------------------------------------ #
def auraengine_state(self):
state = self.__send_lqc_command__(self.client, "auraengine", "state")
return state
# ------------------------------------------------------------------------------------------ #
def current_data(self):
"""
Metadaten des gespielten Tracks im JSON-Format
Beispiel: {"title": "Deserted Cities of the Heart", "filename": "/home/michel/Nas-audio/cream/the_very_best_of/17_Deserted_Cities_of_the_Heart.mp3", "source": "ch2", "on_air": "2014/07/23 23:46:37", "rid": "2"}
"""
# Liquidsoap Kommando
message = self.__send_lqc_command__(self.client, 'currentTrack')
rid = message.strip()
metadata = self.__send_lqc_command__(self.client, 'getMetadata', rid)
data = self._metadata_format(metadata)
if data:
self.success('00', simplejson.dumps(data))
elif rid == '':
self.warning('01')
else:
self.warning('02', rid)
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def get_channel_state(self, channel):
if channel == 'playlist':
channel = 'common'
channels = self.__send_lqc_command__(self.client, 'list_channels', False)
index = channels.index(channel)
state_data = {}
try:
index = channels.index(channel)
status = self.__send_lqc_command__(self.client, 'status', 'mixer', str(index + 1))
states = status.split(' ')
for state in states:
item = state.split('=')
if len(item) > 1:
state_data[item[0]] = item[1]
except:
state_data = {}
self.error('01')
self.notifyClient()
return
self.success('00', simplejson.dumps(state_data))
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def help(self):
"""
Gibt die Hilfe aus
"""
try:
file = open(os.path.dirname(os.path.abspath(__file__)) + '/doc/comba.hlp', 'r')
doc = file.read()
return self.message(doc)
except:
self.warning('01')
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def liquidsoap_help(self):
data = self.__send_lqc_command__(self.client, 'help')
if not data:
self.warning('01')
else:
self.success('00', data)
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def list_channels(self):
"""
Channels auflisten (Simple JSON)
"""
# Liquidsoap Kommando
channels = self.__send_lqc_command__(self.client, 'list_channels')
if not isinstance(channels, list):
self.error('02')
elif len(channels) < 1:
self.warning('01')
else:
self.success('00', channels)
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def playlist_data(self, raw=False):
"""
Aktuelle Playlist Daten im JSON-Format
"""
# Liquidsoap Kommando
data = self.__send_lqc_command__(self.client, 'playlistData')
if not raw:
self.success('00', simplejson.loads(data))
self.notifyClient()
else:
return data
# ------------------------------------------------------------------------------------------ #
def playlist_flush(self):
"""
Aktuelle Playlist leeren
"""
data = self.__send_lqc_command__(self.client, 'flush')
self.success('00')
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def playlist_insert(self, uri, pos):
"""
Track in die Playlist einfuegen
"""
data = self.__send_lqc_command__(self.client, 'insert', uri, pos)
if not self._check_result(data):
self.warning('01')
else:
self.success('00')
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def playlist_load(self, uri):
"""
Playlist laden
@type uri: string
@param uri: Uri der Playlist
"""
try:
xml = urllib.urlopen(uri).read().decode("utf8")
except:
try:
xml = open(uri).read().decode("utf8")
except:
self.error("01", self.lq_error["message"])
self.notifyClient()
return
(num, filename) = tempfile.mkstemp(suffix=".xspf")
with codecs.open(filename, "w", encoding="utf8") as text_file:
text_file.write(xml)
# playlist = parsexml(xml)
self.logger.critical("playlist commented out, due to removal of parsexml")
playlist = dict()
if not isinstance(playlist, dict):
self.error("02")
self.notifyClient()
else:
self.__send_lqc_command__(self.client, "flush")
data = self.__send_lqc_command__(self.client, "loadPlaylist", filename)
if not self._check_result(data):
self.error("01", self.lq_error["message"])
else:
os.remove(filename)
self._updateEventQueue(playlist)
event = {"job": "loadplaylist", "uri": uri}
self.messenger.fire_event("loadplaylist", event, "player")
self.success("00")
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def playlist_move(self, fromPos, toPos):
"""
Playlist-Eintrag von Position fromPos nach Position toPos verschieben
@type fromPos: int
@param fromPos: die Position des Eintrags, der verschoben wird
@type toPos: int
@param toPos: Zielposition
"""
data = self.__send_lqc_command__(self.client, "move", str(int(fromPos) + 1), str(int(toPos) + 1))
if not self._check_result(data):
self.warning("01")
else:
self.success("00")
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def playlist_pause(self):
"""
Playlist pausieren
"""
data = self.__send_lqc_command__(self.client, "pause")
if not self._check_result(data):
self.info("01")
else:
self.success("00")
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def playlist_stop(self):
"""
Playlist stoppen - der Kanal wird deaktiviert
"""
# Kanal 0 (Playlist) deaktivieren
self.__send_lqc_command__(self.client, "deactivate", "0")
data = self.__send_lqc_command__(self.client, "pause")
if not self._check_result(data):
self.info("01")
else:
self.success("00")
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def playlist_play(self, when="now"):
"""
Playlist starten
@type when: string
@param when: Wenn "now" werden alle anderen Kanäle deaktiviert und geskipped
"""
# Playlist Kanal aktivieren
self.__send_lqc_command__(self.client, "activate", "0")
if when == "now":
# immediately skip all playing channels
# and activate the playlist channel
channels = self.__send_lqc_command__(self.client, "list_channels")
if not isinstance(channels, list):
self.error("03")
elif len(channels) < 1:
self.warning("02")
else:
# xrange
for i in range(len(channels)):
status = self.__send_lqc_command__(self.client, "status", "mixer", str(i + 1))
if "selected=true" in status:
status = self.__send_lqc_command__(self.client, "deactivate", str(i + 1))
status = self.__send_lqc_command__(self.client, "skip", "mixer", str(i + 1))
self.__send_lqc_command__(self.client, "activate", "0")
# send the play command
data = self.__send_lqc_command__(self.client, "play")
if not self._check_result(data):
self.info("01")
else:
self.success("00")
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
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)
# self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def playlist_remove(self, pos):
"""
Playlist-Eintrag löschen
@type pos: int
@param pos: Position des Eintrags
"""
data = self.__send_lqc_command__(self.client, "remove", pos)
if not self._check_result(data):
self.info("01")
else:
self.success("00")
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def playlist_seek(self, duration):
"""
Im aktuell spielenden Track auf dem der Playlist "vorspulen"
@type duration: int
@param duration: Dauer in Sekunden
"""
data = self.__send_lqc_command__(self.client, "seek", duration)
# Resultate prüfen
if self._check_result(data):
self.success("00", self.lq_error["value"])
else:
self.warning("01")
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def playlist_skip(self):
"""
Playlist skippen
"""
data = self.__send_lqc_command__(self.client, "skip")
self.success("00")
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def version(self):
"""
get version
"""
data = self.__send_lqc_command__(self.client, "version")
self.success("00", data)
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def uptime(self):
"""
get uptime
"""
data = self.__send_lqc_command__(self.client, "uptime")
self.success("00", data)
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def on_air(self):
"""
get whats playing now
"""
data = self.__send_lqc_command__(self.client, "on_air")
self.success("00", data)
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
def recorder_data(self):
"""
Status-Daten des Recorders
Rückgabe-Beispiel: /var/audio/rec/2014-05-13/2014-05-13-22-00.wav,30 - Aufnahme von 30% der angegebenen Audiodatei
"""
message = self.__send_lqc_command__(self.lqcr, "recorder_data")
l = message.split(",")
data = {}
if not isinstance(l, list):
data = {"file": "", "recorded": ""}
self.warning("01")
else:
data["file"] = l[0]
if len(l) > 1:
data["recorded"] = l[1]
else:
data["recorded"] = ""
self.success("00", data)
self.notifyClient()
# ------------------------------------------------------------------------------------------ #
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 namespace == "recorder":
self.logger.info("LiquidSoapCommunicator is calling " + str(namespace) + "_" + str(command) + "." + 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)
self.logger.debug("LiquidSoapCommunicator got response " + str(result))
self.connection_attempts = 0
return result
except LQConnectionError as e:
self.logger.info("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:
self.logger.info("Rethrowing Exception while trying to send " + str(namespace) + "." + str(command) + str(args))
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 = TerminalColors.RED.value + "socket file " + socket.socket_path + " not found. Is liquidsoap running?" + TerminalColors.ENDC.value
self.logger.critical(msg)
self.auramailer.send_admin_mail("[AuraEngine] CRITICAL Exception", 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 threading
from libraries.enum.auraenumerations import ScheduleEntryType
class LiquidSoapInitThread(threading.Thread):
logger = None
socket = None
active_entry = None
liquidsoapcommunicator = None
def __init__(self):
threading.Thread.__init__(self)
self.logger = logging.getLogger("AuraEngine")
def run(self):
try:
# sleep needed, because the socket is created to 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()
# reset channels and reload them
self.liquidsoapcommunicator.channels = None
channels = self.liquidsoapcommunicator.get_all_channels()
# set every volume to 0
for c in channels:
self.liquidsoapcommunicator.channel_volume(c, "0")
# select all channels
# for c in channels:
# self.liquidsoapcommunicator.channel_activate(c, True)
# setting init params
self.liquidsoapcommunicator.playlist_push(self.liquidsoapcommunicator.config.get("install_dir") + "/configuration/blank.flac")
self.liquidsoapcommunicator.set_http_url("http://stream.fro.at/fro-128.ogg")
# wait another second. lqs really starts slow..
time.sleep(1)
# set active
if self.active_entry is not None:
self.logger.info("LiquidSoapInitThread sets activechannel: "+str(self.active_entry))
channel = self.active_entry.type
if channel != "" and channel is not None:
self.liquidsoapcommunicator.http_start_stop(channel == ScheduleEntryType.STREAM)
self.liquidsoapcommunicator.channel_volume(channel.value, self.active_entry.volume)
else:
self.logger.warning("No active entry in the scheduler! Is a programme loaded?")
self.liquidsoapcommunicator.disable_transaction()
except Exception as e:
self.logger.critical("Liquidsoap connection ERROR! Restart LQ Server! Reason: "+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/>.
#
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)
return "LiquidSoapPlayerClient does not understand fs." + command + str(args)
# ------------------------------------------------------------------------------------------ #
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 set_http_url(self, uri):
self.command('http', 'url', uri)
return self.message
# ------------------------------------------------------------------------------------------ #
def mixerinputs(self):
"""
List all channels on the mixer
@type namespace: string
@param namespace: lqs namespace
@rtype: list
@return: answer of our lqs server
"""
# self.logger.info("listchannels modules/controller/liquidsoap.py")
# 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/>.
#
from modules.communication.liquidsoap.client import LiquidSoapClient
class LiquidSoapRecorderClient(LiquidSoapClient):
# ------------------------------------------------------------------------------------------ #
def record(self, command, *args):
if command == "status":
self.command('record', 'status')
return self.message
# ------------------------------------------------------------------------------------------ #
def recorder_setfilename(self, filename):
"""
Dateinamen für Aufnahme (Vorproduktion) definieren
@type filename: string
@param filename: Dateiname - Angabe ohne Verzeichnis und mit Extension
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
self.command('setfilename', 'record', str(filename))
return self.message
# ------------------------------------------------------------------------------------------ #
def stop_record(self):
"""
Recorder stoppen
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
message = self.command('stop', 'record')
return self.message
# ------------------------------------------------------------------------------------------ #
def start_record(self):
"""
Recorder starten
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
self.command('start', 'record')
return self.message
# ------------------------------------------------------------------------------------------ #
def recorder_data(self):
"""
Daten des recorders erhalten
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
self.command('curfile', 'record')
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 os
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):
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")
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")
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
server = smtplib.SMTP(mail_server, int(mail_port))
server.starttls()
server.login(mail_user, mail_pass)
server.send_message(msg)
server.quit()
# sendmail_location = "/usr/sbin/sendmail"
# p = os.popen("%s -t" % sendmail_location, "w")
# p.write("From: %s\n" % self.from_mail)
# p.write("To: %s\n" % mail_to)
# p.write("Subject: " + subject + "\n")
# p.write("\n") # blank line separating headers from body
# p.write(body)
# status = p.close()
# return status
#
# 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
class ServerRedisAdapter(threading.Thread, RedisMessenger):
debug = False
# logger = None
pubsub = None
config = None
redisdb = None
channel = ""
scheduler = None
redisclient = None
connection_tester = None
liquidsoapcommunicator = None
def __init__(self):
#super(ServerRedisAdapter, self).__init__()
threading.Thread.__init__(self)
RedisMessenger.__init__(self)
# self.logger = logging.getLogger("AuraEngine")
# init
threading.Thread.__init__ (self)
self.shutdown_event = Event()
self.channel = RedisChannel.STANDARD.value
self.section = ''
self.rstore = RedisStateStore()
self.errnr = '00'
self.components = {'controller':'01', 'scheduling':'02', 'playd':'03', 'recorder':'04', 'helpers':'09'}
self.fromMail = ''
self.adminMails = ''
self.redisclient = ClientRedisAdapter()
self.connection_tester = ConnectionTester()
# ------------------------------------------------------------------------------------------ #
def run(self):
self.redisdb = redis.Redis()
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(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(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("swap_playlist_entries") >= 0:
extracted = item["data"].split()[1:3]
param = {"from_index": extracted[0], "to_index": extracted[1]}
self.execute(RedisChannel.MPE_REPLY.value, self.scheduler.swap_playlist_entries, param)
elif item["data"].find("delete_playlist_entry") >= 0:
entrynum = item["data"].split()[1]
self.logger.info("entry to del: " + str(entrynum))
self.execute(RedisChannel.DPE_REPLY.value, self.scheduler.delete_playlist_entry, entrynum)
elif item["data"].find("insert_playlist_entry") >= 0:
extracted = item["data"].split()[1:3]
param = {"fromtime": extracted[0], "source": extracted[1]}
self.execute(RedisChannel.IPE_REPLY.value, self.scheduler.insert_playlist_entry, param)
elif item["data"].find("set_next_file") >= 0:
playlist = item["data"].split()[1]
playlist = playlist[0:len(playlist)-9]
self.execute(RedisChannel.SNF_REPLY.value, self.scheduler.set_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 " + reply + " 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
try:
del self.auracontroller
except:
pass
self.shutdown_event.set()
result = 'failed'
try:
result = 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
"""
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):
RedisMessenger.__init__(self)
# ------------------------------------------------------------------------------------------ #
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):
super(RedisMessenger, self).__init__()
"""
Constructor
"""
self.logger = logging.getLogger("AuraEngine")
self.channel = RedisChannel.STANDARD
self.section = ''
self.rstore = RedisStateStore()
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-%dT%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)
# TODO: hier kann auch was zu redis gepostet werden
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
"""
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:
return ""
return next.decode('utf-8')
# ------------------------------------------------------------------------------------------ #
def set_next_file_for(self, playlisttype, file):
self.rstore.db.set("next_" + playlisttype + "file", file)
__author__ = 'gg'
#
# 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/>.
#
set("log.file.path", "./<script>.log")
set("server.telnet", true)
set("server.telnet.bind_addr", "0.0.0.0")
set("server.telnet.port", 1234)
# ALSA / pulse settings
# durch ausprobieren herausgefunden für asus xonar dgx 5.1
# chip: CMI8788
# driver: snd_oxygen
set("frame.duration", 0.30)
set("alsa.alsa_buffer", 8192) # 7168) # 6144) # 8192) # 10240) #15876
set("alsa.buffer_length", 25)
set("alsa.periods", 0) # assertion error when setting periods other than 0 => alsa default
input_linein = input.alsa(id="linein", bufferize = false)
input_fs = single(id="fs", "/var/audio/fallback/output.flac")
input_http = input.http(id="http", "http://stream.fro.at/fro-128.ogg")
mixer = mix(id="mixer", [input_fs, input_http, input_linein])
output.alsa(id="lineout", bufferize = false, mixer)
#!/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(max_blank=fallback_max_blank, min_noise=fallback_min_noise, threshold=fallback_threshold, mixer)
ignore(fallback_max_blank)
ignore(fallback_min_noise)
ignore(fallback_threshold)
ignore(timeslot_fallback)
ignore(station_fallback)
ignore(show_fallback)
# enable fallback
output_source = mixer # fallback(id="fallback", track_sensitive=false, [mksafe(stripped_stream), timeslot_fallback, show_fallback, 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(source, max_blank=10., threshold=-40.)
else
source
end
# Tell the system when a new track
# is played
source = on_metadata(fun (meta) ->
system('#{list.assoc("install_dir", ini)}/guru.py -c aura -t liquid_startup'), source)
log("channel created")
# Finally apply a smart crossfading
smart_crossfade(source)
end
def create_dynamic_playlist(next)
log("next song is: #{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() =
log("requesting next song for STATION fallback")
result = get_process_lines('#{list.assoc("install_dir", ini)}/guru.py --get-next-file-for "station-fallback" --quiet')
create_dynamic_playlist(result)
end
def create_show_fallback() =
log("requesting next song for SHOW fallback")
result = get_process_lines('#{list.assoc("install_dir", ini)}/guru.py --get-next-file-for "show-fallback" --quiet')
create_dynamic_playlist(result)
end
def create_timeslot_fallback() =
log("requesting next song for TIMESLOT fallback")
result = get_process_lines('#{list.assoc("install_dir", ini)}/guru.py --get-next-file-for "timeslot-fallback" --quiet')
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.queue(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